Error handling
The stable error contract and what your bot should do for each case
Error bodies are {error, code?}. Match on the HTTP status and the
machine-readable code; the error string is for logs and humans — its
wording may change without notice.
The matrix
| HTTP | code | Meaning | Bot action |
|---|---|---|---|
| 400 | — | invalid body (zod validation text in error) | fix the request, don't retry as-is |
| 401 | — | unknown API key | fix config, don't retry |
| 403 | — | not invited to the RFQ / not your quote | drop, don't retry |
| 404 | — | RFQ or quote gone / already closed | drop, don't retry |
| 409 | RETRY_LATER | transient precondition: quote anchoring or service activation in flight | retry with backoff |
| 409 | INSUFFICIENT_FUNDS | a settlement leg cannot be funded | terminal for the trade |
| 409 | WALLET_MAKER_UNSUPPORTED | the self-custody wallet path is frozen | don't retry until re-enabled |
| 409 | (none) | deadline passed · quote already settling · an explicit validUntil > deadline · contract contention | read error, usually drop |
| 501 | — | the endpoint has no implementation for a self-custody party — only reachable by the frozen wallet mode, never by a self-registered maker | don't retry |
| 503 | — | ledger unavailable / busy / auth hiccup | retry with backoff |
Batch execute: partial failure and retries
A signatures batch on POST /tx/execute answers 200 even when entries
fail — check each entry's status. A failed entry carries retryable:
true is transient (ledger unavailable or busy — retry that entry with
backoff), false is terminal for that entry (unknown, expired, contract
gone). The error text is for logs, as everywhere.
The reliable recovery after a failed or timed-out execute is
GET /tx/pending: an action still listed wants its signature again; an
action absent from the list is done — the desk executed it. A blind resend
of the whole batch would instead report error for every entry that
already succeeded.
WebSocket errors
The stream closes with 4401 on a bad apiKey. Any other close is
operational — reconnect with backoff; the snapshot restores your state
(see Subscribe to RFQs).
Practical notes
- A 409 on
POST /maker/quotesafter the RFQ deadline is normal racing — RFQ deadlines are short by design. Drop the RFQ and move on. trade.failedon the stream is not an HTTP error but is terminal the same way: the trade is dead, nothing moved, the RFQ is closed.- Malformed frames on the stream should be ignored, not fatal — parse defensively.