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.
/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).
| Field | Type | Included when | Meaning |
|---|---|---|---|
| agent_version_id | hex | always | Identity + version fingerprint of the calling agent. |
| batch_id | uuid | null | always · not hashed | Merkle batch this receipt was folded into; null until anchored. |
| billable | bool | always | True on exactly one parent receipt per request. |
| child_of | uuid | null | always | Parent receipt id; null on the parent itself. |
| cost_usd | number | always (metering) | Computed cost of the dispatch. |
| finished_at | ISO-8601 | always | Dispatch completion timestamp. |
| inputs_hash | hex64 | always | sha256Hex of the redacted request body — never the body itself. |
| latency_ms | number | always (metering) | End-to-end latency. |
| model | string | always | Resolved model id. |
| outcome | enum | always | success | failed | partial | blocked | escalated | timeout. |
| outputs_hash | hex64 | always | sha256Hex of the response — never the response body. |
| prev_hash | hex64 | always | The previous receipt's receipt_hash; genesis = "0".repeat(64). |
| provider | string | always | Upstream provider slug. |
| receipt_id | uuid | always · not hashed | Server-assigned row id; excluded from the leaf. |
| request_kind | enum | always | parent | routing | cache | guardrail | budget | tool. |
| scope_used | string[] | always (sorted) | Scopes exercised by this dispatch. |
| source | string | always | Always 'gateway' for gateway dispatches. |
| started_at | ISO-8601 | always | Dispatch start timestamp. |
| tenant_id | uuid | always | Owning tenant. |
| tokens_in | number | always (metering) | Prompt tokens. |
| tokens_out | number | always (metering) | Completion tokens. |
Canonicalization order
The leaf is the SHA-256 of the canonical JSON string. Canonicalization is exactly three rules:
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
- Excluded from the leaf hash:
receipt_idandbatch_id. A verifier reconstructs the object, deletesreceipt_idandbatch_id, canonicalizes, hashes, and compares to the storedreceipt_hash. prev_hashIS included — this is the chain. Each receipt'sprev_hashequals the previous receipt'sreceipt_hashin the tenant chain (byreceipt_seq). The first receipt is genesis:"0".repeat(64). Walkingprev_hashback to genesis with no break is the chain proof — altering any intervening receipt breaks it.
DO-NOT-TRACK omissions
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.
// 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.
The evidence-bundle shape
The offline, self-contained evidence bundle returned by /api/proof/bundle — everything a third party needs, with no further calls:
{
"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":"…" }
}