/mcp routes.
REST response envelope
Success
data with meta (page, limit, total).
Error
code field is derived from the HTTP status:
| HTTP | error.code |
|---|---|
| 400 | BAD_REQUEST |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 409 | CONFLICT |
| 429 | TOO_MANY_REQUESTS |
| 5xx | INTERNAL_SERVER_ERROR |
Common HTTP statuses
| Status | Meaning | Retry? |
|---|---|---|
400 | Validation error, malformed JSON, or business rule violation (e.g. nothing to claim) | Fix request - do not retry blindly |
401 | Missing/invalid API key, OAuth token, or HMAC signature | Refresh credentials |
403 | Authenticated but insufficient scope, or MCP session bound to different credential | Check scopes or re-initialize MCP session |
404 | Resource not found, or developer not registered | Verify ids and registration |
409 | Conflict (duplicate resource) | Use idempotency key or fetch existing |
429 | Rate limit exceeded | Retry after Retry-After seconds |
5xx | Server error | Retry with exponential backoff |
Authentication errors
| Message | Cause |
|---|---|
Missing x-timestamp header | x-signature sent without x-timestamp |
Invalid x-timestamp value | Non-numeric timestamp |
Request timestamp expired (drift exceeds 5 minutes) | Clock skew on REST HMAC |
Invalid HMAC signature | Wrong secret, path, method, or body in string-to-sign |
API key does not support HMAC signing | Key created without a secret |
X-EZ-Signature or clock skew.
Rate limits
Authenticated requests include rate limit headers:| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests per 60-second window |
X-RateLimit-Remaining | Requests left in current window |
X-RateLimit-Reset | Unix epoch when the window resets |
Retry-After | Seconds to wait (only on 429) |
Tiers
| Tier | Limit |
|---|---|
free | 60 req/min |
standard | 300 req/min |
premium | 1000 req/min |
MCP JSON-RPC errors
MCP routes (POST /mcp, GET /mcp/sse) return JSON-RPC errors instead of the REST envelope:
| JSON-RPC code | HTTP | Meaning |
|---|---|---|
-32001 | 401 | Unauthorized |
-32002 | 403 | Forbidden (e.g. session/credential mismatch) |
-32003 | 429 | Rate limit exceeded |
-32602 | 400 | Invalid params |
-32603 | 5xx | Internal error |
-32000 | other 4xx | Application error |
MCP session errors
| Scenario | Code | Fix |
|---|---|---|
Missing mcp-session-id after initialize | 400 | Send session header from initialize response |
| Session created with different API key/token | -32002 | Re-run initialize with the same credential |
| Expired or unknown session | 401 | Re-run initialize |
Signal ingestion outcomes
Inbound signal webhooks return 200 with anoutcome field even when no trade executes:
outcome | Meaning |
|---|---|
matched | Signal parsed and accepted for processing |
unmatched | Parser could not match the message |
skipped | Intentionally skipped (e.g. non-entry action, eligibility) |
error | Processing error - check skipReason |
skipped and unmatched as non-retryable unless you change the payload.
Signal validation errors (HTTP 400)
| Error | Cause | Fix |
|---|---|---|
Missing reasoning | Structured eventType without rationale | Add 1-4000 char reasoning |
Invalid eventType | Unknown enum value | Use documented event types |
Missing positionRef | Lifecycle event without reference | Set to entry idempotencyKey or signalId |
Stale clientTimestamp | Market signal older than 5 min | Use current ISO timestamp |
Invalid instrument | Missing token/market for chain | Provide instrument.tokenAddress or coin |
skipReason dictionary (selected)
skipReason | Retryable? | Meaning |
|---|---|---|
parser_unmatched | No | NL text did not match grammar |
agent_status_blocked | No | Agent not beta/public |
agent_paused | No | Agent paused |
token_unresolved | No | Could not map ticker |
token_not_supported | No | Token not on execution venue |
token_blocked_by_whitelist | No | Token not on agent whitelist |
non_entry_action | No | Sell/cancel without open position |
duplicate_request | No | Idempotency key reused (safe) |
lifecycle_update_no_open_trade_found | No | positionRef not found |
Execution error tags (in executionResult.errorMessage)
| Code | Retryable? | Meaning |
|---|---|---|
wallet_not_found | No | Subscriber wallet missing |
subscription_paused | No | Subscription inactive |
signal_no_amount_set | No | No USD size configured |
BUY_FAILED | Maybe | On-chain/venue failure; check details |
hyperliquid_not_available_in_virtual_mode | No | Use live mode for perps |
sell_not_available_in_virtual_mode | No | Virtual mode limitation |
ingestReasons.ts in the backend.
Retry guidance
| Error type | Strategy |
|---|---|
| 429 | Wait Retry-After, then retry |
| 5xx | Exponential backoff (1s, 2s, 4s, …) with jitter, max 3-5 attempts |
| Network timeout | Retry with same idempotencyKey for signal POSTs |
| 401 HMAC | Fix signing - do not retry without correction |
| 400 validation | Fix payload - retries will fail identically |
Debugging tips
- Compare your string-to-sign against the SDK’s
signRestRequestoutput for REST HMAC. - For inbound webhooks, verify canonical JSON key ordering matches
stableJsonin echozero-sdk. - Check
X-RateLimit-Remainingbefore bulk operations. - Use
GET /statusto verify OAuth tokens without hitting scoped routes.