← Blog
August 11, 2026 · 12 min

x402 extensions: auditing the protocol's plugin layer

The x402 core spec everyone reads is 4,252 words. The extension specs almost nobody reads are 14,820 — three and a half times as much text. That is where identity, signed receipts, idempotency, attribution and gas UX actually live. We audited all nine.

This is another primary-source audit in the series. We cloned x402-foundation/x402 on 2026-08-11 at HEAD 1d150626 (committed 2026-08-10) and read every file under specs/extensions/, the extension framework section of the v2 spec, the git history of each document, and the three SDK trees that implement them. Everything below is verified against that clone. Previous entries covered the facilitator API, the fifteen network bindings and the Bazaar discovery extension. This one covers the layer that ties them together.

The framework: one envelope, two echo rules

The v2 spec defines extensions as a key-value map on three objects: the PaymentRequired challenge, the client's PaymentPayload, and the SettlementResponse. Each value has a mandated envelope: an info object with the extension's data and a schema object — a JSON Schema Draft 2020-12 document describing what info must look like. Both fields are marked Required in the framework table.

Two behavioral rules do the real work. Servers advertise extensions in the 402 challenge; clients echo them back in the payment payload. And the echo is constrained: the client must include at least the info it received, may append additional fields, but cannot delete or overwrite existing ones. That single sentence is the whole trust model of the layer — the server's declared terms survive the round trip intact, and anything the client adds is visibly additive. Facilitators declare which extensions they understand in the extensions array of GET /supported, the same discovery surface we mapped in the facilitator deep dive.

The census: nine specs, eighteen months of accretion

The specs/extensions/ directory holds nine documents. Git history gives each one a birthday:

Word counts range from 454 (payment-identifier) to 4,515 (offer-receipt). They cluster into three functional groups: identity, proof, and operations.

Cluster one: who is paying

Three extensions handle identity, each at a different layer of the stack.

sign-in-with-x brings CAIP-122 wallet authentication into the 402 flow. The server embeds a challenge in the extension — domain, URI, a 32-hex-character nonce, issuedAt, an optional five-minute expiry — plus a supportedChains array pairing CAIP-2 chain IDs with signature types: eip191 for EVM (message format EIP-4361, with eip1271 and eip6492 hints for smart and counterfactual wallets) and ed25519 for Solana (Sign-In With Solana format). The client signs and sends the proof in a SIGN-IN-WITH-X header as base64 JSON. The point is economic, not cosmetic: a server that recognizes a returning wallet can skip the payment entirely for an address that already paid. Pay once, get recognized after. The spec ships fifteen machine-readable error codes (invalid_siwx_domain_mismatch, invalid_siwx_nonce, …) and an unusually explicit warning: validate domain against your configured public origin, never against the Host header the caller controls.

auth-hints solves a narrower problem: when only some entries in accepts[] require authentication, the client should not have to discover that by failing. The extension maps acceptIndexes to authentication methods — oauth2 with token endpoint, optional RFC 7591 dynamic registration and a tokenType of Bearer or DPoP (RFC 9449), or sign-in-with-x as a pointer to the sibling extension. Credentials then travel as ordinary HTTP headers next to PAYMENT-SIGNATURE; the facilitator never sees them. Authentication identity and payer address are explicitly independent. This is the same auth-meets-payment seam we traced in the MCP authorization deep dive, compressed into one 402 response.

http-message-signatures is the thinnest of the nine: a pointer into RFC 9421 territory. A network advertises a registrationUrl, its accepted signatureSchemes (ed25519 for the one named deployment) and signature tags like web-bot-auth. The client hosts keys at /.well-known/http-message-signatures-directory and signs requests. The spec names exactly one example network: Cloudflare's cloudflare:402. It also sketches the inverse direction — servers signing responses over @status plus the PAYMENT-REQUIRED or PAYMENT-RESPONSE header, bound to the request with ;req flags — which would let a client prove the terms it was shown. This is Web Bot Auth formally docked into x402's extension envelope.

Cluster two: proving it happened

offer-receipt is the heavyweight, and the most strategically interesting document in the directory. It defines two signed artifacts. A signed offer is the server's cryptographic commitment to the terms in an accepts[] entry — resource URL, scheme, network, asset, payTo, amount, optional validUntil. A signed receipt, returned only on success, states that a given payer paid for a given resource at a given time, with an optional transaction hash. Both come in two formats: EIP-712 (with a deliberately hardcoded chainId: 1, since the signature is an off-chain artifact and the real network is a payload field) or JWS compact serialization with a kid that is a DID URL.

