Why failures need handling
A paid call will fail sometimes, and how the agent reacts decides whether that is a small handled event or a runaway. The wrong reflex is a blind retry: the same call, again, immediately. For most payment failures that does nothing useful and some it makes worse, turning one blocked call into a tight loop that wastes turns and looks exactly like an attack in your logs. The right approach is to classify the failure and respond to its actual cause.
This page is about that handling logic. Preventing overspend in the first place is how-to-prevent-ai-agent-from-overspending, and testing these paths is how-to-test-agent-payments. The payment API product page is the broader reference.
The kinds of failure
There are four causes, and they want different responses, so it is worth holding them distinct.
A payment refused by the spend limit: the call exceeds the per-transaction cap or the period allowance, and the wallet refuses before settlement. An insufficient balance: the wallet is empty and cannot pay. A settlement error: the settle path errored, including a 503 on a network whose chain adapter is still being rolled out, which is transient. And an upstream failure: the payment was fine but the paid service itself returned an error. Lumping these together as "payment failed" and retrying is the mistake; they are different problems with different fixes.
Prerequisites
- A Blockchain0x account with an agent and a
sk_test_key for testing the paths. - An agent that makes paid calls through
createX402Client(see how-to-add-payments-to-langchain-agent for the wiring). - Node 18 or newer. The SDK targets
>=18.
Classify before you react
Get the failure's cause before deciding what to do. On the pay side, a non-2xx from the call tells you it failed, and the status plus any reason tell you why. Branch on the cause rather than treating every failure the same.
const res = await fetchWithPay(url);
if (res.ok) return await res.text();
const status = res.status;
if (status === 402 || status === 403) return { kind: "refused" }; // limit said no
if (status === 503) return { kind: "settle_transient" }; // chain adapter rollout, retryable
if (status >= 500) return { kind: "upstream_error" }; // the service failed
return { kind: "other", status };This is illustrative; map the exact statuses your stack surfaces. The point is that the first thing you do with a failure is name it, because the right response follows from the cause, and a single catch-all branch cannot respond correctly to four different problems.
Respond to each kind
Each cause has a sensible response, and none is an immediate identical retry.
A refusal by the spend limit is terminal for that call: do not retry it, surface that the budget was reached, and let the planner choose a different action or stop. An insufficient balance is also terminal until funded: stop, alert that the wallet needs a top-up, and do not loop. A transient settle error (the 503) is the one case that warrants a retry, with backoff and a cap on attempts, because it may clear; if it persists, escalate. An upstream failure is the service's problem, not the payment's, so handle it like any failed API call, a bounded retry if the operation is idempotent, otherwise surface it.
The shape that works is a small switch on the classified kind, each branch doing the right thing, rather than one retry loop wrapped around everything. That structure is what keeps a failure proportionate to its cause.
Never retry blindly
This deserves its own emphasis because it is the single most common way agents misbehave around payments. A blind retry, calling the same paid endpoint again the instant it fails, is wrong for three of the four failure kinds and only sometimes right for the fourth.
Refusals and empty wallets do not change on retry, so retrying is pure waste and looks like a runaway. Even for transient and upstream errors, an immediate unbounded retry hammers the dependency; the correct version is bounded attempts with exponential backoff and a ceiling. Encode this in both the code and the agent's instructions: tell the planner that a failed paid call means stop or change approach, never repeat the identical call. An agent that respects that is calm under failure; one that does not turns every hiccup into a loop.
A retry policy that is safe
For the one failure kind that warrants a retry, the transient settle error, do it safely rather than in a tight loop. Bounded attempts with exponential backoff and a hard ceiling is the whole policy.
async function settleWithBackoff(call, max = 3) {
for (let attempt = 0; attempt < max; attempt++) {
const res = await call();
if (res.ok) return res;
if (res.status !== 503) return res; // only a transient 503 is worth retrying
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt)); // 0.5s, 1s, 2s
}
return { ok: false, status: 503, exhausted: true };
}Notice what it does not do: it does not retry a refusal or an empty wallet, because those will not change, and it caps attempts so even a persistent 503 cannot loop forever. After the ceiling, it gives up and surfaces the exhausted state so the agent escalates rather than spins. That is the difference between resilient and runaway: a retry that is narrow (only the transient case), backed off (not immediate), and bounded (cannot loop).
Make failures visible
A failed payment the agent hides is worse than one it surfaces. The agent should turn each classified failure into a clear, honest signal, for its own planner and for any human watching.
For the planner, return a structured outcome it can act on, the kind and a short reason, so it can choose to stop, switch approach, or escalate rather than guessing. For humans, log the failure with its classified cause and the identifiers (which call, which agent, what status), so an on-call engineer can see what happened without reconstructing it. And in any user-facing message, be honest about the cause: budget reached, funding needed, or a transient settle issue to retry shortly, never an opaque "something went wrong" that hides whether money was at stake. Visible, classified failures are what let both the agent and you respond correctly instead of treating every problem as the same mystery.
Common pitfalls
Three traps handling failed payments.
One retry loop for all failures. Different causes need different responses. Classify first, then branch.
Treating a 503 as a rejection. The settle 503 during rollout is transient and retryable, not a refusal. Surface it honestly and back off, do not tell the user the payment was declined.
Silent failure. An agent that fails a payment and says nothing, or pretends success, is worse than one that surfaces a clear outcome. Make the failure visible and actionable.
What to ship today
Add a classifier that names a failed paid call's cause from its status, a switch that responds to each kind (stop on refusal, alert on empty wallet, bounded backoff on a transient 503, normal error handling upstream), and an instruction to the planner never to retry an identical failed call. Test each path on Base Sepolia per how-to-test-agent-payments. Pricing is on the pricing page.