Error Codes
Balchemy API errors arrive in more than one shape. This page documents each error envelope, all relevant HTTP status codes, and common error scenarios by domain.
Warning: A
2xxtransport status does not mean the operation succeeded. Canonical failures are returned assuccess: falseenvelopes, and several trading paths return them with an HTTP200. Always inspect the response body before treating a request as successful.
Error envelope format
REST API errors
REST responses use two envelopes. Client code must handle both.
Canonical envelope. The success field, not the HTTP status, carries the outcome. Canonical failures include request metadata when available:
{
"success": false,
"error": {
"code": "PROVIDER_UNAVAILABLE",
"message": "Provider unavailable",
"details": {
"retryable": true
}
},
"meta": {
"requestId": "req_123",
"correlationId": "corr_456"
}
}This envelope can be returned with an HTTP 200. Treat any body containing "success": false as a failure regardless of the status code, and preserve error and meta rather than collapsing a degraded response into successful empty data.
Legacy envelope. Routes that have not been migrated still return status-coded bodies:
{
"statusCode": 401,
"message": "Unauthorized",
"error": "Unauthorized"
}For validation errors, the message field is an array of strings:
{
"statusCode": 400,
"message": ["message must be a string", "message should not be empty"],
"error": "Bad Request"
}For domain-specific service exceptions, an additional code field is included:
{
"statusCode": 503,
"message": "LLM provider unavailable",
"code": "LLM_PROVIDER_UNAVAILABLE",
"error": "Service Unavailable"
}MCP (JSON-RPC) errors
MCP tool calls use the JSON-RPC 2.0 error format:
{
"jsonrpc": "2.0",
"id": "req-uuid",
"error": {
"code": -32603,
"message": "Tool execution failed: insufficient balance",
"data": {
"tool": "trading_solana_jupiter_swap",
"reason": "INSUFFICIENT_BALANCE"
}
}
}Standard JSON-RPC error codes used by the MCP layer:
| JSON-RPC code | Meaning |
|---|---|
-32700 | Parse error — malformed JSON body |
-32600 | Invalid request — missing jsonrpc, method, or id |
-32601 | Method not found — tool does not exist or is not exposed |
-32602 | Invalid params — required parameter missing or wrong type |
-32603 | Internal error — tool execution failed |
HTTP status code reference
| Code | Name | When it occurs |
|---|---|---|
200 | OK | Request succeeded. Response body contains data. |
201 | Created | Resource created successfully (e.g. new agent, new API key). |
400 | Bad Request | Invalid request body, missing required field, or parameter validation failure. |
401 | Unauthorized | Missing Authorization header, expired token, invalid nonce, or wallet mismatch. |
403 | Forbidden | Valid token but insufficient scope, missing CSRF token, or domain validation failure. |
404 | Not Found | Resource does not exist or the authenticated user does not have access to it. |
409 | Conflict | Duplicate resource (e.g. agent already claimed, wallet already connected). |
413 | Content Too Large | Request body or LLM token limit exceeded. |
422 | Unprocessable Entity | Semantic validation failed — body is valid JSON but fields are logically invalid. |
429 | Too Many Requests | Rate limit exceeded. Includes X-RateLimit-* headers. |
500 | Internal Server Error | Unhandled exception in the server. Check Sentry for trace ID. |
503 | Service Unavailable | Trading engine down, LLM provider unreachable, or circuit breaker open. |
Auth errors
| Scenario | Status | Message | Resolution |
|---|---|---|---|
No Authorization header | 401 | Unauthorized | Include Authorization: Bearer <token> on every authenticated request. |
| Expired session token | 401 | Token expired | Re-authenticate. POST /api/nest/auth/refresh renews a session only while the access token is still valid — it verifies the token you send and rejects an expired one, so it cannot recover this state. |
| Invalid nonce | 401 | Invalid or expired nonce | Request a fresh nonce with POST /api/nest/auth/nonce or POST /api/nest/auth/evm/nonce. |
| Nonce already used | 401 | Nonce already consumed | Each nonce is single-use. Request a fresh one for each login attempt. |
| Wallet address mismatch | 401 | Wallet address does not match signed message | The address in the request body must match the wallet that signed the SIWE message. |
| CSRF token missing | 403 | Forbidden | Include X-CSRF-Token on all POST, PATCH, PUT, and DELETE requests. Fetch a token at GET /api/nest/auth/csrf. |
| CSRF token invalid | 403 | Invalid CSRF token | CSRF tokens are bound to sessions. Fetch a fresh token after each login. |
| Insufficient scope (MCP) | 403 | Scope insufficient: required trade, got read | Onboard with the required scope or request a step-up token. See Hub — Scopes. |
| Revoked token | 401 | Token has been revoked | The identity access token was revoked via POST /api/public/erc8004/onboarding/tokens/revoke. Re-onboard to get a new token. |
Trading errors
Rows marked Soft error (200) are returned with an HTTP 200 and a failure body. A client that branches only on the transport status will read these as successful trades. Inspect the response body — a success: false envelope or a failure message field — before recording an order as placed, and never treat a soft error as a reason to resubmit without first checking order state.
| Scenario | Status | Message | Resolution |
|---|---|---|---|
| Trading disabled for agent or legacy bot context | Soft error (200) | "Trading bu bot icin kapali." | Enable trading through the authorized Hub agent/rules flow or legacy compatibility path. |
| Trading service unavailable | Soft error (200) | "Trading servisi su anda kullanilamiyor." | The Rust trading engine is not reachable. Wait and retry. |
| Insufficient balance | 400 | Insufficient balance for requested swap amount | Reduce the swap amount or fund the custodial wallet. |
| Slippage exceeded | 400 | Slippage tolerance exceeded | Increase slippageBps or reduce the trade size. |
| Token not found | 404 | Token not found: <mint> | Verify the mint address or contract address. Use trading_solana_jupiter_tokens_search to resolve. |
| Circuit breaker open | 503 | Circuit breaker open for provider: jupiter | The trading engine has tripped the circuit breaker on a provider due to repeated failures. Wait 60 seconds and retry. |
| Order approval required | 200 (pending) | Order created with status: "pending_approval" | Call trading_orders_approve with the returned order_id, or set autoApprove=true in your order profile. |
| Custodial wallet not provisioned | 400 | Custodial wallet required for this operation | Call trading_wallet_custodial_ensure first to provision a custodial wallet. |
| Kill switch active | 503 | Trading kill switch is active for chain: solana | A platform-level kill switch is active. Trading is temporarily halted. |
MCP errors
MCP tool visibility is runtime-scoped. Use tools/list with the same Hub agent
key that will call the tool; the result reflects operator-controlled exposure,
key scope, and current registry metadata. Read tools, guarded trade tools, and
manage/write-like controls have different replay, step-up, and policy gates.
| Scenario | Status / Code | Message | Resolution |
|---|---|---|---|
| Invalid MCP API key | 401 | Unauthorized | The MCP key is missing, expired, or revoked. Rotate Hub agent keys from Hub → Agents & Keys / agent detail. |
| Tool not found (default mode) | JSON-RPC -32601 | Method not found | The tool is not exposed to this key. Call runtime tools/list with the same scoped key and use the returned tool set, or ask the operator to adjust exposure through the controlled runtime configuration. |
| Tool not found (granular mode) | JSON-RPC -32601 | Tool not found: <name> | The tool name is misspelled, hidden by the current scope, or outside the operator-controlled read-only granular allowlist. Check runtime tools/list and the Tool Catalog. |
| Missing required parameter | JSON-RPC -32602 | INVALID_PARAMS: <field> is required | Include all required parameters documented for the tool. |
| Step-up required for manage scope | 403 | Step-up token required for manage scope | Issue a step-up token through the authorized Hub agent control flow and include it as required by that flow. |
| Agent not found | 404 | Agent not found | The publicId in the MCP endpoint URL does not match any active Hub agent. |
| MCP payload too large | 400 | Request entity too large | Reduce the size of the arguments object in the tools/call payload. |
Agent (ERC-8004) errors
| Scenario | Status | Message | Resolution |
|---|---|---|---|
| Agent already claimed | 409 | Agent already claimed | Each agentId can only be claimed once. Use the existing binding or choose a different agentId. |
| Invalid identity token | 401 | Identity token validation failed | The ES256 JWT is malformed, expired, or signed with an unregistered key. Check token expiry and provider registration. |
| Unknown identity provider | 401 | Unknown identity provider: <provider> | The provider value is not registered. Contact Balchemy to register your provider. |
| Scope denied at onboarding | 403 | Requested scope not available | manage scope is never issued at onboarding time. Start with read or trade. |
| Token JTI already revoked | 400 | Token already revoked | The token was previously revoked. No action needed — the token is already invalid. |
Service exception codes
These codes appear in the code field for server-side service exceptions:
| Code | HTTP Status | Description |
|---|---|---|
AUTHENTICATION_FAILED | 401 | Session authentication failed. |
AUTHORIZATION_FAILED | 403 | Valid session but operation not permitted. |
TOKEN_VALIDATION_FAILED | 401 | JWT or API key validation error. |
INVALID_CREDENTIALS | 401 | Signature verification failed. |
VALIDATION_ERROR | 400 | Input validation failed. |
RESOURCE_NOT_FOUND | 404 | Requested resource does not exist. |
RESOURCE_CONFLICT | 409 | Operation would create a duplicate resource. |
INTERNAL_SERVICE_ERROR | 500 | Unhandled internal exception. |
LLM_PROVIDER_NOT_FOUND | 404 | No LLM provider configured for this agent or compatibility context. |
LLM_PROVIDER_UNAVAILABLE | 503 | LLM provider returned an error or is unreachable. |
LLM_CIRCUIT_BREAKER_OPEN | 503 | LLM circuit breaker is open due to repeated failures. |
LLM_CONFIGURATION_ERROR | 500 | Misconfigured LLM provider settings. |
LLM_RATE_LIMIT_EXCEEDED | 429 | LLM provider rate limit reached. |
LLM_TOKEN_LIMIT_EXCEEDED | 413 | Prompt exceeded the LLM context window. |
SDK error codes
The Agent SDK maps API errors to typed AgentSdkError codes:
| SDK code | Trigger |
|---|---|
auth_error | 401 response from the API |
policy_error | 403 response (scope or CSRF) |
rate_limit_error | 429 response |
provider_auth_error | Identity provider validation failure |
network_error | Network timeout or connection refused |
execution_error | Tool execution returned an error result |
invalid_response | Empty or non-JSON-RPC response body |
unknown_error | Unclassified error |
Catch and inspect the code in your error handler:
import { AgentSdkError } from "@balchemyai/agent-sdk";
try {
const result = await mcp.agentMarketBrief({ query: "Solana launch candidates" });
} catch (err: unknown) {
if (err instanceof AgentSdkError) {
switch (err.code) {
case "auth_error":
// Re-onboard or refresh your MCP key
break;
case "rate_limit_error":
// Back off — Retry-After header value is in err.details
break;
case "network_error":
// Retry with exponential backoff
break;
}
}
}The SDK applies bounded exponential backoff with jitter to network_error and rate_limit_error, and honours Retry-After when the response supplies it. execution_error is also in the SDK's default retryable set, and the exemption is narrower than it looks: the MCP client suppresses execution_error retry for tools/call only. The REST client applies the default policy to both get and post, and it converts any success: false body into an execution_error — so a canonical failure returned to a side-effecting POST is retried up to the default 3 attempts unless you say otherwise.
Warning: Supply your own
shouldRetrypredicate (or an outer guard) for every side-effecting REST call you make through the SDK, including identity-token revoke calls. Relying on the default policy means asuccess: falsePOST is replayed automatically.
Automatic retry is scoped to safe, idempotent read calls. Do not wrap side-effecting trade, manage, approval, withdrawal, wallet, or swap operations in an automatic replay: a network_error means the response was lost, not that the operation was rejected, and replaying it can dispatch a second order. Do not retry 401, 403, invalid request, unsupported chain, unconfigured provider, or deterministic policy and budget blocks — they fail identically on every attempt. Give each operation exactly one retry owner; if the SDK is retrying, your caller must not. For fund-sensitive calls, reconcile order state before deciding whether to send anything again. See Rate Limits for the full retry contract.