Errors
Every refusal is JSON with code and error. Branch on code; error is a sentence for people, and it will be reworded.
data.reads.refreshing says so, and a pull a few minutes later has the newest hours. See freshness and new tokens.The shape
{
"tool": "token_series",
"ok": false,
"code": "tool_error",
"error": "\"base:0x…\" is not in the Blocktronics cache. Use search_tokens to find a cached token.",
"charged": 0,
"data": null
}Every code
tool_errorThe tool couldn't answer that input: a token Blocktronics hasn't indexed, a metric it doesn't know, a span outside the stored days. error says which, in words.
Fix Fix the input. For a token Blocktronics hasn't indexed, find it with search_tokens or quote it with GET /v1/tokens/{chain}/{token}/ingest.
invalid · invalid_chain · invalid_tokenThe body isn't the JSON the endpoint takes, a query asks for nothing, or an ingest path's chain or address is wrong (base, ethereum or robinhood; 0x and 40 hex characters).
Fix GET /v1/catalog shows each tool's input. The ingest body is optional: {"expect": <quoted credits>}.
bad_key · signed_out · key_required · bad_nonce · bad_signaturebad_key: the key is unknown, revoked, ended or mistyped. signed_out: no key was sent. key_required: one of the site's own data feeds was called from outside the site. bad_nonce, bad_signature: a wallet signature (a key from a wallet, or a top-up wallet's proof) was expired, reused or from another wallet.
Fix Send the key as a bearer token in the Authorization header; make a new one on /account#keys if it ended. For data, call the API, not the site's feeds. For a signature, ask for a new message and sign it with the right wallet.
insufficient · refusedThe balance can't cover it. A query or an ingest answers insufficient with credits, needed, available and a suggested topup; a tool answers refused, its data saying what it takes. Refused before anything ran.
Fix Don't retry: it needs credits, not another attempt. Top up credits on your account, or swap through Blocktronics and earn credits back as rebates.
refused · key_scope · not_entitled · ask_first · site_only · topup_not_allowed · over_topup_limit · wallet_unprovenrefused: a tool was refused before it ran (a cap on the key or the account). key_scope: the key can't spend credits. not_entitled: the account can't use this yet. ask_first: over the key's ask-me-first line. site_only: a feed that serves the site's pages only; error names the API tool that serves the same. topup_not_allowed, over_topup_limit: the key can't top up, or not that much at once. wallet_unproven: the top-up wallet hasn't signed for the key yet.
Fix Make a new key on your account page, raise its limit, or do it on the site. For site_only, call the tool it names. For wallet_unproven, POST /v1/topup/wallet and sign the message with that wallet, once.
unknown_toolNot a tool the API serves. error lists the ones it does.
Fix Pick a tool from GET /v1/catalog.
priceThe ingest's quote rose above your expect. credits holds the new figure.
Fix Check the new figure, then POST again with it.
too_largeThe body is larger than any tool's input.
Fix Send only the tool's input.
unsupported_tokenBlocktronics doesn't support this token: wrapped natives, stablecoins, wrapped stocks and similar reference assets are left out of holder analytics. Never quoted, built or charged.
Fix Pick another token.
rate_limited · credential_cap_reached · topup_day_limit · too_many_quotesrate_limited: more than 120 requests a minute on one key, or a free daily allowance per address used up. credential_cap_reached: the key spent its daily credit limit; it resets at 00:00 UTC. topup_day_limit: the day's top-ups are used. too_many_quotes (inside a refused top-up): the paying wallet has 20 open quotes.
Fix Wait the Retry-After seconds. A daily limit is raised on your account page.
internalA problem on our side.
Fix Retry with backoff.
unavailableThe tool didn't answer just then.
Fix Try again in a minute.
No error is ever billed
Every answer in that list costs nothing, and says so. A retry loop on a transient failure costs you time, not credits; see credits and billing.
Retrying safely
Retry 429 after its Retry-After seconds, and 5xx with exponential backoff. Never retry 400, 401, 403 or 404: they will fail the same way every time, and a tight loop on an auth failure is how an integration gets itself rate limited. Never retry 402 either; it needs credits, not another attempt
const TRANSIENT = new Set(["rate_limited", "internal", "unavailable"]);
async function tool(name: string, input: object, key: string, attempt = 0): Promise<unknown> {
const res = await fetch(`https://api.blocktronics.xyz/v1/tools/${name}`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "content-type": "application/json" },
body: JSON.stringify(input),
});
const body = await res.json();
if (res.ok) return body;
// Nothing was charged. Only a transient failure is worth another attempt.
if (!TRANSIENT.has(body.code) || attempt >= 4) throw new Error(`${body.code}: ${body.error}`);
const wait = Number(res.headers.get("retry-after")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
return tool(name, input, key, attempt + 1);
}