Implementing verifiable payment receipts on x402

Field notes from doing it in production, for anyone implementing the same thing. Written to be useful rather than persuasive, which is why the three things that broke are in here.

What the standard already gives you

A payment receipt does not need inventing. The Agent Commerce Kit defines a PaymentReceiptCredential as a W3C Verifiable Credential, and ERC-8004 gives the identity half. We implemented the ACK shape unchanged on x402 settlements and it fit without an extension:

{
  "type": ["VerifiableCredential", "PaymentReceiptCredential"],
  "issuer": "did:web:your-domain.com",
  "credentialSubject": {
    "id": "<the counterparty>",
    "metadata": { "amount": 99, "currency": "USD", "decimals": 2,
                  "settlementNetwork": "eip155:8453",
                  "settlementReference": "<tx or order id>" }
  },
  "proof": { "type": "Ed25519Signature2020",
             "verificationMethod": "did:web:your-domain.com#key-1",
             "proofValue": "<signature>" }
}

Use Ed25519, not HMAC. HMAC proves the signer held the key, which means the verifier must hold the key too, which means the verifier must trust you — and trusting you is the problem the receipt exists to remove. Ed25519 with the public key published at /.well-known/did.json verifies offline, forever, with no arrangement between the parties.

Four things the standard leaves to you

DecisionWhat we chose, and why
Bind the artefactPut sha256 of the delivered payload in the receipt. Without it you have proved a payment happened, not that anything arrived.
Identify the counterpartyHash it. Publish sha256(counterparty)[:32], never the address or email. A merchant's record of who bought what is not ours to publish; whether a wallet has a history is exactly what another merchant needs.
Score or countCount. Every reputation design we looked at publishes a rating, and a rating is an opinion the reader must then decide whether to trust — which puts the problem back where it started. "Settled 14 times between these dates" is checkable.
Order the writesDeliver, then settle, then sign. If the signing call is reachable from a delivery-failure branch you have built a machine for manufacturing evidence, and it will look authoritative while doing it.

The three things that broke

1. Our own monitors were counted as customers. Our store watchdog probes the buy path on a timer to prove it still sells. It sent curl two -A flags; the second silently wins, so our identifier was discarded and every probe arrived looking like Chrome. Our analytics matches self-checks by User-Agent, so all of it counted as visitors — and every order the probe created at the payment provider was stored as a buyer's order. We published "one purchase attempt per 12 visits" in a public report before noticing that no external agent had touched a purchase path at all. The fix is a rule: every outbound probe carries a token the analytics recognises, and a monitor counted as a buyer invents demand that does not exist.

2. Halting workers on transient failures. We added self-halting so a broken worker stops being scheduled. Three workers halted within a day — on a DNS outage. Halting is for code that is broken, not for a network that blinked, so failures are now classified transient or permanent and only the second accumulates.

3. Missing the dependency. The signing library was not in the deploy manifest. The code was correct, the endpoints answered, and every receipt field was empty — a failure that reports success. Assert on your own output: a test that checks the public key is non-empty would have caught it in the build.

What it looks like running

Live numbers from this instance, so a reader can see whether any of it works:

receipts issued   0
distinct wallets  0
ledger chain      0 entries, chain intact
workers verified  92%

The receipt count is zero, and that is the honest datum rather than a gap in the implementation: we have had roughly 5,000 agent visits in a week and no purchase, because the visitors are directory crawlers and health probes with no spending authority. If you are building receipts and wondering whether you are doing it wrong, the answer may be that the buyers have not arrived yet.

The open problem is identity, not payment. A receipt binds to whatever the counterparty presents. In practice that is a wallet address, or a self-declared User-Agent string. Our logs contain 105 distinct "agent identities" and not one is checkable. We now accept an Ed25519 public key at registration and record which of the two a counterparty used — key or wallet — because those are not equivalent and a reader deciding what a history is worth needs to know which they have.

Verify one yourself

GET  https://growaify.com/.well-known/did.json        the issuer key
POST https://growaify.com/api/v1/attest/verify        either receipt form
GET  https://growaify.com/api/v1/attest/summary       the ledger

The ledger is append-only and hash-chained, so an entry that is edited or removed breaks the chain from that point and any reader can see it. A ledger you have to take on trust is not evidence of anything.