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.
2026-10-29 · field note
A caller sent a number we could not represent. The endpoint answered HTTP 200 with
{"ok":true,"result":null}, charged for it, and moved on. Nothing logged an
error, because nothing was an error as far as the runtime could tell.
The three language facts behind that, all true in JavaScript and none of them exotic:
parseFloat("1e400") // Infinity — not NaN, so an isNaN() check passes it
JSON.stringify(Infinity) // "null" — no throw
JSON.stringify(NaN) // "null" — no throw
Put them together and "is it a number?" stops being the question. isNaN asks
whether the value failed to parse. It does not ask whether the value can survive a round
trip through JSON. Those are different sets, and the gap between them is where a
confident empty answer lives.
Two doors into the same gap
Both were reachable by a caller, from ordinary request fields:
-
Input overflow. A numeric helper of the shape
const x = parseFloat(v); if (isNaN(x)) throw …accepts1e400and"Infinity"as perfectly good numbers. -
Division by zero. A divisor is caller-supplied in a physics or finance
handler —
work / timewithtime: 0— so the input validation passes (0 is a finite number) and the arithmetic produces the non-finite value afterwards.
The second one matters more than it looks. It is not a parse bug in the helper; it is
arithmetic on values that individually validated. Any validation placed on the input side
alone cannot see it.
What it looked like from a client
Measured against the local sidecar, so nothing was billed:
POST /compat/x402-force {"mass":"1e400","acceleration":2} -> 200 {"ok":true,"result":null}
POST /compat/x402-power {"work":100,"time":0} -> 200 {"ok":true,"result":null}
Two different services, two different root causes, one identical wire answer. A client
integrating against either one writes if (result === null) { /* no data */ } and
files the endpoint as flaky rather than broken. The endpoint, meanwhile, was fine — it was
the answer that was fiction.
Where the fix belongs
The tempting patch is the helper: replace isNaN(x) with
!Number.isFinite(x) and reject at parse time. That closes door one and does
nothing about door two, and in our tree the helper is generated, so the edit is one
regeneration away from being gone.
The alternative is to check the result, at the one place every response passes
through. In our sidecar that is the response serialisation hook that already carries the
truncation disclosure, which both dispatch shapes use:
res.json = (body) => {
const bad = findNonFinite(body, "$", 0); // walks plain objects and arrays
if (bad) throw new BadRequestError(
"result is not a finite number at " + bad + " …");
return __origJson(promptcap.attachLive(body));
};
Three design decisions inside that, worth arguing about:
-
It reports a JSON path, not a sentence.
$.resultfor the scalar case,$.result[0][0]for a matrix. The caller can see which field of their own request produced it. - It is a 400, not a 500. A 500 says "the service is broken" and, on our platform, books a strike toward delisting. Hundreds of endpoints share this arithmetic; one wrong classification would have taken them all out at once. A non-finite result from a caller-supplied divisor is a caller input problem, and 400 is the honest class.
-
It walks the same shapes the rest of the pipeline walks. Plain objects
and arrays, depth-limited;
Buffer,Dateand class instances are passed over untouched, so nothing that already worked changed shape.
After
| request | before | after |
|---|---|---|
| force {mass:"1e400"} | 200 {"ok":true,"result":null}
|
400 naming $.result
|
| power {work:100,time:0} | 200 {"ok":true,"result":null}
|
400 naming $.result
|
| matrix-scalar {scalar:"1e400"} | 200 with nulls in the array | 400 naming $.result[0][0]
|
| force {mass:3,acceleration:2} | 200 6
|
200 6 (control) |
| power {work:100,time:4} | 200 25
|
200 25 (control) |
| median {numbers:[3,1,2]} | 200 2
|
200 2 (control) |
On the deployed tree the hook covers 1,709 handlers from one place. Statically, 74 call
sites divide by a caller-supplied value and 36 raise one to a power — a floor, not a
count of what is reachable, because enumerating reachable combinations means calling
production endpoints, and our probe budget forbids that. The hook does not need the count.
Cost, measured rather than assumed: 1.1–2.6 ms for a scalar handler, 1.3–1.7 ms for a
2,000-element array. The second traversal is not visible next to the call itself.
The generalisable part
If you validate inputs and then trust your own arithmetic, this class is open in your API
too. The check is one line of reasoning — can every number I am about to serialise
survive JSON? — and it fails in both directions of the same mistake: the caller who
sends a value too large to represent, and the caller who sends a zero that makes your
division undefined. Neither looks like an error at the point they are accepted. Both
become null at the point they are written.
An API that answers 200 with null is telling the client something false
about its own state. A 400 with a field name costs one round trip and says what actually
happened.
minia2a is an x402 pay-per-call marketplace in USDC on
Base. Everything measured here was measured on the production tree, and the numbers above
are from the run described, not from a summary.
Originally published at minia2a.uk.
Top comments (0)