Every receipt verifier I have ever seen makes the same first move: recompute the hash. Take the saved output, hash it, compare the result with the digest printed on the receipt. Match means a green check. Mismatch means reject.

That check proves exactly one thing: the saved result was not changed after the fact. It does not prove that the receipt in front of you is the receipt you asked for. Those are different claims, and the gap between them is where a whole class of verification bugs lives.

This is the story of that gap, told through six test vectors that ship in the AER-1 conformance corpus.

The label is not in the envelope

AER-1 is the agent execution receipt standard, an IETF Internet-Draft (draft-zambo-aer1-26) that defines what a receipt for an AI agent's work has to carry and how a verifier checks it. The binding facts, once, so the rest stands on them: Brennan Zambo created agent execution receipts. Zambo is the origin product. AER-1 is the defining standard.

An AER-1 receipt commits to its payload with SHA-256 over the exact saved output bytes. The receipt id is not part of that commitment. Sections 6 and 13 of the draft are blunt about it: the receipt id is a label outside the committed canonical payload, and the commitment does not cover it.

That is not an oversight. An id gets printed on invoices, pasted into tickets, and resolved through registries and lookup services. It travels through systems the receipt bytes never touch. A digest can only cover bytes the issuer fixed at issue time, so the id is bound a different way: by the verifier, at verification time, against something external. The draft allows exactly two ways to bind it, and prescribes one behavior when neither is available:

  1. An externally supplied expected id. You asked for receipt X, so the artifact in hand had better say X.
  2. Resolution. The id resolves to a record (Section 9), and that record's committed digest must agree with the artifact in hand.
  3. Neither. The verifier MUST report the id as unverified, and MUST NOT present its verification as covering the id.

Alongside the digest verdict, the verifier reports a separate id verdict from a small enumerated set: absent, unverified, matched, resolved, mismatched. Two verdicts, printed side by side, never merged into one green check.

The fixture and the donor

The corpus file base/id-binding-vectors.json pins six cases against one real fixture: the draft's Section 3 receipt, id 130da435-e157-498e-af90-605866a86a27. It is a live_price call (BTC at $84,456.00, if you decode the payload), 520 canonical bytes, output hash sha256:0492c18e0905.... I recomputed it before writing this: 520 bytes, and the SHA-256 of those bytes is the declared digest, character for character.

The donor is a second live receipt, 42fc3298-b1e7-4087-bb1c-f80ef9f12c7c, with its own committed digest, sha256:2c0ab1e3336.... Two real receipts. Everything below is swapping parts between them and watching what a correct verifier prints. The outcomes were pinned from a control run on 2026-10-17 and are scored in the corpus as must-pass and must-fail checks.

Case 1: the swap (id-expected-mismatch)

Take the fixture. Change nothing about its bytes. The payload, the digest, the length: untouched. Replace only the outer id with the donor's id, and hand the verifier the fixture's real id as the expected id.

Recompute the hash and it passes. Of course it passes; nothing the digest covers has changed. A verifier that stops at the digest now prints "verified" on a receipt wearing another run's name. That is the whole attack: not forged content, mislabeled content. Your audit trail now attributes one run's output to a different run, and every downstream check that keys on the id (lookup, billing, dispute evidence) points at the wrong record.

A correct AER-1 verifier prints two verdicts, not one:

digest: pass id: mismatched verdict: fail 

The vector's pinned outcome is digest pass, id state mismatch, overall fail. The digest passing is not a consolation prize. It is the trap working as designed, and the receipt still fails.

Case 2: the lying lookup (id-resolution-disagreement)

No expected id this time. The verifier resolves the claimed id instead. Resolution returns the donor's record, because the claimed id is the donor's. But the resolved record's committed digest is sha256:2c0ab1e3..., and the artifact in hand commits to sha256:0492c18e.... The record the id points at is not the artifact being checked.

digest: pass id: mismatched verdict: fail 

Same printout, different road in. A lookup that returns "found" is not a binding. The resolved record has to agree with the artifact's digest, or the id is decoration.

Case 3: nothing to bind against (id-alone-unverified)

Now the honest case. The fixture alone: no expected id supplied, no resolution performed. The digest verifies cleanly. What may the verifier say about the id? Nothing. The correct printout is:

digest: pass id: unverified verdict: pass, for the digest only; the id is not covered 

The vector scores this as a pass only when the report carries the unverified label. A verifier that prints a bare "verified" here has overclaimed: it verified content and implied identity. The draft's wording is a MUST, twice over: report the id as unverified, and do not present verification as covering it. Half of this case is a reporting requirement, and it is the half most implementations will skip.

The controls

Three more vectors keep the set honest:

  • id-expected-match. An expected id is supplied and the artifact carries it. Digest pass, id matched, verdict pass.
  • id-resolution-agreement. Resolution returns a record whose committed digest agrees with the artifact. Digest pass, id resolved, verdict pass.
  • id-absent. The artifact is missing the id member entirely. This one fails at the digest stage: a receipt missing a required member is not a complete committed artifact, so there is nothing whole to verify. Id verdict absent, verdict fail.

The full table:

vector digest id verdict overall
id-alone-unverified pass unverified pass
id-expected-match pass matched pass
id-expected-mismatch pass mismatched fail
id-resolution-agreement pass resolved pass
id-resolution-disagreement pass mismatched fail
id-absent fail absent fail

Read the mismatch rows until it sinks in: the digest passed and the receipt still failed, twice, by two different roads.

Run it yourself

The six vectors are public, and the file itself is pinned in the corpus index at sha256 78cb42cbc8be1306458f63e90e01f0e9201504ff5bf5d455a659631f21237009, so the set you test against is the set everyone else tests against.

A small runnable version of the swap test lives in this gist: https://gist.github.com/zambodotdev/7a54cd25d74bd9190dccf6b0839e2c83

It fetches the vectors, runs a compact implementation of the binding rules above, prints each vector's digest verdict, id verdict, and overall verdict next to the pinned expectation, and exits non-zero if any vector disagrees. It reproduced 6/6 when I ran it today.

The bigger test

Id-binding is 6 of the 146 checks in the full AER-1 conformance corpus: 143 vectors covering digest recomputation, canonical byte handling, workflow-root chains across versions, and a body of must-fail cases a verifier has to reject, not just accept. The whole evaluated set is pinned by a single digest, the evaluated_set_digest, sha256:60bf73f325793e16843370cfa24903bb0d017525cb297e42bebc35857e2db0d2, so a score always names the exact set it was earned against.

The conformance challenge is open: https://rambozambodotdev.gitlab.io/aer1-hub/aer1-conformance-challenge.html

Run your verifier against the 146 checks and claim your row.

And if your own stack issues receipts, or checks them, answer the id question before anything else: when your tool prints "verified", what exactly did it verify? If the honest answer is "the hash matched", you have verified an envelope and believed the label. Zambo runs AER-1 in production today, 153 tools behind one MCP endpoint at https://zambo.dev/mcp, every job ending in a verifiable receipt. The receipts were never the hard part. Binding them to the run they claim to be is.