Docs/Agents

Error handling

The SDK throws ObscuraError with a typed code for every failure mode. Switch on err.code for user-facing responses, or rethrow unknown codes.

The pattern

agent.ts·ts
import { Obscura, ObscuraError } from "@obscura-app/sdk";

const agent = new Obscura({
apiKey: process.env.OBSCURA_KEY!,
baseUrl: process.env.OBSCURA_BASE_URL!,
});

try {
const res = await agent.fetch(url);
return await res.json();
} catch (err) {
if (err instanceof ObscuraError) {
  switch (err.code) {
    case "over_cap":
      // Monthly spend cap hit. Raise it in the dashboard or wait
      // for the calendar-month reset.
      throw new Error("This agent has no budget left this month.");
    case "insufficient_funds":
      // Encrypted balance is below the requested amount. Top up.
      throw new Error("Agent needs a top-up before retrying.");
    case "agent_inactive":
      // Agent paused or cancelled in the dashboard.
      throw new Error("Agent is inactive.");
    case "invalid_token":
      // API key revoked, rotated, or mistyped.
      throw new Error("Obscura API key invalid — rotate in dashboard.");
    case "invalid_challenge":
      // The merchant sent a malformed 402 — not your fault.
      throw new Error("Merchant payment request was invalid.");
    case "conflict":
      // The same spend intent is already in flight. Don't retry.
      throw new Error("Duplicate spend already processing — wait, don't retry.");
    case "signing_failed":
      // Umbra didn't finalize. May be in flight on-chain — DON'T retry
      // until reconciliation has run, or you risk double-charging.
      throw new Error("Payment is in flight — check status before retrying.");
    case "network_error":
    case "timeout":
      // Couldn't reach Obscura, or the sign request exceeded the timeout.
      throw new Error("Obscura unreachable — retry in a moment.");
    default:
      throw err;
  }
}
throw err;
}

Every error code

CodeStatusWhen it fires
over_cap402Agent's monthly spend cap would be exceeded. Terminal — raise cap or wait for month reset.
insufficient_funds402Agent's encrypted balance is below the requested amount. Terminal — top up before retrying.
agent_inactive403Agent is paused or cancelled. Terminal.
missing_token401No Bearer token on the request to Obscura. The SDK sets this for you — only fires if apiKey is empty.
invalid_token401API key is unknown, revoked, or mis-formatted. Terminal.
invalid_challenge400Merchant's 402 response is malformed or for the wrong network/mint. Terminal.
conflict409An identical spend intent (same agent + URL + amount + payTo + asset) is already in flight. Terminal — let the original finish.
rate_limited429Agent has 3+ parallel spends in flight. Retry after a backoff.
signing_failed500Umbra MPC didn't finalize within timeout. The on-chain debit may still land — DO NOT retry without checking reconciliation.
bad_request400SDK-level constructor issue (e.g. missing apiKey) or a malformed sign-request body.
server_error500Unhandled Obscura backend error. Retried automatically by the SDK with exponential backoff.
no_payment_required_headerMerchant returned 402 but without a PAYMENT-REQUIRED header (protocol violation on the merchant).
network_errorCouldn't reach Obscura. Check baseUrl / connectivity. Retried automatically.
timeoutSign request exceeded signTimeoutMs (default 60s). The mixer transfer may still complete — verify before retrying.
unknownCatch-all for unrecognized error codes. Treat as a retryable server-side issue.

Built-in retries

The SDK retries transient failures automatically — network_error, timeout, server_error, rate_limited — with exponential backoff (500ms → 1s → 2s, capped at 8s, default 2 retries). Tune via signMaxRetries and signRetryBaseMs.

Terminal codes are never retried by the SDK, no matter how high you set signMaxRetries:

CodeWhy never retried
conflictOriginal is in flight — duplicating it just blocks on the same lock.
over_capCap won't change between retries.
insufficient_fundsBalance won't change without a top-up.
agent_inactiveStatus flag — won't flip without dashboard action.
invalid_tokenAuth won't fix itself.
missing_tokenSame — auth misconfigured.
invalid_challengeMerchant payload is malformed.
no_payment_required_headerMerchant violated the x402 spec.
bad_requestCaller-side error.

signing_failed is a special case — it's NOT retried by the SDK because the on-chain debit may have already landed, and a blind retry would double-charge. Surface it to your caller and let them decide after reconciliation completes.