Provider selection
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 is registered under it. Without one it walks the built-in list in this order and returns the first that serves the chain and, when asked for one, the capability:
- Providers with configured keys. Every environment variable a provider reads must be set.
ETHERSCAN_API_KEYalone puts Etherscan first for the ten chains it serves. - Keyless providers. Blockscout, Mempool, Blockstream, TONAPI, Aptos, Koios and the Arweave gateway, in registry order.
- Any registry entry that serves the chain. Keyed providers without a key land here, so a Solana read without
SOLSCAN_API_KEYstill resolves to Solscan and fails with anAuthErrorthat says what is missing. - Blockscout, when no provider claims the chain at all. It then answers with
UnsupportedChainError, which is more useful than nothing.
When a capability is asked for 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.
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 is a keyed provider in tier 1 when BLOCKCHAIR_API_KEY is set, and it joins the keyless tier only during the fallback pass described below. So a Bitcoin balance on a machine without keys goes to Mempool first, and Blockchair is what it retries on.
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 }. If the callback throws RateLimitError or PlanRestrictedError and the provider was chosen automatically, it runs once more on the next candidate that serves the chain and capability. Every other failure stays with the first provider, and an explicit preferred is never retried, because you asked for that one.
import { withProvider } from "@agntn/explorers";
const balance = await withProvider(
undefined,
"ethereum",
({ provider, chain }) => provider.getBalance("0x…", chain),
"balances",
);
Two consequences worth knowing. The callback can run twice, so it belongs to reads and nothing that has 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.
Default chains
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. Etherscan, Blockscout and Blockchair default to Ethereum. The table is PROVIDER_DEFAULT_CHAIN, exported.
Strictness
An explicit provider on a chain it cannot serve is an UnsupportedChainError, not a silent switch to one that can. Misconfiguration stays visible. The Explorer has a provider select for exactly this: pick Mempool on Ethereum and read the error.