Flagship concept

Pay per verified unit

Define the unit and its acceptance checks before delivery, then pay for the units that pass. In the Spoolis flagship example, 98 accepted units at $1 each produce $98 earned.

Updated August 13, 2026

Unit-level acceptance avoids whole-job disputes

The usual flow is simple but coarse: you 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. You define the unit, agree on its checks, verify each delivered unit, and count value only for the units that pass.

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 run reaches a definite result, it calculates earned cents as accepted units multiplied by the unit amount. The signed Outcome Receipt carries the combined result, the earning rule, a rejection summary, and a digest of the per-unit results.

outcome-receipt.json
{
  "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.

prompt.txt
Run the Spoolis one-call unitized verification example end to end in the zero-login sandbox. Start with POST /api/sandbox/verify, not the bilateral lifecycle. 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)')

VERIFY_JSON=$(curl -sS -X POST "$BASE_URL/api/sandbox/verify" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"conditions":[{"description":"Every company record includes company_name and website_url.","deterministic_check":{"checker":"completeness","required_fields":["company_name","website_url"]}},{"description":"Every website_url is a valid URL.","deterministic_check":{"checker":"url_format","field":"website_url"}}],"max_amount_cents":3,"unit":{"total_units":3,"unit_amount_cents":1},"evidence":{"type":"dataset","rows":[{"company_name":"Acme Robotics","website_url":"https://acmerobotics.com"},{"company_name":"Blue Harbor Labs","website_url":"https://blueharbor.example"},{"company_name":"Cardinal Systems"}],"provenance":"api_response"}}')
VERIFY_JSON="$VERIFY_JSON" node -e 'const x=JSON.parse(process.env.VERIFY_JSON); const expected=["accepted","can_rerun","can_resubmit_evidence","decision_authority","earned_cents","next_step","receipt","receipt_url","rejected","rejections","settlement_eligible","spool_id","terminal","uncertain","unit_results","verification_run_id"]; if (JSON.stringify(Object.keys(x).sort()) !== JSON.stringify(expected)) throw new Error("Unexpected response fields: "+JSON.stringify(x)); if (typeof x.spool_id !== "string" || !x.spool_id.startsWith("spl_")) throw new Error("Missing spool_id"); if (x.earned_cents !== 2 || x.accepted !== 2 || x.rejected !== 1) throw new Error("Unexpected earned arithmetic: "+JSON.stringify(x)); if (!Array.isArray(x.rejections) || x.rejections.length !== 1 || x.rejections[0].unit !== 3 || typeof x.rejections[0].reason !== "string") throw new Error("Unexpected rejections: "+JSON.stringify(x.rejections)); if (!x.receipt || typeof x.receipt !== "object" || typeof x.receipt.signature !== "string") throw new Error("Expected a signed receipt"); if (x.receipt.environment !== "demo" || !x.receipt.units || x.receipt.units.total !== 3 || x.receipt.units.accepted !== 2 || x.receipt.units.rejected !== 1 || x.receipt.amounts.earned !== "0.02") throw new Error("Unexpected receipt: "+JSON.stringify(x.receipt)); const acceptance=x.receipt.agreement && x.receipt.agreement.acceptance; if (!acceptance || acceptance.mode !== "unilateral" || acceptance.provider_accepted_in_spoolis !== false || acceptance.accepted_by !== "initiator") throw new Error("Unexpected acceptance boundary: "+JSON.stringify(acceptance)); if (typeof x.receipt_url !== "string" || typeof x.verification_run_id !== "string") throw new Error("Missing receipt_url or verification_run_id"); if (typeof x.terminal!=="boolean"||typeof x.settlement_eligible!=="boolean"||typeof x.can_resubmit_evidence!=="boolean"||typeof x.can_rerun!=="boolean"||!x.next_step||x.next_step.type!==x.next_step.action||typeof x.decision_authority!=="string") throw new Error("Missing verification state contract: "+JSON.stringify(x)); require("node:fs").writeFileSync("outcome-receipt.json",JSON.stringify(x.receipt,null,2)+"\n"); console.log(JSON.stringify(x,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/v1/receipts/$RECEIPT_ID/status"

Report the printed spool ID, 2 accepted records, the rejected record and its reason, 2 cents earned, verification run ID, demo receipt ID, receipt URL, offline receipt-verification result, and optional status response. State that the provider was identified but did not accept the criteria in Spoolis. Then explain that a payment stack may act on the verified earned amount under its own authorization policy. State clearly that this demo artifact does 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.

Pay per verified unit · Spoolis