onSale is awaited: say so, and prove it - #5
Merged
Merged
Conversation
The hook has always been awaited before the receipt goes out, inside a try/catch, but the JSDoc promised the opposite. That matters where it is most likely to be relied on: an edge runtime can stop the moment a response is sent, so 'fire and forget' there means the sale is silently lost. Two tests now hold the behaviour down, including that a throwing hook still sells the pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE
ralyodio
added a commit
to profullstack/tipoffwatch.com
that referenced
this pull request
Sep 6, 2026
The gateway awaits onSale before sending the buyer's receipt (its JSDoc said otherwise; profullstack/x402-gateway#5 fixes the comment and tests the behaviour). Dropping the promise made the write fire and forget, which is fine while a long-lived server keeps running and silently loses the sale anywhere the runtime can stop once the response is sent. Returning it makes the sale land first. A rejection is still swallowed by the gateway, so a database failure never costs a buyer the pass it paid for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE
ralyodio
added a commit
to profullstack/genrewatch.com
that referenced
this pull request
Sep 6, 2026
The gateway awaits onSale before sending the buyer's receipt (its JSDoc said otherwise; profullstack/x402-gateway#5 fixes the comment and tests the behaviour). Dropping the promise made the write fire and forget, which is fine while a long-lived server keeps running and silently loses the sale anywhere the runtime can stop once the response is sent. Returning it makes the sale land first. A rejection is still swallowed by the gateway, so a database failure never costs a buyer the pass it paid for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE
ralyodio
added a commit
to profullstack/tipoffwatch.com
that referenced
this pull request
Sep 6, 2026
* Write down what the crawler paywall does, and put it on a public board The gateway has been answering 402 and selling day passes for weeks and none of it was recorded: a sale existed only for as long as the response took to send. So 'what did the paywall earn' and 'which agent is the customer' had no answer. Two tables now hold it. A sale is money (crawl_sales, unique on the payment ref so a settlement delivered twice books once). A refusal is demand (crawl_demand, counted per agent per day rather than stored per request, so thousands a day stay small enough to keep forever). Both writes are fire and forget. Bookkeeping must not fail a payment the buyer already made, or turn a 402 into a 500. The board shows the two sides apart: agents paying ranked by money, agents not paying ranked by volume. The same agent can sit on both, and the numbers are never pooled, because 'paid us $1' and 'asked 5,000 times for free' are not the same fact. No commission rate anywhere, since nobody earns here. /leaderboard is in openPaths with and without the trailing slash: the gateway prefix-matches only entries ending in a slash, so the bare path would open the index and still charge for every board on it. Verified against a throwaway Postgres: all 34 migrations apply, a duplicate ref books once, five refusals make one row of five, a live 402 from ClaudeBot increments crawl_demand, and GPTBot reads the board at 200 while / stays 402. A DATE column bound as a JS Date 500s the whole board, so the day is sent as a day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE * Return the sale promise, so the write lands before the receipt The gateway awaits onSale before sending the buyer's receipt (its JSDoc said otherwise; profullstack/x402-gateway#5 fixes the comment and tests the behaviour). Dropping the promise made the write fire and forget, which is fine while a long-lived server keeps running and silently loses the sale anywhere the runtime can stop once the response is sent. Returning it makes the sale land first. A rejection is still swallowed by the gateway, so a database failure never costs a buyer the pass it paid for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio
added a commit
to profullstack/genrewatch.com
that referenced
this pull request
Sep 6, 2026
* Write down what the crawler paywall does, and put it on a public board Ported from the tipoffwatch change. The gateway has been answering 402 and selling day passes without recording any of it, so neither the revenue nor the customer survived the response. Two tables: crawl_sales (money, unique on the payment ref so a settlement delivered twice books once) and crawl_demand (refusals, counted per agent per day rather than stored per request). Both writes are fire and forget, because bookkeeping must not fail a payment or turn a 402 into a 500. The board shows the sides apart: agents paying ranked by money, agents not paying ranked by volume. The same agent can sit on both and the numbers are never pooled. This gateway had no openPaths at all, so the board needed them added, with and without the trailing slash. Verified against a throwaway Postgres: 19 migrations apply, a duplicate ref books once, five refusals make one row of five, a live 402 from CCBot increments crawl_demand, GPTBot reads the board at 200 and still gets 402 on /. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE * Return the sale promise, so the write lands before the receipt The gateway awaits onSale before sending the buyer's receipt (its JSDoc said otherwise; profullstack/x402-gateway#5 fixes the comment and tests the behaviour). Dropping the promise made the write fire and forget, which is fine while a long-lived server keeps running and silently loses the sale anywhere the runtime can stop once the response is sent. Returning it makes the sale land first. A rejection is still swallowed by the gateway, so a database failure never costs a buyer the pass it paid for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio
added a commit
to profullstack/rssamplifier.com
that referenced
this pull request
Sep 6, 2026
traffic_hourly has counted who asks and how many we turn away since 2026-09-02. The half it could never answer is the half with money in it: the gateway has been selling day passes and writing none of it down, so a sale existed only for as long as the response took to send. crawl_sales is that half. One row per sale, unique on the payment ref so a settlement delivered twice books once. The hook returns its promise rather than dropping it, because the gateway awaits onSale before the receipt goes out (its JSDoc said otherwise; profullstack/x402-gateway#5), and a rejection is swallowed there, so a database failure still sells the pass it was paid for. The board projects both tables instead of keeping a third copy that could disagree with either. Only badges get a table, being awarded rather than derived. Sales are tagged with classifyAgent, so a family reads the same on both sides. The two sides never share a list. Agents paying are ranked by money; agents asking are ranked by requests and by refusals. 'Paid us $3' and 'asked 209,000 times' are not the same fact. /leaderboard is in OPEN_PATHS with and without the trailing slash: the gateway prefix-matches only entries ending in a slash, so the bare path would open the index and still charge for every board on it. Verified against a local libSQL file: the migration applies, a duplicate ref books once, a sale with no payer still lands, the route handler serves both sides off real traffic_hourly rows, RSS names the side, a share card renders and an unknown board 404s. The existing 382 web tests still pass. Claude-Session: https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The
onSalehook has always been awaited before the buyer's receipt is sent, inside a try/catch. The JSDoc promised the opposite:That is a dangerous thing to get backwards. Anyone reading the old comment would write a fire-and-forget hook, and in the runtime where this gateway most often sits, that silently loses sales: an edge function can stop the moment the response is sent, so an un-awaited database write never lands. The sale is gone and the buyer still has the pass.
Two tests now hold the real behaviour down:
onSalefinishes before the 200 receipt is returnedonSalestill sells the pass, because accounting must never cost a buyer what it paid forNo behaviour change, so this is a patch. Found while wiring the sales ledger on tipoffwatch and genrewatch, which record a sale in this hook.
🤖 Generated with Claude Code
https://claude.ai/code/session_0144uEVbZK3jdaQkwcLYTXPE