Field study · 1–3 September 2026

Seven of the ten agent-payment systems I read can pay twice when the reply is lost

In three days I read the settlement path of ten agent-payment implementations — x402 facilitators, agent wallets, payment SDKs. Seven can charge a payer twice for one purchase. Not through a replay, and not because their defences are missing: in every case the guards were present, correct, and unable to help.

10money paths read
7can pay twice
3survived every mode

The moment

A payment settles on chain. Before the confirmation gets back, the connection drops — a timeout, a 502, a throttled RPC read. The code now has to decide what happened, and it cannot know.

Almost every implementation resolves that the same way: it files “I don’t know” under “it failed.” Failure means the payer still owes, so the server answers 402 Payment Required. A conforming client does exactly what the protocol tells it to — it signs a new authorization and pays.

That second payment carries a fresh nonce. It is not a replay. Every replay guard in the stack examines it and correctly lets it through, because it is a genuinely new, genuinely valid payment. The first transfer confirms. The customer has paid twice, and both charges are individually legitimate.

“Could not determine” is a terminal answer. It is never “did not happen.” Every verdict on this page is measured against that one line. It is §4.3 of a retry-safety proposal I co-authored for MCP — a working draft with no sponsor yet, not an adopted standard — and the receiver obligation I argued for in the x402 settlement-status thread.

How the ten were read

Two tiers of evidence, and every verdict is labelled with the one it rests on.

  1. Code read. A line-by-line walk of the settle, retry and signing paths. Before a finding is written, I check it against the implementation’s own defences — idempotency keys, replay sets, on-chain nonce checks — and only report it if it survives all of them.
  2. Battery run. A deliberately hostile facilitator settles the payment and then misbehaves in seven ways — hangs past the timeout, returns 502 after settling, re-challenges a paid request, breaks the reconciliation read — while counting how many payments actually landed. One is safe. Two is the double charge.

Four of the seven now carry a runnable red/green test filed on the project’s own issue. One is a full on-chain reproduction: the implementation’s unmodified merchant code, a local chain, and a proxy that fails only the receipt read — which produced two real transfers and a recipient balance of exactly double for a single purchase.

What this study is not

Ten is not a random sample. I chose implementations that move real money on agent-payment rails, and several were chosen precisely because they were commercially active. A different ten would give a different number. Read “seven of the ten I read”, not “seventy per cent of the ecosystem”.

Six of the seven are code reads, not live reproductions. The control flow, the missing dedup and the fresh-nonce behaviour are verified from source; the second charge is the consequence that follows. Only one has been driven end-to-end on a chain so far.

Nobody is named here while their finding is open. Each was reported privately or as a finding-first issue with the fix attached. Names appear on the index only after a fix ships — as credit, with the time it took.

The seven shapes

Described by structure, not by author. The same category error appears in Rust, TypeScript, PHP and Go.

The catch that spans the send

A try block wraps both the signing and the network send that already carries a signed payment. Its handler is written for signing failures — so a send that fails after the money moved falls through and pays again.

The receipt read treated as a broadcast failure

The transaction is broadcast, then the receipt read times out. Both throw from the same function, so the caller cannot tell “nothing was sent” from “it is in the mempool”. It answers 402.

The dropped hash

The failure type has no field for a transaction. The hash of a payment that is very likely to land exists for one line and is then structurally unrecoverable — nothing downstream can reconcile.

The clock-seeded idempotency key

The key is minted per call from the clock or a random source, so it dedupes retries inside one call and none of the retries an agent framework actually makes.

The layer that branches on success alone

The layer below preserves the hash and marks the outcome ambiguous — correctly. The layer above looks only at success: false, keeps the balance owed, and settles it again on the next sweep.

The audit row that is never written

A timeout returns before the record is written, so the reconciliation net that depends on that record never fires, and the store answers payment-required.

The cap that counts per payment, not per total

Spend guards check and record across an ambiguous boundary, so concurrent or retried payments each pass a limit that the pair of them breaks.

Two of the seven had idempotency keys. One had an on-chain authorization check before settling. The guards were correct and fired correctly — the category error happened one line earlier.

What the three safe ones do differently

This is not a story about universally broken code. Three implementations survived every mode, and all three do the same two things:

The fix is that short. It is not a rewrite; it is refusing to convert an unknown into a negative.

Test your own in about a minute

The battery is MIT-licensed and runs against any client that can make one purchase. It stands up a facilitator that settles and then misbehaves, and counts how many payments actually arrived.

hostile-facilitator run -- <your one-purchase command>
Get the battery See the full index