Guides

Verify a Receipt

Prove what happened and what it cost — verify a signed Work Receipt offline, against published keys, without calling Ductor.

A Receipt is the artifact you hand an auditor, a customer, or a counterparty when they ask what exactly happened, and what did it cost. This guide is the counterparty's side: given a receipt, prove it is authentic — offline, with nothing but the receipt bytes and Ductor's published public keys.

ductor · work receiptshipping
work
lead_9f42 · converted
route
dec_01j8k… · thompson_sampling@v3 · explanation attached
tools
2 invocations · manifest sha256:ab12…9e
cost
57 250 000 µUSD
consent
trustedform:cert_77a…
settlement
stl_31c… · committed
signatureed25519 · key rcpt-2026-q3 · a41f…9c0d

What you are verifying

The signed unit is a canonical byte string — a versioned, deterministic serialization of the receipt's envelope. Three design choices make offline verification actually mean something:

Everything that matters is inside the signature
The algorithm and signing-key id are bound into the signed bytes under a domain-separation constant, so a signature can never be replayed onto different claims, a different scheme, or a different system's payloads.
Lifecycle state is outside it
Whether a receipt has settled is derived from immutable facts, never written into signed content — so a receipt settling later cannot disturb a signature you already verified.
Use the returned canonical bytes
The API supplies canonical signing bytes and their payload hash. Verify those bytes directly; do not reconstruct them with your client's JSON serializer.

The procedure

The whole path, and the two places a forged or altered receipt is caught:

no yes no yes bytes altered match Receiptenvelope + signed bytes key_id knownin JWKS? FAILunknown key key valid atissued_at? FAILoutside window Recompute payload hashover stored canonical bytes Ed25519 verifydetached signature FAILtampered AUTHENTICclaims are as signed

Every failure edge is terminal: there is no fallback key, no re-serialization attempt, and no partial pass.

Fetch the published keys. Ductor serves its receipt-signing public keys as a standard JWKS document — Ed25519 keys (OKP/EdDSA), each with a key id:

curl -s https://api.ductor.io/.well-known/work-receipts/jwks.json

Cache it. The published JWK fields are kty, crv, x, kid, use, and alg; they do not include validity windows or revocation metadata. To reproduce the hosted verifier's policy checks offline, also obtain the trusted key-validity policy from the deployment operator. A cached key alone proves the signature against that key, not its current revocation status.

Select the key. The receipt's key_id names the key that signed it. An unknown key id fails verification — no fallback, no "try them all".

Check the key policy against issuance. The hosted verifier checks that the key is neither disabled nor revoked and that issued_at is inside its configured [not_before, not_after) window when those bounds are present. For offline verification, apply the operator-provided policy; JWKS alone cannot supply these checks. Disabled and revoked keys are excluded from the published JWKS. Keep retired public keys available when old receipts must remain verifiable, and refresh your trusted policy when revocation matters.

Decode, recompute the hash, then verify the signature. REST returns canonical_payload as Base64 bytes. The governed receipt tool uses canonical_base64. Decode that field, and parse the signature envelope ed25519:<key_id>:<base64url_signature> (unpadded Base64url); its key id must match the receipt's key_id. SHA-256 over the canonical bytes must equal the receipt's payload_hash — the independent tamper check — and the Ed25519 signature must verify over those same bytes with the selected public key.

sum := sha256.Sum256(canonical)
if hex.EncodeToString(sum[:]) != receipt.PayloadHash {
    return Tampered
}
ok := ed25519.Verify(publicKey, canonical, signature)

The verdict is structured, not boolean — each check answers separately:

Prop

Type

A hosted second opinion exists — VerifyWorkReceipt on the API, and the governed work_receipt.verify tool for agents. It applies the deployment's configured key policy as well as the hash and signature checks.

Full receipt retrieval and hosted verification require work_receipt:read in the authenticated tenant. Bespoke MCP work_receipt.get and work_receipt.verify enforce that permission on every call through the canonical explicit-scope, wildcard, or role-bundle rules, even with an older manifest. Missing, anonymous, expired, or unauthorized cross-tenant principals are refused before private lookup. Keep the manifest lease and exact discovered tool version/schema pins; those grants supplement current permission.

