Errors & retries
Branch on success, not on the status code
Every failure — validation, upstream, rate limiting — returns the same envelope:
{
"success": false,
"error": {
"code": "MISSING_PARAM",
"message": "marketUid is required",
"details": null
}
}
| Field | Use |
|---|---|
error.code | Stable and machine-readable. Branch on this. |
error.message | For humans and logs. Wording may change; do not parse it. |
error.details | Optional extra context; shape varies by code. |
The HTTP status is not a reliable signal. A missing required parameter
currently comes back as 502 with ORIGIN_FAILED, not as 400:
curl -i "https://portal.1delta.io/v1/data/lending/pools" # no chainId
HTTP/2 502
{
"success": false,
"error": {
"code": "ORIGIN_FAILED",
"message": "All origins failed",
"details": [
{ "origin": 0, "status": 500, "details": "{\"ok\":false,\"error\":\"chainId is required\"}" }
]
}
}
So the only correct check is:
const body = await res.json();
if (!body.success) {
throw new Error(`${body.error.code}: ${body.error.message}`);
}
Checking res.ok first will mislabel a validation mistake as an outage.
Codes
| Code | Meaning | Retry? |
|---|---|---|
MISSING_PARAM | A required parameter was absent. | No — fix the request. |
INVALID_PARAM | Present but malformed: bad marketUid, unknown lender, non-integer amount. | No — fix the request. |
NOT_FOUND | No such market, pool, or position. | No. |
VALIDATION_FAILED | A pre-flight on-chain check could not be confirmed. | Sometimes — often an RPC hiccup. |
ACTION_FAILED | The transaction could not be built: no route, insufficient liquidity, unsupported path. | Sometimes — retry with different amounts or slippage. |
ORIGIN_FAILED | An upstream data source or protocol origin failed. Also what a missing parameter currently produces. | Yes, if the request itself is valid. |
Statuses you should handle
| Status | Meaning | Handling |
|---|---|---|
200 | Success. Still check success in the body. | — |
400 | Validation error. | Fix the request. |
404 | No such resource. | Fix the request. |
429 | Rate limited. | Back off exponentially, then retry. |
500 | Server error. | Retry with backoff. |
502 | Upstream origin failed — or a bad parameter. Inspect error.code. | Retry only if the request is valid. |
Rate limits
Requests are limited per IP when unauthenticated. Send an API key to raise the limit:
curl -H "x-api-key: YOUR_KEY" "https://portal.1delta.io/v1/data/chains"
Get a key at auth.1delta.io.
An unrecognised key is ignored, not rejected — a bad key never causes a
401, it just leaves you on the unauthenticated limit. If you are being
throttled while sending a key, verify the key is actually valid rather than
assuming the header is being read.
Treat 429 as retryable with exponential backoff and jitter rather than coding
against a fixed budget.
async function call(url: string, attempt = 0): Promise<any> {
const res = await fetch(url, { headers: { "x-api-key": process.env.ONEDELTA_KEY! } });
if (res.status === 429 || res.status >= 500) {
if (attempt >= 4) throw new Error(`giving up after ${attempt} retries: ${res.status}`);
const backoff = 2 ** attempt * 500 + Math.random() * 250;
await new Promise((r) => setTimeout(r, backoff));
return call(url, attempt + 1);
}
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
return body;
}
Note this still retries a 502 caused by a bad parameter. Inspect
error.code === "ORIGIN_FAILED" versus a validation code if you want to avoid
burning retries on a request that can never succeed.
Failures that happen on-chain, not in the API
A 200 with calldata does not guarantee the transaction succeeds. The usual
causes of a revert after a successful build:
- An approval from
actions.permissionswas not mined before the action was sent. - More than one entry from
actions.alternativeswas executed. - The quote went stale — rates and routes move every block.
slippagewas too tight for the route, or was passed as a percentage instead of basis points.- The position's health factor would drop below the liquidation threshold.
Simulate first where the endpoint supports it: POST the same parameters with
the user's current balanceData and aprData, or pass simulate=true on
lending actions, and inspect the projected health factor before signing.