Guide

Balances

getBalance on every chain and the Balance shape. ENS resolution. UTXO totals and the mempool delta. Unspent outputs. Why blockNumber is sometimes null.

The call

ts
const balance = await provider.getBalance(address, "ethereum");

Every provider except Helius and Aptos implements it. Helius has no REST balance endpoint and Aptos has no explorer API at all, so both throw UnsupportedOperationError and their capabilities.balances is false.

The shape

ts
interface Balance {
  address: string;
  chain: ChainKey;
  fetchedAt: string; // ISO, when the provider finished the read
  blockNumber: number | null; // the height the answer describes, when the explorer says
  blockHash: string | null;
  balance: string; // smallest unit, exact
  balanceFormatted: string; // decimal, exact
  funded?: string; // UTXO explorers only: cumulative received
  spent?: string; // UTXO explorers only: cumulative spent
  unconfirmed?: string; // UTXO explorers only: signed mempool delta, not in balance
  symbol: string; // ETH, BTC, AR, DCR…
}

balance is a string because a JavaScript number can't hold 6.712150161831460931 ETH in wei without losing the tail. balanceFormatted is the same value with the decimal point in place, computed with formatWei(value, decimals) and nothing else. Neither is rounded.

fetchedAt is always there. blockNumber and blockHash are there when the explorer's answer names a block and null when it doesn't, so an unknown chain position is explicit instead of a zero that looks like genesis. Today only Blockchair passes a height through, from the context.state of its dashboard answer; every other provider's balance endpoint says nothing about a block, so both fields stay null there.

funded and spent come from explorers that keep cumulative totals: Mempool, Blockstream and Blockchair on the Bitcoin family, Haskoin on Bitcoin Cash, dcrdata on Decred, Koios on Cardano. They're absent everywhere else rather than set to zero.

unconfirmed is the signed sum of what's sitting in the mempool for the address, from Mempool, Blockstream, WhatsOnChain, Blockbook and dcrdata. It's kept out of balance on purpose: a balance that moves before a block confirms it isn't a balance, it's a guess. The CLI prints it as Unconfirmed delta when it's there. Haskoin splits it another way: a mempool spend already leaves its balance, and unconfirmed holds only the outputs waiting in the mempool, so it's never negative. The sum is the same.

Unspent outputs

ts
const utxos = await provider.getUtxos?.(address, "bitcoin");

Mempool and Blockstream on the chains they serve, WhatsOnChain on Bitcoin SV, Blockbook on Bitcoin Gold, Haskoin on Bitcoin Cash. Each Utxo is txid, vout, value and valueFormatted, confirmed, and blockNumber, blockHash and timestamp when the funding transaction is in a block, null while it waits in the mempool. WhatsOnChain, Blockbook and Haskoin name the height and not the hash, so their blockHash stays null. This is the spendable set, the thing a balance on Bitcoin is actually made of. The CLI's explorers utxos prints fifty of them with the count and the confirmed total of all of them on top, and the tool explorers_utxos lists the same fifty with total next to them. getUtxos() itself returns everything. Mempool and Blockstream refuse the list past a few hundred outputs with a 400, which the library hands you as an HTTPError, so an address that collects dust for a living won't answer there. WhatsOnChain and Haskoin page it instead, and both providers stop after ten pages.

Has it shown its key?

ts
const reveal = await provider.getPubkey?.("1XPTgDRhN8RFnzniWCddobD9iKZatrvH4", "bitcoin");
reveal?.pubkey; // "0434417d...", from the newest spend, in reveal.txid

A P2PKH address is a hash. The key behind it stays private until the first spend. After that it's on chain for good, sitting in the input that spent it. Does that matter? For a puzzle, a lot. Kangaroo and BSGS start from a public key, so an address that never spent gives them nothing to chew on.

getPubkey() walks the address feed newest first and stops at the first input that carries the key. P2PKH keeps it at the end of the scriptSig. P2WPKH and P2SH-wrapped P2WPKH keep it in the witness. Taproot hides nothing at all: the address is the output key, so source comes back as output with the transaction that paid it. Mempool serves it on Bitcoin, Litecoin and Pepecoin, Blockstream on Bitcoin.

A null pubkey means one of two things, and spent tells you which. false means nothing ever left the address, so its own history holds no key. Its twin might, though: a compressed key behind 1... also owns the bc1q... with the same hash, so ask that one too. true means it spent through a script this read doesn't parse, a multisig say, or only before its newest 1000 transactions. Pepecoin stops at 250, because Peppool allows 15 requests a minute and nobody wants to wait out the window. The address counters answer first, so an address that never spent costs one or two requests and a busy one at most forty pages.

ENS

resolveEns("vitalik.eth") goes through public HTTP resolvers, no key and no keccak, and returns the address or null. resolveAddresses(input, chain) does the classification for you: an address or a hash passes through, a .eth name gets resolved, and a name that doesn't resolve is a NotFoundError. The CLI, the tools and the docs worker all go through it, so explorers vitalik.eth and explorers 0xd8dA…6045 are the same read.

resolveAddresses also takes a list, up to twenty entries, which is what explorers_balance and explorers balance use when you hand them several addresses at once.

Decimals

formatWei(value, decimals) defaults to 18. Bitcoin, Bitcoin Cash, Bitcoin SV, Bitcoin Gold, Litecoin, Dogecoin, Pepecoin, Zcash, Blockstream and Decred amounts are 8, Solana 9, Arweave 12, Cardano 6, TON 9, TRON 6, Sui 9, Stellar 7, eCash 2. The providers apply the right one, so balanceFormatted is always correct; the default only matters when you format balance yourself.

Reading ahead

The Explorer shows the full shape for any address, unspent outputs included on the Bitcoin family. The landing walks through three addresses on three chains, recorded through the library and swapped for live answers as the worker replies.