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.
Stored bytes are the truth
Verification runs against the exact canonical bytes that were signed, persisted alongside the receipt. You never re-serialize and hope your JSON matches.

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 and a validity window:

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

Cache it. Everything after this step is offline.

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 window against issuance. The key must have been valid when the receipt was issuedissued_at inside [not_before, expires_at). Retiring a key never invalidates the receipts it legitimately signed; a revoked key disappears from the JWKS entirely.

Recompute the hash, then verify the signature. 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 — running exactly this procedure. It is a convenience, not a dependency.

Reading what you verified

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

  • worker identifies who did the work — including, for an agent, the pinned definition (id, version, hash) it executed under. For non-assignment outcomes — a rejection, a drop — worker is explicitly null, and that is a valid receipt: proof of what didn't happen is still proof.
  • 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.

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.

Where to go deeper