# ERROR CODES

Stable, machine-readable error codes for every WDK + USDT0 failure mode. Each code tells you exactly what happened and what to do about it.

## Error Payload Format

All error responses use this JSON structure. The code field is machine-readable and stable across API versions. The message is human-readable and may change.

```json
HTTP/1.1 410 Gone
Content-Type: application/json

{
  "error": {
    "code": "P402_QUOTE_EXPIRED",
    "message": "The quote q_123 expired at 2026-04-16T12:01:00.000Z.",
    "details": {
      "quoteId": "q_123",
      "expiredAt": "2026-04-16T12:01:00.000Z"
    }
  }
}
```

## HTTP Status Mapping

| HTTP Status | Meaning | Retry? |
| --- | --- | --- |
| 400 Bad Request | Malformed payload. Your code is wrong. | No — fix the request first. |
| 401 Unauthorized | Signature or authentication failure. | No — re-sign with correct parameters. |
| 402 Payment Required | Insufficient balance. | No — top up the wallet first. |
| 409 Conflict | Replay detected — nonce already used. | No — generate a new nonce. |
| 410 Gone | Quote expired. | Yes — re-quote and resubmit. |
| 422 Unprocessable | Policy denied the route. | Yes — with a different route. |
| 503 Service Unavailable | No route available or receipt delayed. | Yes — with backoff. |
| 504 Gateway Timeout | Settlement not finalized in SLA. | Poll receipt endpoint; do not retry settle. |

## Full Error Reference

`P402_QUOTE_EXPIRED` HTTP 410

**Trigger**  
The quote TTL (60 seconds) elapsed before the settlement was submitted.

**Recovery**  
Re-request the quote (/api/v1/liquidity/quote) and prompt the user to sign within the TTL. Do not retry the settle call with the expired quoteId.

`P402_ROUTE_UNAVAILABLE` HTTP 503

**Trigger**  
No viable route exists for the requested asset and constraints at this time.

**Recovery**  
Relax constraints (increase maxFeeBps or maxLatencyMs), or change the sourceAssets order. Retry with exponential backoff. If persistent, fall back to USDC.

`P402_POLICY_BLOCKED_ROUTE` HTTP 422

**Trigger**  
The policy engine denied the selected route (e.g. route is on a blocked jurisdiction).

**Recovery**  
Select a different route from the quote options that is compliant with your account policy. Do not retry the same routeId.

`P402_SIGNATURE_REJECTED` HTTP 401

**Trigger**  
The EIP-712 signature failed verification. Common causes: wrong chainId, wrong contract address, wrong nonce, or malformed signature bytes.

**Recovery**  
Rebuild the authorization object from scratch. Verify chainId matches the token's deployment chain. Re-sign with the WDK adapter and resubmit.

`P402_AUTH_INVALID` HTTP 400

**Trigger**  
The authorization payload is structurally malformed (missing fields, wrong types, invalid address format).

**Recovery**  
Check all required fields: from, to, value, validAfter, validBefore, nonce. Ensure nonce is a 0x-prefixed 32-byte hex string.

`P402_INSUFFICIENT_BALANCE` HTTP 402

**Trigger**  
The wallet does not hold enough of the source asset to cover amount + fees.

**Recovery**  
Check the wallet balance before submitting. Offer a fallback route with a cheaper asset (e.g. try USDC if USDT0 balance is insufficient).

`P402_SETTLEMENT_TIMEOUT` HTTP 504

**Trigger**  
The on-chain transaction was submitted but not finalized within the SLA window (30 seconds on Base).

**Recovery**  
Poll GET /api/v1/receipts/{receipt_id} with exponential backoff. The transaction may still finalize. If receipt shows settled: true, the payment succeeded.

`P402_RECEIPT_UNAVAILABLE` HTTP 503

**Trigger**  
Receipt generation is delayed — the transaction settled on-chain but the receipt record is not yet written.

**Recovery**  
Retry GET /api/v1/receipts/{receipt_id} with 2-second intervals for up to 60 seconds. Keep the txHash visible in your UI so users can verify on a block explorer.

`P402_REPLAY_DETECTED` HTTP 409

**Trigger**  
The nonce in the authorization has already been used in a prior settlement for this wallet.

**Recovery**  
Generate a new nonce (32 random bytes) and re-sign. Never reuse nonces. Use the receipt scheme instead if you need idempotent retries.

## Recommended Retry Strategy

```typescript
const RETRYABLE_CODES = new Set([
  'P402_QUOTE_EXPIRED',
  'P402_ROUTE_UNAVAILABLE',
  'P402_POLICY_BLOCKED_ROUTE',
  'P402_SETTLEMENT_TIMEOUT',
  'P402_RECEIPT_UNAVAILABLE',
]);

async function settleWithRetry(payload: SettlePayload, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await fetch('/api/v1/router/settle', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

if (response.ok) return response.json();

const error = await response.json();
    const code = error.error?.code;

if (!RETRYABLE_CODES.has(code) || attempt === maxAttempts) {
      throw new Error(`Settlement failed: ${code}`);
    }

// For expired quotes: re-quote before retrying
    if (code === 'P402_QUOTE_EXPIRED') {
      const newQuote = await requestQuote(payload);
      payload = { ...payload, quoteId: newQuote.quoteId, routeId: newQuote.routes[0].routeId };
    }

// Exponential backoff: 1s, 2s, 4s
    await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
  }
}
```
