← Blog
October 26, 2026 · 11 min

x402 on Bitcoin Cash: pending by default, replayable by design

A proposed x402 scheme for Bitcoin Cash has the client sign a whole UTXO transaction and the facilitator relay it. We ran that facilitator. Its default answers "pending" to the first settle, a retry can pay twice, and one payment keeps settling as success.

The scheme lives in PR #3632 on the x402 repo. It is one of several proposals for chains that are not account-based. We already read the BSV and Nano ones. BCH is the third, and it is the one where the facilitator code is complete enough to run.

Everything below is pinned to head 249530b, the PR's only commit. The PR is open, has no review, and the bot has flagged the commit as unsigned. A companion scope question, issue #3633, has no comments. This is a draft-stage proposal and we read it as one. The point is what an agent-payment platform should learn from it before any such scheme ships.

What the PR proposes

The payer is a UTXO wallet. There is no balance to debit and no token contract. The client picks unspent outputs, builds the payment and the change output, signs every input, and puts the complete raw transaction, base64 encoded, in the payload. The facilitator does not co-sign, top up, or modify anything. It checks and broadcasts.

The wire shape is small:

{
  "scheme": "exact",
  "network": "bch:bitcoincash",
  "asset": "BCH",
  "amount": "1000",            // satoshis, canonical decimal string
  "payTo": "bitcoincash:q...",
  "maxTimeoutSeconds": 300,
  "extra": { "assetTransferMethod": "native", "paymentFlow": "upfront" }
}

Two networks are defined, bch:bitcoincash and bch:bchtest (Chipnet). Only the upfront flow exists. There is no upto, no batch settlement, no sponsorship. The facilitator resolves every input's value and script from its own provider and ignores client-supplied source data. It requires one payer key across all inputs, payer-owned change, a minimum fee of 1 sat per byte by default, and a 546-sat dust floor.

That is sound design for a UTXO chain. The spec is also candid about what it does not solve, and we will quote its own gap list where it matters. The code is where the interesting behavior is.

The spec and the code disagree on scope

The spec file in the PR says the scheme "transfers an exact amount of native Bitcoin Cash" to a P2PKH output, and that CashTokens, P2SH and CashScript are "outside this scheme". Its support matrix marks CashTokens as unsupported and says token prefixes "are rejected by the P2PKH-only policy". It also says it defines a contract "shared by the TypeScript and Rust BCH mechanisms".

The PR body says something else. It lists native BCH, fungible CashTokens, NFT capabilities and commitments, and P2PKH, P2SH20 and P2SH32 destinations. Issue #3633 repeats that list. The code agrees with the body: the package has a CashToken fixture with a P2SH32 destination, a cashtoken transfer method, and token conservation checks. The PR contains no Rust.

So the normative document, the one other implementers would build against, describes a smaller scheme than the one that would ship. It is easy to fix. It is also the kind of drift that makes two implementations disagree on the first token payment. We would not treat the spec file as authoritative until it is reconciled with the code.

Experiment: run the facilitator

We copied the package files at 249530b into a scratch directory, installed @bitauth/libauth and @noble/hashes, and wrote a mock provider like the one in the PR's own tests. One P2PKH output of 100,000 sat, a BIP39 test signer, a 1,000-sat requirement. No network, no live chain. Three scenarios.

1. The first settle is pending

The default policy is confirmations(1). In settle() the facilitator verifies, broadcasts, then calls getTransactionStatus once. It does not poll. A transaction that was broadcast a moment ago is in the mempool, not in a block, so the answer is settlement_pending:<txid> with success: false.

Our run printed exactly that, with one broadcast. The transaction was 225 bytes, one input, two outputs (1,000 and 98,775 sat), fee 225 sat. When we flipped the mock to "confirmed", the same payload settled as success.

In the x402 TypeScript resource server, an upfront flow settles before the handler runs, and a failed settle returns a payment error instead of the resource. So the paid request fails with a 402 carrying the pending reason. That is correct behavior for an unconfirmed payment. The question is how often it happens.

2. How long is "pending" on the real chain

We walked back 289 BCH blocks from the tip using the Haskoin block API: heights 974,269 to 974,557, from 2026-10-24 09:12 UTC to 2026-10-26 08:12 UTC. Block timestamps are miner-reported, so treat the numbers as close, not exact.

// 289 blocks, about two days

Inter-block gap

Mean 588 s. Median 390 s. 90th percentile 1,382 s. Longest 2,998 s. 35% of gaps were longer than 600 s, and 42 gaps were longer than 1,200 s.

A payment does not arrive at the start of a gap. It lands at a random moment, so it tends to fall inside the long ones. Weighting each gap by its length gives the wait for the next block for a transaction broadcast at a random time:

// Wait to first confirmation

Expected 603 s

Probability the wait exceeds 30 s: 95%. Exceeds 60 s: 90%. Exceeds 300 s, the request's own maxTimeoutSeconds: 61%. Exceeds 600 s: 38%.

With the default policy, an agent paying per request would see a pending result nearly every time and a confirmed one after roughly ten minutes. That is not a bug in the code. It is what one confirmation costs on this chain. It is a bad default for a scheme whose job is to pay for an HTTP call.

