Errors & limits
The error envelope, status codes, the unregistered-asset 404, and how to retry.
Error envelope
Every non-2xx response is JSON with one error object:
{
"error": {
"_tag": "BadRequestError",
"message": "Invalid mint",
"details": { "code": "invalid_mint" }
}
}_tagnames the error class and maps to the HTTP status (table below).messageis human-readable and may change; do not branch on it.detailsis optional. When present it is an object; stable machine-readable cases carry adetails.code. Branch on_tagfirst, thendetails.code.- Error responses are never cached (
Cache-Control: no-store) and carry anx-request-idheader.
Status codes
| Status | _tag | When |
|---|---|---|
400 | BadRequestError | Invalid query, body or path parameter. details often lists allowed values. |
401 | UnauthorizedError | Missing or inactive API key. |
403 | ForbiddenError | The key lacks a required scope (details.required, details.granted). |
403 | AssetAdvisoryError | The requested token carries a compromised or blocked advisory where that is refused. |
404 | NotFoundError | Unknown resource. v1 also uses it for unregistered mints (details.code: "asset_not_registered"). |
429 | RateLimitedError | Per-key rate limit, monthly quota, or a provider lookup budget was exceeded. |
500 | UpstreamHttpError and others | An upstream provider or internal error. Retry with backoff. |
504 | CloudRunTimeoutError | A backend call timed out. Retry with backoff. |
The API sends no rate-limit headers and no Retry-After. On 429 and 5xx, back off exponentially with jitter (for example 250 ms, 500 ms, 1 s, 2 s, each plus up to 150 ms of jitter) and cap the attempts. Never retry 400, 401, 403 or 404 without changing the request.
Unregistered assets
v1 serves registered assets only. A mint that is not a variant of a registered asset returns 404 with _tag: "NotFoundError" and details.code: "asset_not_registered":
{
"error": {
"_tag": "NotFoundError",
"message": "Asset not registered",
"details": {
"code": "asset_not_registered",
"mint": "<mint>",
"hint": "v1 serves registered assets only. To track other tokens, add them to a token list (POST /v2/lists, then PUT /v2/lists/{slug}/members/{mint}) and read them with GET /v2/lists/tokens?lists={slug}."
}
}
}- Returned by
GET /v1/assets/resolve,GET /v1/assets/risk-summary,GET /v1/assets/searchfilters, andGET /v1/assets/{assetId}with every route under it. - The batch endpoints never fail the request.
POST /v1/assets/market-snapshotskeeps the row as{ address, token: null, hasMarket: false }andGET /v1/assets/variant-marketskeeps it withassetId,chain,marketandexecutionQualityallnull. - Do not retry. A mint does not become registered with time. Track the token with a Token List instead; see Registered assets only for what counts as registered.
Advisory refusals
Read endpoints never refuse a flagged mint; they return it with its advisory so you can show the warning. The AssetAdvisoryError tag (403, details.code of advisory_compromised or advisory_blocked) is reserved for surfaces that would act on a token rather than describe it. Do not retry it: the flag is lifted by an operator, not by time. See Advisories.
Rate limits and quotas
Limits are enforced per API key:
- a short burst window and a sustained window on request rate,
- a monthly request quota,
- on the live risk read behind
GET /v1/assets/risk-summary, a per-key budget of live provider lookups per 10 minutes. When it is spent the endpoint still answers200withhasInsufficientData: truerather than failing.
Exceeding a rate limit or the quota returns 429 RateLimitedError. The API does not send rate-limit headers or Retry-After; back off exponentially with jitter and keep parallel fan-out bounded. Cached data (curated lists, charts, profiles) is safe to re-request; the response Cache-Control header tells you how long it stays fresh.
Retrying
export async function fetchWithRetry(url: string, init: RequestInit, maxRetries = 4): Promise<Response> {
for (let attempt = 0; ; attempt += 1) {
const res = await fetch(url, init);
const retryable = res.status === 429 || (res.status >= 500 && res.status < 600);
if (!retryable || attempt >= maxRetries) return res;
const delay = 250 * 2 ** attempt + Math.floor(Math.random() * 150);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}Retry 429 and 5xx. Never retry 400, 401, 403 or 404 without changing the request.