Beta — Smart contract audit in progress. We recommend keeping wallet balances under $100 USDC.
CardZero

title: Error codes description: "Every error CardZero can return, what it means, and how to recover."

CardZero errors have a stable code (machine-readable) plus a message (human). HTTP status indicates the category. Check code for programmatic dispatch.

Format

{
  "error": "INSUFFICIENT_BALANCE",
  "message": "Wallet balance too low for this payment"
}

Authentication errors (401, 403)

| Code | When | Recover | | --- | --- | --- | | UNAUTHORIZED | Missing / malformed Authorization header | Check format Bearer <token> | | INVALID_API_KEY | API key not recognized or revoked | Get new key from Owner | | API_KEY_NOT_BOUND_TO_WALLET | API key exists but has no wallet | Re-issue via /api-key/rotate | | FORBIDDEN | Action not allowed for this auth | Check ownership / role |

Validation errors (400)

| Code | When | Recover | | --- | --- | --- | | INVALID_VERSION | wallet version not "v2" or "v3" | Use a valid value | | INVALID_AMOUNT | amount format wrong or ≤ 0 | Use decimal string, e.g. "1.0" | | INVALID_ADDRESS | EVM address malformed | 0x + 40 hex chars | | INVALID_BUDGET | budgetUsdc unparseable | BigInt-safe string in microUSDC | | INVALID_TITLE | title > 200 chars or wrong type | Shorten | | INVALID_DESCRIPTION | description > 2000 chars | Shorten | | INVALID_EVALUATOR_RULE | rule.type not in {manual, json_schema, http_check} | Use valid type | | INVALID_HASH | contentHash not 32-byte hex | keccak256 + 0x prefix + 64 hex chars | | INVALID_NAME | name length out of [1,50] | Trim | | INVALID_USERNAME | username out of [3,20] chars | Adjust | | EXPIRY_TOO_SHORT | expiredAt < now + 86400 | Set ≥ 1 day in future | | MISSING_* | Required field absent | Add the field |

Resource errors (404, 409)

| Code | When | Recover | | --- | --- | --- | | WALLET_NOT_FOUND | wallet ID doesn't exist | Check ID | | JOB_NOT_FOUND | jobId doesn't exist | Check ID | | PAYMENT_NOT_FOUND | paymentId doesn't exist | Check ID | | USER_NOT_FOUND | user ID invalid | Check ID | | WALLET_BUSY | Another payment in flight for same wallet | Retry in a few seconds | | JOB_BUSY | Another transition in flight for same Job | Retry in a few seconds | | WALLET_NOT_ACTIVE | Wallet still in "pending" state | Owner must claim first | | CLAIM_IN_PROGRESS | Concurrent claim attempt | Wait 5 minutes; auto-recovers | | CLAIM_KEY_INVALID | Bad / expired / used key | Generate a new claim key (POST /wallets) | | IDEMPOTENCY_INCOMPLETE | Same idempotencyKey used; previous attempt mid-failed | Manual recovery; contact support |

Spending policy errors (400)

Enforced on-chain; no API workaround.

| Code | When | Recover | | --- | --- | --- | | INSUFFICIENT_BALANCE | Wallet doesn't have enough USDC | Owner funds wallet | | WALLET_FROZEN | Owner froze the wallet | Owner unfreezes | | TX_LIMIT_EXCEEDED | Single payment > per-tx limit | Split payment or Owner raises limit | | DAILY_LIMIT_EXCEEDED | Total today > daily limit | Wait for UTC midnight or Owner raises limit | | NOT_IN_WHITELIST | Recipient not in whitelist (when set) | Owner adds to whitelist | | WALLET_EXPIRED | Past wallet's expiresAt | Owner extends expiry |

Job-specific errors (400, 403)

| Code | When | Recover | | --- | --- | --- | | CLIENT_NOT_FOUND | API key not bound to a wallet | Re-issue API key | | CLIENT_NOT_ACTIVE | Client wallet not yet claimed | Claim first | | NOT_CLIENT | API key's wallet ≠ Job's client | Use correct API key | | NOT_PROVIDER | API key's wallet ≠ Job's provider | Use correct API key | | EXTERNAL_PROVIDER | Provider must submit on-chain directly | Use Provider's own infra | | INVALID_STATUS | Job state doesn't allow this action | Check Job state | | NOT_YET_EXPIRED | claimRefund called too early | Wait until expiredAt | | SOFT_START_CAP | Beta budget cap (max $100 USDC) | Wait for cap removal | | SELF_JOB | Client and Provider are same wallet | Use a different Provider |

Chain errors (502)

| Code | When | Recover | | --- | --- | --- | | CHAIN_CREATE_FAILED | On-chain createJob reverted | Investigate via Basescan; DB row already rolled back, retry safe | | NOT_ONCHAIN | DB has Job, but chain doesn't (rare race) | Manual cleanup; contact support | | RPC_TIMEOUT | Blockchain RPC didn't respond in 60s | Retry; CardZero falls back to backup RPC |

Configuration errors (500, 503)

| Code | When | Recover | | --- | --- | --- | | V3_NOT_CONFIGURED | V3 factory address missing in server env | Server-side; report to ops | | EVALUATOR_UNCONFIGURED | Evaluator EOA missing | Server-side | | INTERNAL | Generic 500 | Retry; report if persistent |

Rate limiting (429)

| Code | When | Recover | | --- | --- | --- | | RATE_LIMITED | Exceeded rate limiter | Wait for window reset (Retry-After header) |

Webhook errors

| Code | When | Recover | | --- | --- | --- | | WEBHOOK_NO_SECRET | Wallet has no webhook_secret | Hit GET /webhook-secret once to lazy-generate |

Onramp errors

| Code | When | Recover | | --- | --- | --- | | CDP_NOT_CONFIGURED | Coinbase Onramp credentials missing | Server-side | | INVALID_WALLET | Wallet doesn't belong to user | Check ownership |

Decision tree for callers

HTTP 401/403  →  re-auth
HTTP 400 + INVALID_*  →  fix request
HTTP 400 + spending policy  →  Owner action needed
HTTP 404  →  check IDs
HTTP 409  →  retry with backoff (idempotency-safe)
HTTP 429  →  honor Retry-After
HTTP 502/503  →  retry with backoff (downstream issue)
HTTP 500  →  retry once, then escalate

When in doubt

Check the response body's message for human-readable detail. Most messages include the specific value that failed validation. If the error doesn't match anything above, it might be a new code — file an issue at github.com/mrocker/CardZero.