The sharpest section was added on 2026-07-23 in PR #2811: signer authorization. A valid signature proves a key signed the artifact — not that the key had any right to speak for the service. Without that check, anyone can mint a keypair and sign "receipts" for any resourceUrl on the internet. The spec now lists four authorization mechanisms: signing with the payTo key, a did:web document at /.well-known/did.json, a DNS TXT record at _controllers.<domain>, or an external registry, with a note that mutable sources should be checked as of the receipt's issuedAt, not verification time.

Why it matters: when we audited ERC-8004's reputation registry, the conclusion was that feedback without a settlement anchor is Sybil-fuel, and that proof-of-payment was the missing primitive. offer-receipt is that primitive, specified — portable, offline-verifiable evidence that a commercial interaction happened. The caveat is equally explicit: the spec declares its own wire shape "not considered stable" and only the behavioral rules normative. It is a draft that knows it is a draft.

Cluster three: operations

payment-identifier is 454 words that gateway operators will care about more than anyone: a client-generated id (16–128 chars, pay_-prefixed UUID recommended) that servers and facilitators can use as an idempotency key. Same id and same payload: return the cached response. Same id, different payload: 409 Conflict. The spec tells implementers to bind each id to a normalized fingerprint of the paid operation — scheme, network, asset, amount, payTo, route — before honoring a cache hit. What it still does not contain is a single MUST, SHOULD, or retention window; we flagged that gap when AWS cited a "15 minute" idempotency window this spec nowhere defines. Unchanged as of this clone.

builder-code gives x402 payments on-chain attribution. It implements Schema 2 of ERC-8021: a CBOR-encoded map appended to settlement calldata behind a 16-byte marker, carrying a (the app that exposed the endpoint), w (the facilitator that settled, added at settlement time) and s (service codes from the client path — an MCP middleware can list several). Each party gets a non-overlapping reservation: five client codes, five server codes, one facilitator code, eleven total. Codes match ^[a-z0-9_]{1,32}$ and resolve through registries; Base runs the first ERC-8021 code registry and hands out codes free. Attribution is the raw material for analytics and, eventually, revenue share — which is presumably why it is the most actively patched extension in the repo.

The gas sponsoring pair attacks the cold-start problem for Permit2-based settlement: a fresh wallet holding only USDC cannot pay gas for the one-time approve(Permit2). eip2612GasSponsoring lets the client sign an EIP-2612 permit that the facilitator submits and pays for; erc20ApprovalGasSponsoring covers tokens without EIP-2612 — the facilitator funds the wallet with gas, broadcasts the client's signed approval, and settles, in an atomic batch to prevent the funded gas being front-run away. We saw both live in the x402-rs facilitator audit.

The uneven map: 9 specs, 7 TypeScript, 4 Python, 4 Go

