The request lifecycle
One fetch call can involve two HTTP requests, a policy evaluation, several balance reads, a
budget reservation, and exactly one signature. This page is the whole sequence, in order.
The order is the design. Almost every safety property tx402 offers is “X happens before Y”, so reading this page is the fastest way to understand what the SDK actually guarantees.
The happy path
Section titled “The happy path”your call │ ├─ 1. capture the body replayable, or you supply a bodyFactory ├─ 2. send the request unmodified — tx402 adds nothing yet │ │ ← 402 Payment Required, with a PAYMENT-REQUIRED header │ ├─ 3. decode strictly size, depth, duplicate keys, ≤32 requirements ├─ 4. normalize merchant's shape → tx402's, amounts to integers ├─ 5. POLICY domain → network → scheme/asset → per-request → per-hour ├─ 6. plan routes balances read concurrently, candidates ranked ├─ 7. RESERVE atomic, from your local budget ├─ 8. sign exactly one authorization, fresh nonce ├─ 9. retry once carrying PAYMENT-SIGNATURE │ │ ← 200, with a PAYMENT-RESPONSE header │ ├─ 10. read settlement └─ 11. COMMIT the reservation becomes spendIf the resource does not answer 402, steps 3 onward never happen. A non-paying request costs
you nothing but the request itself.
The three orderings that matter
Section titled “The three orderings that matter”Policy and reservation both precede signing
Section titled “Policy and reservation both precede signing”Steps 5 and 7 complete before step 8, always, on every attempt — not just the first. This is SEC-002 and SPEC §6.6, and it is what makes the guarantees below true rather than usually true:
- A request your policy refuses costs zero signatures. Not “a signature that is discarded” — the signer is never called, so a hardware wallet never prompts and a KMS never logs a use.
- A budget cap cannot be exceeded by a race. The reservation is atomic and the store owns the comparison, so two concurrent requests cannot both see room for the last dollar.
--dry-runstops between 6 and 7. It is not a separate code path pretending to be the real one; it is the real one, halted.
Every attempt re-plans from scratch
Section titled “Every attempt re-plans from scratch”If the merchant answers the paid retry with another 402, tx402 does not reuse anything.
It re-runs policy, re-plans routes, takes a new reservation, and produces a new
signature with a fresh nonce. Nothing carries over — not the challenge, not the route, not the
authorization.
That costs a little work and buys the property that matters: a re-priced offer is honoured as a
new offer, and no authorization is ever transmitted twice. maxPaidAttempts (default 2)
bounds the loop, and exhausting it is a typed terminal error rather than a bare 402.
Release before transmission, retain after
Section titled “Release before transmission, retain after”This is the asymmetry that protects your money, and it is SPEC §6.7.
Before the signature reaches the network, any failure releases the reservation. Nothing was sent, so nothing can have settled, and holding budget would be wrong.
After the signature is on the wire, a failure means tx402 cannot know whether the
merchant settled. A timeout, a connection reset, a 5xx, and a redirect it declined to follow all
say the same thing: the outcome is unknown. So the reservation is retained until its TTL and
you get AmbiguousPaymentError.
Retaining is the conservative choice. Releasing would hand budget back for money that may have moved, and the same dollar could then be spent again inside the same hour.
Failure modes, by where they happen
Section titled “Failure modes, by where they happen”| Where | Example | Reservation | You get |
|---|---|---|---|
| Before policy | Reserved header, unreplayable body | none taken | TX402_RESERVED_HEADER, TX402_NON_REPLAYABLE |
| Policy | Over your cap, disallowed domain | none taken | TX402_POLICY_BUDGET, TX402_POLICY_DOMAIN |
| Planning | No viable route | none taken | TX402_LIQUIDITY with per-network deficits |
| Planning | No signer for any offered chain | none taken | TX402_SCHEME_UNSUPPORTED |
| Signing | Signer refused or returned garbage | released | TX402_SIGNER |
| After transmission | Timeout, reset, 5xx, same-origin 3xx | retained | TX402_PAYMENT_AMBIGUOUS |
| After transmission | Cross-origin redirect (SEC-005) | retained | TX402_REDIRECT_BLOCKED, paid: "unknown" |
| After transmission | PAYMENT-RESPONSE present, undecodable |
retained | TX402_PAYMENT_AMBIGUOUS, settlement-metadata-unparseable |
| Refused, no settlement | 4xx with no PAYMENT-RESPONSE |
released | TX402_RESOURCE_DELIVERY, paid: false |
| Delivered, not settled | success: false in PAYMENT-RESPONSE |
released | TX402_RESOURCE_DELIVERY |
| Settled, resource refused | 403 with a successful settlement | committed | TX402_RESOURCE_DELIVERY, paid: true |
| Settled, ledger write failed | Your spend store rejected the commit | retained | TX402_RESOURCE_DELIVERY, paid: true |
The last three rows are the ones worth reading twice, and an earlier revision of this
page had the third of them wrong — it said a settled 403 releases (the S15 audit’s O44).
Settlement evidence outranks the status line: if the merchant’s own PAYMENT-RESPONSE
reports a successful settlement, the money moved, so the spend is committed and you are
told paid: true whatever the status line said. The same 403 with no settlement claim is a
refusal and releases. And a settled payment whose ledger write fails is still a settled
payment, so it is reported paid: true and is never retryable — retrying is the one action
that can pay twice. See ADR-016 and ADR-017.
TX402_LIQUIDITY and TX402_SCHEME_UNSUPPORTED are deliberately different errors. “Everything
was attempted and fell short” and “nothing was even attempted” send an operator to two
different places, and reporting the second as the first sends them to fund a wallet that was
never the problem.
Replayable bodies
Section titled “Replayable bodies”tx402 may need to send your body twice: once on the unpaid request, once on the paid retry. For
a string, a Uint8Array, or a plain object, it captures the bytes and replays them.
A stream cannot be replayed. Rather than consume it and fail confusingly on the retry, tx402
refuses upfront with TX402_NON_REPLAYABLE — unless you supply a bodyFactory that can produce
the body again:
await tx402.fetch(url, { method: "POST", bodyFactory: () => createReadStream("large.bin"),});With a factory, you own replay semantics; tx402 calls it once per transmission.
Deadlines
Section titled “Deadlines”Every deadline in tx402 — per-RPC-provider, and the paid retry — is enforced by racing the work against a timer in tx402’s own control flow. Cancellation is requested as a courtesy but never trusted.
That sounds like an implementation detail and is not. An earlier version composed
AbortSignals, and the composition could be garbage-collected before it fired: a paid retry to
a merchant that accepted the connection and never answered would hang forever instead of
raising the AmbiguousPaymentError it owes you. Silence in exactly the case where money may have
moved. The current design cannot fail that way, because nothing that could be collected is
load-bearing.
Diagnostics
Section titled “Diagnostics”tx402 never writes to the console. It emits a structured event stream you route wherever you like:
const tx402 = createTx402Client({ signers: { evm }, logger: { debug: log, info: log, warn: log, error: log },});Events cover request.started, payment.required, policy.checked, route.planned,
sign.started, request.retried, payment.completed, and request.failed. Every event is
redaction-safe by construction: identifiers, hashes, atomic amounts, and categories only.
Signatures, keys, authorization payloads, and RPC URLs with credentials never enter the stream,
and the test suite proves it by seeding real secrets into every input and searching the whole
serialised output for each one.