Guide

Getting Started

Install the package and read one balance. The same shape comes back whether Etherscan or Mempool or dcrdata or an Arweave gateway answered.
The API and the tool list can still move. Pin exact versions if you build on it now.

Why this exists

Every block explorer answers the same questions and none of them agree on how. Etherscan wants a chain id and a key in the query, Blockscout wants neither, Koios wants the address in a POST body, TONAPI answers in nanotons and Arweave in winstons, and Solscan and Helius can't even agree on which Solana questions exist. Keep one client per explorer in a script or an agent and you've got nineteen ways to misread a number.

So @agntn/explorers puts one abstract Provider in front of all of it. Two required reads, eight optional ones, one Balance, one Transaction, one Utxo, one TokenBalance, one GasData, one BlockInfo. Amounts are strings in the chain's smallest unit, so nothing gets rounded before you decide it should be. And when you don't say which provider, it picks one from the keys you have and tells you which.

Install

shell
pnpm add @agntn/explorers

Node.js 26 or newer.

First call

Blockscout needs no key, so this works with no config at all:

tsbalance.ts
import { create, resolveEns, resolveProvider } from "@agntn/explorers";

const provider = await create(resolveProvider(undefined, "ethereum"));
const address = await resolveEns("vitalik.eth");
if (!address) throw new Error("ENS name did not resolve");

const balance = await provider.getBalance(address, "ethereum");
const transactions = await provider.getTxHistory(address, "ethereum", { limit: 10 });

console.log(`${balance.balanceFormatted} ${balance.symbol}`);
console.log(transactions.map((transaction) => transaction.hash));

Set ETHERSCAN_API_KEY and the same two lines go through Etherscan instead. Nothing else changes. The whole ranking is in Provider selection.

Same call, any chain

ts
const mempool = await create("mempool");
await mempool.getBalance("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "bitcoin");

const dcrdata = await create("dcrdata");
await dcrdata.getBlockInfo(1000, "decred");

const arweave = await create("arweave");
await arweave.getBalance("FPjbN_btYKzcf8QASjs30v5C0FPv7XpwKXENBW8dqVw", "arweave");

The Explorer runs these exact calls against the docs worker, so you see what comes back before you write a line.

Optional operations

Required operations live on Provider. Optional ones stay absent when a backend can't serve them, so check both the flag and the method:

ts
if (provider.capabilities.contractInfo && provider.getContractInfo) {
  const contract = await provider.getContractInfo(
    "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
    "ethereum",
  );
  console.log(contract.isVerified, contract.name);
}

There's no stub that returns convincing nonsense. A provider without contract metadata has no getContractInfo, full stop.

What ships

OperationMethodWhere
native balancegetBalanceevery provider except Helius, eCash Explorer and Aptos
transaction historygetTxHistoryevery provider except Aptos
one transactiongetTxDetailall but TONAPI, Blockberry, eCash Explorer and Aptos
unspent outputsgetUtxosMempool, Blockstream, WhatsOnChain, Blockbook, Haskoin
shown public keygetPubkeyMempool, Blockstream
contract metadatagetContractInfoEtherscan, Blockscout
token holdingsgetTokenBalancesEtherscan, Blockscout, Helius, Koios, Horizon
token transfersgetTokenTransfersEtherscan, Blockscout, Horizon
gas suggestionsgetGasDataEtherscan, Blockscout, Mempool on Bitcoin and Litecoin, Horizon
one blockgetBlockInfoall but TONAPI, Helius, Blockberry, Koios and Aptos

The per provider detail, with the traps, is on the provider pages. The per chain view is on Chains.

One provider, no registry

ts
import { Mempool } from "@agntn/explorers/providers/mempool";

const mempool = new Mempool({ timeout: 30_000 });

A sub path import gives you the class and leaves the other eighteen out of your bundle. create() is asynchronous for the same reason: it imports the one provider it was asked for and nothing else. Everything the registry answers without an instance, providers(), has(), supportsChain(), supportsCapability(), getDefaultURL() and resolveProvider(), stays synchronous and reads only metadata.

Next