← Blog
October 17, 2026 · 18 min

Two payments, one receipt: auditing x402's receipt layer

We made two separate x402 payments to the same route in the same second. Each payment had its own nonce and its own settlement. The seller signed a receipt for each, and the two receipts were identical, byte for byte, signature included. Five open pull requests in the x402 repository are trying to fix what a receipt proves.

x402 settles money well. The facilitator returns a transaction hash, and anyone can look it up. What happens around that transfer is less clear. Which terms did the seller offer? Which payment does a receipt refer to? Which bytes came back? And can a third party check any of it a month later, without trusting the seller's logs?

This week those questions all came up at once. Between October 12 and 17, five pull requests on receipts saw activity in the x402 repository, along with one SDK bug report. This post reads them against the extension that is already merged, and measures what that extension delivers today.

What we read and ran, all on 2026-10-17. The x402 repository at commit faa1c69: the merged offer-receipt extension (spec v0.6), issue #3824 and its fix #3826, and the open PRs #3827, #3830, #3783, #3604 and #2666. The @x402/* packages at 2.28.0, the current npm release. We ran five probes:

The short version. Besides its version number, the merged receipt signs five fields: the network, the resource URL, the payer, the issue time and an optional transaction hash. It does not sign the amount, the payee, the request or the response. The flag that adds the transaction hash does not work in the current TypeScript SDK. Without the hash, two payments in the same second get the same receipt. On Base, 22% of an hour's EIP-3009 settlements had a twin in the same block with the same payer, payee and amount. Signed offers are rare: 32 of 2,452 Bazaar hosts. And 21 of the 22 EIP-712 signers publish neither of the two records the spec suggests for proving that a key speaks for a domain. The proposals fix this in layers: a salted settlement link, a receipt that hashes the response bytes, a buyer-signed delivery claim, and a join to an external receipt format.

What the merged extension signs

The Offer and Receipt extension defines two artifacts. We first covered it in the audit of the extensions layer. Both artifacts are signed with EIP-712 or JWS. The EIP-712 domain is fixed: name "x402 offer" or "x402 receipt", version "1", chainId 1. The spec uses EIP-712 "purely as an off-chain signing format."

A signed offer commits to one entry of accepts[]. It covers resourceUrl, scheme, network, asset, payTo, amount and an optional validUntil. It travels in the 402, at extensions["offer-receipt"].info.offers[].

The receipt travels in the settlement response. Its payload has six fields: version, network, resourceUrl, payer, issuedAt and an optional transaction. The spec says a receipt confirms "that payment was received and service was delivered." Nothing in the payload names an amount, a payee, the offer it settles, the request, or the response. By default the transaction is left empty. The spec calls this "privacy-minimal" and says it reduces correlation risk.

Only one reference SDK implements the extension. TypeScript has @x402/extensions/offer-receipt. The Python and Go extension directories at faa1c69 have no offer-receipt module.

Two payments, one receipt

We built the setup from the repository's own offer-receipt example. It is an Express seller using paymentMiddleware, with an EIP-712 issuer that signs with a dedicated viem key. The buyer uses x402Client with a fresh EVM account and signs real EIP-3009 authorizations. The facilitator is a local mock. It approves every verify and answers each settle with a new random transaction hash. No chain is involved, so the run shows what the SDK does and nothing else.

The first route declared declareOfferReceiptExtension({ includeTxHash: true }). The buyer paid, the settlement returned 0x2cb2bd82…, and the receipt came back with "transaction": "". The offer and the receipt both verified, and both recovered to the issuer key. The PAYMENT-RESPONSE header was 1,500 characters long.

This is issue #3824, filed on October 15. Its author corrected the diagnosis the same day. The cause is aliasing. In createPaymentRequiredResponse, line 915 of x402ResourceServer.ts sets response.extensions = extensions. That is the same object the HTTP layer later hands to settle. The offer-receipt hook then writes its signed offers into the response. The route's declaration is gone before the receipt hook reads includeTxHash. We hot-patched that one line in the published 2.28.0 build to copy the object instead. On the next run, the receipt carried the settlement hash. The fix, PR #3826, was still open when we checked.

The second route used the default, includeTxHash: false. The buyer paid it twice in a row, and both receipts got the same issuedAt second. The two payments used different EIP-3009 nonces, and the facilitator returned two different hashes. The receipts were identical:

// receipt #1 and receipt #2, as returned (same payer, same route, same second)
{ "format": "eip712",
  "payload": { "version": 1, "network": "eip155:84532",
               "resourceUrl": "http://127.0.0.1:4021/no-tx",
               "payer": "0x031e…Cb8d", "issuedAt": 1792228045, "transaction": "" },
  "signature": "0x05ee97f1…b126f61b" }

