---
title: "The request lifecycle"
description: "What tx402 does between your call and the response, and why the order is the way it is."
source: https://docs.tx402.io/guides/lifecycle/
---

# 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

```text
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. EXPOSE fence                durable, before the wire; the hold stops expiring
  ├─ 10. retry once                 carrying PAYMENT-SIGNATURE
  │
  │   ← 200, with a PAYMENT-RESPONSE header
  │
  ├─ 11. read settlement
  └─ 12. COMMIT                     the reservation becomes spend
```

Step 9, the **EXPOSE fence**, records the reservation durably the instant before the signed request
is transmitted. It moves the reservation to `exposed`, which **removes its expiry**: an exposed hold
never expires on its own. If the fence write fails, the request is not sent (nothing can have moved)
and a retryable `TransportError` is raised. Only a reservation that was never transmitted still
expires at its 120-second TTL — which is exactly the case where expiry is safe.

If 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

### Policy and reservation both precede signing

Steps 5 and 7 complete before step 8, always, on **every** attempt — not just the first. This
is the ordering the whole design rests on, 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-run` stops 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

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

This is the asymmetry that protects your money.

**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. Because the EXPOSE fence already ran, the reservation is
**exposed** — it does **not** expire, and it keeps consuming the cap — until an operator reconciles
it. You get `AmbiguousPaymentError`.

Holding 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. The exposed hold is never
auto-retried and never dropped by a TTL; it waits for an operator to confirm whether it settled.

The `AmbiguousPaymentError` you receive always carries `reservationExpiresAtEpochMs` in its details,
and the name misleads: it is **not** a deadline for the exposed hold. It is the reservation's
original **pre-fence TTL** — the 120-second expiry it carried before step 9's EXPOSE fence removed
that expiry — retained only as informational context (which is why it reads as a timestamp already
in the past or near future). An exposed hold does **not** expire when that instant passes, or ever;
do not wait for it to lapse. It is released only when an operator
[reconciles it](/operations/exposed-reconciliation/).

## 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   | **exposed** | `TX402_PAYMENT_AMBIGUOUS`                       |
| After transmission        | Cross-origin redirect                  | **exposed** | `TX402_REDIRECT_BLOCKED`, `paid: "unknown"`     |
| After transmission        | `PAYMENT-RESPONSE` present, undecodable | **exposed** | `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`, `paid: false`        |
| Settled, resource refused | 403 **with** a successful settlement   | **committed** | `TX402_RESOURCE_DELIVERY`, `paid: true`        |
| Settled, ledger write failed | Your spend store rejected the commit | **exposed** | `TX402_RESOURCE_DELIVERY`, `paid: true`         |

**The last three rows are the ones worth reading twice.**
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.

`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

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:

```ts
await tx402.fetch(url, {
  method: "POST",
  bodyFactory: () => createReadStream("large.bin"),
});
```

With a factory, you own replay semantics; tx402 calls it once per transmission.

## 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
`AbortSignal`s, 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

tx402 never writes to the console. It emits a structured event stream you route wherever you
like:

```ts
const tx402 = createTx402Client({
  signers: { evm },
  logger: { debug: log, info: log, warn: log, error: log },
});
```

Events cover all fourteen names the client emits. Ten fall on the path a paid call produces them
in: `request.started`, `payment.required`, `policy.checked`, `route.planned`, `budget.reserved`,
`sign.started`, `sign.completed`, `request.retried`, `payment.exposed`, `payment.completed`. Four
are conditional: `request.failed` on any failure; `spend.frozen` when a reserve is denied because
the scope — or the whole store — is frozen by the kill switch; `recipient.pinned` when a
trust-on-first-use pin is established inside the reserve; and `recipient.rejected` when an attempt
is refused because its recipient is not pinned. Both SDKs export the list as `EVENT_NAMES` (and
TypeScript a `Tx402EventName` union), so a caller can switch on it exhaustively without string
literals.

The level is part of the contract, and it is not uniform:

