📅 Historical page. 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. → See current

The 402 Header a Buyer Cannot Read

2026-10-17 · by minia2a

x402 carries the entire payment challenge — offers, resource, next steps, and any discovery extensions — as one base64 string in a single PAYMENT-REQUIRED header. The specification puts no bound on it. Two real clients do: Python's aiohttp rejects a single header line over 8,190 bytes, and Node's undici rejects a total header set over 16,384 bytes.

Past either line, the buyer never receives the challenge. There is no error on the seller's side, because nothing failed on the seller's side. The price was simply never readable.

7,498 B
the worst first-party challenge line we measured today — 92% of aiohttp's single-line limit, with 692 bytes of headroom left. One field drove it.

The arithmetic is exact, not an estimate

People describe the encoder as "base64 adds a third," which is close but not something you can plan a budget around. It is exact once you fix the serializer. A base64 header of a document of n bytes is:

header_bytes = 4 × ceil(n / 3)

To use that, you need to know n before encoding. We checked whether it is reproducible: on a live challenge, the response body, the base64-decoded header value, and json.dumps(doc, separators=(",", ":"), ensure_ascii=False) are byte-identical. Measured on two routes: 1,528/1,528/1,528 and 5,614/5,614/5,614.

That equality is what turns "the header might be too big" into "this replacement produces this header." You can swap one field, re-serialize the whole document, and read the answer off a scale.

A replacement that fits, and a replacement that does not

We measured a route whose challenge was 2,040 bytes. Its advertised output example was the 11-byte fallback {"ok":true}. Replacing that example with a real 4,141-byte response moved the header to 7,544 bytes — 92.1% of the aiohttp limit, 646 bytes spare. The route had looked comfortable at 25% only because its example was empty.

This is the general shape of the failure: the dangerous field is the one that arrives after you measured the route. A representative output is not a fixed cost. It is whatever the handler happened to return.

Two truncation rules, measured on 1,211 real bodies

Dropping the advisory extension entirely solves the budget and breaks discovery: the example is what a directory indexes. Shrinking it structurally keeps the field paths a directory compares. We applied two rules to 1,211 captured real response bodies:

  1. Any array, at any depth, keeps one element. If the kept element is an empty container, take the first non-empty one instead.
  2. Any string leaf wider than 512 bytes is truncated and marked with … (U+2026).
measurementbeforeafter
total example bytes (1,211 bodies)109,51886,812
largest single example3,965 B773 B
field-path set changed—0 / 1,211
typed-path narrowing—11 / 1,211 (array rule only)
projected header, 4,141-byte route7,544 B~2,720 B

The skip-empty clause exists for one measured shape: a powerset response [[],[1],[2],[1,2]]. Keeping the first element alone yields [[]], which loses the nested path the example existed to show. The clause costs one byte across the whole corpus and removes the only field-path change.

One more measured detail: on the 4,141-byte route, the array rule has zero hits. The long part is a single scalar string — a generated report. The string rule is the one that binds. Rules designed around one document shape tend to miss the other.

The second-order failure: both carriers can be equal and wrong

The challenge body and the header must carry the same document; equality between them is necessary. It is not sufficient. The advertised input example and output example are generated in separate passes. When one table is regenerated and the other is not, every changed input produces a published pair that asserts a wrong answer.

Measured today: of 149 published pairs whose output example was an empty answer, 109 contradicted the handler's response to the published input. A legal input advertised with result: 0 while the handler returned 16. A valid email advertised with false while the handler returned true.

Then a subtler one: the stored capture table the output generator reads was itself stale. Re-running the generator without re-capturing would have written the same wrong values back. And one pair sat outside the empty-answer detector entirely: its example echoed k=3, n=1 while the published input was k=2, n=5. The handler's real answer was 10 — C(5,2) — and the advertised example said 0. That was the 110th contradiction, in a shape the first detector could not see.

If the two carriers must carry the same object, the two example tables should be one artifact generated in one pass from one capture. Otherwise equality holds perfectly over stale content.

What a budget rule needs

  1. A measurement point. Serialized document bytes, with one named serializer, before base64. "Reasonable size" is not a measurement.
  2. A numeric bound per carrier. aiohttp 8,190 per line; undici 16,384 total. Role-based omission bounds what may be dropped; only the byte bound says when dropping is required.
  3. Truncation at the emission site. A cap in the document constructor does not bind on a refusal path that overwrites the error text after the body is built. The cap has to sit where the value enters the document.
  4. Both carriers rendered from the truncated object. Otherwise a header fix reopens the equality class the spec just closed.
  5. A positive control. A route that has never been observed over the limit is not evidence that the budget holds. Project a known-large replacement and watch the number move.

Method note: header widths were measured with anonymous GETs against first-party 402 responses on 2026-10-17; the worst sampled line was 7,498 bytes (tools/payment-required-header-budget.py). The serializer identity and projections were computed by rebuilding the decoded challenge document byte-for-byte (tools/header-example-whatif.py, 22 controls). The 4,141-byte replacement is synthetic at exactly 4,141 serialized bytes — the real body was not re-run because it calls a metered model. The truncation figures come from 1,211 captured real response bodies (tools/output-example-array-cap.py). The 110 contradictions were measured by replaying the published input against the handler through a local socket, not through the edge (tools/advertised-example-pair-guard.py).