Conversation
Every distinct Origin got its own bucket with the full default allowance. The header is supplied by the caller and, for anything that is not a browser, entirely attacker-controlled — so a caller could send a value nobody had seen, receive a fresh allowance, and repeat. The number of buckets was unbounded and so was the spend. The budget was decorative. Found while verifying that dataprovenance-app needs no change: a request carrying Origin: https://evil.example was not merely allowed, it was allocated its own separate budget alongside the app's. Now only origins NAMED in POOL_DAILY_ALLOWANCES get a bucket of their own, which is what naming them is for. Everything else — unlisted origins and callers sending no Origin at all — shares one bucket. A CLI is not privileged over an unknown website, and neither can take the app's allowance. Still per size in both cases, since sizes differ in cost by powers of two. The response now reports which bucket was charged. An unlisted caller sharing the common budget may find it already spent by somebody else, and that is much less confusing when the response says so rather than appearing to be about them. Four tests, including that the shared bucket does not swallow the configured origins — the fix must not solve one problem by creating the opposite one. Full suite: 1107 passed, 25 skipped.
* Let callers pay for a pooled batch The pool was free-within-a-budget or nothing. A caller who exhausted the daily allowance had no route except waiting for midnight, and no way to pay for more — on a gateway whose whole purpose is selling access to Swarm storage. The mechanism was already half-built. The acquire handler has always read request.state.x402_payer and registered the batch to that wallet when present, but the payment dependency was never attached to the pool router, so the attribute was always None and every acquire fell to the "shared" path. That branch has never executed in production. require_x402_payment could not simply be attached. It answers 402 to any caller presenting neither an X-PAYMENT header nor X-Payment-Mode: free, and the callers this endpoint exists for present neither — a browser app sends what the browser sends. Attaching it would have broken every existing caller in exchange for making payment possible. So settle_payment_if_offered: a payment presented is verified and settled exactly as elsewhere; no payment presented is not an error, and the handler falls through to the daily allowance. A paid acquire bypasses the allowance and consumes none of it. The allowance bounds what the operator gives away; it has no business limiting what someone has paid for. POOL acquire joins PROTECTED_ENDPOINTS so it has a price, which is what that list means — not that a request without payment is refused. A test pins that distinction, and that the router carries the optional dependency rather than the mandatory one, because swapping them looks harmless and would 402 every existing caller. The exhausted-allowance message now offers the paid route on THIS endpoint rather than sending the caller to direct purchase with its minute of latency. That was the honest answer before paying here was possible; it no longer is. Full suite: 1109 passed, 25 skipped. * Charge a premium for a pooled batch, and price it at all Two things, one of which was a plain defect. /pool/acquire matched no branch in the pricing dispatcher and fell through to the final return: X402_MIN_PRICE_USD, described as "Gateway operation", regardless of what was requested. A depth-20 batch costs eight times a depth-17 one, so the quote bore no relation to the batch handed over. It was harmless only because paying for a pooled batch was impossible until now — the moment payment works, it would have sold every size at the floor. It is now priced from the requested depth, then marked up by X402_POOL_MARKUP_PERCENT, default 100 (double). The premium is the point rather than an afterthought. The pool is a different product from a purchase: the capital is already spent, the batch is kept alive whether or not anyone takes it, and it may expire unused. What the caller buys is the absence of the minute an on-chain purchase takes, and that convenience has a real cost behind it. Priced from what the pool PAID — STAMP_POOL_DEFAULT_DURATION_HOURS + 1, matching _purchase_stamp, where the extra hour clears Bee's minimum-validity floor — not from the TTL the caller receives. Pricing off the remaining TTL would fluctuate per request and would charge least for the batch that had cost most to keep alive. A negative markup cannot discount below cost; misconfiguration should not sell batches for less than they cost. Pinned by a test. At the current chain price, with production's 24h setting: a pooled small quotes $0.0145 at cost and $0.029 with the premium, against $0.0139 for a direct 24h purchase. A medium is eight times that. Full suite: 1114 passed, 25 skipped. * Write the pool markup to the servers X402_POOL_MARKUP_PERCENT was not in deploy.yml, so setting it as a repository variable would have had no effect — the same propagation trap that already caught the allowance settings, checked for this time rather than discovered afterwards. Defaults to 100 in the workflow, matching the code, so a host that has not set the variable behaves identically either way.
/pool/acquire lets a settled x402 payment bypass the daily allowance, on the reasoning that the budget bounds what the operator gives away rather than what someone has paid for. That reasoning only holds where the payment is worth something. Production runs X402_NETWORK=base-sepolia against the public x402.org facilitator. USDC there is free from a faucet, so shipping the bypass unguarded would have replaced a bounded giveaway with an unbounded one: any caller could top up for nothing and draw batches bought with real BZZ, capped only by STAMP_POOL_MAX_PURCHASES_PER_HOUR — about 4 BZZ a day against a wallet holding 16. The bypass is now honoured only on a network recognised as mainnet. X402_ALLOW_TESTNET_PAID_BYPASS=true overrides it so staging can still exercise the paid path. An unrecognised network name counts as a testnet: refusing a real payment is recoverable, giving away batches is not. The payment is still settled and the batch still registered to the payer — only the bypass is withheld, so the caller keeps its normal allowance instead of being refused outright. The 429 message is conditional to match: where paying would not help, it points at direct purchase rather than sending someone to a path that takes their payment and still refuses them. 1120 passed, 25 skipped.
The message told the caller a direct purchase 'costs the same'. It does not necessarily: pool acquires carry X402_POOL_MARKUP_PERCENT (100) while a direct purchase carries X402_MARKUP_PERCENT (50). Say what is actually true — the direct route is not drawn from the pool, so the limit does not apply — and name the tradeoff that is real, which is latency.
* Withhold the pool allowance bypass for testnet payments /pool/acquire lets a settled x402 payment bypass the daily allowance, on the reasoning that the budget bounds what the operator gives away rather than what someone has paid for. That reasoning only holds where the payment is worth something. Production runs X402_NETWORK=base-sepolia against the public x402.org facilitator. USDC there is free from a faucet, so shipping the bypass unguarded would have replaced a bounded giveaway with an unbounded one: any caller could top up for nothing and draw batches bought with real BZZ, capped only by STAMP_POOL_MAX_PURCHASES_PER_HOUR — about 4 BZZ a day against a wallet holding 16. The bypass is now honoured only on a network recognised as mainnet. X402_ALLOW_TESTNET_PAID_BYPASS=true overrides it so staging can still exercise the paid path. An unrecognised network name counts as a testnet: refusing a real payment is recoverable, giving away batches is not. The payment is still settled and the batch still registered to the payer — only the bypass is withheld, so the caller keeps its normal allowance instead of being refused outright. The 429 message is conditional to match: where paying would not help, it points at direct purchase rather than sending someone to a path that takes their payment and still refuses them. 1120 passed, 25 skipped. * Drop an unbacked price claim from the exhausted-allowance message The message told the caller a direct purchase 'costs the same'. It does not necessarily: pool acquires carry X402_POOL_MARKUP_PERCENT (100) while a direct purchase carries X402_MARKUP_PERCENT (50). Say what is actually true — the direct route is not drawn from the pool, so the limit does not apply — and name the tradeoff that is real, which is latency.
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.
Promotes the pool work to production. Merge #328 into
devfirst — this PR tracks the branch and will pick it up automatically. Without it, this fixes one hole and opens another.Why this is worth doing now
Production's daily allowance is currently decorative.
main'spool_allowance._key()gives every distinctOriginstring its own 20-a-day budget, so a caller can senda1.example, thena2.example, and keep drawing pooled batches indefinitely. The only backstop isSTAMP_POOL_MAX_PURCHASES_PER_HOUR(10) — roughly 4.3 BZZ a day against a wallet holding 16.35 BZZ.What lands
Unlisted origins share one bucket (#326). Only origins named in
POOL_DAILY_ALLOWANCESget a budget of their own; everything else sharesPOOL_DEFAULT_DAILY_ALLOWANCE. Rotating the header stops minting new budgets.Originremains attribution, not authentication — a browser cannot forge another site's, which is what stops other websites spending our postage, but any non-browser client can claim any origin. The budget is the control, not the header.Paid pool access (#327, closes #67).
/pool/acquireis priced from what the pool actually paid —STAMP_POOL_DEFAULT_DURATION_HOURS + 1at the requested depth — plusX402_POOL_MARKUP_PERCENT, set to 100 on both environments. Previously the path matched no branch in the pricing dispatcher and fell through toX402_MIN_PRICE_USD, so a depth-20 batch was quoted the same as the floor. The router usessettle_payment_if_offeredrather thanrequire_x402_payment, because the latter answers 402 to any caller sending no payment header — which is every current caller.The bypass is withheld on testnets (#328). See that PR: production runs
X402_NETWORK=base-sepoliaagainst the publicx402.orgfacilitator, where USDC is free from a faucet.Configuration
Already set on both environments, so no post-merge work:
POOL_DAILY_ALLOWANCEShttps://dataprovenance.app=20https://dev.dataprovenance.app=20POOL_DEFAULT_DAILY_ALLOWANCEX402_POOL_MARKUP_PERCENTX402_ALLOW_TESTNET_PAID_BYPASSdeploy.ymlwrites all four, so they reach the containers rather than sitting in GitHub doing nothing.Tests
1120 passed, 25 skippedin 21s, run againstdevlocally. There is no CI test gate on this repo.Known limitations, deliberate
One batch in the pool.
STAMP_POOL_RESERVE_SMALL=1on production. A paying caller who arrives while the single batch is out gets a 409 until the replacement lands, about a minute. Raising the reserve costs standing BZZ for demand that has not appeared yet, so this is worth revisiting once paid access sees real use rather than pre-emptively.Paid access earns nothing yet. With production on
base-sepolia, the markup is correct code waiting on a configuration decision. Nothing currently tracks moving to Base mainnet; that needs its own issue.