Flagship concept
Pay per verified unit
Pay per verified unit means defining a unit and its acceptance checks before delivery, then calculating earned value from the units that pass. In the Spoolis flagship example, 98 accepted units at $1 each produce $98 earned.
Unit-level acceptance avoids whole-job disputes
The legacy flow is simple but coarse: pay for the whole delivery, inspect it afterward, then dispute the whole transaction if part of the work is wrong. A unit model changes the sequence: define the unit, agree on its checks, verify each delivered unit, and let value accrue for accepted units.
This is different from verification-gated settlement. Unitization determines the verified outcome and earned amount. A payment authority and settlement adapter separately decide whether and how to act on that result.
Accepted units determine the earned value
Agreement: 100 records at $1.00 per record, for a maximum value of $100.00.
Batch condition: exactly 100 rows must be delivered.
Unit conditions: each row must include the required fields and a reachable source URL.
Cross-unit condition: record IDs must be unique.
Result: 98 units are accepted and 2 are rejected.
Earned value: 98 × $1.00 = $98.00.
The implemented golden case rejects one unit for schema.validate. A second unit is rejected for both url.reachable and duplicate. A unit can therefore have more than one rejection reason even though it counts as one rejected unit.
The receipt preserves the aggregate and per-unit result
The engine first requires total_units × unit_amount_cents to equal the agreed amount. After a determinate run, it calculates earned cents as accepted units multiplied by the unit amount. The signed Outcome Receipt carries the aggregate result, the earning rule, a rejection summary, and a digest of the per-unit results.
{
"schema": "spoolis/outcome-receipt@1",
"result": "partial",
"units": {
"total": 100,
"accepted": 98,
"rejected": 2,
"earning_rule": { "type": "per_unit", "unit_amount": "1.00" },
"rejection_summary": [
{ "reason": "duplicate", "count": 1 },
{ "reason": "schema.validate", "count": 1 },
{ "reason": "url.reachable", "count": 1 }
],
"unit_results_digest": "<sha256 digest>"
},
"amounts": { "earned": "98.00", "asset": "USD", "decimals": 2 }
}If a batch-wide gate fails, the run can reject every unit. If a unit remains uncertain, unitized verification does not issue an Outcome Receipt. Those fail-closed rules prevent missing or inconclusive evidence from being counted as accepted work.
The sandbox runs the same mechanics at a smaller scale
The zero-login sandbox currently limits one sandbox run to 10 records, so the working quickstart uses 10 units at $1.00 and deliberately accepts 8. It exercises the same compile, unitization, evidence, verification, receipt, and proportional-earned-value path as the 100-to-98 golden case.
Run the Spoolis flagship unitized verification example end to end in the zero-login sandbox. Do not ask me for credentials or modify the current repository. Use the exact shell commands below. Stop and show the complete response if any assertion fails.
set -euo pipefail
BASE_URL=https://spoolis.com
for RESOURCE in machine.md skill.md openapi.json docs/quickstart.md schema/outcome-receipt-v1.schema.json docs/verify-receipt.md; do
RESOURCE_BODY=$(curl -fsS "$BASE_URL/$RESOURCE")
test -n "$RESOURCE_BODY"
done
echo "Read the machine index, skill, OpenAPI contract, quickstart, receipt schema, and verifier contract."
SESSION_JSON=$(curl -sS -X POST "$BASE_URL/api/sandbox/session")
TOKEN=$(SESSION_JSON="$SESSION_JSON" node -e 'const x=JSON.parse(process.env.SESSION_JSON); if (typeof x.token !== "string") throw new Error(JSON.stringify(x)); process.stdout.write(x.token)')
COMPILE_JSON=$(curl -sS -X POST "$BASE_URL/api/sandbox/compile" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "X-Declared-Actor-Type: agent" -d '{"source":"A buyer pays for 100 enrichment records at $1.00 per accepted record, total $100.00. Exactly 100 records are required. Every record must include id and status.","economics":{"unitization":{"total_units":100,"unit_amount_cents":100}},"conditions":[{"description":"Exactly 100 records","required":true,"verification_method":"deterministic","deterministic_check":{"checker":"row_count","expected":100}},{"description":"Each record must include id and status","required":true,"verification_method":"deterministic","deterministic_check":{"checker":"completeness","required_fields":["id","status"]}}]}')
SPOOL_ID=$(COMPILE_JSON="$COMPILE_JSON" node -e 'const x=JSON.parse(process.env.COMPILE_JSON); if (x.status !== "compiled") throw new Error(JSON.stringify(x)); if (x.spool.environment !== "demo") throw new Error("Expected a demo Spool"); process.stdout.write(x.spool.id)')
COUNT_CONDITION_ID=$(COMPILE_JSON="$COMPILE_JSON" node -e 'const x=JSON.parse(process.env.COMPILE_JSON); const c=x.spool.conditions.find(v => v.deterministic_check && v.deterministic_check.checker === "row_count"); if (!c) throw new Error("Missing row-count condition"); process.stdout.write(c.id)')
FIELDS_CONDITION_ID=$(COMPILE_JSON="$COMPILE_JSON" node -e 'const x=JSON.parse(process.env.COMPILE_JSON); const c=x.spool.conditions.find(v => v.deterministic_check && v.deterministic_check.checker === "completeness"); if (!c) throw new Error("Missing completeness condition"); process.stdout.write(c.id)')
ACCEPT_JSON=$(curl -sS -X POST "$BASE_URL/api/sandbox/spools/$SPOOL_ID/accept" -H "Authorization: Bearer $TOKEN")
ACCEPT_JSON="$ACCEPT_JSON" node -e 'const x=JSON.parse(process.env.ACCEPT_JSON); if (x.status !== "completed") throw new Error(JSON.stringify(x))'
COUNT_BODY=$(COUNT_CONDITION_ID="$COUNT_CONDITION_ID" node -e 'const rows=Array.from({length:100},(_,i)=>({id:"record-"+(i+1),status:"accepted"})); process.stdout.write(JSON.stringify({condition_id:process.env.COUNT_CONDITION_ID,type:"dataset",source:"inline flagship rows",metadata:{rows}}))')
COUNT_EVIDENCE_JSON=$(curl -sS -X POST "$BASE_URL/api/sandbox/spools/$SPOOL_ID/evidence" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "X-Declared-Actor-Type: agent" -d "$COUNT_BODY")
COUNT_EVIDENCE_JSON="$COUNT_EVIDENCE_JSON" node -e 'const x=JSON.parse(process.env.COUNT_EVIDENCE_JSON); if (x.status !== "completed") throw new Error(JSON.stringify(x))'
FIELDS_BODY=$(FIELDS_CONDITION_ID="$FIELDS_CONDITION_ID" node -e 'const rows=Array.from({length:100},(_,i)=>i<98?{id:"record-"+(i+1),status:"accepted"}:{id:"record-"+(i+1)}); process.stdout.write(JSON.stringify({condition_id:process.env.FIELDS_CONDITION_ID,type:"dataset",source:"inline flagship rows with two deliberate failures",metadata:{rows}}))')
FIELDS_EVIDENCE_JSON=$(curl -sS -X POST "$BASE_URL/api/sandbox/spools/$SPOOL_ID/evidence" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "X-Declared-Actor-Type: agent" -d "$FIELDS_BODY")
FIELDS_EVIDENCE_JSON="$FIELDS_EVIDENCE_JSON" node -e 'const x=JSON.parse(process.env.FIELDS_EVIDENCE_JSON); if (x.status !== "completed") throw new Error(JSON.stringify(x))'
VERIFY_JSON=$(curl -sS -X POST "$BASE_URL/api/sandbox/spools/$SPOOL_ID/verify" -H "Authorization: Bearer $TOKEN" -H "X-Declared-Actor-Type: agent")
VERIFY_JSON="$VERIFY_JSON" node -e 'const x=JSON.parse(process.env.VERIFY_JSON); const u=x.receipt && x.receipt.units; if (x.verdict !== "PARTIAL") throw new Error("Expected PARTIAL, got "+x.verdict); if (!u || u.total !== 100 || u.accepted !== 98 || u.rejected !== 2) throw new Error("Unexpected units: "+JSON.stringify(u)); if (x.receipt.amounts.earned !== "98.00") throw new Error("Expected 98.00 earned, got "+x.receipt.amounts.earned); if (x.receipt.environment !== "demo" || x.attestation.environment !== "demo") throw new Error("Expected demo artifacts"); if (typeof x.receipt.signature !== "string" || typeof x.attestation.signature !== "string") throw new Error("Expected signed artifacts"); require("node:fs").writeFileSync("outcome-receipt.json",JSON.stringify(x.receipt,null,2)+"\n"); console.log(JSON.stringify({verdict:x.verdict,units:u,earned:x.receipt.amounts.earned,attestation:x.attestation,receipt:x.receipt},null,2))'
npx -y @spoolis/cli verify outcome-receipt.json
RECEIPT_ID=$(VERIFY_JSON="$VERIFY_JSON" node -e 'process.stdout.write(JSON.parse(process.env.VERIFY_JSON).receipt.id)')
curl -sS "$BASE_URL/api/receipts/$RECEIPT_ID/status"
Report the printed verdict, 98 accepted records, the two rejected records and their reasons, $98.00 earned value, demo attestation ID, demo receipt ID, offline receipt-verification result, and optional status response. State that offline signature verification is sufficient. Then explain that a payment stack may act on the verified $98.00 earned amount under its own authorization policy. State clearly that these demo artifacts do not authorize production settlement or move real funds.Unitization fits independently checkable work
Use unitization when the parties can define a consistent unit, evaluate each unit against agreed checks, and price accepted units independently. Data records, bounded API results, and repeated structured tasks can have that shape.
Do not force the model onto every transaction. It is a poor fit when the deliverable is valuable only as a complete whole, quality is mainly holistic, units depend on one another, or a protocol already enforces atomic deterministic fulfillment. For unitized work, the built-in settlement flow captures the exact pro-rata earned amount on a partial result; fail and uncertain results stay unsettled for review. Read when to use Spoolis before choosing the pattern.