Error shape and rate limits
Every failure has the same JSON shape, written by the API's exception filter. Rate limits are counted per route and per client IP.
Updated
The body
{ "statusCode": 401, "message": "Invalid or revoked API key", "path": "/agent/job" }| Field | Always | Meaning |
|---|---|---|
| statusCode | yes | The HTTP status, repeated in the body |
| message | yes | A string, or an array of strings when body validation fails |
| code | no | A stable machine-readable code, present only when the error sets one |
| path | yes | The request URL that failed, query string included |
- statusCode
- Always
- yes
- Meaning
- The HTTP status, repeated in the body
- message
- Always
- yes
- Meaning
- A string, or an array of strings when body validation fails
- code
- Always
- no
- Meaning
- A stable machine-readable code, present only when the error sets one
- path
- Always
- yes
- Meaning
- The request URL that failed, query string included
There is no request id or timestamp in the body. Log the status, path and your own request time on your side.
Two endpoints return an error object with a success status instead: for a bad type or amount the quote replies {"error": "…"} with a 200 and build-tx with a 201, so check the body on any 2xx. Validation errors that reject a request body return a 400 with a message or an array of messages.
Status codes
| Code | When you get it |
|---|---|
| 400 | Body fails validation — bad order params, malformed address, oversized upload fields |
| 401 | A guarded route hit without credentials — missing X-API-Key, or an expired session cookie |
| 403 | Authenticated but refused: read-only key on sign-and-submit, key used from a non-allowlisted IP, polling someone else's agent job |
| 404 | Unknown token, quote asset, stock symbol, or route — 404s are for things that don't exist, not things you're not allowed to see |
| 413 | Image upload over 5 MB |
| 422 | An order that can't take the change: repricing a TWAP, DCA or closed order, or a DCA the wallet can't fund |
| 429 | Throttled — see the limits below |
| 503 | Token detail when the chain read fails and the token isn't indexed yet — retry shortly |
- 400
- When you get it
- Body fails validation — bad order params, malformed address, oversized upload fields
- 401
- When you get it
- A guarded route hit without credentials — missing X-API-Key, or an expired session cookie
- 403
- When you get it
- Authenticated but refused: read-only key on sign-and-submit, key used from a non-allowlisted IP, polling someone else's agent job
- 404
- When you get it
- Unknown token, quote asset, stock symbol, or route — 404s are for things that don't exist, not things you're not allowed to see
- 413
- When you get it
- Image upload over 5 MB
- 422
- When you get it
- An order that can't take the change: repricing a TWAP, DCA or closed order, or a DCA the wallet can't fund
- 429
- When you get it
- Throttled — see the limits below
- 503
- When you get it
- Token detail when the chain read fails and the token isn't indexed yet — retry shortly
Rate limits
| Class | Limit |
|---|---|
| Default | 200 requests / minute per route, per client IP (GET /config reports it as platform.rateLimit) |
| POST /wallet/sign-and-submit | 30 / minute per IP, plus 20 signing requests / minute per user, which returns 403 "Signing rate limit exceeded" |
| /auth/login | 10 / 15 min |
| /auth/reauth | 5 / 15 min |
| POST /wallet/export-key | 3 / hour — exporting a private key is throttled hardest |
| Logo reads, IPFS reads, public profiles | Exempt — they're hot paths for every page render |
- Default
- Limit
- 200 requests / minute per route, per client IP (GET /config reports it as platform.rateLimit)
- POST /wallet/sign-and-submit
- Limit
- 30 / minute per IP, plus 20 signing requests / minute per user, which returns 403 "Signing rate limit exceeded"
- /auth/login
- Limit
- 10 / 15 min
- /auth/reauth
- Limit
- 5 / 15 min
- POST /wallet/export-key
- Limit
- 3 / hour — exporting a private key is throttled hardest
- Logo reads, IPFS reads, public profiles
- Limit
- Exempt — they're hot paths for every page render
The default is config-driven and can change: read GET /config rather than hardcoding a guess. Throttled routes send X-RateLimit-Limit-global, X-RateLimit-Remaining-global and X-RateLimit-Reset-global; a 429 adds Retry-After-global, the seconds to wait.
429 is not a bug to retry around blindly
If a loop is hitting the throttle, the answer is almost always a stream — /tokens/stream pushes every price update over SSE and removes the polling entirely.