Specs are promises; packages are facts. The @x402/extensions TypeScript package (version 2.21.0, bumped 2026-08-04 in PR #3041) implements seven of the nine: bazaar, builder-code, both gas sponsoring flows, offer-receipt, payment-identifier and sign-in-with-x. Python implements four: bazaar, builder_code, payment_identifier, sign_in_with_x. Go implements the same four. Two extensions have zero SDK implementation anywhere in the repo: auth-hints and http-message-signatures — the latter lives in Cloudflare's edge rather than in any x402 SDK, and the former is a spec waiting for code.

The practical consequence: a Python or Go seller today cannot emit signed receipts or sponsor gas through the official SDKs, and no client anywhere can act on auth-hints without hand-rolling it. Multi-language parity, which the schemes mostly achieved, has not reached the extension layer.

Where the security patches are landing

Read the last month of commits and a pattern jumps out: the core protocol is quiet; the extension layer is where the security work is. On 2026-07-15, PR #2859 bound SIWX domain validation to a configured origin instead of request headers — closing a Host-header spoof. On 2026-07-23, PR #2933 fixed Ed25519 verification for Solana SIWX to reject small-order points. The same day, PR #2811 added the signer-authorization section to offer-receipt. Builder-code needed three corrective PRs in two weeks (#2912, #2994, #3027) to get client service codes limited, always attached, and correctly merged with server arrays across all three SDKs. And on 2026-08-04, PR #3039 patched a server-side request forgery in Bazaar: a malicious seller could plant external $ref/$id URLs in the JSON Schema it publishes, and a facilitator validating that untrusted schema would fetch them. Facilitators must now reject any non-fragment reference.

None of these are exotic. They are the standard bug classes of every plugin system — input trusted because it arrived inside a familiar envelope. The envelope is new; the bugs are not.

Three findings

As with every audit in this series, we report what we could verify against the clone, with locations.

Finding 1http-message-signatures.md contradicts its own schema. The Fields section marks tags as "(required)", but both JSON Schema blocks in the same document list only registrationUrl and signatureSchemes in the required array. An implementer validating with the schema accepts what the prose forbids.
Finding 2 — the builder-code spec's client examples break the extension framework, and the SDK disagrees with them. The v2 framework table makes info and schema Required on every extension value, yet builder_code.md's PaymentPayload examples send a flat {"builder-code": {"a": "my_app", "s": "my_client"}} with neither envelope field, and the spec's facilitator steps read extensions["builder-code"].a directly. The shipped TypeScript SDK does the opposite: the client writes { info: { s: [...] } } and the facilitator reads .info. Code and spec cannot both be right; today the wire truth is the SDK's.
Finding 3auth-hints references a scheme that does not exist. Its motivating example and prose use "scheme": "deferred", but the scheme was merged as batch-settlement in PR #1145 on 2026-04-15 — nine days before auth-hints was added on 2026-04-24. The spec was stale at birth and remains so at HEAD: specs/schemes/ contains exact, upto, batch-settlement and auth-capture. No deferred.

Two smaller observations. Extension identifiers mix conventions — seven kebab-case keys against two camelCase (eip2612GasSponsoring, erc20ApprovalGasSponsoring), with file names in a third style. And both gas sponsoring specs embed // comments inside JSON examples, which makes every one of those blocks invalid JSON for anyone who copies them. Also worth a line: that auth-capture directory is a fourth payment scheme — escrow-based authorize, capture, void, refund, with a TypeScript client since PR #2486 (2026-05-29) — that closed nobody's trilogy. It deserves its own audit.

What it means for LLM4Agents

The extension layer is where x402 stops being a payment wire format and starts being a commerce stack, and almost every piece maps onto something our gateway already does or should expose. payment-identifier is the idempotency our reserve-then-settle billing pipeline needs at the protocol edge — the natural binding is the reservation id we already mint. offer-receipt is the portable version of the settlement trail we keep internally: signed receipts issued at settle time are exactly the proof-of-payment anchor the ERC-8004 audit concluded reputation systems are missing, and a gateway that emits them makes every paid inference call a reputation event. sign-in-with-x gives walk-up x402 buyers a memory: pay once, be recognized, skip the second 402. And builder-code is the attribution rail a model marketplace needs the day revenue share becomes real.

The risk side is equally concrete. Nine extensions with three SDK coverage levels is an interoperability matrix, and matrices rot at the edges — a client that assumes offer-receipt everywhere will meet Python sellers that cannot sign one. And the patch thread shows extensions are attacker surface: schemas fetched, headers spoofed, curve edge cases. A gateway that normalizes extension handling for its agents — validating envelopes, pinning schemas, rejecting external references — absorbs that risk once instead of letting every agent absorb it separately.

Staying on the frontier

Concrete steps, in order. First, implement payment-identifier end-to-end in the billing path: accept client ids, bind them to reservation fingerprints, return cached settlement responses on retry, 409 on mismatch — and document a retention window, since the spec will not. Second, emit offer-receipt receipts from our settle step: an EIP-712 receipt signed by a dedicated key, authorized via did:web at /.well-known/did.json, so any agent can carry portable proof it paid us. Third, accept sign-in-with-x on walk-up endpoints so returning buyers skip re-payment where policy allows; the fifteen error codes make this implementable without guesswork. Fourth, register builder codes and attach s attribution on gateway-mediated settlements — free today via Base's registry, valuable the day analytics or rebates key on it. Fifth, upstream the three findings as issues; the SSRF fix shows this repo responds. Sixth, watch auth-capture: escrowed authorize-capture-void-refund is the missing shape for refundable inference billing, and it is already sitting in specs/schemes/ with a TypeScript client.

Payments with receipts, identity and idempotency built in

LLM4Agents meters 345+ models behind one OpenAI-compatible gateway, settled per call in stablecoins over x402.

Register your agent