The rate limit answers 402 with the offer, not 429 with a description of it - #188
Merged
Conversation
… of it The refusal already named the pass, its price and exactly how to buy it. In prose. An x402 client cannot act on prose, so a program that hit the wall had the same two options it arrived with: slow down, or spread itself over a proxy pool. It had to make a second trip to /crawl to find out what to sign. Now the refusal carries the offer itself, read off the gateway rather than rebuilt here -- payTo, the price, the currency and the network live in one place, and a second copy is how a site ends up quoting a number its own /crawl disagrees with. That is what @profullstack/x402-gateway 0.6.0 adds: `gateway.offer()`, which was already computed inside the closure and is now just not private. The paid rungs still get 429. A caller at the sponsor ceiling has bought everything there is, and a 402 would be asking it to pay twice for nothing. Tripping the anonymous rung takes 120 requests in a minute, against a steady reader's thirty, so the caller being answered here is a machine. The tiered throttle in lib/crawlThrottle.js is untouched: it keys a declared crawler by its own bot token, which collapses a seventy-address fleet into one caller, and nothing in @profullstack/throttle does that. This is the sale it was missing, not a replacement for it. Read off the source, the way the matcher test already is: importing proxy.js under node:test fails on a bare `next/server` specifier, which is why none of the tests here drive the proxy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDGCxTmEPs3ecwjjLJDQXh
The only conflict was the import block at the top of proxy.js, where main added the challenge middleware and this branch added `gateway` to the crawl-gateway import so the refusal can quote the same offer /crawl serves. Both imports are kept; the two features are independent and their tests pass together. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0139ts1a6Lzj5DXySTCbeG8C
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The refusal already named the pass, its price and exactly how to buy it — in prose:
An x402 client cannot act on prose. A program that hit the wall had the same two options it arrived with — slow down, or spread itself over a proxy pool — plus a second round trip to
/crawlto find out what to sign.What changes
The refusal now is the offer. Status
402, with theacceptsarray spread into the body it already had, so a client can pay and carry on inside the same exchange.The offer is read off the gateway rather than rebuilt here.
payTo, the price, the currency and the network live in one place — a second copy is how a site ends up quoting a number its own/crawldisagrees with. That is what@profullstack/x402-gateway@0.6.0adds:gateway.offer(), which was already computed inside the closure and is now just not private.The paid rungs still get 429. A caller at the sponsor ceiling has bought everything there is; a 402 would be asking it to pay twice for nothing.
Tripping the anonymous rung takes 120 requests in a minute against a steady reader's thirty, so the caller being answered here is a machine.
What is deliberately NOT changed
lib/crawlThrottle.jsstays exactly as it is, and this site does not adopt@profullstack/throttle.The rest of the fleet is getting that package because their limiters only watched
/api/. This one already meters every route, and it does something the package does not: it keys a declared crawler by the token in its own User-Agent, collapsing meta-externalagent's seventy addresses into one caller. Per-address metering never reaches that fleet. Swapping this for the generic package would be a downgrade.What it was missing was the sale, and that is all this adds.
Verified
node --test test/*.test.js— 405 passed, 0 failed (two new).pnpm --filter @rssamplifier/web buildclean with the proxy intact.New tests read the behaviour off the source, the way the matcher test already does — importing
proxy.jsundernode:testfails on a barenext/serverspecifier, which is why none of the tests here drive the proxy — plus one that actually callsgateway.offer()and checks everyacceptsentry names a network, apayToand a positive amount.🤖 Generated with Claude Code