Guide

Provider selection

How resolveProvider ranks candidates. When withProvider retries. Why an explicit provider is never second guessed.

The ranking

resolveProvider(preferred, chain, capability) answers a name without loading a single provider module. With an explicit preferred it returns that name, or throws UnknownProviderError if nothing's registered under it. Without one it walks the built-in list in this order and returns the first that serves the chain and, when you asked for one, the capability:

  1. Providers with configured keys. Every environment variable a provider reads has to be set. ETHERSCAN_API_KEY alone puts Etherscan first for the ten chains it serves.
  2. Keyless providers. Blockscout, Mempool, Blockstream, TONAPI, Aptos, Koios, the Arweave gateway, dcrdata, Horizon, Blockbook, Haskoin and eCash Explorer, in registry order.
  3. Any registry entry that serves the chain. Keyed providers without a key land here, so a Solana read without SOLSCAN_API_KEY still resolves to Solscan and fails with an AuthError that says what's missing. Better than a silent Blockscout.
  4. Blockscout, when no provider claims the chain at all. It then answers with UnsupportedChainError, which is more useful than nothing.

When you ask for a capability and nobody serves it on that chain, selection falls back to the ranking by chain alone, so the error you get is the typed limitation of a provider that at least knows the chain.

ts
import { resolveProvider } from "@agntn/explorers";

resolveProvider(undefined, "ethereum"); // "etherscan" with a key, "blockscout" without
resolveProvider(undefined, "bitcoin", "gasData"); // "mempool"; Blockchair and Blockstream have no fee data
resolveProvider("koios"); // "koios", whatever the chain

Blockchair is the odd one

Blockchair works without a key and works better with one. It's a keyed provider in tier 1 when BLOCKCHAIR_API_KEY is set. Without the key it shows up only in the fallback pass described below, and behind every provider that needs no key at all, because keyless Blockchair blacklists an IP quickly. So a Bitcoin balance on a machine without keys goes to Mempool first and retries on Blockstream, a Bitcoin Cash one goes to Haskoin, eCash history goes to eCash Explorer, while an Ethereum read Blockscout couldn't serve still lands on Blockchair.

WhatsOnChain follows the same rule with WHATSONCHAIN_API_KEY. It's the only Bitcoin SV provider, so on that chain the rule changes nothing but the rate limit.

The one retry

withProvider(preferred, chain, run, capability) is what the CLI, the MCP server and both agent extensions call. It resolves a provider, creates it, and runs your callback with { chain, name, provider }. The provider retries the same backend with backoff before RateLimitError leaves. If it still throws RateLimitError or PlanRestrictedError, never answers (TransportError, unless you aborted it), or answers with a 5xx HTTPError, and the provider was chosen automatically, withProvider runs once more on the next candidate that serves the chain and capability. Every other failure stays with the first provider, a 404 or a 400 included, since another explorer would say the same about that input. An explicit preferred is never switched, because you asked for that one.

ts
import { withProvider } from "@agntn/explorers";

const balance = await withProvider(
  undefined,
  "ethereum",
  ({ provider, chain }) => provider.getBalance("0x…", chain),
  "balances",
);

A plan limit doesn't lift between two calls, so withProvider remembers it. After a PlanRestrictedError on a read that passed its capability, automatic reads of the same operation on the same chain ask that provider last for the next hour. A free Etherscan key then costs one refused request per chain and operation each hour, not one per read. Another key starts from scratch, and an explicit preferred is asked anyway. When nobody else serves the read, the refused provider still gets it, so you still see its error.

Two consequences worth knowing. The callback can run twice, so it belongs to reads and nothing with side effects. And if the fallback fails with an AuthError, an UnsupportedChainError or an UnsupportedOperationError, you get the first provider's error back, because the second one's complaint is about its own configuration, not about your request. Any other failure of the fallback is what you get, with the first provider's error on its cause.

To call a read off, pass an AbortSignal as the sixth argument, after capability and input. It reaches the provider withProvider creates, so every request and every 429 wait stops when the signal aborts. An aborted read doesn't move to the next provider either, whatever the first one threw on the way out. You called it off, that's not an outage.

Default chains

Without a chain, the address decides when its format fits exactly one chain: a Bitcoin address reads Bitcoin, a TRON address reads TRON, and so on for every format @agntn/chains can tell apart. Forks share formats, so a legacy 1... address fits Bitcoin SV too. It still reads Bitcoin, where far more of them live, unless you name a provider that serves only Bitcoin SV: with -p whatsonchain the same address reads Bitcoin SV. inferChain(address, provider) is that check, exported. An EVM address fits every EVM chain, and an ENS name or an unknown format fits none, so for those the defaults below still apply.

With neither provider nor chain, a read starts on Ethereum. An explicit provider without a chain keeps that provider's default: Bitcoin for Mempool and Blockstream, Solana for Solscan and Helius, TON, TRON, Aptos, Sui for Blockberry, Cardano for Koios, Arweave for the gateway, Decred for dcrdata, Stellar for Horizon, Bitcoin SV for WhatsOnChain, Bitcoin Gold for Blockbook, Bitcoin Cash for Haskoin, eCash for eCash Explorer. Etherscan, Blockscout and Blockchair default to Ethereum. The table is PROVIDER_DEFAULT_CHAIN, exported.

Strictness

An explicit provider on a chain it can't serve is an UnsupportedChainError, not a silent switch to one that can. Misconfiguration stays visible, which is the point. The message names the providers that can, though, so the next call isn't a guess. A missing operation gets the same list. When nobody serves the read on that chain, it ends with no provider serves this read on solana. The Explorer has a provider select for exactly this: pick Mempool on Ethereum and read the error.