Unified block explorer provider library. Normalizes balances, tx history, contract info, token holdings, gas data, and block info across multiple chains and explorer APIs. Exports both CLI (explorers binary) and programmatic API.
| Provider | Auth | Chains | Capabilities |
|---|---|---|---|
| etherscan | API key (free: 5 req/s) | eth, base, arbitrum, optimism, polygon, bsc, avalanche, gnosis, linea, bera | Full: balances, tx, transfers, contract, tokens, gas, block |
| blockscout | none | eth, base, arbitrum, optimism, polygon, gnosis, linea, scroll, zksync, avalanche | Full: balances, tx, transfers, contract, tokens, gas, block |
| blockchair | optional key | bitcoin, eth, ecash | balances, tx, block |
| mempool | none | bitcoin, litecoin, pepecoin | balances, tx; gas and block on Bitcoin and Litecoin |
| blockstream | none | bitcoin | balances, tx detail/history, block |
| solscan | SOLSCAN_API_KEY |
solana | balances, tx detail/history, block |
| helius | HELIUS_API_KEY |
solana | tx detail/history, tokens; no balance endpoint |
| ton | none | ton | balances, tx |
| tronscan | TRONSCAN_API_KEY |
tron | balances, tx detail/history, block |
| aptos | none | aptos | none; required methods throw |
| blockberry | BLOCKBERRY_API_KEY |
sui | balances, tx history |
| koios | none | cardano | balances, tx detail/history, tokens |
| arweave | none | arweave | balances, tx detail/history, block |
| dcrdata | none | decred | balances, tx detail/history, block |
- Chain names normalized via
normalizeChain(), which takes display names as well as aliases likeethereum,mainnet,arb,btc - Native and token amounts use strings in each chain's smallest unit — call
formatWei(value, decimals)with the asset's decimals; its default is 18 noUncheckedIndexedAccessandnoImplicitOverrideare enabled — guard indexed access and mark overrides explicitly- Provider registration runs off a manifest:
src/providers/index.tslists every built-in as{ key, chains, capabilities, defaultURL?, load }, andcore/registry.tsturns that list into its map on the first registry call. The class itself only ownsstatic readonly key - Provider backends are explorer/indexer APIs, including documented gateway APIs. Judge support by the service and response contract, not REST versus GraphQL: a gateway may expose REST routes shared with nodes. Do not silently switch to another node to fill a missing capability. Unsupported operations stay absent; required methods without a supported service contract throw
UnsupportedOperationError. - Bitcoin, Litecoin and Pepecoin transactions from
mempoolcarry their OP_RETURN pushes inTransaction.opReturn; each payload keeps its rawhexand gets atextreading only when the bytes are printable UTF-8 - CLI default subcommand:
balance(for address-like input) orproviders(no input) - Error hierarchy:
ExplorerError→HTTPError,AuthError,RateLimitError,PlanRestrictedError,NotFoundError,UnsupportedChainError,UnsupportedOperationError,UnknownProviderError - HTTP client uses
ofetchwith a 15s default timeout and preserves out-of-range JSON integers as strings
src/core/types.ts- re-exportsChainKeyfrom@agntn/chains; owns transaction, balance, token, contract, gas, block, and provider-config typessrc/core/provider.ts— abstractProviderbase class and optional operation contractsrc/core/errors.ts— ExplorerError hierarchy + normalizeErrorsrc/core/registry.ts— Provider registry built frombuiltinson first use;create()is async and imports one provider (register, create, providers, has)src/core/resolve.ts- Auto-select built-in providers by env vars and chain, with one retry after a rate or plan limitsrc/core/client.ts— HTTP client wrapper (ofetch)src/core/ens.ts— ENS resolution (public APIs, no keccak dependency)src/core/input.ts— User input classification (address/txhash/ens)src/providers/*.ts— One file per provider, each exporting its class, listed inbuiltinsand built as its own bundle entrysrc/commands/*.ts- CLI subcommands (balance, tx, contract, tokens, transfers, gas, block, providers)src/cli.ts— Citty CLI entry point
balance, tx, contract, tokens, transfers, gas, block, providers - all support -c (chain), -p (provider). tx accepts -m history|detail to resolve ambiguous hash/address formats. transfers accepts -t to limit results to one token contract. tx, balance, tokens and transfers support ENS.
- Etherscan: 5 req/s free tier, needs
ETHERSCAN_API_KEY - Blockscout serves the complete holding array from
/addresses/:address/token-balances; large wallets can produce multi-megabyte responses, so this read allows 60 seconds unlessProviderConfig.timeoutoverrides it. - Blockchair: data format differs between UTXO (bitcoin, ecash) and EVM chains; eCash amounts are satoshis at 2 decimals (100 satoshis = 1 XEC)
- Solscan, Helius, TONAPI, TRONSCAN, Aptos, and Blockberry are single-chain providers and throw
UnsupportedChainErrorfor other chains. - Helius Enhanced Transactions v0 exposes no REST balance endpoint, so
getBalancethrowsUnsupportedOperationError; the key travels as theapi-keyquery parameter, whichsanitizeUrlredacts. - Helius
getTokenBalancescalls DASsearchAssetson the RPC root, so it answers over JSON-RPC and a failure arrives aserrorinside a 200 response. Pages hold 1000 assets and the walk stops after 20 of them. - Arweave uses gateway REST for
/wallet/{address}/balanceand/block/height/{height}, and GraphQL for transactions. It merges owner and recipient queries, removes self-transfer duplicates, and caps the history window atpage * limit <= 1000withlimitfrom 1 to 100.baseUrlis the gateway root for both APIs. Balance snapshot height/hash stay null because the endpoint does not return them. Block gas fields use"0"as the existing non-EVM convention; storage price quotes are not gas data. Contracts and token operations stay unsupported. Amounts use winstons (12 decimals), missing recipients stay empty strings, and bundle fees are not attributed to individual data items. Arweave transaction IDs and addresses share their shape, so CLI detail reads require-m detail. - Aptos Explorer has no documented account/history API;
aptosremains registered with false capabilities and throwsUnsupportedOperationErrorinstead of using fullnode REST. - TONAPI and Blockberry do not expose block lookup compatible with the library's single block-number contract, so
blockInfois unsupported. - Mempool: Bitcoin, Litecoin and Pepecoin; Litecoin uses litecoinspace.org, while peppool.space serves balances and transactions but lacks fee recommendations and complete normalized block metadata. Peppool paginates confirmed address history with
?after_txid=, not Esplora's/txs/chain/:txidroute - Blockstream serves Bitcoin through the Esplora wire format; its
/api/fee-estimatesresponse does not match Mempool's recommendation shape, so gas data stays unsupported - Koios answers on POST with the address or hash in the request body, and the public instance rejects a body over 5120 bytes, so
getTxHistoryaskstx_infofor 70 hashes at a time and reorders the answer, which comes back in the endpoint's own order - Koios
address_infoships the whole UTxO set of an address, 222 kB for a busy one, sogetBalancenarrows the payload with the PostgRESTselectparameter; the endpoint still builds that set before it answers, and a busy address takes 3 to 9 seconds against the 15-second client timeout - Koios keeps the phase-2 validity flag behind the heavier
_scriptspayload, so a Cardano transaction reads assuccesseven when a failing script consumed its collateral;isContractInteractioncomes from the presence of collateral inputs - dcrdata uses Insight for Decred balances, transaction history/details and blocks.
baseUrlis the Insight API root. Amounts use atoms (8 decimals). Balances add the mempool delta; funded/spent totals remain confirmed and snapshot fields stay null. The balance is not a spendability check. Chain/address format checks use@agntn/chains; checksum validation belongs to the service. History usesfrom/topagination, supports limits 1 to 250 and both sort directions, and rejects unsupported block bounds. Transaction values pair with one addressed output, with full inputs/outputs inraw; positive confirmations do not independently prove stake-vote approval. Block responses are arrays and count regular plus stake transactions; miner stays empty and gas fields use "0". Insight's estimatefee is only a relay fee, so gas data remains unsupported.
Three-layer design: CLI → Core → Providers.
graph TB
CLI["CLI (citty)"] --> Core
Core --> Providers
Providers --> External["External APIs"]
PiExt["Pi Extension"] -.-> Core
PiExt -.-> Providers
Types["types.ts"] -.-> Chains["@agntn/chains (runtime dep)"]
- CLI Layer (
cli.ts,commands/*.ts): citty-based CLI, lazy-loads subcommands via dynamicimport().cli-args.tsnormalizes bare address input tobalancesubcommand. - Core Layer (
core/*.ts): Domain types, provider registry (built lazily from the barrel list), HTTP client (ofetch, 15s timeout), ENS resolution (public APIs), input classification, error hierarchy. - Provider Layer (
providers/*.ts): 14 providers. Each file defines API types, helper mappers and a concreteProvidersubclass with a static registry key, exports that class, and ships as its own bundle socreate()can import it alone. - Pi Extension (
packages/pi/extensions/explorers.ts): Exposes 9 tools to Pi coding agent, matching the MCP server's tool set. Lazy-loads livesrc/from a checkout and the relativedist/module from an installed package, without self-importing the package by name.packages/omp/extensions/explorers.tsregisters the same nine for OMP.
- Multi-chain EVM (etherscan, blockscout): support 10 EVM chains each
- Bitcoin/Ethereum bridge (blockchair): dashboard API for Bitcoin, Ethereum and eCash
- Esplora-compatible UTXO (mempool, blockstream): Mempool serves Bitcoin, Litecoin and Pepecoin; Blockstream serves Bitcoin as an independent backend
- Single-chain non-EVM (solscan, helius, ton, tronscan, aptos, blockberry, koios, arweave, dcrdata): capabilities mirror only their explorer APIs; Aptos is explicitly unsupported
- Lazy registration:
providers/index.tsexportsbuiltinswith metadata and aloadper provider, andcore/registry.tsbuilds its map the first time anything asks the registry.create(name)awaitsload()once and caches the class; every metadata question stays synchronous.register(providerClass, meta)covers provider classes living outside the package. - Nothing runs on import: library modules evaluate to declarations only. Derived values wait for their first use, such as
entries()in the registry,decoder()in mempool andagent()in the HTTP client.dist/cli.mjsis the one bundle that runs on load, because it starts the CLI, andsideEffectsinpackage.jsonsays so. - Measuring that claim:
pnpm buildprintsSide effectsper bundle, but withsideEffectsdeclared the number is circular, since the bundler believes the field. For a real reading, drop the field, rebuild, and compare: everything exceptdist/cli.mjsthen comes back under 1 kB, andINSPECT_BUILD=1 pnpm buildshows the remainder is the bundler runtime plus bareofetchand@agntn/chainsimports, not our code. - String-only values: All wei/satoshi/native amounts are strings (
Balance.balance,TokenBalance.balance). The HTTP boundary preserves unsafe JSON integers as strings;formatWei()converts amounts for display. - Optional methods:
getTxDetail,getContractInfo,getTokenBalances,getTokenTransfers,getGasData, andgetBlockInfoare optional onProvider. Always check both thecapabilitiesgetter and method presence before calling. - Dynamic CLI imports: Each subcommand is lazily loaded via
() => import('./commands/X.js').then(m => m.default). Citty loads command declarations for help, so runtime core imports belong insiderun()or execution helpers. - Chain normalization:
normalizeChain()delegates togetChain()from@agntn/chainsand returns the canonicalChainKey. Aliases and display names both resolve (ethereum→eth,btc→bitcoin,arb→arbitrum). Missing input defaults toeth; unknown names and the empty string throw. - Provider auto-selection:
resolveProvider()checks env vars, chain support, and an optional requested capability without loading provider modules.withProvider()keeps explicit choices strict and retries automatic reads once on another available built-in afterRateLimitErrororPlanRestrictedError. Its callback must be safe to run twice. - Error sanitization:
HTTPErrorstrips API keys from URLs in error messages.normalizeError()wraps unknown errors into typedExplorerErrorsubclasses.
- Writing a provider file without adding its chains and capabilities to
builtins— the class never reaches capability-aware routing, andtest/unit/registry.test.tsfails - A top-level call in a module the library entry can reach (
new Set(),Object.keys(), a decoder, a prebuilt map) — it pins that module into every consumer bundle, whichpnpm buildreports as growingSide effects - Calling an optional provider method without checking
capabilitiesand method presence — unsupported operations stay absent at runtime - Assuming EVM address formats work on non-EVM chains (Solana base58, TON base64, TRON base58/hex)
- Hardcoding chain names — always use
normalizeChain()for user input
Covered (31 test files): provider base/registry, provider resolution, HTTP client, path safety, amount formatting, errors, input classification, chain normalization, CLI argument routing, extension integration, plus all fourteen providers. CLI coverage: help without backend imports, errors for unknown chains, provider listing, and mocked balance and transaction reads. Successful contract, token, transfer, gas, and block command execution remains untested. Test style: Focused unit tests for local contracts and mocked explorer-API responses; public no-key providers may additionally use live roundtrips.
@agntn/chains: canonical chain registry.ChainKeyfor keys,getChain()for alias resolution,create(key)for per-chain metadata like symbol and chain ID. Stays external to the bundle, so a consumer and this library share one registry instead of two.citty: CLI frameworkconsola: Loggingofetch: HTTP clientobuild: Build tool (bundle mode)vitest: Testing
pnpm build # obuild → dist/
pnpm dev # obuild --stub (watch mode)
pnpm typecheck # build, then tsc --noEmit
pnpm test # vitest watch
pnpm test:run # vitest single run
pnpm release # test, changelog, tag, push; CI publishes the tag