Thirteen explorers. One shape.
Etherscan, Blockscout, Mempool, Solscan, Koios and eight more behind one TypeScript contract. Balances, transactions, token transfers, contracts, tokens, gas and blocks on 23 chains, amounts as exact strings, and a provider picked for you from the keys you have. A library, a CLI, an MCP server and Pi and OMP extensions, all reading through the same code.
or look something up
- 13
- providers
- 23
- chains
- 8
- operations
- 9
- agent tools
Balances
An address in, one Balance out
getBalance(address, chain) returns the same six fields whether the answer came from Etherscan, Mempool or an Arweave gateway. ENS names resolve first, so vitalik.eth is as good as the hex. This panel walks through 3 addresses on three chains and swaps each recorded sample for the live answer from the docs worker as it arrives.
- balance is a string in the chain's smallest unit; balanceFormatted is the same number for humans
- fetchedAt on every read, blockNumber and blockHash when the explorer says which block it meant, null when it does not
- UTXO explorers add funded and spent totals; the rest leave them out rather than send zero
balanceFormatted
6.71215016 ETH
balance "6 712 150 161 831 460 931" · a string in the smallest unit
- chain
- ethereum · Ethereum
- provider
- blockscout · Blockscout
- address
- 0xd8dA6BF269…7aA96045
- blockNumber
- null
- fetchedAt
- 2026-09-05 15:44
Transactions
History and detail, same Transaction
getTxHistory pages through an address, getTxDetail reads one hash, and both hand back the same Transaction. The raw explorer answer rides along in raw for the day you need a field nobody normalized.
- hash, from, to, value, fee, status and the token transfers inside, on every chain that has them
- to is null for contract creation, never an empty string pretending to be an address
- OP_RETURN payloads on Bitcoin, Litecoin and Pepecoin come as hex, plus text when the bytes are printable
getTxHistory{ limit: 5 }
5 rows · sample
- 0 ETHsuccess
0x18fbf4798992…f083cbab
0xFD8d…dC48 → 0xd8dA…6045 · block 25882055 · 2026-09-01 · 0x4f414d20
- 0 ETHsuccess
0xc26d92ef86e1…8395a01b
0xFD8d…dC48 → 0xd8dA…6045 · block 25881912 · 2026-09-01 · 0x4f414d50
- 0.000066 ETHsuccess
0x9e614e2ccc3d…1d41fab8
0x899A…3871 → 0xd8dA…6045 · block 25834334 · 2026-08-25
- 0.000409 ETHsuccess
0x2a6b0fd28b54…429437ba
0xb8aE…6B89 → 0xd8dA…6045 · block 25830440 · 2026-08-25
- 0.000409 ETHsuccess
0xff3d62e0f231…e81abc1b
0xb8aE…6B89 → 0xd8dA…6045 · block 25823663 · 2026-08-24
Provider selection
Keys first, keyless next, Blockscout last
Set ETHERSCAN_API_KEY and Ethereum reads go through Etherscan. Unset it and they go through Blockscout. Nothing else in your code changes. The ranking never loads a provider module, so listing candidates is free and the one that answers is the only one imported.
- resolveProvider() ranks configured keys, then keyless providers, then anything else that serves the chain
- withProvider() retries once on the next candidate after a RateLimitError or a PlanRestrictedError
- An explicit --provider stays strict: a wrong chain is an UnsupportedChainError, not a silent switch
resolveProvider(undefined, "ethereum", "balances")
no keys on the worker
10 other providers do not serve ethereum and are never asked. A rate limit or a plan restriction on the first one moves the read to the next, once.
Providers
Thirteen backends, honest about what they serve
Etherscan wants an API key and a chain id, Koios wants the address in a POST body, the Arweave gateway answers half its questions over GraphQL. Each provider keeps that to itself and maps its answers onto the shared types. A capability a backend cannot serve is missing, not stubbed with convincing nonsense.
- capabilities is a per-provider flag set, and an optional method is absent when the flag is false
- Aptos stays registered with no capabilities rather than hiding a fullnode RPC behind an explorer name
- Your own backend is one class extending Provider plus an entry with its chains and a loader
Agents
Nine tools, three hosts
explorers mcp serves the tools over stdio, the Pi and OMP extensions render them in the terminal. All three go through the same withProvider() as the CLI, so the selection, the keys and the one retry behave the same wherever the call starts.
- explorers_balance takes one address or up to twenty, and resolves ENS names before it reads
- Every tool is read-only and says so in its annotations; nothing here signs or sends
- A provider that cannot serve an operation answers with UnsupportedOperationError, not an empty list
toolexplorers_balance
MCP · Pi · OMP
input
{
"address": "vitalik.eth",
"chain": "ethereum"
}output
{
"provider": "blockscout",
"data": {
"address": "0xd8dA6BF269…7aA96045",
"chain": "ethereum",
"balance": "6712150161831460931",
"balanceFormatted": "6.71215016",
"symbol": "ETH",
"blockNumber": null,
"fetchedAt": "2026-09-05T15:44:39.434Z"
}
}One interface
Same calls, every provider
Provider is the abstract base with two required reads and six optional ones. Concrete classes implement the mappers and the explorer calls, nothing else leaks upward. A sub path import like @agntn/explorers/providers/mempool gives you one backend without the other twelve in your bundle.
- getBalance and getTxHistory on every provider; the other six only where they are real
- ExplorerError, HTTPError, AuthError, RateLimitError, PlanRestrictedError, NotFoundError and three more
- API keys are stripped from every URL before an error message exists
import { create, resolveAddresses, resolveProvider } from "@agntn/explorers";
// blockscout: keyless, nothing to configure
const name = resolveProvider(undefined, "ethereum");
const provider = await create(name);
const [address] = await resolveAddresses("vitalik.eth", "ethereum");
const balance = await provider.getBalance(address, "ethereum");
const history = await provider.getTxHistory(address, "ethereum", { limit: 5 });
balance.balanceFormatted; // "6.71215016" ETH, exact, a string
history[0]?.hash; // "0x18fbf47989…83cbab", same Transaction shape from BlockscoutStart with one command
Pre-1.0, so pin exact versions. Everything here reads; nothing signs, sends or holds a key for you. Keep your explorer keys in the environment and out of your code.