payloads identical:   true
signatures identical: true   // deterministic ECDSA over identical bytes

A receipt like this is a statement about a payer, a URL and a second. It is not a statement about one payment. A buyer holding two of them cannot show that it paid twice. Anyone who counts receipts will count one. Reputation systems want this primitive. When we audited ERC-8004, a proof-of-payment anchor was exactly what its feedback layer was missing.

How often do payments look alike?

A receipt does not need to be identical to be ambiguous. Without a transaction hash, a verifier can only pair a receipt with an on-chain payment through the payer. The offer adds the payee and the amount, and issuedAt adds a time. So we measured how often those fields fail to pick out a single payment.

We read one hour of Base mainnet: blocks 52,717,590 to 52,719,389, from 08:08:47 to 09:08:45 UTC on October 17. We collected every AuthorizationUsed log on USDC and joined each one to the Transfer that follows it. That is the rail x402's exact EVM scheme uses for USDC. Not every EIP-3009 transfer is an x402 payment, so read these as rail-level numbers.

The hour held 4,176 settlements, from 1,204 payers to 232 payees. In 2,921 of them (69.9%), the same payer, payee and amount showed up at least once more within the hour. In 935 (22.4%), the twin was in the same two-second block. Those 935 came from 16 payers, in 263 groups of up to 11 identical payments in one block. The busiest payer settled 542 times in the hour. The median payer settled once.

A payer that settles hundreds of times an hour at one price is an automated client, the case x402 was built for. For those payments, the receipt plus the offer cannot single out a settlement. Only the transaction hash can, or the authorization nonce.

Receipt v2: a salted commitment to the nonce

PR #3827, opened October 15 by babyblueviper1 with contributions from holistis, adds receipt payload version 2. It has two new signed fields, authorizationMethod and authorizationCommitment. The commitment covers the EIP-3009 nonce. The seller holds that nonce before settlement runs, so unlike transaction, it does not depend on the settle result reaching the signer:

authorizationCommitment = "0x" || hex( SHA-256(
    "x402-receipt-authorization/0" || 0x00 || method || 0x00
    || asset(20) || authorizer(20) || nonce(32) || salt(32) ) )

We recomputed the PR's worked example from the text alone. The 141-byte preimage hashed to 0x5d905bbf…6af23b, the value the spec prints.

The salt carries all the privacy, and the PR makes its rules normative. It must be 32 random bytes per payment. It must never be derived from the nonce, the payer or any other public field. It never goes inside the receipt. The server returns it next to the receipt, at extensions["offer-receipt"].info.authorizationSalt. Forwarding a receipt therefore does not forward the link.

The reason is the log. AuthorizationUsed indexes the authorizer, and the receipt names the payer. One query lists every nonce that payer ever consumed. An unsalted hash of the nonce would fall to that list. For the busiest payer in our hour, that is 542 hashes.

We re-ran the PR author's live checker against Base Sepolia. On October 15, the test payer had 12 consumed nonces. Today Blockscout returned 15. Exactly one opened the commitment, to settlement 0x1345479a…, and authorizationState returned true.

The PR is careful about what this proves. It claims "no new pinpointing," not unlinkability. Until a commitment is opened, it is only the signer's statement. A seller could sign two receipts with different salts for one settlement. So anyone counting settlements "MUST count opened commitments only." The PR also asks a question it does not settle: should the version be selected by the payload version, as it does now, or by bumping the EIP-712 domain version?

PR #3830 carries the same idea to Solana as svm-memo. It commits to the mint (32 bytes), the payer (32 bytes) and the SHA-256 of the memo. Uniqueness comes from the exact SVM scheme's random memo nonce. A seller that pins extra.memo to one fixed value breaks it.

What the signatures are worth: 32 hosts in the Bazaar

We went back to the CDP Bazaar snapshot from our October 16 census. It is a full read of the catalog: 43,338 resources on 2,452 hosts. Of those, 668 resources (1.5%) on 32 hosts carry an offer-receipt entry. Those resources took 4,773 of the 906,040 calls in the trailing 30 days, or 0.53%.

The catalog stores the signed offers it saw: 1,199 of them, 796 in EIP-712 and 403 in JWS. All 796 EIP-712 signatures verify. None was signed with the payTo key. The spec allows payTo signing as the simplest approach, but says production "SHOULD use a dedicated signing key." The 22 EIP-712 hosts use 18 distinct keys. Nine of those hosts are subdomains of one domain, forgemesh.io.

A valid signature is half of the check. Section 4.5.1 says verifiers "MUST confirm that the signing key is authorized." Otherwise "an attacker can generate a valid key pair, sign an offer or receipt for any resourceUrl," and it will verify. The spec lists the options: sign with the payTo key, list the key in a did:web document, publish a _controllers DNS TXT record, or use an external registry.

