Skip to main content
When a user connects multiple wallets — for example, an EVM and a Solana account, or several EVM networks — you almost always want to display each balance side by side. The right tool for this is getMultichainTokenBalances: it batches every chain, every network, and every address into a single API call, so you avoid the N×M round-trip explosion of looping getTokenBalances (or getNativeBalance) over each wallet.

Why getMultichainTokenBalances

getMultichainTokenBalances accepts a list of balanceRequests. Each entry says: for this address on this chain, get the balances on these networks. The API fans out, dedups, and returns a single grouped result.
{
  balanceRequest: {
    filterSpamTokens: true,
    balanceRequests: [
      { address: '0xabc…', chain: 'EVM', networkIds: [1, 137, 56] },
      { address: 'CKEAuq…', chain: 'SOL', networkIds: [101] },
    ],
  }
}
Each balance row in the response carries isNative, symbol, price, decimals, rawBalance, etc. — so a portfolio view never needs a second call for token metadata or prices. Use filterSpamTokens: true to drop airdrop spam server-side, and whitelistedContracts per request entry to pin specific tokens you care about.

Building the request from connected wallets

Group the user’s connected walletAccounts by chain, then collect the network IDs each address should query. getWalletAccounts is the source of truth — it includes both verified and unverified accounts, so filter on walletAccount.verifiedCredentialId if you only want verified ones.
import {
  getWalletAccounts,
  getMultichainTokenBalances,
} from '@dynamic-labs-sdk/client';

// Customize per-app: which networks you care about for each chain
const NETWORKS_BY_CHAIN = {
  EVM: [1, 137, 56, 8453], // Ethereum, Polygon, BNB, Base
  SOL: [101],
  BTC: [0],
};

export const getAllBalances = async () => {
  const walletAccounts = getWalletAccounts();

  // Group addresses by chain (you may have several EVM accounts)
  const byChain = walletAccounts.reduce((acc, walletAccount) => {
    const networkIds = NETWORKS_BY_CHAIN[walletAccount.chain];
    if (!networkIds) return acc;

    acc.push({
      address: walletAccount.address,
      chain: walletAccount.chain,
      networkIds,
    });
    return acc;
  }, []);

  if (byChain.length === 0) return [];

  const chainBalances = await getMultichainTokenBalances({
    balanceRequest: {
      filterSpamTokens: true,
      balanceRequests: byChain,
    },
  });

  return chainBalances;
};

Reading the response

The response is grouped by chain → network → balance rows. Each row carries the metadata you need to render a portfolio cell without a second fetch.
[
  {
    chain: 'EVM',
    walletAddress: '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0',
    networks: [
      {
        networkId: 1,
        balances: [
          {
            symbol: 'ETH',
            isNative: true,
            balance: 0.42,
            rawBalance: 4.2e17,
            decimals: 18,
            price: 3200.55,
            marketValue: 1344.23,
            logoURI: 'https://…',
          },
          {
            symbol: 'USDC',
            isNative: false,
            address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
            balance: 250.0,
            rawBalance: 2.5e8,
            decimals: 6,
            price: 1.0,
            marketValue: 250.0,
            logoURI: 'https://…',
          },
        ],
      },
    ],
  },
]
Common ways to slice it for the UI:
// Flatten into a single list of "wallet × chain × network × token" rows
const tokenRows = chainBalances.flatMap((chainBalance) =>
  chainBalance.networks.flatMap((network) =>
    network.balances.map((tokenBalance) => ({
      walletAddress: chainBalance.walletAddress,
      chain: chainBalance.chain,
      networkId: network.networkId,
      ...tokenBalance,
    }))
  )
);

// Sum marketValue for a portfolio total
const portfolioTotalUsd = tokenRows.reduce(
  (sum, tokenRow) => sum + (tokenRow.marketValue ?? 0),
  0
);

// Group by token symbol for a "holdings" view
const balanceBySymbol = tokenRows.reduce<Record<string, number>>(
  (accumulator, tokenRow) => {
    accumulator[tokenRow.symbol] =
      (accumulator[tokenRow.symbol] ?? 0) + tokenRow.balance;
    return accumulator;
  },
  {}
);

Patterns

Pin specific tokens per chain

When you only care about a stablecoin or a handful of governance tokens, pass whitelistedContracts per request entry. The API skips the rest, response stays small.
await getMultichainTokenBalances({
  balanceRequest: {
    filterSpamTokens: true,
    balanceRequests: [
      {
        address: walletAccount.address,
        chain: 'EVM',
        networkIds: [1, 137],
        whitelistedContracts: [
          '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC mainnet
          '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC polygon
        ],
      },
    ],
  },
});

Disable spam filtering for power-user views

filterSpamTokens: false returns the raw set, including airdrop spam. Use this on a “see all tokens” toggle, not in the default view.

Refresh on walletAccountsChanged

useWalletAccounts already covers this in React. For vanilla apps, subscribe via onEvent:
import { onEvent } from '@dynamic-labs-sdk/client';

onEvent({
  event: 'walletAccountsChanged',
  listener: () => {
    getAllBalances().then(render);
  },
});

When NOT to use it

  • You only need the native gas balance — use getNativeBalance for an RPC-direct read.
  • You have a single wallet on a single chain (one or more networks of that chain) and prefer the simpler call shape — use getTokenBalances.