Docs/Merchants

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:

server.ts·ts
app.use((err: unknown, _req, res, _next) => {
console.error("[paid-route]", err);
res.status(500).json({ error: "internal" });
});

Every response shape

StatusBodyMeaning
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).
500next(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:

ReasonCause
envelope is not valid base64 JSONThe 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 ETAAgent presented an envelope addressed to a DIFFERENT merchant. Either misconfiguration or a deliberate replay attempt.
asset mint does not match routeEnvelope 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 routeEnvelope amount differs from this route's pay.charge amount. Agent submitted a stale or wrong-priced challenge.
resource URL does not match requestEnvelope 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 chainRPC 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:

  1. Forward verification failures to your log aggregator (Datadog, Axiom, Sentry).
  2. Alert on sustained RPC error fetching queue tx rates — indicates RPC provider problems.
  3. Don't alert on invalid_payment noise — 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.