0.4.0: a free allowance that answers with a price, not a 429 - #6
Merged
Merged
Conversation
Named crawlers were the only thing this could charge. The other half of the traffic is an ordinary client hammering a site far past what a reader would, and the usual answer to that is 429: go away and try later. That is the right answer when you have nothing to sell. freeQuota: 100 // 100 requests a minute, free, no account benefits: ['No rate limit', 'Bulk export endpoint'] Past the allowance the gateway answers 402 with the offer, the RateLimit headers, and a body naming what ran out, when it comes back, and what a pass costs. The moment a caller runs out of free requests is the best sales pitch a site will ever get: it has just shown it wants more than the free tier and is still holding the request. ON ROTATING ADDRESSES, WHICH DEFEATS THIS. The default identity is the caller's address, so a proxy rotation walks straight past it. That is deliberate and documented rather than patched. Residential bandwidth is sold by the gigabyte and passes a dollar on the first day of any crawl worth rotating for; the rotation still fetches every page one at a time, so it buys no speed; and the pass is flat with nothing to maintain. Someone who spends more on proxies than the pass costs, to avoid the pass, has made an arithmetic mistake, and the throttled page is where we point that out. Detection is a race that re-runs every time someone changes tactics. Price is not. Where a caller can be identified properly, `identify` keys the allowance on an API key or account and it becomes exact. TWO ORDERING CHANGES THAT MATTER. A valid pass is now read before anything that can refuse, so a pass holder is neither charged as a crawler nor metered. It used to be read only after the crawler lists matched, which was correct while the lists were the sole reason to refuse and is not once an allowance exists. The sales page, robots.txt and .well-known stay reachable with the allowance spent. Being unable to reach the page that sells the fix would be the worst possible failure of a throttle whose whole purpose is to sell something. `handle` still answers with a Response or nothing. Growing that contract to smuggle RateLimit headers out on the pass-through path would have broken every adapter that checks it for truth, so the allowance is advertised on the 402, which is where it is read anyway. 21 new tests, including that a pass lifts the throttle, that a training crawler is still charged on its first request rather than getting a generous free tier as a loophole, that an unidentifiable caller counts as within the allowance rather than being charged for our own gap, and that a throttled reader is not called a training crawler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKuF2jCbRj3GZTQVtwhi5m
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.
Named crawlers were the only thing this could charge. The other half of the traffic is an ordinary client hammering a site far past what a reader would, and the usual answer to that is
429 Too Many Requests: go away and try later. That is the right answer when you have nothing to sell. Here we do.Past the allowance the gateway answers 402 with the offer, the RateLimit headers, and a body naming what ran out, when it comes back, and what a pass costs. The moment a caller runs out of free requests is the best sales pitch a site will ever get: it has just demonstrated it wants more than the free tier and is still holding the request.
On rotating addresses, which defeats this
The default identity is the caller's address, so a proxy rotation walks straight past it. That is deliberate and documented rather than patched. The arithmetic already argues for paying:
Someone who spends more on proxies than the pass costs, in order to avoid the pass, has not been defeated by cleverness. They have made an arithmetic mistake, and the throttled page is where we point it out. Detection is a race that re-runs every time someone changes tactics. Price is not.
Set
benefitsto the things a rotation genuinely cannot get, a bulk endpoint above all, and evasion stops being worth the effort rather than being blocked. Where a caller can be identified properly,identifykeys the allowance on an API key or account and it becomes exact.Two ordering changes that matter
A valid pass is now read before anything that can refuse, so a pass holder is neither charged as a crawler nor metered. It used to be read only after the crawler lists matched, which was correct while the lists were the sole reason to refuse and is not once an allowance exists.
The sales page,
robots.txtand.well-known/stay reachable with the allowance spent. Being unable to reach the page that sells the fix would be the worst possible failure of a throttle whose entire purpose is to sell something. There is a test for it.A contract I deliberately did not grow
handle()still answers with aResponseor nothing. I first had the pass-through path return a bare object carrying RateLimit headers, which would have broken every adapter that checks the return value for truth. The allowance is advertised on the 402 instead, which is where it gets read anyway.Tests
21 new, 76 total, all green. The ones worth naming:
🤖 Generated with Claude Code
https://claude.ai/code/session_01TKuF2jCbRj3GZTQVtwhi5m