Meter every route, and pay partners what a pass was actually worth - #36
Merged
Conversation
Two things, and the second one was found on the way to the first.
**The throttle.** The gate charges crawlers that say who they are. Nothing
charged the ones that do not, and nothing counted a page route at all. That
is the shape that failed on coinpayportal on 2026-09-08: a headless browser
found a route nobody had listed and walked 19,000 of its URLs a day for two
days, declaring nothing, tripping no list. So @profullstack/throttle now
meters every route at 100 requests a minute per caller, answered 402 with
this buyer's own offer rather than 429.
The price here is the buyer's own -- a dollar a day at list, less the more it
has spent -- so the gateway is chosen per request, and a refusal has to quote
the price that buyer would actually pay. One throttle per gateway, sharing
ONE store: if the counting split along with the price, a caller crossing a
discount threshold mid-window would be handed a fresh hundred requests for
the privilege. Both halves are tested.
**The bug.** apps/web asked for x402-gateway ^0.1.0 and got 0.1.0, whose
onSale payload has no totalCents and no days; both arrived in 0.3.0 with
multi-day passes. pricing.js reads them anyway, and everything downstream
treats a missing figure as nothing:
partners.js:95 !Number(undefined) is true, so splitSale returns 0 and
the publishers whose writing was crawled are paid nothing
attribution.js:47 totalCents 0, so no niche revenue is booked and no
operator is owed a share
queries.js:1006 the crawl_sales row stores a null total_cents
So the seller side of the marketplace has been recording sales it never paid
out on, for as long as passes have been sold. Bumping to ^0.6.0 fixes it
going forward; the rows already written are still wrong, and crawl_sales
wants a look for null total_cents before anyone trusts that ledger.
A caret range on a 0.x version only matches patches, which is why ^0.1.0 sat
five minors behind without anything ever saying so.
510 tests pass, seven of them new.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDGCxTmEPs3ecwjjLJDQXh
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
The 402 test passed here and failed in CI. gatewayAt memoises one gateway per price, and pricing.js builds the list-price one at import -- so when pricing.test.js loaded first, the cached list-price gateway had been built before this file set COINPAY_X402_KEY, could not take money, and every refusal was a 429. Locally the files happened to load the other way round. Asking for a price nothing else uses means the gateway is always constructed after the environment is set. Verified in both file orders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDGCxTmEPs3ecwjjLJDQXh
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.
Two things, and the second was found on the way to the first.
🚨 The bug: every crawl pass sold paid partners nothing
apps/web/package.jsonasked for"@profullstack/x402-gateway": "^0.1.0"andbun.lockresolved 0.1.0. That version'sonSalepayload is:No
totalCents. Nodays. Both arrived in 0.3.0 with multi-day passes.lib/pricing.jsreads them anyway, and everything downstream treats a missing figure as nothing:partners.js:95!Number(undefined)istrue→splitSalereturns 0; the publishers whose writing was crawled are paid nothingattribution.js:46-47Number(undefined) || 0→ no niche revenue booked, no operator owed a sharequeries.js:1006crawl_salesrow stores a nulltotal_centsSo the seller side of the marketplace has been recording sales it never paid out on, for as long as passes have been sold.
Bumping to
^0.6.0fixes it going forward. The rows already written are still wrong —crawl_saleswants a look for nulltotal_centsbefore anyone trusts that ledger.A caret range on a
0.xversion only matches patches, which is why^0.1.0sat five minors behind without anything ever saying so.The throttle
The gate charges crawlers that say who they are. Nothing charged the ones that do not, and nothing counted a page route at all.
That is the shape that failed on coinpayportal on 2026-09-08: a headless browser found a route nobody had listed and walked 19,000 of its URLs a day for two days, declaring nothing, tripping no list.
@profullstack/throttlenow meters every route at 100 req/min per caller, answered402with this buyer's own offer rather than429./auth//healthz/mcpOne throttle per gateway, sharing one store. The price here is the buyer's own, so the gateway is chosen per request and a refusal has to quote the price that buyer would pay. But if the counting split along with the price, a caller crossing a discount threshold mid-window would be handed a fresh hundred requests for the privilege. Both halves have a test.
Verified
bun test— 510 passed, 0 failed, across 25 files. Seven new tests intest/throttle.test.js, driven through the site's owngatewayAt()rather than a stand-in, since the per-buyer price is the part worth testing.The gateway also deduped:
^0.5.0on the throttle side could not resolve 0.6.0 (caret on 0.x is patch-only), so a nested second copy was being installed until@profullstack/throttle@0.2.2raised that floor.🤖 Generated with Claude Code