REST uses the field names above and adds ok. The bespoke MCP work_receipt.verify tool returns KeyFound, WithinWindow, HashMatch, SignatureValid, and Reason, with no ok field. All four check fields must be true for a complete pass.

New-signature readiness is separate from historical verification. Issuance startup proves the active private/public pairing, and each signing call checks current availability plus the canonical payload's actual issuance timestamp. An expired signing key blocks new signatures, while a receipt issued within its window can still verify against its retained public key and current policy. Issuance can be disabled with public keys only. Changing key policy requires the existing deployment restart; there is no silent fallback or hot reload.

A valid signature authenticates Ductor's recorded claims. For held RFQ work, the signed acceptance records the independently journal-backed output-schema verdict evaluated before settlement. Cryptographic verification authenticates that verdict; it does not establish factual accuracy or quality beyond the published schema. See Clear AI work.

Reading what you verified

For held RFQ work, first find the original signed receipt through GET /api/agent-rfqs/{environment_id}/{session_id}/observation, or generated governed MCP api_agentrfqservice_getagentrfqobservation. The buyer needs routing_market:read and access to the requested environment. This read-only snapshot correlates market, award, admitted execution, journal, and exact original issuance identity; it does not supply signing bytes or perform verification. Retrieve those bytes with the separately authorized WorkReceipt getter, then apply the procedure above.

Issuance can be pending, dead-lettered, missing, or mismatched after money has settled. A signed original may also exist before its id is backfilled on the award, and a later amendment does not replace that original. Launch-void awards currently have no WorkReceipt. Observation makes these states visible without repairing them; its unsigned response is not independent cryptographic proof or a payout-account attestation.

The separate configured recovery worker can enqueue an absent original request from validated terminal financial evidence, or link an already signed exact original without altering its bytes. It never substitutes an amendment or manufactures an absent original after reversal. Pending requests keep their persisted retry schedule; restore the same key while issuance remains pending. A dead request without an original is not automatically revived. See original receipt recovery for bounds and refusal cases. Continue to verify the original independently; automatic linkage is not signature verification.

A verified receipt is a set of claims, each anchored to deeper evidence:

  • worker identifies the selected worker and its source definition. RFQ award receipts snapshot the original awarded pins; re-pinning or deleting a registration before delayed issuance cannot replace them. Failed award receipts retain this worker identity. Ordinary unassigned routing outcomes may have worker: null.
  • acceptance records the accepted flag/reason/version; task, schema, input and actual output hashes; source and governed execution definitions; session, turn, manifest, policy receipt, run, step, cost, and evaluation time. All are signed. A committed full RFQ receipt requires an accepted verdict. A completed rejected task instead records completed, accepted: false, and released.
  • route carries the decision id and a digest of its explanation — the snapshotted why, not a live reconstruction.
  • tools carries the content hash of the exposure manifest the agent acted under, joining the per-invocation receipts from the governed tool surface.
  • cost is exact: int64 micro-units, with total equal to the sum of its breakdown by construction.
  • settlement references the journal transactions that moved the money — arriving at issuance when settlement already committed, or later as a signed amendment that never mutates the original.

Held awards preserve their original payout account through registration rotation, deletion, capture retry, and claw-back. The receipt signs totals and transaction references; it does not sign account details or a digest of journal entries. Proving the actual payout owner requires trusted tenant-scoped journal retrieval and reconciliation. The hosted receipt verifier does not perform that check.

Amendments chain: a claw-back, a late settlement, each is a new signed receipt referencing its predecessor, and the chain reads newest-last with the original intact. Verify each link the same way.

Shared receipts

A receipt shared outside the tenant is a distinct, re-signed redacted object — issued under a revocable, expiring grant, resolved by an opaque high-entropy token. It verifies on its own signature. The full receipt is never public, and a redacted receipt is never a blanked copy of it. RFQ redacted projections omit private acceptance and run/session evidence; obtain the full original to verify selected execution and acceptance.

Where to go deeper