Agentic week: the paywall and the handler read different requests
A paywall only works if it inspects the same request the handler serves. Four bugs merged this week say that, in four different x402 adapters, it did not.
Between 11 and 18 September, 30 pull requests merged into x402-foundation/x402. The authorship split is the usual one: PhilBot402 with 11, phdargen with 7, mintlify's docs bot with 4 — 22 from the maintainer and its automation — and 8 from five outside accounts. But the outside eight are where the interesting failure mode lives.
The thread running through the week is not a feature. It is a gap between what a system records and what actually happened: between what the payment hook reads and what the route handler receives, between what a settlement transaction says and who initiated it, between a skill's text and the server that wrote it.
1. Four adapters, one class of bug
The x402 HTTP packages wrap a framework's request in an adapter so that payment hooks — onProtectedRequest, onPaymentRequired — can look at the request before deciding whether to grant access, what to charge, or what to advertise in the 402. The adapter is the hook's only view of the request. This week four separate fixes landed, all saying the same thing: that view was wrong.
Issue #3445, opened 10 September by sunruize93-cmyk, is the clearest. In @x402/next, calling await context.adapter.getBody() inside a hook consumed the POST body. If the hook then granted access, the route handler's own request.json() threw TypeError: Body is unusable: Body has already been read. A second adapter read returned undefined. Inspecting the body to make an access decision destroyed the body you were deciding about. PR #3451 (viviviviviid, merged 15 September) reads from a clone instead.
PR #3454 is the one with the widest blast radius. In @x402/axios, when the transport did not expose a final URL, the fallback resolved the request URL with WHATWG rules and ignored Axios's own joining and its params. With baseURL: "http://localhost/v1", url: "/quote" and params: { city: "Seoul" }, the server saw /v1/quote?city=Seoul and the hook saw /quote. The fix routes the fallback through the calling instance's getUri(config).
Two more from the same contributor cover query parameters. PR #3455: for GET /quote?tag=a&tag=b, Hono's c.req.queries('tag') returns both values, but the adapter returned only 'a'. PR #3456: for ?tag=&tag=b, the Next adapter tested the existing value for truthiness, so a leading empty string counted as absent and getQueryParams() returned { tag: 'b' } while the handler kept ['', 'b'].
/quote and a handler that serves /v1/quote?city=Seoul are not enforcing the same policy. Any route-scoped rule — per-path pricing, per-parameter tiering, body-based access checks — inherits the divergence.
Two details are worth noting for anyone maintaining their own middleware. First, all four came from outside the maintainer set, found by reproducing against published packages (@x402/[email protected], @x402/[email protected], @x402/[email protected]) rather than against main. Second, each PR reports the pre-fix failure count — 5 of 40 failing in Axios, 4 of 19 in Hono, 3 of 20 in Next — which is the discipline that separates a real regression test from one written after the fix. We wrote about the seller middleware surface in the x402 seller stack; this is the part of it that is easiest to get subtly wrong.
2. Casper lands, and EIP-3009 gets cloned again
PR #2877 merged on 15 September after two months open: 67 files, 6,034 additions, 39 commits, from davidatwhiletrue. It adds @x402/casper — a TypeScript implementation of the v2 exact scheme for Casper CEP-18 tokens using CEP-3009 transfer_with_authorization.
That name is the story. CEP-3009 is Casper's port of the same primitive EIP-3009 defines on EVM: a signed authorization the payer produces and someone else submits. The networks are casper:casper and casper:casper-test; the asset is a 32-byte contract package hash as 64 hex characters with no 0x or hash- prefix; payTo is a 33-byte address as 66 hex characters with a 00 account-hash or 01 package-hash prefix; and payment requirements must carry extra.name and extra.version for the CEP-3009 EIP-712 domain. The testnet default asset is csprUSD.
The facilitator design is the part worth borrowing. verify() always does live preflight checks, and if a speculative RPC URL is configured for the network it runs Casper speculative execution instead — one simulation covering balance, nonce reuse and entry-point failures, rather than three separate reads of balance, authorization_state and transfer_with_authorization support. The README notes the speculative endpoint is usually exposed separately, commonly on port 7778.
As with Cardano in last week's roundup, the package is merged but not published: @x402/casper returns 404 on the npm registry as of 18 September. The pattern is now consistent enough to plan around — a chain is in the repo well before it is installable. The broader map of what each chain binding has to supply is in the network bindings audit.
Two smaller chain changes shipped the same day. PR #3457 adds Celo USDT (0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e) and USAT (0xD2ab3C9A02DBBAB236BfEC45D1d755DF4267F771) as default assets on eip155:42220, both 6 decimals, with the EIP-712 domain version pinned to "1" because version() reverts on both contracts — verified on-chain by recomputing each DOMAIN_SEPARATOR(), and distinguished from Celo's 18-decimal fee-currency adapters that carry the same names. The facilitators page now points at x402.celo.org.
And PR #3503, merged 17 September, fixes a failure mode that reads as a warning label for every chain binding: the Stellar exact facilitator always bid BASE_FEE, 100 stroops, as the inclusion fee, with no way to change it. On pubnet, Soroban transactions at that bid are often not included, so settle timed out while verify had passed. The fix adds an inclusionFeeStroops option, counted against maxTransactionFeeStroops in verify, defaulting to 100 so nothing changes silently. A fee constant that is correct on testnet and wrong on mainnet is a facilitator that reports success at verify and nothing at all afterwards.
3. The MCP timeout gets a ceiling
Last week the Cardano merge pushed the facilitator HTTP client from 30 to 90 seconds and made the seller's advertised maxTimeoutSeconds the MCP client's abort timer. This week the same arc acquired its other half.
PR #3481 (phdargen, 15 September, 16 files) adds maxRequestTimeoutSeconds, a client-owned ceiling on waits derived from the accept, defaulting to 600 seconds, with the initial 402 probe at min(300s, cap) and timer-safe clamping for absurd accepts. A per-call timeout still overrides the derived value and is not limited by the cap.
The bug it fixes on the way is instructive: in TypeScript, auto-pay was passing the probe's options into the paid retry, so paid waits stayed pinned at 300 seconds no matter what the seller advertised. The client had a mechanism for honoring the seller's window and was not using it on the one request that needed it. Go and Python got shared timeout helpers in the same PR, and Go's v1 paid retries now take a deadline from the accept like v2 does. The Go and Python ports of the original derivation landed as #3442 and #3443.
Read the two weeks together and the design is now explicit: the seller proposes a window, the buyer bounds it. A seller can no longer hold a buyer's agent for an arbitrary duration by advertising a large maxTimeoutSeconds, and a buyer no longer aborts a settlement the seller told it would be slow. Releases followed on 15 September — @x402/core 2.26.0 on npm and x402 2.23.0 on PyPI. Downloads of @x402/core for the 30 days ending 17 September: 1,070,730.
4. MCP's Skills extension goes Final
SEP-2640 merged on 13 September, 54 commits after it was opened on 23 April. It defines io.modelcontextprotocol/skills: a convention for serving Agent Skills over MCP on top of the existing Resources primitive, now published as an official extension.
The mechanics are deliberately thin. Each file of a skill directory is a resource, conventionally under skill://<skill-path>/<file-path>, where the final path segment must equal the name in the skill's SKILL.md frontmatter — so the skill name is recoverable from the URI without reading the file. A server declaring the extension implements skills/list and skills/get; resources/directory/read is optional and gated behind a directoryRead: true capability setting. A listing entry is a complete manifest: verbatim frontmatter as JSON, plus every file with a SHA-256 digest and a byte size, or the literal string "dynamic" when digests cannot be published.
The security section is where this becomes an infrastructure question rather than a packaging one. Skill content is instructional text delivered to a model, so the SEP requires hosts to treat it as untrusted input, to tag it with its originating server at the point it enters model context, and to never present an MCP-served skill as indistinguishable from a local one. The Agent Skills allowed-tools field must be ignored for MCP-origin skills unless the user has explicitly approved that grant — "a remote server populating allowed-tools is requesting elevated access on the host, not declaring a property of its own environment." Approval is content-bound: if a later entry advertises a different resources set, the prior approval is revoked. And digests are explicitly "not a security boundary" — they are unsigned and come from the same server as the content.
The deferred-features appendix is the most useful page in the document. Archive distribution — pre-packed tar and ZIP of a whole skill — was removed during review because safely unpacking a remote archive means defending against decompression bombs, path traversal, symlinks escaping the directory, Unicode-normalization collisions that overwrite SKILL.md, setuid bits and device nodes, and every host would have to get all of it right. The cost is one round trip per file. The SEP takes that cost on purpose. Our agent threat model treats server-supplied instructions as an injection surface; this is the first MCP extension that writes the same assumption into normative requirements.
5. TRM puts a number on who is actually paying
On 9 September TRM Labs published a measurement of x402 settlement covering everything settled through known facilitators since May 2025: roughly $52.7 million across 198.9 million settlement transactions on Base, Solana and Polygon.
Two findings. The unambiguous one: $52.47 million of $52.68 million, or 99.6 percent, settled in USDC. The agent-payment economy on public rails is a stablecoin economy, and the asset question is effectively closed.
The contested one: after removing self-payments, flows concentrated in one or two payers, and sellers with fewer than ten distinct buyers — about half of all settled volume — TRM bounded the agentic share of the remaining $25.62 million between 0.6 and 7.5 percent, using a permissive test and a strict one rather than a single figure. The screening leans on payment-size heuristics, and TRM notes it could miss a genuine agent buying the same service at a fixed price repeatedly.
The conclusion is not bearish on the rail. TRM's argument is that the plumbing works and what is missing is "accurate registration, counterparty reputation an agent can check on its own, and monitoring built for volume rather than value." That is close to what we found measuring ERC-8004 registrations empirically: the registries exist, and almost nobody is in them.
What it means for LLM4Agents
The adapter cluster is the item with direct operational weight. LLM4Agents is a gateway: the component that decides whether a call is paid and at what price is separate from the component that forwards the call to a model provider. That is structurally the same split as hook and handler, and it fails the same way. If the metering path normalizes a request differently from the forwarding path — a dropped repeated parameter, a consumed body, a URL resolved without its base path — we charge for one call and serve another. The mitigation is not a code review; it is a test that asserts both paths see byte-identical inputs, and a rule that the request is parsed exactly once and passed down as a value.
The MCP timeout ceiling changes a default we should adopt rather than invent. A seller advertising maxTimeoutSeconds is making a claim about its own settlement latency, and a buyer that honors it without bound has handed a stranger control of its concurrency budget. A 600-second client-owned cap with a shorter probe is a sane shape for any agent calling paid endpoints it did not write, and it belongs in the buyer defaults, not in per-integration configuration.
The Skills extension is an opportunity with a sharp edge. Serving a skill next to the tools it describes is exactly what a paid MCP surface should do: a gateway can ship the instructions for using its own routing, fallback and billing semantics as a versioned, digest-addressed artifact instead of a README a model never reads. The edge is that the same mechanism lets any paid server we route through push instructions into a customer's agent. Both sides of that are ours to handle — publish skills with stable digests, and treat skills arriving from upstream servers as untrusted text with a visible origin.
TRM's number is the uncomfortable one, and it is useful precisely because it is uncomfortable. If between 0.6 and 7.5 percent of screened x402 commerce is plausibly agentic, then settlement volume is a bad proxy for demand and a worse one for product-market fit. The measurement that matters for us is not dollars moved but distinct agents that paid more than once, which is a number we can compute on our own traffic and nobody else can compute for us.
Staying on the frontier
Five things, in order.
First, write the divergence test. One test per protected route that issues a real request with a repeated query parameter, an empty leading parameter, a base path and a JSON body, and asserts that the metering layer and the forwarding layer observed identical values. This is a day of work and it closes the entire class of bug that took four PRs upstream.
Second, adopt the buyer timeout shape. Derive waits from the seller's maxTimeoutSeconds, cap them at a client-owned ceiling in the 600-second range, probe at no more than 300, and make the per-call override explicit. Then check the code path that actually pays, not just the one that probes — that is where the upstream bug was.
Third, publish an LLM4Agents skill over MCP. Our MCP server already exposes tools; a skill:// entry with complete resources digests turns "here is a tool" into "here is how to choose a model, handle a 402 and reconcile a settlement." Start with the digest-backed form, not "dynamic", so hosts can content-bind approval.
Fourth, harden the inbound skill path. If the gateway ever consumes MCP servers on a customer's behalf, apply the SEP's rules now rather than after the first incident: origin visible to the model, allowed-tools ignored unless explicitly approved, reads bound to the originating server, names resolved per-origin.
Fifth, instrument the metric TRM says is missing. Distinct paying agents, repeat rate, and payments per agent per day — published for our own traffic. In a market where nobody can tell an agent from a cron job on-chain, an operator that can prove the difference off-chain has something no block explorer provides.
Pay per call, in stablecoins, over an OpenAI-compatible API
Register an agent, fund it, and start routing. No prepaid credit, no monthly minimum.
Register an agent