l402: tell the truth when the gateway cannot issue an invoice - #323
l402: tell the truth when the gateway cannot issue an invoice#323ryanthegentry wants to merge 2 commits into
Conversation
Boltz disabled swap creation at 2026-08-03T08:52:56Z. For the rest of the day every challenge attempt failed, and the index answered the one agent engaging with its paywall in two ways that were each worse than saying nothing. export.csv returned a bare 402 whose body advised adding `?l402=require`. 402explorer/0.1 followed that advice — every one of its five hits is trailed about three seconds later by a `?l402=require` request. That parameter sets the free limit to 0 on purpose, so the challenge path ran, failed, and fell through to `429 Rate limit exceeded. Try again later.` with Retry-After: 60. So a well-behaved client asking to pay was told it had been rate-limited and should retry in a minute, for an upstream outage that no retry would ever clear, once every seventy minutes for five hours. Both call sites now return 503 with reason `l402_gateway_unavailable`, a message that names the cause, and a Retry-After that does not imply near-term recovery. The two "no challenge" branches are split apart: a thrown error is an outage and gets the 503, while a null challenge means no gateway is configured at all, which stays a 402 but no longer points at `?l402=require` — the same absent gateway serves both, so the old text just bounced callers between two dead ends. Also adds a payments block to the digest, sourced from the gateway's own counters. The index keeps no macaroon, token or settlement table, so it could not answer "has anyone ever paid?" — which is how a user-agent slipping below a top-15 traffic cutoff got reported as a lost paying buyer when the gateway's counters said challenges issued, none paid, nothing earned. The block reports nulls plus a reason when the gateway is unreadable, never zeroes, and never fails the digest: an unreachable gateway is exactly when the digest matters most. Tests: 2015 pass / 5 skip root, 135 pass / 2 skip router, 87 mcp-server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Boltz said today the shutdown is indefinite — no ETA, "do not expect swap services to resume shortly", and they are openly unsure swaps resume at all. The reason is not a bug: months of rising automated probing, several contained exploits, and attackers iterating faster than they can patch. Against that, the Retry-After of 300 this shipped with was a smaller version of the lie it was written to remove. Five minutes tells a caller the problem is nearly over when nobody knows that. Default is now 3600 and reads from L402_UNAVAILABLE_RETRY_AFTER_SECONDS, so an operator can raise it without a deploy. The message drops "outage" for "upstream failure with no estimated restoration time", and a test now forbids "temporarily", "shortly", "soon" and "brief" in that body. We do not know when this comes back. Saying otherwise is the whole failure mode. Tests: 2023 pass / 5 skip. Lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Update — the shutdown is indefinite, so the 503 needed resizingBoltz, 2026-08-05: swaps stay disabled until further notice. Months of rising automated, AI-assisted probing, several contained exploits, "attackers now iterate faster than a team our size can find and patch." No ETA. "Do not expect swap services to resume shortly." What that changes hereThe 503 and the machine-readable But The message drops "outage" — which implies something that passes — for "upstream failure with no estimated restoration time". A test now forbids The digest
|
What happened
Boltz disabled swap creation at 2026-08-03T08:52:56Z. Every challenge attempt failed for the rest of the day. The index handled that in two ways that were each worse than saying nothing, and they chained.
export.csvreturned a bare 402 whose body advised adding?l402=require.402explorer/0.1followed that advice — every one of its five hits that day is trailed ~3s later by a?l402=requirerequest. That parameter sets the free limit to 0 on purpose so a challenge gets minted; the challenge path then failed and fell through to429 Rate limit exceeded. Try again later.withRetry-After: 60.A well-behaved client asking to pay was told it had been rate-limited and should retry in a minute, for an upstream outage no retry would ever clear — once every ~70 minutes for five hours.
What changed
Honest degradation. Both call sites (
export.csvroute,sendL402Challenge) now return503withreason: "l402_gateway_unavailable", a message naming the cause, and aRetry-Afterthat does not imply near-term recovery.The two "no challenge" branches are split apart, because they were never the same thing:
?l402=require. The same absent gateway serves both, so the old text just bounced callers between two dead ends.The paid path is asserted unchanged: a working gateway still returns 402 +
WWW-Authenticate+ invoice on both routes.Payment observability. The index has no macaroon, token or settlement table, so it could not answer "has anyone ever paid?". That gap is why a user-agent slipping below a 7d top-15 traffic cutoff got reported as a lost paying buyer, when the gateway's own counters said: challenges issued, none paid, nothing earned. The digest now carries a
paymentsblock read from the gateway. It reports nulls plus a reason when unreadable — never zeroes, which would read as "we know, and the answer is none" — and never fails the digest, since an unreachable gateway is exactly when the digest matters most.Note for deploy
The Atlas watchdog's
HEAD /api/v1/export.csvwill now see 503 instead of 402 while the gateway is down. That is intended — it should notice — but it is a new alert surface.Tests
Root 2015 pass / 0 fail / 5 skip · router 135 / 0 / 2 skip · mcp-server 87 / 0. Lint clean.
New:
test/l402-degraded-path.test.js(9),test/digest-payments.test.js(6). Both written RED first against a real ephemeral server and a stub gateway returning the 500 the partner gateway actually returned during the outage.🤖 Generated with Claude Code