Solana getTokenAccountBalance returns four views of one SPL token balance: amount (raw integer string in base units), decimals (the mint's decimal places), uiAmount (a JSON number scaled by decimals), and uiAmountString (the same scaled value as an exact decimal string). The human-readable balance is amount divided by 10^decimals. Because JSON numbers are floating point, uiAmount can lose precision for large amounts or high-decimal tokens, so uiAmountString is the field to trust for money. This guide explains the token-account model, shows runnable Node.js examples that derive an associated token account and recompute the human amount, and provides a results table for verifying behavior against your own RPC endpoint.
The SPL token-account model behind getTokenAccountBalance
On Solana, an SPL token balance does not live on a wallet. It lives on a token account: a separate on-chain account that holds a raw integer amount in base units and a reference to the mint that defines the token. The mint is the authority for decimals, the number of base units that make up one whole token. The human-readable balance is therefore amount / 10^decimals, a division you must perform yourself or let the RPC do for you. The Solana token documentation describes this mint-decimals-base-units relationship and the associated token account (ATA) derivation that links an owner and a mint to a deterministic token account address.
This separation matters because getTokenAccountBalance takes a single token account address, not a wallet address. A wallet address is a system account that holds lamports; a token account is a different account type that holds an SPL token amount. Passing a wallet address to getTokenAccountBalance is a common bug that produces an error or an unexpected account. To read a wallet's balance for a specific mint, you first derive the ATA from the owner and mint, then query that token account. For a broader treatment of account types and rent, see Reading Solana accounts and token balances.
- Token account: holds a raw integer amount in base units plus a mint reference.
- Mint: defines decimals, the number of base units per whole token.
- Human-readable balance: amount / 10^decimals.
- ATA: deterministic token account derived from owner + mint.
The getTokenAccountBalance response contract
The Solana getTokenAccountBalance reference documents the method as taking a single token account address and returning a value object with amount, decimals, uiAmount, and uiAmountString, wrapped in the standard JSON-RPC 2.0 response envelope with a context slot. The JSON-RPC 2.0 specification defines that envelope: a jsonrpc version, an id, and either a result or an error. The context slot tells you which ledger slot the node used to answer, which is how you reason about freshness.
The four fields are four views of one balance. amount is the raw integer as a string, in base units. decimals is the mint's decimals. uiAmount is a JSON number equal to amount scaled by decimals. uiAmountString is the same scaled value expressed as a decimal string. The RPC returns amount as a string because raw token amounts can exceed the safe integer range of many languages; it returns uiAmountString for the same reason. The method reads exactly one token account, so it is not a portfolio query.
- amount: raw integer string, base units.
- decimals: mint decimal places.
- uiAmount: JSON number, amount scaled by decimals.
- uiAmountString: exact decimal string, amount scaled by decimals.
- context.slot: ledger slot used to answer.
Why uiAmountString is the field to trust for money
JSON numbers are floating point in most runtimes, including JavaScript. A value like 1234567.89 cannot always be represented exactly, and the problem grows with large amounts or high-decimal tokens. uiAmount is a JSON number, so it can lose precision in transit or in your parser. uiAmountString is a decimal string, so it preserves every digit the node produced. For accounting, reconciliation, or any comparison that must be exact, parse uiAmountString as a decimal string or a big-number type rather than trusting uiAmount.
This is not a Solana-specific quirk; it is a property of JSON numbers. The safe pattern is to treat amount and uiAmountString as the authoritative fields and treat uiAmount as a convenience for display only. If you must use uiAmount, round it deliberately and never use it as a key or an equality check. The Solana getTokenAccountBalance reference documents both fields, so the choice is yours, but the precision guarantee belongs to the string.
- uiAmount is a JSON number and may lose precision.
- uiAmountString preserves exact digits.
- Use amount + decimals for exact math.
- Use uiAmount only for display.
Deriving the associated token account before querying
Because getTokenAccountBalance expects a token account, the first step for a wallet-and-mint query is deriving the ATA. The ATA is a program-derived address computed from the owner, the token program, and the mint. The Solana token documentation covers this derivation. If the ATA does not exist yet, the query will fail or return an empty result; you may need to create it or handle the missing-account case. For wallets with many token accounts, enumerating them is a separate task covered in getTokenAccountsByOwner pagination for large wallets.
A subtle point: a wallet can have multiple token accounts for the same mint if they were created outside the ATA convention. The ATA is the canonical one, but it is not the only possible one. If your balance looks wrong, confirm you are querying the token account you think you are. The Solana API guide (RPC Assistant) is a useful companion for endpoint-level questions.
- Derive the ATA from owner + mint before querying.
- Handle the missing-account case explicitly.
- A wallet may hold more than one token account per mint.
- Confirm the token account address before trusting the balance.
Runnable Node.js example with @solana/web3.js
The example below derives the ATA for an owner and mint, calls getTokenAccountBalance, prints all four fields, and recomputes the human amount from amount and decimals to show the values agree. It uses @solana/web3.js and a public RPC endpoint placeholder. Replace the endpoint and the owner/mint values with your own. The recomputation uses a big-integer-safe approach so it does not inherit the float problem.
Run it with Node.js after installing @solana/web3.js. The output should show amount, decimals, uiAmount, uiAmountString, and a recomputed amount that matches uiAmountString. If the ATA does not exist, the call throws; catch that case and report it rather than assuming a zero balance.
// npm install @solana/web3.js
const { Connection, PublicKey, getAssociatedTokenAddress } = require('@solana/web3.js');
async function main() {
const endpoint = 'https://api.mainnet-beta.solana.com';
const connection = new Connection(endpoint, 'confirmed');
const owner = new PublicKey('REPLACE_WITH_OWNER_WALLET');
const mint = new PublicKey('REPLACE_WITH_MINT');
const ata = await getAssociatedTokenAddress(mint, owner);
console.log('ATA:', ata.toBase58());
try {
const res = await connection.getTokenAccountBalance(ata);
const { amount, decimals, uiAmount, uiAmountString } = res.value;
console.log('amount:', amount);
console.log('decimals:', decimals);
console.log('uiAmount:', uiAmount);
console.log('uiAmountString:', uiAmountString);
// Recompute human amount from raw amount and decimals without floats.
const raw = BigInt(amount);
const scale = BigInt(10) ** BigInt(decimals);
const whole = raw / scale;
const frac = raw % scale;
const fracStr = frac.toString().padStart(decimals, '0').replace(/0+$/, '');
const recomputed = fracStr.length ? `${whole}.${fracStr}` : whole.toString();
console.log('recomputed:', recomputed);
console.log('match:', recomputed === uiAmountString);
} catch (err) {
console.error('Query failed (missing ATA or bad account?):', err.message);
}
}
main();Raw JSON-RPC example over fetch
If you prefer not to add a dependency, the same query works over raw JSON-RPC. The request body follows the JSON-RPC 2.0 specification: a jsonrpc version, an id, a method, and params. The method is getTokenAccountBalance and the single param is the token account address. The response contains the same four fields plus the context slot. This example uses fetch and prints the parsed value.
Use this form when you want to see the exact wire response, including the context slot and any error object. It is also the easiest way to test a specific RPC endpoint before wiring it into an application. Replace the endpoint and the token account address with your own.
// Node.js 18+ (global fetch)
async function getBalance(tokenAccount) {
const endpoint = 'https://api.mainnet-beta.solana.com';
const body = {
jsonrpc: '2.0',
id: 1,
method: 'getTokenAccountBalance',
params: [tokenAccount]
};
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const json = await res.json();
if (json.error) {
console.error('RPC error:', json.error);
return;
}
const { amount, decimals, uiAmount, uiAmountString } = json.result.value;
console.log('slot:', json.result.context.slot);
console.log('amount:', amount);
console.log('decimals:', decimals);
console.log('uiAmount:', uiAmount);
console.log('uiAmountString:', uiAmountString);
}
getBalance('REPLACE_WITH_TOKEN_ACCOUNT');Results table for verifying your own endpoint
Because provider behavior and node versions can vary, verify the response contract against your own endpoint rather than trusting a single example. Fill the table below with real values from your RPC provider. The recomputed amount should match uiAmountString exactly; if it does not, check your decimal handling or your parser. Record the context slot so you can reason about freshness.
This table is a measurement method, not a benchmark. It does not assert any provider's latency or throughput. It simply lets you confirm that amount, decimals, uiAmount, and uiAmountString are consistent for the accounts you care about.
- mint: the token mint address.
- token account: the token account you queried.
- raw amount: the amount field as returned.
- decimals: the decimals field as returned.
- uiAmount: the JSON number as returned.
- uiAmountString: the decimal string as returned.
- recomputed amount: amount / 10^decimals computed by you.
- match: whether recomputed equals uiAmountString.
- context slot: the slot from the response.
getTokenAccountBalance vs getTokenSupply vs getBalance
These three methods answer different questions and are easy to confuse. getBalance returns lamports for a native system account, not an SPL token balance. getTokenAccountBalance returns the balance of one SPL token account. getTokenSupply returns the total supply of a mint, not any individual holder's balance. If you want a wallet's holdings across many mints, none of these alone is enough; you need getTokenAccountsByOwner, covered in getTokenAccountsByOwner pagination for large wallets.
A practical rule: use getBalance for SOL, getTokenAccountBalance for a specific token account, and getTokenSupply for mint-level totals. Mixing them up is a frequent source of wrong numbers. For Token-2022 mints, transfer fees and withheld amounts can further complicate what a holder actually controls, which is discussed in Token-2022 transfer fees and withheld amounts.
- getBalance: lamports for a native account.
- getTokenAccountBalance: one SPL token account.
- getTokenSupply: total supply of a mint.
- getTokenAccountsByOwner: all token accounts for a wallet.
Decimals vary by mint and must be read, never assumed
Decimals come from the mint and are not universal. USDC uses 6 decimals, many Solana tokens use 9, and others differ. Assuming 9 for every token will produce wrong human-readable balances. The only authoritative source is the mint account, and getTokenAccountBalance conveniently returns the decimals alongside the amount, so you do not need a second call in the common case. Still, treat decimals as data, not a constant.
If you cache decimals, invalidate the cache when the mint changes or when you switch tokens. A stale decimals value silently corrupts every derived balance. The Solana token documentation is the reference for how decimals are stored and used.
- USDC: 6 decimals.
- Many Solana tokens: 9 decimals.
- Always read decimals from the mint or the response.
- Invalidate cached decimals when the mint changes.
Limitations and tradeoffs
getTokenAccountBalance reads exactly one token account. It is not a portfolio method and will not enumerate a wallet's holdings. For that, use getTokenAccountsByOwner. The decimals field is authoritative only when it reflects the mint; if you derive decimals elsewhere, you own that risk. uiAmount precision is not guaranteed because it is a JSON number, so uiAmountString or amount plus decimals is the safe path.
Freshness is another tradeoff. A just-submitted transaction may not be reflected at the commitment level you query. Use an appropriate commitment and, if you need to wait, poll or subscribe rather than assuming the first read is final. For subscription-based updates, see accountSubscribe encoding: base64 vs jsonParsed. Finally, provider behavior for error shapes and context fields is documented but can vary by provider, so test against the endpoint you actually use.
- Reads one token account only.
- Decimals is authoritative only from the mint.
- uiAmount precision is not guaranteed.
- Freshness depends on commitment and timing.
- Error shapes can vary by provider.
Troubleshooting wrong account type, UI confusion, and stale reads
Wrong account type: if you pass a wallet address, the node may return an error or a different account's data. Derive the ATA from owner and mint first, and confirm the address you query is a token account. UI confusion: if your UI shows a different number than the RPC, check whether the UI is using uiAmount (float) or uiAmountString (exact), and whether it applied the correct decimals. A mismatch usually means a float rounding or a wrong decimals assumption.
Stale reads: if a recent transfer is missing, check the context slot and the commitment level. A read at a lower commitment may lag. Re-query at a higher commitment or wait for confirmation. If the ATA does not exist, the call fails rather than returning zero; handle that case explicitly so your UI does not show a misleading balance. For endpoint-level issues, the Solana API guide (RPC Assistant) and API service pages are useful references.
- Wrong account type: derive and verify the ATA.
- UI confusion: prefer uiAmountString and correct decimals.
- Stale reads: check context slot and commitment.
- Missing ATA: handle the error, do not assume zero.