EIP-712 artifacts carry no kid, so a verifier has to guess where to look. We looked where the spec points. For each host and its parent domain, we queried _controllers TXT records and fetched /.well-known/did.json.

Signer authorization, 32 offer-signing hosts: zero publish a _controllers TXT record. Of the 22 EIP-712 hosts, one lists its signing address in a did.json. For the other 21, a third party can check the signature but not the authority behind it. All 10 JWS hosts publish a did:web document that resolves their kid.

We found two smaller problems. First, every one of the 1,199 stored offers had passed its validUntil before the snapshot was taken. A catalog copy of an offer is evidence of past terms, not a live quote. Second, 181 EIP-712 offers sign accepts on Solana (115), Algorand (33) and Stellar (33). Section 3.2 fixes chainId at 1 so that EIP-712 works "including non-EVM networks like Solana." Section 3.1.1 still says an EIP-712 artifact's network "MUST be eip155:<chainId>." The SDK follows 3.2.

The catalog also shows sellers inventing their own receipt keys: otto-content-receipt on 63 resources, tracefast-receipt on 20 and delivery on 18. One resource declares delivery-receipt.

delivery-receipt: binding the bytes

PR #3783, opened October 12 by johnInarti after the discussion in issue #3758, is the most complete of the five. It adds 2,278 lines of spec and test vectors. Its receipt binds what offer-receipt leaves out. That covers the settlement locator (network, transaction and the transfer's logIndex), the paid terms (scheme, asset, payTo, amount, payer), the request (resourceUrl, method), and the response status and content type. It also binds the content itself, the key-directory epoch and digest, and the issue time.

Content is committed two ways. In body mode, the receipt carries the SHA-256 of the exact response body. In stream mode, it chains a digest over Server-Sent Events frames. The chain starts at d_0 = SHA-256("x402-delivery-stream/1"), and each frame adds its length and its data. The receipt arrives in a terminal event named x402-delivery-receipt. That framing fits a streamed LLM completion.

The signed message is always 88 octets: "x402-delivery-receipt/1", a line feed, and the hex SHA-256 of the RFC 8785 canonical payload. Every receipt carries two signatures. Ed25519 serves the in-session path. ML-DSA-65 (FIPS 204) serves the archival path, so the receipt stays usable as evidence "without depending on a signature scheme that a cryptographically relevant quantum computer would break."

Size drives the transport. An ML-DSA-65 signature is 3,309 bytes. An inline receipt makes the header about 8 KB, so by default the header carries only a reference of at most 1,024 octets. The receipt itself is served at an immutable URL addressed by its id. Keys live in a signed, epoch-chained directory at /.well-known/x402-receipt-keys.

Verification has five levels: integrity, authentic, trusted, settlement and delivery. A receipt is valid for the client only if all five pass. A missing or failed receipt "cannot undo a payment," and the client "MUST NOT retry the payment automatically." That last rule matters for agents. A buyer that treats a missing receipt as a failed call will pay twice.

We checked the vectors with our own code. We canonicalized each payload with an RFC 8785 library, recomputed the receipt id and rebuilt the 88-octet message. Ed25519 verified through Node's OpenSSL. ML-DSA-65 and ML-DSA-87 verified through @noble/post-quantum, the same library family the reference uses, so that check is not fully independent. Flipping one bit of the message failed all three. The stream-digest vector matched step by step.

The one Bazaar resource that declares the extension belongs to FractalAI, at fractalai.net.co. Its live directory is at epoch 1, published October 10. We recomputed the root, and both governance signatures verified. But the stable URL also serves three unsigned members, chain, supersedes and annotation. The last one states that none of the three is covered by the signature. Section 7.3 says a verifier must check that the document has "exactly the members of §7.2." A strict client that fetches the stable URL would reject it. The immutable copy at /epoch/1 has exactly the specified members and passes.

This receipt proves which bytes the issuer committed to. It does not prove which model produced them. That question needs the attestation layer from our post You paid for Opus. What proves Opus answered?

The buyer's side, and the join

PR #3604, the Buyer Delivery Claim, flips the signer. The payer signs delivered: yes | no | partial, an evidence hash and a settlementRef, with EIP-191 personal_sign over a content-hash claim id. The reference implementation publishes claims as EAS attestations on Base, Optimism and Ethereum mainnet, and can mirror them into ERC-8004's giveFeedback().

The PR states its weak spots plainly. False claims cost nothing, and the author reports testing and rejecting nine mitigations. settlementRef is not bound to the rest of the claim. And assetType takes one of four values: gpu-hours, storage, api-credits or bandwidth. None of them is inference.

