read.wallet.balances
Check native ETH and ERC20 token balances for a wallet address using multicall. Returns raw and formatted balances to verify sufficient tokens before deposit or adding liquidity.
Instructions
Get native ETH and ERC20 token balances for a wallet address. Reads directly from chain via RPC multicall. Use before write.account.add_liquidity or write.account.deposit to verify the wallet has sufficient tokens. Returns both raw balance (smallest unit/wei) and formatted (human-readable) per token.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| wallet_address | Yes | Wallet address to check balances for | |
| token_addresses | Yes | ERC20 token contract addresses to check | |
| chain_id | No | Chain ID: 8453 (Base), 130 (Unichain), or 10 (Optimism) |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| native | Yes | ||
| tokens | Yes |
Implementation Reference
- src/tools/read/wallet.ts:42-108 (handler)The main handler function for the 'read.wallet.balances' tool. It validates inputs, builds multicall contracts for each ERC20 token (balanceOf, decimals, symbol), fetches native ETH balance in parallel, and returns formatted token balances along with native ETH balance. Returns JSON content with structured output.
async ({ wallet_address, token_addresses, chain_id }) => { try { const validChainId = validateChainId(chain_id); const wallet = validateAddress(wallet_address, "wallet_address"); const client = getPublicClient(validChainId, chains); // Build multicall: for each token get balanceOf, decimals, symbol const calls = token_addresses.flatMap((addr) => [ { address: addr as `0x${string}`, abi: erc20Abi, functionName: "balanceOf" as const, args: [wallet], }, { address: addr as `0x${string}`, abi: erc20Abi, functionName: "decimals" as const }, { address: addr as `0x${string}`, abi: erc20Abi, functionName: "symbol" as const }, ]); const [nativeBalance, ...multicallResults] = await Promise.all([ client.getBalance({ address: wallet }), ...(calls.length > 0 ? [client.multicall({ contracts: calls, allowFailure: true })] : []), ]); const results = (multicallResults[0] ?? []) as { status: string; result?: unknown }[]; const tokens = token_addresses.map((addr, i) => { const balRes = results[i * 3]; const decRes = results[i * 3 + 1]; const symRes = results[i * 3 + 2]; const balance = balRes?.status === "success" ? String(balRes.result) : "0"; const decimals = decRes?.status === "success" ? Number(decRes.result) : 18; const symbol = symRes?.status === "success" ? String(symRes.result) : "???"; return { address: addr, symbol, decimals, balance, formatted: formatUnits(BigInt(balance), decimals), }; }); const result = { native: { symbol: "ETH", balance: String(nativeBalance), formatted: formatUnits(nativeBalance, 18), }, tokens, }; return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }], structuredContent: result, }; } catch (err) { return { content: [ { type: "text" as const, text: `Error: ${err instanceof Error ? err.message : String(err)}`, }, ], isError: true, }; } }, - src/tools/read/wallet.ts:18-109 (registration)The 'registerWalletTools' function registers the 'read.wallet.balances' tool on the MCP server via server.registerTool(), along with its metadata (annotations, description, input/output schemas).
export function registerWalletTools( server: McpServer, chains: Record<ChainId, ChainConfig>, api: ArcadiaApiClient, ) { server.registerTool( "read.wallet.balances", { annotations: { title: "Get Wallet Balances", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, description: "Get native ETH and ERC20 token balances for a wallet address. Reads directly from chain via RPC multicall. Use before write.account.add_liquidity or write.account.deposit to verify the wallet has sufficient tokens. Returns both raw balance (smallest unit/wei) and formatted (human-readable) per token.", inputSchema: { wallet_address: z.string().describe("Wallet address to check balances for"), token_addresses: z.array(z.string()).describe("ERC20 token contract addresses to check"), chain_id: z.number().default(8453).describe(CHAIN_ID_DESCRIPTION), }, outputSchema: WalletBalancesOutput, }, async ({ wallet_address, token_addresses, chain_id }) => { try { const validChainId = validateChainId(chain_id); const wallet = validateAddress(wallet_address, "wallet_address"); const client = getPublicClient(validChainId, chains); // Build multicall: for each token get balanceOf, decimals, symbol const calls = token_addresses.flatMap((addr) => [ { address: addr as `0x${string}`, abi: erc20Abi, functionName: "balanceOf" as const, args: [wallet], }, { address: addr as `0x${string}`, abi: erc20Abi, functionName: "decimals" as const }, { address: addr as `0x${string}`, abi: erc20Abi, functionName: "symbol" as const }, ]); const [nativeBalance, ...multicallResults] = await Promise.all([ client.getBalance({ address: wallet }), ...(calls.length > 0 ? [client.multicall({ contracts: calls, allowFailure: true })] : []), ]); const results = (multicallResults[0] ?? []) as { status: string; result?: unknown }[]; const tokens = token_addresses.map((addr, i) => { const balRes = results[i * 3]; const decRes = results[i * 3 + 1]; const symRes = results[i * 3 + 2]; const balance = balRes?.status === "success" ? String(balRes.result) : "0"; const decimals = decRes?.status === "success" ? Number(decRes.result) : 18; const symbol = symRes?.status === "success" ? String(symRes.result) : "???"; return { address: addr, symbol, decimals, balance, formatted: formatUnits(BigInt(balance), decimals), }; }); const result = { native: { symbol: "ETH", balance: String(nativeBalance), formatted: formatUnits(nativeBalance, 18), }, tokens, }; return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }], structuredContent: result, }; } catch (err) { return { content: [ { type: "text" as const, text: `Error: ${err instanceof Error ? err.message : String(err)}`, }, ], isError: true, }; } }, ); - src/tools/output-schemas.ts:40-55 (schema)The WalletBalancesOutput Zod schema defining the output shape: a 'native' object (symbol, balance, formatted) and a 'tokens' array (address, symbol, decimals, balance, formatted).
export const WalletBalancesOutput = z.object({ native: z.object({ symbol: z.string(), balance: z.string(), formatted: z.string(), }), tokens: z.array( z.object({ address: z.string(), symbol: z.string(), decimals: z.number(), balance: z.string(), formatted: z.string(), }), ), }); - src/tools/index.ts:34-79 (registration)The 'registerAllTools' function calls 'registerWalletTools(server, chains, api)' on line 46 to wire up all wallet tools including 'read.wallet.balances'.
export function registerAllTools( server: McpServer, api: ArcadiaApiClient, chains: Record<ChainId, ChainConfig>, ) { // Read tools registerAccountTools(server, api, chains); registerPoolTools(server, api); registerAssetTools(server, api); registerStrategyTools(server, api); registerPointsTools(server, api); registerGuideTools(server); registerWalletTools(server, chains, api); registerAssetManagerTools(server); // Write tools — account registerCreateTool(server, chains); registerDepositTool(server, chains); registerWithdrawTool(server, chains); registerBorrowTool(server, chains, api); registerRepayTool(server, chains); registerAddLiquidityTool(server, api, chains); registerRemoveLiquidityTool(server, api); registerSwapTool(server, api); registerDeleverageTool(server, api); registerStakeTool(server, api); registerCloseTool(server, api, chains); // Write tools — wallet registerApproveTool(server, chains); // Write tools — pool (lending tranches, ERC-4626) registerPoolDepositTool(server, chains); registerPoolRedeemTool(server, chains); // Write tools — asset managers registerRebalancerTool(server, chains); registerCompounderTools(server, chains); registerYieldClaimerTools(server, chains); registerCowSwapperTool(server, chains); registerMerklOperatorTool(server, chains); registerSetAssetManagersTool(server, chains); // Dev tools registerSendTool(server, chains); } - src/clients/chain.ts:23-34 (helper)The getPublicClient helper used by the handler to create/retrieve a viem public client for the given chain, which executes RPC calls (getBalance, multicall).
export function getPublicClient(chainId: ChainId, chainConfigs: Record<ChainId, ChainConfig>) { let client = clients.get(chainId); if (!client) { const config = chainConfigs[chainId]; client = createPublicClient({ chain: viemChains[chainId], transport: http(config.rpcUrl), }); clients.set(chainId, client); } return client; }