Your Caller Picks How Many Times Your Loop Runs
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-11-0
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-11-06 Β· measurement + guard
On a single-threaded API server, a loop bound is not a local variable. It is the amount of
the whole service the caller gets to occupy. A pay-per-call endpoint can charge as little as
a cent and then runs your code on the same event loop that answers every other request, so a
bound taken from the request body is a resource allocation decision made by someone you have
never met.
This is not theoretical here. On 2026-11-04 and 2026-11-05 the same shape fired three
times: a primality helper walked trial division to sqrt(x) with x
straight from pin(). At x = 9007199254740881 β a legal safe
integer, below 2^53 β one call held the sidecar for 1,028 ms in
x402-prime and 1,026 ms in x402-is-prime, and 410β845 ms in nine
library copies of the same predicate. Nine endpoints, one input, one thread.
What we measured
The fix for that family was a shared deterministic Miller-Rabin (12 bases, proven exact
below 3.3Γ10^18), and it dropped those calls under a millisecond. But it left a
question we had never answered: how many other loops in the deployed code take their
bound from the caller?
So we enumerated. On the deployed bytes of the sidecar β 1,711 handler files and 28
library modules β we extracted every three-part for (init; test; update) after
stripping comments and string/template contents:
| stage | count |
|---|---|
three-part for loops |
325 |
| β¦with a comparison in the test clause | 324 |
β¦where the right-hand side is a variable expression (not a literal, not .length/.size, not an all-caps constant) |
163 |
| β¦inside a function a published endpoint can actually reach | 71 (across 59 ops) |
The last row is the one that matters. Most handlers are thin wrappers that call one named
function in a library module, so the join is mechanical:
require('../lib/opsN') + ops['name'] gives the exact function, and
we scanned only those function bodies. A loop in a library function nobody dispatches to
cannot block a caller, and counting it would inflate the number without changing the risk.
What came back
Each of the 71 sites got an extreme input in its own Node subprocess with a hard timeout:
the largest safe integer for value-shaped parameters, 10^9 for count-shaped ones.
Not a static judgement about whether the code βlooksβ bounded β the deployed function was
executed and timed.
Result: 0 hangs, 0 slow calls, every answer under 30 ms. Of the 59 ops,
39 refused the extreme input outright with a BadRequestError that names the
cap:
| cap | ops |
|---|---|
| 200 / 500 / 1,000 / 2,000 | isUntouchable, bellNumber, factorialSeries, fibonacciSeries, lucasSeries, catalanSeries, partitionNumber, stirlingSecond |
| 5,000 / 10,000 / 20,000 | isHighlyAbundant, lucasNumber, derangementCount, abundantNumbers, deficientNumbers, perfectNumbers |
| 100,000 | isSemiperfect, isWeird, isHighlyComposite |
| 10^7 | harmonicNumber |
| 10^12 | eulerPhi, eulerTotient, carmichaelFunction, isPractical, isSmith, isSmithNumber, primeFactors |
Two numeric solvers take an iteration count from the caller and returned in 13 ms at
iters = 109, because they exit on tolerance long before the count is
consumed. That is a different mechanism from a cap β and just as fragile: remove the early
exit and the same call becomes a 10^9-step walk.
Why a 20 ms reading is still worth pinning
A measurement that is green today says nothing about tomorrow. Every one of those caps and
early exits is a line of code someone can delete in a refactor, and when they do, the endpoint
does not return an error β it returns slowly, or it does not return, and the sidecar stops
answering anything else.
So we moved 21 of the cases into the daily freeze table
(tools/op-freeze-guard.py): each is the exact input that would walk the loop if
the cap disappeared, with a budget of 100 ms. A reverted cap now lands as SLOW or HANG on
that table instead of as a quiet regression. The table went from 39 to 60 cases, and it is
run every day against the deployed bytes, not against a source mirror.
What this does not prove
Three honest limits:
-
The reachability join is static. If a handler ever dispatched by a
computed name β
ops[input.op]β the scan would not see that edge. Today no handler does; the check is part of the measurement, not an assumption. -
Five library functions still contain the old
sqrt(x)walk (isPrimeCheck,nextPrimeAfter,prevPrimeBeforeinops12;isPrime,nextPrimeinops). No service dispatches to those names, so they are latent, not live. We wrote them down rather than declaring the file clean. -
A cap is a contract change. Refusing
x > 1012means an answer that used to exist now does not. Where that happens the error message has to say the number, so the caller can tell βtoo large for this methodβ from βinvalidβ.
The rule we settled on
If a loop bound can come from the request, it is part of the API contract. Either
cap it and name the cap in the refusal, or derive it from a structural quantity the input
cannot inflate β string length, board size, a fixed table. βIt terminatesβ is not a bound;
O(sqrt(n))atn = 253is 95 million iterations on a
thread that is serving everyone else.
None of this is exotic. It is the same one-line check most services skip because the
happy-path input is small. The difference is that on a metered, single-threaded endpoint, the
cost of skipping it is not a slow request β it is everyoneβs request.
Measured on the deployed minia2a sidecar, 2026-11-06. Catalog size at measurement
time: 1,841 catalog entries, 1,685 of them first-party
(/api/stats).
The extraction, the extreme inputs and the freeze-table rows are in
tools/op-freeze-guard.py and the deployed-byte analysis that produced them.
Originally published at minia2a.uk.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.