Balances
Every account reads its own balances: getBalance() for the network’s gas coin and
getTokenBalance(token) for a token contract. The wallet decides which tokens to read, formats
the amounts and adds prices. This page builds that in a few small functions.
Snippets come from examples/balances, which our CI typechecks and tests. The live run reads
real balances on Base.
npm install @anywalletkit/kit @anywalletkit/wallet-evm @anywalletkit/networks @anywalletkit/swap2-assetsimport { bigintToUnits, type IWalletAccountReadOnly } from '@anywalletkit/core'import { Wallet } from '@anywalletkit/kit'import { builtinNetwork } from '@anywalletkit/networks'import { ccChain } from '@anywalletkit/swap2-assets'import { WalletManagerEvm, type EvmProvider } from '@anywalletkit/wallet-evm'One account per chain
Section titled “One account per chain”/** One registration per EVM network, each with its own RPC list. */export function balancesWallet(seedPhrase: string, rpcs: Record<number, EvmProvider[]>): Wallet { const wallet = new Wallet(seedPhrase) for (const [id, provider] of Object.entries(rpcs)) { wallet.registerWallet(`eip155:${id}`, WalletManagerEvm, { provider, chainId: Number(id) }) } return wallet}Register each EVM network on its own chain id so it gets its own RPC list. The same seed gives the same address on every EVM network, but the balances are separate. To take the RPC lists from the user’s settings, see Networks.
Which tokens to read
Section titled “Which tokens to read”A chain has no call that lists the tokens an address holds. The wallet keeps a list.
/** A token the wallet reads a balance for. `token: null` is the network's gas coin. */export interface ListedToken { chain: string token: string | null symbol: string decimals: number /** `catalog`: shipped with the SDK; `user`: the user added it by contract address. */ source: 'catalog' | 'user'}
/** The gas coin from the network table, the catalog's tokens, then the tokens the user added. */export function tokenList(chain: string, userTokens: ListedToken[] = []): ListedToken[] { const network = builtinNetwork(chain) const native: ListedToken[] = network ? [{ chain, token: null, symbol: network.symbol, decimals: network.decimals, source: 'catalog' }] : [] const catalog: ListedToken[] = (ccChain(chain)?.assets ?? []) .filter((a) => a.contract) // the native entry is already in from the network table .map((a) => ({ chain, token: a.contract!, symbol: a.symbol, decimals: a.decimals, source: 'catalog' })) const known = new Set(catalog.map((t) => t.token!.toLowerCase())) const own = userTokens.filter((t) => t.chain === chain && t.token && !known.has(t.token.toLowerCase())) return [...native, ...catalog, ...own]}- The gas coin comes from the network table in
networks: its symbol and decimals per network. POL on Polygon, MNT on Mantle. Never hardcode “ETH”. - The catalog in
swap2-assetslists the native coin and the major tokens per chain, with contract addresses and decimals. It is a short curated list, not a full token list. - The user’s tokens are the ones they added by contract address. Keep them in your own
storage, mark them as user-added, and drop duplicates of catalog tokens. Custom
tokens does this with
@anywalletkit/tokens: it reads the token from the chain and warns about look-alikes before it is saved.
To find tokens outside the list, use a holdings indexer. Balances and prices lists providers per chain. Show what it finds behind a “hidden” filter: most unknown tokens are spam.
Read the balances
Section titled “Read the balances”export interface BalanceRow extends ListedToken { /** Base units. `null` when this read failed; never show a failed read as 0. */ amount: bigint | null error?: string}
/** Every listed balance of one account. One failing token does not hide the others. */export async function readBalances(account: IWalletAccountReadOnly, tokens: ListedToken[]): Promise<BalanceRow[]> { const reads = await Promise.allSettled(tokens.map((t) => (t.token ? account.getTokenBalance(t.token) : account.getBalance()))) return tokens.map((t, i) => { const r = reads[i]! return r.status === 'fulfilled' ? { ...t, amount: r.value } : { ...t, amount: null, error: String((r.reason as Error)?.message ?? r.reason) } })}- Amounts are
bigintin base units. 1 USDC is1_000_000n, 1 ETH is10n ** 18n. - A failed read is not zero. One token contract can revert or one RPC can time out. Keep
the row with its error and show “couldn’t load”, never
0. A wrong zero makes users think their funds are gone. - Chains without tokens. Bitcoin and Zcash accounts throw on
getTokenBalance. Read onlygetBalance()there. Solana, TON, Tron and NEAR accounts read tokens the same way as EVM: an SPL mint, a jetton master, a TRC-20 contract or a NEP-141 account.
Format the amount
Section titled “Format the amount”/** Exact decimal string from base units, cut to `maxFraction` digits (rounded down). No Number on the way. */export function formatAmount(amount: bigint, decimals: number, maxFraction = 6): string { const negative = amount < 0n const abs = negative ? -amount : amount const base = 10n ** BigInt(decimals) const whole = (abs / base).toLocaleString('en-US') const fraction = (abs % base).toString().padStart(decimals, '0').slice(0, maxFraction).replace(/0+$/, '') return `${negative ? '-' : ''}${whole}${fraction ? '.' + fraction : ''}`}Format from the bigint itself. Number(amount) loses precision above 2⁵³, and some Hermes
builds return wrong values above 2⁶³, so an 18-decimal balance can show a wrong number. When
you need a Number for math (a USD value, a chart), use bigintToUnits(amount, decimals) from
core. It is exact for display and pricing. Never use it for an amount you sign.
Every chain at once
Section titled “Every chain at once”/** Balances on every chain at once. A chain whose RPC is down comes back with its error, not as zero. */export async function portfolio(wallet: Wallet, chains: string[], userTokens: ListedToken[] = []) { const results = await Promise.allSettled(chains.map(async (chain) => readBalances(await wallet.getAccount(chain), tokenList(chain, userTokens)))) return chains.map((chain, i) => { const r = results[i]! return r.status === 'fulfilled' ? { chain, rows: r.value } : { chain, rows: [] as BalanceRow[], error: String((r.reason as Error)?.message ?? r.reason) } })}Read the chains in parallel. A chain whose RPC is down comes back with its rows marked as failed, and the rest of the screen still loads.
When to refresh:
- when the screen opens and when the app returns to the foreground;
- after the user sends, once the receipt arrives. Until then, show your own pending row: a balance read right after a broadcast may not include it yet;
- on pull to refresh. Avoid fast timers: each refresh is one call per token per chain, and public RPCs rate-limit quickly.
Prices
Section titled “Prices”The SDK has no price source. You bring a price API and call it through your server, so its key stays off the device. The function below takes it as a parameter.
/** Your price API, behind your server: USD per whole token, keyed by `chain/token` (token = contract, or `native`). */export type PriceSource = (keys: string[]) => Promise<Record<string, number>>
export const priceKey = (t: ListedToken) => `${t.chain}/${t.token ? t.token.toLowerCase() : 'native'}`
/** USD value per row and a total. A token with no price is `null` and stays out of the total. */export async function valued(rows: BalanceRow[], prices: PriceSource) { const quotes = await prices(rows.filter((r) => r.amount !== null && r.amount > 0n).map(priceKey)) const out = rows.map((r) => { const price = quotes[priceKey(r)] const usd = r.amount !== null && price !== undefined ? bigintToUnits(r.amount, r.decimals) * price : null return { ...r, usd } }) return { rows: out, totalUsd: out.reduce((sum, r) => sum + (r.usd ?? 0), 0), unpriced: out.filter((r) => r.usd === null && (r.amount ?? 0n) > 0n).length }}- Look up by contract address, never by symbol. Symbols collide, and fake tokens copy real ones.
- No price is
null, not $0. Show “no price” and leave the token out of the total. - Price only what the user holds. Zero balances never reach the API.
- Cache prices for a minute or more and show when a price is stale.
Providers to compare are listed on Balances and prices.