Account Balances
The user's money across connected accounts at a glance, with a net total, an allocation bar and each account's balance.
Experience · Shares patterns with KPI Card, Insight Card
≈ 1,479 tokens per use (tool 1,003 · guidance 362 · typical payload 114)How token costs are estimated
Example: Account Balances
What's my total balance?
Total balance
$56,354.94
Up +3.12%This month
- USDT Wallet 60%
- Bank account 40%
When to use
- The user asks about their money across connected accounts: "What's my total balance?", "How much is in each account?", "How's my net worth this month?"
- The agent has the balances from a connected banking, brokerage or wallet tool.
When not to use
- The agent doesn't have the balances from a tool result. Never invent or estimate them.
- The user asks about transactions or spending. Use a Data Table, a Chart or a KPI Card.
- The user wants to move money, pay or trade. This card has no such actions; confirm those through the host's own flow.
- It's one account's balance. Say it in a sentence.
How the agent should use it
- Pass every balance exactly as the source returned it, and let the component add them up. Only pass
totalwhen the source reports one. - Mark credit cards and loans with kind
creditorloan. They're listed under Liabilities and subtracted from the total, never drawn in the allocation bar. - For an account in another currency, pass
currencyand the source's convertedbalanceInDisplay. Never convert with a rate you made up. - Pass
maskonly as the source already masked it, never a full account number, IBAN or wallet address. - Set
hideAmountswhen the user asks to hide amounts or is sharing their screen. - Add
reconnecttoactionsonly when an account has an error and the host can reconnect it.
Totals
The total is net worth: assets minus what's owed on liabilities, computed in integer minor units so the cents never drift. When there are liabilities, they're listed under Liabilities as negative amounts, and screen readers also hear "Net of $2,140 owed" with the total.
- Liabilities (
credit,loan) are read as the amount owed, whichever sign the source uses, and shown as negative. - Other currencies count only with
balanceInDisplay. The row shows the converted balance with the original underneath ("€20,712.40"). Without it the account shows in its own currency, is marked "Not in total", and a note under the total says "Excludes 1 unconverted". - A stated
totalis shown as given. If it's more than 0.01 away from the accounts, a note says "Total differs from accounts" instead of hiding the mismatch.
Cents are dimmed by splitting Intl's formatToParts, so it works for any currency and locale: $56,354.94, 20.712,40 €, or ¥1,234,567 with no cents at all.
Allocation bar
Segments are proportional to each asset account's share of assets (positive balances only), drawn with the neutral chart scale in account order. The bar has no labels: the same swatch sits beside each account's name in the list. Screen readers get the shares as a list ("USDT Wallet 60%, Bank account 40%"), rounded with largest remainders so they add up to 100%.
Privacy
- Masking is defensive. Whatever arrives in
mask, the component never shows more than the first 4 and last 4 characters, with "…" between (a0xprefix doesn't count).GB12 2381…25shows asGB12…25; a raw12345678as…5678. - Hide amounts replaces every amount with
$••••(the currency symbol stays). The eye button is a toggle (aria-pressed, labelled "Hide amounts"), and screen readers hear "hidden" instead of the figure.hideAmountssets the starting state; the user's toggle wins.
Status and freshness
- Accounts with an
errorshow it with a warning icon, plus a Reconnect button whenreconnectis inactions. - Balances older than 24 hours add "Updated 3 days ago" to the account's quiet line, after the institution and mask. It's computed after mount, so server and client render the same markup. Pass
nowto compare with a fixed moment instead. - The change line ("+3.12% This month") uses an arrow and a sign as well as green or red, so it never relies on color alone.
Streaming
The header arrives first. The total stays a skeleton until every account is in, so it never shows a running total that jumps; accounts appear one by one.
Installation
npx shadcn@latest add https://your-site.com/r/account-balances.json
Usage
import { tool } from "ai"
import { accountBalancesTool } from "@/components/genui/account-balances.schema"
const tools = {
[accountBalancesTool.name]: tool({
description: accountBalancesTool.description,
inputSchema: accountBalancesTool.inputSchema,
// The card answers the question, so the tool can return immediately.
execute: async () => ({ shown: true }),
}),
}Every button is a handoff to your app. Map results to your own screens:
<AccountBalances
{...part.input}
status={part.state === "input-streaming" ? "streaming" : "ready"}
onResult={(r) => {
if (r.action === "add-account") openAddAccount()
if (r.action === "view-details") openAccounts()
if (r.action === "reconnect") openReconnect(r.accountId)
}}
/>The result is one of:
{ action: "add-account" }
{ action: "view-details" }
{ action: "reconnect", accountId: "savings" }With LangGraph
Push it from a graph node with ui.push({ name: "show_balances", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
There's no finished state to restore. Render the stored tool input again; the card stays live.
Schema
| Prop | Type | Notes |
|---|---|---|
title | string? | Default "Total balance" |
currency | string | ISO 4217 code every total is shown in |
total | number? | As the source reports it. Computed from the accounts when omitted. Data-bound |
change | { percent?, amount?, period }? | percent in percent units (3.12 = +3.12%). Data-bound |
accounts | Account[] | 1–20. Data-bound |
hideAmounts | boolean | Start masked. Default false |
actions | ("add-account" | "view-details" | "reconnect")[]? | Default ["add-account", "view-details"] |
locale | string? | BCP 47 locale for numbers |
Account:
| Field | Type | Notes |
|---|---|---|
id | string | Returned with reconnect |
name | string | |
kind | "checking" | "savings" | "credit" | "investment" | "crypto" | "cash" | "loan" | "other" | credit and loan are liabilities |
institution | string? | |
mask | string? | Already masked by the source; clipped to first 4 and last 4 |
balance | number | In the account's currency. For liabilities, the amount owed |
currency | string? | When it differs from the display currency |
balanceInDisplay | number? | Converted by the source. Required when currency differs |
asOf | string? | ISO 8601. Older than 24 hours shows "Updated …" |
error | string? | e.g. "Reconnect needed" |
Host props: status (streaming or ready), now, onResult and className.