ID@agntn/explorersv0.6.1
Nineteen explorers. One shape.
Etherscan, Blockscout, Mempool, Solscan, Koios, dcrdata and the rest behind one TypeScript contract. Balances, transactions, unspent outputs, tokens, contracts, gas and blocks as exact strings, and a provider picked for you from the keys you have. Library, CLI, MCP, Pi and OMP all read through the same code.
- Providers
- 19
- 12 keyless
- Chains
- 30
- 12 EVM
- No key
- 24 chains
- read a balance
$ pnpm add @agntn/explorersAn address in, one Balance out
getBalance(address, chain) returns the same shape 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. Each one starts as a recorded sample and gets swapped for the worker's live answer the moment it lands.
- 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 doesn't
- UTXO explorers add funded and spent totals and keep the mempool delta in unconfirmed; the rest leave them out rather than send zero
6.71215016 ETH
"6 712 150 161 831 460 931" smallest unit
- Provider
- Blockscout
- Address
- 0xd8dA6BF2…A96045
- In / out
- not sent on Ethereum
- Block
- not named, null
- Fetched
- 2026-09-05 17:24
History and detail, same Transaction
getTxHistory pages through an address, getTxDetail reads one hash, and both hand back the same Transaction. The explorer's own answer rides along in raw for the day you need a field nobody normalized. That day comes, usually on Bitcoin.
- hash, from, to, value, fee, status and the token transfers inside, on every chain that has them
- to is null without a recipient, createdContract names a deployment, and the one empty string is an Arweave data upload, never an address
- OP_RETURN payloads on Bitcoin, Litecoin and Pepecoin come as hex, plus text when the bytes are printable
- 0x18fbf47989…83cbab0 ETH0xFD8d…dC48 → 0xd8dA…6045 · block 25882055 · 2026-09-01 · 0x4f414d20
- 0xc26d92ef86…95a01b0 ETH0xFD8d…dC48 → 0xd8dA…6045 · block 25881912 · 2026-09-01 · 0x4f414d50
- 0x9e614e2ccc…41fab80.000066 ETH0x899A…3871 → 0xd8dA…6045 · block 25834334 · 2026-08-25
- 0x2a6b0fd28b…9437ba0.000409 ETH0xb8aE…6B89 → 0xd8dA…6045 · block 25830440 · 2026-08-25
- 0xff3d62e0f2…1abc1b0.000409 ETH0xb8aE…6B89 → 0xd8dA…6045 · block 25823663 · 2026-08-24
No recipient reads none, an Arweave upload data upload.
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 that gets imported.
- resolveProvider() ranks configured keys, then keyless providers, then anything else that serves the chain
- withProvider() retries once on the next candidate after a rate or plan limit, no answer or a 5xx
- An explicit --provider stays strict: a wrong chain is an UnsupportedChainError, not a silent switch
Ranking
- 01Blockscout "blockscout"keyless
- 02Etherscan "etherscan"needs a key
- 03Blockchair "blockchair"optional key
With keys set
- Picks
- Etherscan
- Env
- ETHERSCAN_API_KEY
16 other providers don't serve ethereum and are never asked. A rate or plan limit on the first moves the read to the next one, once.
19 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. An operation a backend can't serve is missing, not stubbed with convincing nonsense, and Aptos stays registered with none.
Import@agntn/explorers/providers/<key>
| Operations | ||||
|---|---|---|---|---|
| Etherscan | key | 10 chains |
| 8 of 10 |
| Blockscout | none | 10 chains |
| 8 of 10 |
| Blockchair | optional | 7 chains |
| 4 of 10 |
| Mempool | none | 3 chains |
| 7 of 10 |
| Blockstream | none | 1 chain |
| 6 of 10 |
| Solscan | key | 1 chain |
| 4 of 10 |
| Helius | key | 1 chain |
| 3 of 10 |
| TONAPI | none | 1 chain |
| 2 of 10 |
| TRONSCAN | key | 1 chain |
| 4 of 10 |
| Aptos Explorer | none | 1 chain |
| 0 of 10 |
| Blockberry | key | 1 chain |
| 2 of 10 |
| Koios | none | 1 chain |
| 4 of 10 |
| Arweave gateway | none | 1 chain |
| 4 of 10 |
| dcrdata | none | 1 chain |
| 4 of 10 |
| Horizon | none | 1 chain |
| 7 of 10 |
| WhatsOnChain | optional | 1 chain |
| 5 of 10 |
| Blockbook | none | 1 chain |
| 5 of 10 |
| Haskoin | none | 1 chain |
| 5 of 10 |
| eCash Explorer | none | 1 chain |
| 2 of 10 |
Eleven 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 selection, keys and the one retry behave the same wherever the call starts. Your context window gets the normalized object and nothing else.
- 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
- Over MCP, raw records, ABIs and source stay out of the answer until a call asks for them
- A provider that can't serve an operation answers with UnsupportedOperationError, not an empty list
Ethereum
The agent gets the normalized Balance and nothing else, the raw answer stays on the server until a call asks.
- address
- "vitalik.eth"
- chain
- "ethereum"
- provider
- "blockscout"
- balanceFormatted
- "6.71215016" ETH
- blockNumber
- null
Same calls, every provider
Provider is the abstract base with two required reads and eight 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 eighteen in your bundle.
- getBalance and getTxHistory on every provider; the other eight 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
Read
import { create, resolveAddresses, resolveProvider } from "@agntn/explorers";// blockscout: keyless, nothing to configureconst provider = await create(resolveProvider(undefined, "ethereum"));const [address] = await resolveAddresses("vitalik.eth", "ethereum");const balance = await provider.getBalance(address, "ethereum");const history = await provider.getTxHistory(address, "ethereum", { limit: 5 });// "6.71215016" ETH, then 0x18fbf479…83cbab firstStart with one command
One install gives you the library, the explorers CLI and the MCP server. Ask for a provider, or let the keys you have pick one, and read.
- PinPre-1.0, so pin exact versions.
- KeysExplorer keys live in the environment, never in your code.
- ReadEverything here reads. Nothing signs, sends or holds a wallet.
First balance
$ pnpm add @agntn/explorersimport { create, resolveProvider } from "@agntn/explorers";const provider = await create(resolveProvider(undefined, "ethereum"));const balance = await provider.getBalance("0xd8dA…6045", "ethereum");balance.balanceFormatted; // a string, exact