Error codes
This guide covers /api/chain/* responses. Deepstate market data (/deepstate/v1/*) only ever emits the flat gateway shape below, never the envelope shape; see Deepstate Market Data → Errors.
OneSource can emit three error shapes depending on where the failure originates. Your client needs to handle all three:
- Envelope errors: from the OneSource service itself (parameter validation, on-chain lookups, upstream RPC issues). Carries
{error: {code, message}, meta}. - Gateway errors: from the API gateway in front of OneSource (auth, payment, routing). Flat
{error, message}. - 402 + MPP RFC 7807 errors: payment-protocol-specific shapes the gateway forwards verbatim. Different again.
The HTTP status is the only thing all three share. Branch on it when in doubt; use the body to read details.
Envelope errors (from the OneSource service)
{
"error": {
"code": 400,
"message": "address param required"
},
"meta": { "endpoint": "/api/chain/live-balance", "request_id": "abcdef0123456789" }
}
error.codeis the HTTP status as an integer (400,402,500,502), mirroring the status line. It is not a stable string identifier.error.messageis human-readable English produced by the handler. The wording is not a stable contract; don't parse it.meta.request_idmirrors the server-side trace. Always log it.datais omitted on error (not present asnull).
Branch on the HTTP status (or equivalently error.code), and use error.message for diagnostics.
What you'll see by status
| HTTP | When | Example error.message |
|---|---|---|
400 | Parameter missing, malformed, or wrong type. | "address param required", "invalid number \"notanumber\": expected decimal or 0x-prefixed hex", "to and data fields required", "invalid JSON body" |
404 | Path doesn't match any route on OneSource (the gateway forwarded an unknown path). Unregistered /api/chain/* paths never get this far: the gateway catches them and returns the flat shape below. | "404 page not found" |
500 | Unhandled internal failure. | Captured handler message. |
502 | Upstream RPC node returned an error; the message passes the upstream verbatim. | "ownerOf failed: RPC error 3: execution reverted: ERC721: owner query for nonexistent token", "getLogs failed: RPC error -32602: query exceeds max results 20000, retry with the range ..." |
The 400 bucket is by far the most common; pre-validate parameters client-side against the API Reference before sending.
"Resource not found" doesn't mean 404
OneSource does not return 404 for missing on-chain resources. The semantics vary by endpoint, and you generally need to look at the response body:
| Endpoint | Unknown / missing resource returns |
|---|---|
/api/chain/block/{n} (past tip or future) | 200 + {"data": {"result": null}, ...} |
/api/chain/tx/{hash} (unknown hash) | 200 + {"data": {"transaction": null, "receipt": null}, ...} |
/api/chain/receipt/{hash} (unknown / unmined) | 200 + {"data": {"result": null}, ...} |
/api/chain/nft-owner (nonexistent token_id) | 502 envelope error; the RPC revert ("execution reverted: ERC721: owner query for nonexistent token") passes through |
/api/chain/ens/{name} (no resolver) | 200 + {"data": {"name": "...", "error": "no resolver set for this name"}, ...}: inline data.error string, NOT the envelope error field |
Polling on HTTP 404 to detect "tx pending" or "NFT doesn't exist" will fail. The reliable patterns:
// "Is this tx confirmed?": receipt by hash
const { data } = await res.json();
if (data?.result === null) return 'pending';
return { status: 'confirmed', receipt: data.result };
// "Does this NFT exist?": catch the 502 revert
if (!res.ok && body?.error?.code === 502 &&
body.error.message.includes('owner query for nonexistent token')) {
return 'does-not-exist';
}
Gateway errors (auth, payment, routing)
Returned by the gateway before the request reaches OneSource, so there's no meta block. Flat shape:
{ "error": "invalid_token", "message": "Invalid or expired API key" }
error is a lowercase snake_case identifier. Branch on it the same way you would on an HTTP status.
error | HTTP | Cause | Recovery |
|---|---|---|---|
invalid_token | 401 | Authorization: Bearer sk_… was present but the key isn't recognized or has been revoked. | Re-copy the key from app.onesource.io → API Keys. Rotate if compromised. |
api_key_required | 401 | The endpoint requires a Bearer key but x402/MPP isn't accepted, or both x402 and MPP are disabled. An anonymous call to a paid endpoint returns 402 with a payment challenge instead (see the next section). | Add Authorization: Bearer sk_…. |
plan_required | 403 | The endpoint requires an active paid subscription and yours isn't active. | Check your subscription status at app.onesource.io or call the endpoint with x402 / MPP instead. |
daily_limit_exceeded | 429 | Your account's daily request quota is spent. | Wait for the 00:00 UTC reset, move overflow to x402 / MPP, or contact support for a higher cap. See Usage quotas. |
monthly_limit_exceeded | 429 | Your account's monthly request quota is spent. | Route overflow to x402 / MPP until the calendar month rolls over, or contact support for a higher cap. |
rate_limited | 429 | An extreme burst tripped the backend's abuse guard. This is not a plan limit. | Back off and retry; Retry-After: 1 is set. If you see it in normal operation, report it. |
auth_unavailable | 503 | The gateway couldn't reach the validation service. Transient. | Retry with backoff. |
invalid_payment | 400 | x402 / MPP payment header was present but malformed. | Re-sign with the current challenge. |
verification_failed | 502 | Payment header was well-formed but the facilitator couldn't verify it. | Re-sign and retry; if persistent, the facilitator is degraded. |
upstream_error | 502 | Gateway reached OneSource but OneSource returned an unrecoverable error. | Retry with backoff. |
not_found | 404 | Either no upstream is configured for this host, or the path is an unregistered /api/chain/* route. The gateway knows the complete /api/chain/* route set, so it can identify a typo there as a bad path. | Check the hostname, and check the path against the API Reference. |
Unknown paths outside /api/chain/* are not enumerated by the gateway, so they come back as api_key_required (401) rather than not_found. A 401 on a path you believe is real is worth re-checking for a typo before assuming it's an auth problem.
Payment-required (402) and MPP RFC 7807
402 from an anonymous call
An unauthenticated call to a paid endpoint comes back with HTTP 402 and a third hybrid shape: error is a string (like gateway flat shape) but a meta block is present too:
{
"error": "Payment required",
"meta": {
"cost_usdc": "0.001",
"endpoint": "/api/chain/network-info",
"network": "eip155:8453"
}
}
The response also carries the protocol-specific challenge headers:
Payment-Required: <base64>: x402 challenge (price in USDC on Base, recipient address, expiry).WWW-Authenticate: Payment ...(one or more): MPP challenges (Tempo network, both one-shotchargeandsessionflavors).
A single 402 advertises every supported protocol; clients pick the one they speak and ignore the rest. Sign and retry; payment-aware HTTP clients (@x402/fetch v2 for x402, MPP-aware fetchers for Tempo) handle this loop for you. See x402 on Base or MPP on Tempo.
MPP credential errors (RFC 7807)
A malformed MPP Authorization: Payment ... credential comes back from the MPP SDK in RFC 7807 problem+json form, neither envelope nor gateway-flat:
{
"type": "https://mpp.dev/errors/malformed-credential",
"title": "Malformed Credential",
"status": 400,
"detail": "mpp: invalid credential encoding: json decode: invalid character ..."
}
Branch on type (a stable URL) when handling MPP-specific failures.
The gateway also exposes quota headers (X-Quota-Day-Limit, X-Quota-Day-Used, X-Quota-Month-Limit, X-Quota-Month-Used) on every API-key response, including on a 429.
Anatomy of recovery logic
A client that recovers cleanly branches on HTTP status. Normalize the three body shapes up front so the rest of the logic stays simple:
function parseError(status: number, body: any): { code: number | string; message: string } | null {
// RFC 7807 (MPP credential errors): { type, title, detail, status }
if (body?.type && body?.title) {
return { code: body.type, message: body.detail ?? body.title };
}
if (!body?.error) return null;
// Envelope shape: { error: { code: <int>, message }, meta }
if (typeof body.error === 'object') return body.error;
// Gateway flat shape: { error: "<snake_case>", message }
// Also handles the 402 hybrid: { error: "Payment required", meta }
return { code: body.error, message: body.message ?? '' };
}
async function callSkills(url: string, init: RequestInit, attempt = 0): Promise<unknown> {
const res = await fetch(url, init);
const body = await res.json();
const err = parseError(res.status, body);
if (!err) return body.data;
// Retryable: transient gateway/upstream failures + 5xx from OneSource.
const retryable =
err.code === 'auth_unavailable' ||
err.code === 'upstream_error' ||
err.code === 'verification_failed' ||
(typeof err.code === 'number' && err.code >= 500);
if (retryable && attempt < 3) {
await new Promise(r => setTimeout(r, 2 ** attempt * 500 + Math.random() * 250));
return callSkills(url, init, attempt + 1);
}
throw new APIError(err, res.status);
}
Note: don't branch on err.code === 404 to detect "resource not found": OneSource returns 200 + data.*: null for missing on-chain resources. See the "Resource not found" section above.
A few notes on what this sketch deliberately doesn't do:
- No
429branch. Quota rejections need handling that differs by cause, which the flat retryable test above can't express:rate_limitedclears in a second and setsRetry-After, whiledaily_limit_exceededandmonthly_limit_exceededwill not clear on any retry loop. See Usage quotas for a429-aware version. - No 402 branch. Payment-aware HTTP clients (
@x402/fetch, MPP fetchers) handle the402 → sign → retryloop for you. If you're writing one of those clients yourself, branch on402and read thePayment-Required/WWW-Authenticateheaders.
Always log request_id
meta.request_id (envelope shape only; gateway errors don't carry one) is the single most useful field for support. It mirrors the server-side trace; including it in your error log lets the team find the corresponding entry in one query.
When in doubt
Open a bug report including:
- The endpoint and parameters.
- The full response body and HTTP status.
meta.request_idif the failure came from OneSource (envelope shape).- The approximate time of the failure.
Filed via the 1s_report_bug MCP tool (if you're using the OneSource MCP Server) or as a GitHub issue.