A 402 isn’t always a failure.

On an unpaid x402 request, HTTP 402 is the expected handshake. The endpoint returns payment requirements, the client authorizes the quoted USDC amount, and the request runs again with payment attached.

But what about 402-replay-failed? That’s a different event. Treating every 402 as permission to pay again can produce duplicate attempts, hide a client bug, or leave an agent stuck in a payment loop.

Read the status and the error code

HTTP status tells you the broad class. The body and payment metadata tell you what happened.

A first unpaid call might return a small body:

{
  "error": "Payment required"
}

The payment requirements can arrive in x402 response metadata. Your client should validate the network, asset, recipient, and amount before authorizing anything.

A later 402 might carry a machine-readable code:

{
  "error": {
    "code": "402-replay-failed",
    "message": "The submitted payment proof has already been used",
    "retryable": false
  }
}

Those responses share an HTTP status, but they demand different actions. The first asks for payment. The second says the attached proof can’t be accepted again.

So don’t branch on response.status === 402 alone.

Normalize inconsistent bodies

Endpoints won’t all return identical JSON. One may use error, another may use message, and another may return plain text. Agents need one local shape they can reason about.

{
  "http_status": 402,
  "code": "402-replay-failed",
  "message": "The submitted payment proof has already been used",
  "retryable": false,
  "payment": {
    "network": "base",
    "amount_usdc": "0.01"
  },
  "raw": {}
}

Keep the raw response. It’s useful when an endpoint introduces a field your normalizer doesn’t know yet.

And never invent retryability from the word error. Derive it from the status, the code, response headers, and whether the request could have completed before the connection failed.

Use a bounded retry policy

A routing agent can apply four practical rules:

  • An unpaid 402 with valid payment requirements can be retried once after authorization.
  • 402-replay-failed shouldn’t reuse the same proof. Surface it unless the endpoint supplies fresh payment requirements and your client can confirm the prior call didn’t settle.
  • A 400 or 422 needs a changed request. Retrying the same body wastes money and time.
  • A 429 or temporary 5xx can be retried after the stated delay, but paid calls need an idempotency key or a settlement check first.

The word “once” matters. Set a payment-attempt counter per logical request and stop after the configured limit.

if (res.status === 402 && isFreshPaymentChallenge(res)) {
  if (attempt.paymentCount >= 1) throw normalizeError(res);
  return retryWithPayment(res);
}

if (errorCode(res) === "402-replay-failed") {
  throw normalizeError(res);
}

What if the network dies after payment submission? Don’t assume failure. Record the payment identifier and request identifier before sending, then query any available receipt or status route before creating a new authorization.

Preserve enough context for the caller

agentutility’s registry has 796 endpoints priced from $0.001 to $0.50 per call. A blind retry at the low end is still a bug. At the high end, it’s an expensive one.

Return the endpoint name and quoted amount with every payment error. Include the Base network identifier, request ID, and payment identifier when present. Four fields give a human enough evidence to inspect the call, and they give another agent enough data to decide what happens next.

For an agent-facing message, be direct:

{
  "action": "surface",
  "reason": "payment-proof-replayed",
  "endpoint": "/v1/example",
  "amount_usdc": "0.01",
  "next_step": "Create a new logical request only after checking prior settlement"
}

A payment challenge is an instruction. A replay failure is a stop signal. Your client has to know the difference.