| Level                | Events                                                                                          |
| :------------------- | :------------------------------------------------------------------------------------------------ |
| `info`               | `request.started`, `payment.required`, `policy.checked`, `route.planned`, `budget.reserved`, `request.retried`, `recipient.pinned` |
| `debug`              | `sign.started`, `sign.completed` — a signature attempt is detail, not a milestone                |
| `info` **or** `error` | `payment.exposed` — `info` when the reservation is exposed just before transmission; `error` when that write fails and the transmit is aborted |
| `info` **or** `warn` | `payment.completed` — `warn` on any of three merchant faults, listed below |
| `warn`               | `spend.frozen` — a reserve was denied because the scope or the whole store is frozen; `recipient.rejected` — an attempt was refused because its recipient is not pinned |
| `warn` **or** `error` | `request.failed` — `warn` when the outcome is ambiguous and the money is still reserved, `error` otherwise |

**Three of the fourteen are emitted once per request. The happy-path eight are emitted once per
attempt**, so a call the merchant re-challenges emits each of those eight twice — as the
"every attempt re-plans from scratch" section above implies, and as both languages do
identically. The remaining three are per-attempt conditionals: `spend.frozen` is
`budget.reserved`'s denial counterpart and fires in its place when the scope is frozen;
`recipient.rejected` fires when the attempt's recipient is refused; and `recipient.pinned` fires
once, alongside `budget.reserved`, on the attempt whose reserve establishes a first-use pin. A
frozen or recipient-refused reserve ends the request, so neither denial repeats.

| Cardinality           | Events                                                                                                          |
| :-------------------- | :---------------------------------------------------------------------------------------------------------------- |
| Once per **request**  | `request.started`, `payment.completed`, `request.failed`                                                        |
| Once per **attempt**  | `payment.required`, `policy.checked`, `route.planned`, `budget.reserved`, `sign.started`, `sign.completed`, `request.retried`, `payment.exposed`, `spend.frozen`, `recipient.pinned`, `recipient.rejected` |

**This matters for anything that counts.** `budget.reserved` is in the second group, so a metric
built on it counts *attempts at spending*, not requests — which is usually what you want, but only
if you know it. `request.failed` is in the first: it is emitted from a single place that picks its
level from the final disposition, so a failure counter counts failed requests and never
double-counts one.

`payment.completed` is emitted at `warn` rather than `info` for exactly three reasons, and only
the first two mean the merchant gave you nothing to reconcile with:

| `reason`                                 | What the merchant did                                                        |
| :--------------------------------------- | :----------------------------------------------------------------------------- |
| `payment-response-absent`                | Answered `200` with no `PAYMENT-RESPONSE` at all — committed on its word alone |
| `payment-response-unparseable`           | Sent settlement metadata that does not decode                                 |
| `settlement-succeeded-resource-unusable` | Reported a **successful** settlement and then failed to deliver — `paid` is `true` here |

An alert that treats `payment.completed` at `warn` as "merchant supplied no settlement evidence"
will fire on the third row too, where the evidence exists and the delivery is what broke.

**`budget.reserved` is the one to watch.** It is emitted at `info` immediately before a signer
becomes reachable, which makes it the last event before money can move — so it is the event
worth alerting on, and it is one of the ten a successful paid call emits (`"events": 10` under
`--json`). A listener that only handles `sign.started` sees nothing at `info` at all. 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.

:::note[Operator actions are not request-path events]
Freezing or unfreezing a scope is an **admin-plane** action, reported by the
[`freeze` / `unfreeze` CLI verb](/guides/cli/#operator-verbs)'s own output — not
a request-path event, so there is no `freeze.enabled` in `EVENT_NAMES`. What a *request* sees when a
scope is frozen is `spend.frozen`, emitted in `budget.reserved`'s place. Likewise, hitting the
cumulative cap is not a separate event: it is `request.failed` carrying
`BudgetExceededError.details.capKind: "cumulative"` — the same event a per-hour refusal produces
with `capKind: "per-hour"`. The four request-path events **0.2.0 adds** are `payment.exposed`,
`spend.frozen`, `recipient.pinned`, and `recipient.rejected` (the changelog's list); `request.failed`,
though conditional, is a pre-existing 0.1.0 event, not one of the additions.
:::

:::tip[See a real challenge decoded]
The [402 Inspector](https://tools.tx402.io/inspect) probes a live x402 endpoint and shows what it charges,
on which network, to whom — decoded by `decodePaymentRequired`, the same function this guide describes.
:::

## More documentation

- Documentation index (Markdown): https://docs.tx402.io/sitemap.md
- Machine index: https://docs.tx402.io/llms.txt · full text: https://docs.tx402.io/llms-full.txt
- This page: https://docs.tx402.io/guides/lifecycle/

