x402 on BSV: the facilitator is the recipient's wallet
Every x402 scheme we have audited so far lets a third-party facilitator verify and submit the buyer's payment. A proposed scheme for BSV cannot, by construction: the destination is derived from a secret only the payer and the recipient share, so the facilitator has to be the recipient's own wallet. That choice, and the decision to ship a whole transaction in a header, is where the interesting questions start.
PR #2890 adds an exact scheme for BSV (Bitcoin SV) plus a TypeScript reference package, @x402/bsv. It was opened on 2026-07-17 by sirdeggen and is still open as of today, 2026-10-24. At the head we read, 22b6695 (a merge of upstream main on 2026-10-23), it has 49 commits and 43 changed files. It has one approving review, from andyrowe (who runs a BSV x402 service of their own) on 2026-08-19, and 39 comments. The description discloses that it was drafted with AI assistance and reviewed by the author, and that an adversarial review pass produced 24 fixes.
We read the spec (specs/schemes/exact/scheme_exact_bsv.md, 173 lines), the facilitator and money-parser code, the PR thread, and the upstream BRC-121 profile it adapts. We then computed what the payload does to a header. This post covers five things: what is signed, who can verify it, how big the header gets, how time and replay are handled, and what "settled" means.
What the buyer sends
On EVM the buyer signs a few hundred bytes of typed data. On BSV the buyer's wallet builds, funds and signs a real transaction: one P2PKH output of exactly amount satoshis, with the buyer paying the miner fee. There is no gas sponsorship and no token contract. asset is the literal string BSV.
The transaction travels as BEEF, a serialization that carries the transaction plus its SPV ancestry (the parent transactions and Merkle paths needed to validate it without a node). The payload has five fields:
// payload (PAYMENT-SIGNATURE, v2 only)
{
transaction: "AQEBAQFXpc4W8...", // base64 BEEF / Atomic BEEF
derivationPrefix: "Cn0z2Q08xUzc", // random nonce, min 8 bytes
derivationSuffix: "MTcwMDAwMDAwMDAwMA==", // base64(decimal unix ms)
senderIdentityKey: "02ab1c...", // 33-byte compressed key, hex
outputIndex: 0
}
payTo is not an address. It is the recipient wallet's BRC-100 identity public key, 66 hex characters, and it never appears on chain. Each payment pays a fresh child key derived with BRC-42 from that identity key, the prefix, and the suffix. The suffix is not opaque: it must decode to the decimal Unix-millisecond time at which the client built the payment. That one field does double duty as a derivation input and as the freshness proof.
Why the facilitator has to be the recipient
BRC-42 derivation uses an ECDH shared secret between payer and recipient. An outside party sees a P2PKH output and cannot tell whether it pays the key derived from payTo. Verification rule 7 in the spec is exactly that check, and the spec says plainly that a facilitator without the recipient wallet cannot perform it.
So the spec adds rule 2: payTo must equal the identity key of the facilitator's own wallet, and a facilitator must refuse payments for keys it does not hold. settle then calls the wallet's internalizeAction, which SPV-validates the BEEF, derives the child private key, confirms the output pays it, and takes custody. The spec lists two deployments: in-process inside the resource server, or a self-hosted facilitator service. A shared multi-tenant facilitator is out of scope. The PR notes two extension paths for that case, both unimplemented: a plain-P2PKH addressing variant (which drops the privacy model) or a key-linkage proof (BRC-69, verifiable with the Schnorr proof of BRC-94) that would let a third party verify the destination without custody.
Compare this with the rails we have audited. On EVM, the facilitator pays gas and submits a transfer. On Solana it co-signs as fee payer. On Nano it broadcasts a block the buyer already made valid. In all three the facilitator is a service the seller can swap out. Here it is a wallet holding the seller's funds, in the seller's own process or infrastructure. There is no separation between the party that verifies and the party that is paid.
That has a real upside. There is no third party to trust with custody, and a payment cannot be redirected. It also has a cost: the "facilitator ecosystem" that x402 assumes (shared, hosted, interchangeable) does not exist for this scheme. A seller who wants BSV runs a hot wallet.
The payload is a transaction, and headers have limits
The x402 v2 HTTP transport puts the whole PaymentPayload in PAYMENT-SIGNATURE as base64-encoded JSON. The BSV payload contains transaction, which is itself base64 BEEF. So the BEEF is base64-encoded twice: once inside the JSON, once around the JSON. BRC-121, the profile this scheme adapts, sends the BEEF in an x-bsv-beef header encoded once. The x402 wrapping is strictly bigger.
The size of a BEEF is not set by the payment. BRC-62 requires every direct parent transaction in full, so it is set by the history of the coin the wallet picked. A September report in the PR thread, by the team behind the Hodos browser wallet, measured this on mainnet with three consecutive payments from one wallet to one site:
BEEF bytes base64 in header result
779 ~1.0 KB 200
92,837 123,784 B 431 "Exceeded maximum HTTP header buffer size of 100KB"
849 ~1.1 KB 200
The middle payment was 150 satoshis, between the other two in amount. It was an outlier only in which coin funded it. The same report is on the BRC-121 issue tracker. It adds three points worth keeping. The usable budget is not the quoted cap, because base64 costs a third and the cap covers the whole header block (cookies, user agent and the other x-bsv-* headers), which they put near 68 to 73 KB of BEEF on a 100 KB limit. A "prefer small parents" rule is an ordering, not a guarantee: theirs was calibrated to a 1 MiB relay and let the 92,837-byte bundle through. And the failure grows as a wallet is used.
We did the arithmetic for x402. We took the spec's own example PaymentPayload, replaced transaction with a zero-filled BEEF of a given size, JSON-encoded it, and base64-encoded the result. This is our computation, not a measurement of a live server, and a real payload adds a few hundred bytes of fields.
BEEF bytes BEEF as base64 PAYMENT-SIGNATURE value
779 1,040 2,048 B
849 1,132 2,172 B
92,837 123,784 165,708 B
The same 92,837-byte bundle that overflowed a 100 KB limit in BRC-121 form becomes about 165 KB in x402 form. Turning the question around, the largest BEEF that fits a single PAYMENT-SIGNATURE header line (name, separator and value) under common limits is roughly:
limit on the header line max BEEF
8,190 B (aiohttp default) ~4.2 KB
16,384 B (Node 16 KiB) ~8.8 KB
102,400 B (100 KB) ~57.2 KB
These are ceilings for the one header alone. Cookies and every other header come out of the same block. The two small payments above (779 and 849 bytes) clear all three. A BEEF with one large parent clears none of the first two and, at 92,837 bytes, not the third either.
We covered the 8,190 and 16 KiB limits when we looked at x402 from the browser. There the problem was a PAYMENT-REQUIRED that grew with the seller's catalog. Here the growth is on the buyer's side, driven by wallet state the seller cannot see and the 402 response cannot announce. The Hodos report asks BRC-121 to publish a size bound, let the 402 advertise one, and have senders check the whole header block before minting. It also floats moving the bundle to the request body, which it acknowledges is a wire-format change. We would add one point for x402: a size hint belongs in the PaymentRequirements extra field, which this scheme currently leaves empty, so a buyer can refuse before it builds anything.
Time: one field, two jobs
Freshness is checked against the verifier's clock. The default window is 30 seconds (DEFAULT_PAYMENT_WINDOW_MS = 30_000) and it is symmetric: a timestamp more than 30 seconds in the future is rejected too. At settle the past-facing side is extended by maxTimeoutSeconds, so a payment that verified cannot expire while the handler runs. With the spec's example maxTimeoutSeconds of 60, the settle deadline is 90 seconds after the timestamp.
Two implementers in the thread pushed on this. The Hodos wallet caches a minted payment for 25 seconds per URL and amount, which leaves five seconds for round-trip time and clock skew between payer and verifier. A payment reused at 24 seconds against a verifier running a few seconds fast fails as stale, and the failure looks intermittent. The spec is silent on whether a client may reuse a payload at all. The second implementer, who runs a plain-P2PKH variant of BSV exact on mainnet, argues the timestamp is mostly importing clock-skew fragility, because replay is already caught by txid dedup. They suggest anchoring freshness to a server-issued absolute expiry in the challenge so the payer's clock drops out. As of the last commit we read, the spec text has neither change.
The replay defence is layered and worth reading closely. The facilitator keeps an in-memory map of settled txids. In the reference code the entry lives for the settlement window plus a 60-second margin, with a floor of 600,000 ms. The entry is written before internalizeAction runs, so two concurrent /settle calls cannot race the wallet, and it is deleted if the wallet throws or rejects. On top of that the wallet's isMerge result is used, but only together with a newly internalized satoshi count of zero. A self-payment returns isMerge: true on its first settle, so the flag alone would reject a legitimate payment. And isMerge is a wallet-toolbox extension, not part of core BRC-100, so a conforming wallet may not report it.
The consequence is in the spec's own words: the dedup record is process-local, so operators running several instances or restarting between attempts must route a payment to a consistent instance or use a shared record. That is the same shape as the dedup-in-whichever-layer-nobody-reviews finding in our Sui audit. Nothing on chain stops a second settle of the same BEEF into a wallet that does not report merges. The facilitator's memory does.
What "settled" means: zero confirmations
success: true reflects wallet acceptance of a transaction that is typically not yet mined. The spec states this directly: settlement is zero-confirmation, the payer holds the keys to the inputs until the transaction is mined, and operators should price resources accordingly. It recommends propagating through a broadcaster such as ARC promptly after internalization, and allows holding success until broadcast acceptance. That is a weaker promise than the Base case we measured in our settlement finality audit, where a preconfirmed receipt still precedes finality. Here the buyer could in principle spend the same coin elsewhere.
The honest summary is that the scheme is fit for what BRC-121 targets, small per-request amounts, and says so. It is not a scheme where success can be taken as final for a large payment.
Two smaller rules that deserve credit
Strict equality. BRC-121 accepts overpayment. The x402 scheme requires the output to carry exactly amount. A reviewer who built a BSV merchant gateway argued for this in the thread: a checkout has human recovery for a mismatch (a refund, a top-up), and a machine 402 exchange has none, so equality leaves only two outcomes, paid or retry. Quote drift is handled by pinning the amount in the challenge and bounding it with the freshness window.
Refusing ambiguous chains. BSV, BTC and Bitcoin Cash share a genesis block, so a genesis-only bip122:000000000019d6689c085ae165831e93 cannot say which chain it means. The scheme uses the registered bsv CAIP-2 namespace (ChainAgnostic/namespaces#190, merged 2026-08-18) with bsv:mainnet, bsv:testnet, bsv:ttn and bsv:tstn, and requires clients and facilitators to refuse ambiguous Bitcoin-family identifiers rather than default to BSV. A wrong-chain payment of the right amount is unrecoverable, so a hard refusal is the right default. The facilitator also checks the wallet's getNetwork() against the requested network. The earlier interoperable address audit made the same argument for payTo on other chains.
A dependency on a rate API
Prices are set in satoshis. For dollar prices the package ships createWhatsOnChainMoneyParser, which converts USD to satoshis from the WhatsOnChain exchange-rate API. It caches a rate for 60 seconds and, if a refresh fails, serves a cached rate for up to 600 seconds before throwing. The PR description's live smoke test priced $0.001 at 7,440 satoshis with BSV at $13.44, which matches the arithmetic. Because the amount is pinned in the requirements, the rate can move without breaking an in-flight payment.
The parser has no cached rate on cold start, so a failed first fetch throws. We called the same endpoint twice from this host today and got HTTP 429 both times. That may be an artefact of a shared address, and we only note it as a reminder that an unauthenticated third-party feed is part of the seller's availability path.
What it means for LLM4Agents
We do not run a BSV rail and this post is not a plan to. The PR is open, its only approval is from another BSV implementer, and the Go and Python ports are explicitly deferred. But the scheme exposes three assumptions our stack currently inherits from EVM and Solana.
First, that the facilitator is a swappable service. For BSV it is a hot wallet inside the settlement path. If any future rail looks like this, "the facilitator" stops being a vendor choice and becomes custody we operate, with the key-management and incident-response work that implies.
Second, that the payment header is small. Our buyers send one signed authorization. A rail whose payload embeds a transaction makes header size a function of wallet state. The right place to catch that is a size check on the buyer side before anything is signed, and a limit announced in the 402.
Third, that a timestamp in the signed object is cheap. It is only cheap if verifier and payer clocks agree. Any rail that stamps time into the payload moves clock skew into our failure budget, and the failure is intermittent by nature.
Staying on the frontier
Concrete steps, in order.
One. Add a payload-size assertion to the buyer test harness: for each scheme, build the largest realistic payload and assert the encoded PAYMENT-SIGNATURE line stays under 8,190 bytes, and report the margin. Today it is a formality. It becomes the gate for any scheme that carries a transaction.
Two. Add a clock-skew matrix to the same harness: shift the payer clock by plus and minus 5, 15 and 30 seconds against any scheme that carries a timestamp, and record which combinations reject. That turns the 25-second caching argument in this thread into a test we can run on our own rails.
Three. Write down, per rail, what success: true promises: final, preconfirmed, or accepted-but-unmined. Our finality work covers EVM and Solana. Zero-confirmation is a third class, and a price floor rule for it should exist before a rail does.
Four. Keep the facilitator interface honest about who holds the key. If we ever abstract over rails, the interface should let a rail declare "verification requires the recipient's key", so routing never sends such a payment to a shared facilitator.
Five. Watch three things on #2890: whether the spec adds a reuse rule or an absolute-expiry freshness model, whether anyone proposes a size hint or moves the BEEF out of the header, and whether known-answer vectors appear for the exact-amount check, the millisecond suffix and the replay signals. A reviewer in the thread offered to write them once a second implementation exists. Without vectors, two ports will disagree on exactly those rules.
Six. Keep the neighbours in view. The Canton and Bitcoin Cash scheme PRs are also open. Each native-coin rail answers the same questions about who verifies, what is signed, and when it is final. We now have three answers on file, Nano, Sui and BSV, and they disagree.
Pay per call, in stablecoins, over an OpenAI-compatible API
One 402, one signature, 345+ models. No subscription, no minimum.
Register your agent