Error handling
The middleware handles x402 errors itself — a payment-less request
becomes a 402 with a price-quote body, a malformed envelope becomes a
402 with invalid_payment and a human-readable reason. Your handler
never runs on a broken payment.
Exceptions bubble into Express's next(err) so you can catch them in a
global error handler:
app.use((err: unknown, _req, res, _next) => {
console.error("[paid-route]", err);
res.status(500).json({ error: "internal" });
});Every response shape
| Status | Body | Meaning |
|---|---|---|
| 402 | { x402Version, accepts: [...] } | No PAYMENT-SIGNATURE sent. Body holds the price quote; a PAYMENT-REQUIRED header carries a base64 copy. |
| 402 | { error: "invalid_payment", reason } | Envelope decode/check failed (bad scheme, recipient mismatch, asset mismatch, amount mismatch, resource mismatch, replayed queueSignature, or on-chain queue tx missing/failed). |
| 500 | next(err) | Unexpected throw — usually an RPC connection error or a config error in pay.charge(). |
Reasons you'll see in invalid_payment
The reason string in the 402 body is the merchant SDK's internal
verification message. Common values:
| Reason | Cause |
|---|---|
envelope is not valid base64 JSON | The PAYMENT-SIGNATURE header isn't a base64 envelope. Either the agent SDK is misconfigured or someone is poking your endpoint by hand. |
unsupported scheme: … | The envelope's scheme is not umbra-mixer-v1. Older or non-Obscura clients. |
recipientEtaAddress does not match merchant ETA | Agent presented an envelope addressed to a DIFFERENT merchant. Either misconfiguration or a deliberate replay attempt. |
asset mint does not match route | Envelope mint differs from the merchant's configured stablecoin. The agent's Obscura backend and your SDK must agree on the mint. |
amount does not match route | Envelope amount differs from this route's pay.charge amount. Agent submitted a stale or wrong-priced challenge. |
resource URL does not match request | Envelope was prepared against a different URL. Cross-route replay attempt. |
queueSignature already used (replay) | Same envelope re-presented within replayWindowMs. The agent SDK is mis-retrying — investigate. |
queue tx not found on chain | RPC returned null. Queue tx may not be confirmed yet — the agent SDK shouldn't have presented the envelope this early. |
queue tx failed on chain: … | Queue tx landed but reverted. Should never happen on a healthy Obscura backend; surface this to the operator. |
RPC error fetching queue tx: … | Your RPC is having issues. Switch to a paid tier or a different provider. |
Why 402 instead of 500 for verification failures
The agent is better equipped to handle payment failures than your
server is. A 402 with invalid_payment is an actionable response: the
agent SDK can choose to retry with a fresh signature, or surface the
specific reason to its caller. A 500 would hide the detail and trigger
the wrong recovery path.
Logging
Verification failures and unexpected throws don't log themselves —
attach a global error middleware (above) and log
req.path, the invalid_payment reason, and any exception trace.
For production:
- Forward verification failures to your log aggregator (Datadog, Axiom, Sentry).
- Alert on sustained
RPC error fetching queue txrates — indicates RPC provider problems. - Don't alert on
invalid_paymentnoise — agents probing your endpoint with malformed payloads is normal.
What never reaches your handler
Your handler only runs after a confirmed on-chain queue transaction verified to match this route. So you never need to handle:
- Unsigned requests (→ 402 with quote).
- Malformed envelopes (→ 402 with
invalid_payment). - Cross-merchant or cross-route replays (→ 402 with
invalid_payment). - Wrong network / mint mismatches (→ 402 with
invalid_payment). - Failed or missing on-chain queue tx (→ 402 with
invalid_payment).
If your handler runs, the queue tx has landed, the encrypted-balance deduction has finalized via Arcium MPC, and a UTXO addressed to your ETA exists in the mixer tree waiting to be claimed.