Docs / Verify / Receipt schema

// FROZEN · PUBLISHED · VERSION 2

The receipt schema. Verify it yourself.

Every gateway dispatch emits a Receipt v2: a sorted-key JSON object, SHA-256 hashed into a leaf, folded into a Merkle tree, and anchored on a public ledger. You can reconstruct and verify any receipt with this page and a SHA-256 implementation. No Agentics service required.

This page renders the schema from the public endpoint /api/proof/schema when it is reachable, so the documentation can never drift from the frozen definition. Checking the live endpoint…

The canonical field list

All 21 fields, in alphabetical order — which is the canonical serialization order, because canonicalization sorts keys ascending. receipt_id and batch_id are present on the wire but excluded from the leaf hash (they are server-assigned and not known when the leaf is computed).

FieldTypeIncluded whenMeaning
agent_version_idhexalwaysIdentity + version fingerprint of the calling agent.
batch_iduuid | nullalways · not hashedMerkle batch this receipt was folded into; null until anchored.
billableboolalwaysTrue on exactly one parent receipt per request.
child_ofuuid | nullalwaysParent receipt id; null on the parent itself.
cost_usdnumberalways (metering)Computed cost of the dispatch.
finished_atISO-8601alwaysDispatch completion timestamp.
inputs_hashhex64alwayssha256Hex of the redacted request body — never the body itself.
latency_msnumberalways (metering)End-to-end latency.
modelstringalwaysResolved model id.
outcomeenumalwayssuccess | failed | partial | blocked | escalated | timeout.
outputs_hashhex64alwayssha256Hex of the response — never the response body.
prev_hashhex64alwaysThe previous receipt's receipt_hash; genesis = "0".repeat(64).
providerstringalwaysUpstream provider slug.
receipt_iduuidalways · not hashedServer-assigned row id; excluded from the leaf.
request_kindenumalwaysparent | routing | cache | guardrail | budget | tool.
scope_usedstring[]always (sorted)Scopes exercised by this dispatch.
sourcestringalwaysAlways 'gateway' for gateway dispatches.
started_atISO-8601alwaysDispatch start timestamp.
tenant_iduuidalwaysOwning tenant.
tokens_innumberalways (metering)Prompt tokens.
tokens_outnumberalways (metering)Completion tokens.

Canonicalization order

The leaf is the SHA-256 of the canonical JSON string. Canonicalization is exactly three rules:

canonicalize(v) = recursively sort keys ascending, drop null / undefined, and emit no whitespace. The leaf is sha256Hex(JSON.stringify(canonicalize(receipt))). Because keys are sorted, the field table above (alphabetical) is the serialization order.

Hash membership & prev_hash linkage

DO-NOT-TRACK omissions

Under debug:false or posture hashed_only, inputs_hash and outputs_hash are set to the sentinel "0".repeat(64) — not dropped. Dropping them would change the canonical shape and break cross-receipt diffing, so the fields stay and are zeroed instead.

Every metering and integrity field — tokens_in, tokens_out, cost_usd, latency_ms, prev_hash, billable — is still present. The receipt still chains and still anchors. The rationale is plain: tokens, cost, and latency are metering, never content; only content-derived fields are zeroed. You get a fully verifiable receipt that proves a call happened, what it cost, and that it was governed — without recording a single byte of prompt or response.

Verify it yourself

Copy-paste, dependency-free. This is the same sha256Hex + sorted-pair fold the server uses — byte-for-byte reproducible.

JavaScript
// 1. Take the receipt object from /api/proof/bundle (the `receipt` field). // 2. Remove the two server-assigned fields: delete receipt.receipt_id; delete receipt.batch_id; // 3. Canonicalize: sort keys recursively, drop null/undefined, no whitespace. const canon = (v) => Array.isArray(v) ? v.map(canon) : v && typeof v === "object" ? Object.keys(v).sort().reduce((o, k) => (v[k] != null && (o[k] = canon(v[k])), o), {}) : v; const leaf = await sha256Hex(JSON.stringify(canon(receipt))); // 4. Compare to the published receipt_hash: leaf === bundle.receipt_hash; // → leaf integrity // 5. Fold the Merkle proof (sorted-pair, NO L/R flags): const hashPair = (a, b) => sha256Hex(a <= b ? a + b : b + a); let acc = leaf; for (const sib of bundle.merkle.siblings) acc = await hashPair(acc, sib); acc === bundle.merkle.root; // → membership in the anchored batch // 6. Confirm the on-chain anchor: the tx memo bytes === merkle.root. // Open bundle.anchor.explorer_url and read the Memo instruction.

spec_ref: gateway-build/00-foundation-contracts.md §1, §9

Try it on the live sample

The hash below is the committed public sample receipt. Click verify — the leaf is recomputed and the Merkle proof folded entirely in your browser, with no call back to Agentics.

In-browser verification Anchored

The evidence-bundle shape

The offline, self-contained evidence bundle returned by /api/proof/bundle — everything a third party needs, with no further calls:

JSON
{ "bundle_version": "1.0", "receipt": { /* the 21-field Receipt v2 object */ }, "canonical_string": "{…sorted-key JSON, the exact bytes hashed…}", "receipt_hash": "<sha256 of canonical_string>", "chain": [ { "receipt_seq":0, "receipt_hash":"…", "prev_hash":"…" } ], "merkle": { "index":2, "siblings":["…"], "root":"…" }, "anchor": { "chain":"<public-ledger>", "txid":"…", "block":0, "memo":"<== merkle.root>", "explorer_url":"…" }, "algorithm": { "leaf":"…", "merkle":"…", "spec_ref":"…" } }