Guide

Provider selection

How resolveProvider ranks candidates, when withProvider retries, and 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 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:

  1. Providers with configured keys. Every environment variable a provider reads must be set. ETHERSCAN_API_KEY alone puts Etherscan first for the ten chains it serves.
  2. Keyless providers. Blockscout, Mempool, Blockstream, TONAPI, Aptos, Koios and the Arweave gateway, 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 is missing.
  4. 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.

@agntn/explorers·MIT license· Read-only. Addresses you type go to a public explorer API, never to a wallet.