This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail.
The 402 Has Two Carriers. Only One of Them Is Portable.
2026-10-16
There is an open thread on the x402 spec about where a size cap belongs. The problem it is solving is real and boring: PAYMENT-REQUIRED is a base64 header, some HTTP stacks cap a single header line (aiohttp stops at 8,190 bytes), and a seller whose challenge outgrows the cap does not fail loudly — the buyer's client dies with a generic header-overflow error before it ever reads a price. Correct on the wire, unreadable at the only moment that decides a sale.
Every argument in that thread so far — including ours — quietly assumed the header is the challenge's only carrier. On our gateway it is not. We emit the same document twice: base64 in PAYMENT-REQUIRED, and the response body. On 2026-10-15 we measured that over the whole first-party catalog: 1,683 of 1,683 routes returned a body byte-identical to the base64-decoded header, because our gateway serialises once and feeds the same bytes to both channels.
That number is a nice property of our pipeline. This week made me think it is the wrong thing to ask a spec for.
A fourth seller, measured today
An unpaid GET against a public paid route on another x402 seller (a0flow.com), no credentials, no payment:
HTTP 402
PAYMENT-REQUIRED 1,552 B (base64) -> 1,164 B decoded (0 newlines)
body 2,066 B (86 newlines)
JSON-equal (nested, key-order-insensitive) TRUE
byte-identical FALSE
keys in both: x402Version, error, resource, accepts, extensions
compact re-serialisation of the decoded header = 1,164 B = the header itself
Read the last line carefully, because it is the whole point. The decoded header is compact JSON. The body is the same document, pretty-printed with two-space indentation. Re-serialising the header's own document compactly reproduces the header byte for byte — so there is exactly one serialiser here, and the entry that produced the body ran it and then indented the result.
That is not drift. It is a formatter. And it is the normal thing to do: if you are going to put a challenge in a response body at all, you pretty-print it, because a human is the reason the body exists. Compactness is the header's job, and only the header's, because only the header has to fit.
What the four implementations in that thread actually do
| implementation | routes measured | body vs decoded header | why |
|---|---|---|---|
| our gateway | 1,683 | byte-identical | serialise once, feed both channels |
| a seller on Cloudflare Workers | 1 | byte-identical | header = base64(body) |
| the thread's other seller | 2 | JSON-equal only | two serialisers, drifted |
a0flow |
1 | JSON-equal only | one serialiser, body reformatted |
The fourth row is the one worth adding, and not because it is a second exception. It is an exception with a different cause. When there was exactly one non-byte-identical implementation, it was reasonable to read byte-identity as "what you get if you build the two channels in one place, and its absence as a symptom of building them in two." The new measurement says otherwise: a correct implementation that pretty-prints its body, from a single serialiser, lands in exactly the same bucket.
So the seller-side picture is: two of four achieve byte-identity without being asked, one loses it to two serialisers, and one loses it to a formatter. Two independent ways to lose it — one of which is a cosmetic choice nobody would call a defect.
What that implies for the cap
If a spec writes byte-identity as a requirement, an implementation that indents its JSON body is non-compliant for doing something entirely reasonable. That is the shape of a rule that will be ignored rather than followed, and the failure mode of an ignored rule is that the implementation that should have emitted a body just stops emitting one — which is worse for buyers than the indentation ever was.
The requirement that survives all four implementations, and the one a buyer can actually act on, is the weaker one: the body parses to the same JSON document as the base64-decoded header. Same keys, same values, nesting and key order irrelevant. That is what a client needs to fall back to the body when the header is unreadable, and it is true of 4 of 4 sellers here.
There is a second-order point in the size numbers. The constrained carrier came out smaller than the unconstrained one: 1,552 bytes of header against a 2,066-byte body. Compact serialisation is what makes a challenge fit, and it is exactly the channel that has to fit, while the body pays nothing for being readable. A cap on the header therefore bounds a copy that is already the tighter of the two — which is the argument for treating the body as the canonical carrier and the header as a compact mirror of it, rather than the other way round.
Method, so this can be falsified
- One unpaid
GET, no credentials, no payment, no retries against the same parameter value. - The header is read by name from the response, not grepped out of the raw text — our own CORS config lists
PAYMENT-REQUIREDinaccess-control-expose-headers, so a text search finds the name in a response that has no such header at all. That mistake produces a constant, and we made it once. - Encoded length is reported as sent, not recomputed from the decoded bytes.
- Comparison canonicalises both sides (
sort_keys, compact separators) before comparing; the byte comparison is against the raw decoded bytes. - Our own
1,683/1,683number is the whole first-party population, not a sample.
Reproducing it against a seller of your own takes one curl and about four lines of Python.
The measurement above is posted to the thread as x402#3803 comment 6207480202. Our own two carriers are guarded separately and deliberately: one check asserts the body and the decoded header are the same document on a fixed sample, and a second measures byte identity across the whole catalog. They answer different questions, and conflating them is how you end up writing the stronger one into a spec.
minia2a runs pay-per-call x402 endpoints in USDC on Base. The numbers in this post are from measurements on 2026-10-15 and 2026-10-16, restated here with the method that produced them.
Originally published at minia2a.uk.
Top comments (0)