PR #2666, Settlement-Receipt Binding, has been open since June 19. It joins a settlement record to an external receipt format, vaara.receipt/v1. That format is now an individual IETF draft, draft-sirkkavaara-vaara-receipt, at revision 14 as of October 16. The join key is a SHA-256 over six action fields, with amount and chain left out on purpose. The settlement side binds the net balance change to payTo, not a parse of the transaction. A fee split gets one record per recipient. Its worked example, which the PR describes as a real Polygon settlement, splits 2 JPYC into 1 for a merchant and 1 for a fee recipient.

Around these sit three more threads. Issue #3379 on "proof-of-done" has 57 comments. PR #3377 proposes encrypted "durable evidence" of delivered content, and PR #3291 proposes a "resolution lineage" for disputes. Our ACK receipt audit asked the same question of another stack: a receipt proves what its signer chose to bind, and nothing more.

// Five artifacts, one payment

Who signs what

offer-receipt v1 (merged): the seller signs payer, URL and time. The amount appears only in the offer. Settlement is bound only if transaction is set, which the SDK currently drops. No content.

offer-receipt v2 (#3827, #3830): adds a salted commitment to the EIP-3009 nonce or the SVM memo, opened only by the salt holder. No content.

delivery-receipt (#3783): the seller signs terms, settlement locator, request and response digest, with dual Ed25519 and ML-DSA-65 signatures and an epoch-chained key directory.

Buyer Delivery Claim (#3604): the payer signs the outcome plus an evidence hash. The settlement reference is unbound and lying is free.

Settlement-Receipt Binding (#2666): a content-addressed join from a facilitator-verified settlement record to an external receipt format.

What it means for LLM4Agents

Today our gateway sits at the bottom of this ladder. An unpaid request to /v1/chat/completions returns a 402 with no extensions block, and so no signed offer. A paid response carries X-Model-Used, X-Tokens-Input, X-Tokens-Output and X-Cost-Usd-Cents. Those headers are accurate. None of them is signed. A buyer agent can read them but cannot take them to a third party.

For an inference gateway, the useful receipt is not the one that is merged. Offer-receipt v1 binds none of the fields an LLM call is priced on. A useful receipt would bind the model requested and the model that served, the token counts, the price basis, the settlement, and a digest of the completion. delivery-receipt covers the settlement and the bytes, and its stream mode was written for SSE. It has no fields for model or usage. The Buyer Delivery Claim has no asset type for inference. The gap that is specific to LLM calls is open.

There is a threat too. Reputation layers like ERC-8004 feedback, EAS claims and Bazaar quality scores reward sellers whose evidence can be checked. Unsigned billing headers will count for less over time. And adopting offer-receipt v1 as it ships today would hand buyers receipts that cannot be counted.

Staying on the frontier

1. Sign offers now, and publish who signs them. Add offer-receipt offers to our 402 using a dedicated EIP-712 key, never the payTo key. List its address in /.well-known/did.json on api.llm4agents.com and in a _controllers TXT record with DNSSEC. Signed quotes are cheap, and they make our price evidence. Publishing the key is what 21 of 22 EIP-712 signers skipped.

2. Do not ship receipts with an empty transaction. If we issue offer-receipt receipts, set includeTxHash and pin an SDK release that includes the #3826 fix. Before trusting that release, run a regression test like our harness. When v2 merges, generate a fresh 32-byte salt per payment and return it outside the receipt.

3. Prototype delivery-receipt for completions. Start with body mode on non-streaming calls, with reference transport and a key directory under /.well-known/x402-receipt-keys. Then add stream mode over our SSE frames. Keep the directory's stable URL to exactly the specified members.

4. Propose an inference profile upstream. Bring the LLM fields to #3758 and #3783: requested model, served model, input, output and reasoning tokens, and the price basis. Ask #3604 for an inference asset type. A gateway that ships these fields first gets to shape them.

5. Give our buyers a verifier. Our MCP tools and client snippets should check other sellers' offers and receipts, including the signer-authorization step, and store receipts together with salts. They should treat a missing receipt as evidence, never as a reason to pay again.

6. Re-run the census monthly. Track how many Bazaar hosts sign offers, how many publish their key, and how many issue delivery-receipt. Also track the same-block twin rate on Base. When these numbers move, buyers will start to expect receipts.

The receipt layer will settle on something. Today it is five proposals, one merged spec that signs a payer and a timestamp, and a bug that hides the one field that tells payments apart. As in our finality audit, the word in the response promises more than the bytes behind it.

Pay per call, in stablecoins, over an OpenAI-compatible API

One 402, one signature, 345+ models. No subscription, no minimum.

Register your agent