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.
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.
| Decision | What we chose, and why |
|---|---|
| Bind the artefact | Put sha256 of the delivered payload in the
receipt. Without it you have proved a payment happened, not that anything arrived. |
| Identify the counterparty | Hash 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 count | Count. 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 writes | Deliver, 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. |
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.
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.
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.