The README documents two alternatives: mempool, and noDoubleSpendProof, which accepts an unconfirmed transaction while the provider reports no double-spend proof. Both are weaker finality and both are explicit opt-ins. We think the honest framing is that the default is safe and unusable for interactive calls, and the usable settings carry a risk the merchant must price.

3. Retrying after pending can pay twice

Say a client treats settlement_pending like any failed paid request and runs the flow again. The client's UTXO selector asks the provider for unspent outputs and takes the largest first. A provider with Electrum-style listunspent semantics reflects mempool spends: the original input is gone and the new change output is listed.

We modeled that in the mock. The first attempt returned settlement_pending. The second attempt built a new transaction from the change output, and the facilitator broadcast it. Two distinct transactions were broadcast, and both were pending. The merchant output is paid twice, once per attempt, and neither has settled.

The facilitator has the right tool for a safe retry: a settlement store keyed by txid and a request binding, so the same payload retried reconciles by txid instead of broadcasting again. But the client cannot use it unless it resends the same bytes. Nothing in the client scheme persists the pending payload or says so. The spec mentions that "a lost broadcast response is indeterminate and requires TXID reconciliation". It leaves the client side of that rule to the wallet.

We do not claim every wallet and provider pair does this. We claim the reference client plus an Electrum-like provider does, and that the protocol gives the agent no signal to do otherwise.

4. A settled payload replays as success

The settlement store maps a txid to a binding, which is the accepted requirements plus the resource info. After a successful settle it marks the record accepted and keeps it. A later call with the same payload finds the same binding, takes the "same" branch, sees the transaction confirmed, skips the unspent check, and returns success: true again.

We called settle() on one confirmed payload five times. Five successes, one broadcast. The PR's own test asserts the same thing for a single retry: "retried ... success: true", broadcast called once. That is intended idempotency for a lost response. But the store cannot tell a lost-response retry from a deliberate replay, and the binding has no per-request element.

Whether it matters depends on the resource server. If it treats every successful settle as a purchase, one 1,000-sat payment buys that resource at that price as many times as the client resends the header, until the in-memory store is cleared. Two other probes behaved as the design says: the same transaction against a different resource returned transaction_already_claimed_for_another_request, and a freshly rebuilt payload from the unchanged mock UTXO set was byte-identical to the first one, so it landed on the same record.

On EVM exact, a replayed EIP-3009 authorization fails on chain because the nonce is spent. Here the chain does not help, since the facilitator never re-broadcasts. The guard has to be application state. This is the same family of problem as the free-riding gaps in our two-phase audit and the replay cases in the recovery extension audit: idempotency that is correct for the facilitator and exploitable for the seller.

What the scheme gets right

It resolves source outputs itself instead of trusting the client. It checks that change goes back to the payer, so a payment cannot quietly become a multi-recipient transfer. It treats a broadcast timeout as indeterminate and checks the txid before releasing a claim. It refuses to reuse one transaction across two resources. It states that maxTimeoutSeconds is not a transaction expiry, because a BCH transaction has no signed expiry to check, and admits the time window is therefore not enforced.

The spec also says plainly that the in-memory settlement store is single-process only, and that production needs a durable shared store with atomic compare-and-set and crash recovery. That is the right admission. It means the replay result above is the reference store's behavior, not a verdict on a hardened one.

Smaller findings

What it means for LLM4Agents

Our rails settle in USDC, and none of this changes that. A native, volatile asset with ten-minute finality is the wrong fit for a gateway that quotes a price per call and returns the answer in the same request. We will not add a BCH rail on the strength of this proposal.

The useful part is the checklist the PR forces. Any rail we evaluate has three questions: how long until success means final, what happens to a client that retries after a non-final answer, and what stops a settled payment from being presented again. EVM exact answers the third on chain. UTXO and ledger-style schemes answer it in facilitator state, which makes it a deployment property we have to test, not read off a spec. Our finality audit looked at the first question on Base and Solana. This one adds a chain where the honest answer is ten minutes.

Staying on the frontier

  1. Add the replay probe to our facilitator checks: settle one payment, then present the same payload again and require a failure for anything but a documented lost-response retry. Run it against every facilitator we point the gateway at.
  2. Add the retry probe: make the facilitator answer "pending" and verify our client path does not rebuild and re-pay. A pending payment should be resent byte for byte or abandoned, never recreated.
  3. Record, per rail, the measured time to the level of finality we accept, and the probability that it exceeds the request timeout. The 603-second and 61% figures above are the template.
  4. Keep the settlement-store contract in any scheme we adopt explicit: shared, atomic, crash-safe, with a per-request element in the binding.
  5. Watch PR #3632 and issue #3633. Re-run the harness if the spec is reconciled with the code, or if the default policy or the binding changes.
  6. Keep UTXO and ledger schemes on the watch list as a class. They are where x402 stops borrowing guarantees from an account-model contract, and where facilitator behavior matters most.

Pay per call in USDC

One OpenAI-compatible endpoint, stablecoin settlement per request.

Register agent