Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides12 min read

Reading Solana Accounts: getAccountInfo, Rent Exemption, and Token Balances

Learn how Solana account data, rent, and SPL token balances work, and which RPC method to use for each read.

TL;DR

Every piece of state on Solana lives in an account: a 32-byte address, a lamport balance, an owner program, an executable flag, a rent epoch, and an opaque data buffer. To read a wallet's SOL balance use getBalance; to read full account metadata and data use getAccountInfo; to read SPL token holdings you must query token accounts via getTokenAccountsByOwner, not the wallet's lamport balance. Rent-exemption is enforced by maintaining a minimum lamport balance that scales with account data size, retrievable via getMinimumBalanceForRentExemption. This guide explains the mechanics and provides runnable examples to avoid common misreads.

Direct Answer: Which RPC Method Should You Use?

If you need a wallet's SOL (lamport) balance, call getBalance with the wallet address. If you need the full account state—owner, executable flag, rent epoch, and the raw data buffer—call getAccountInfo. If you need SPL token balances (e.g., USDC or an NFT), do not call getBalance on the wallet or the mint; instead call getTokenAccountsByOwner to list token accounts owned by the wallet, optionally filtered by mint. For a mint's total supply or largest holders, use getTokenSupply and getTokenLargestAccounts. This distinction is the root of most 'blank account' or 'zero balance' bugs.

The Solana JSON-RPC reference at solana.com/developers is the authoritative source for method semantics. OnFinality's Solana RPC endpoints and providers (RPC Assistant) page lists endpoints that support these methods.

  • getBalance – returns only the lamport balance of any account (wallet, mint, token account).
  • getAccountInfo – returns the full account object: lamports, owner, executable, rentEpoch, and data.
  • getTokenAccountsByOwner – returns token accounts (with their balances) owned by a wallet, optionally filtered by mint.
  • getTokenLargestAccounts – returns the largest token accounts for a given mint.
  • getTokenSupply – returns the total supply of a mint.

How Solana Stores State: The Account Model

Solana is not a traditional key-value store with separate tables for balances and smart-contract storage. Instead, every piece of state is an account—a single data structure with a fixed header and an opaque byte array. The header contains: lamports (the SOL balance in lamports, 1 SOL = 1e9 lamports), owner (the program that owns this account and can modify its data), executable (whether the account is a program), rent_epoch (the next epoch at which rent will be collected), and data (a variable-length byte buffer).

The account's data is entirely opaque to the runtime; only the owner program is allowed to write to it. For a system-owned account (like a wallet), the data is empty. For an SPL Token account, the data is a 165-byte binary structure that encodes the mint, owner, balance, and other fields. This design is why reading a token balance requires deserializing account data, not just reading a number.

The Solana documentation on accounts and the AccountInfo structure provide the canonical model. OnFinality's Solana network overview gives context on how accounts fit into the broader chain.

  • Account address: 32-byte ed25519 public key.
  • Lamports: the smallest SOL unit; 1 SOL = 1,000,000,000 lamports.
  • Owner: the program that may modify the account's data.
  • Executable: true only for program accounts.
  • Rent epoch: the next epoch when rent is due (if not rent-exempt).
  • Data: an opaque byte array, often serialized with bincode or custom layouts.

Rent and Rent Exemption: Why Minimum Balances Exist

To prevent state bloat, Solana charges rent to accounts that store data. Rent is paid from the account's lamport balance at each epoch boundary. However, if an account holds at least the minimum balance for rent exemption, it is exempt from rent forever. This minimum is size-dependent: larger data buffers require a larger lamport deposit.

The getMinimumBalanceForRentExemption RPC method returns the exact lamport amount needed for a given data size. For example, an SPL token account with 165 bytes of data requires a specific minimum (documented in the Solana source, but you can query it live). If an account falls below this threshold, it becomes 'rent-paying' and may be garbage-collected if its balance reaches zero.

When you create a token account via the SPL Token program, the system automatically transfers the rent-exempt minimum from the funding wallet. This is why you often see a small SOL balance in token accounts. The Solana rent documentation explains the economic rationale. For practical RPC usage, always call getMinimumBalanceForRentExemption with the data length of the account you plan to create.

  • Rent is collected from accounts that are not rent-exempt.
  • Rent-exempt accounts pay no rent and are never collected.
  • Minimum balance = f(data size), query via getMinimumBalanceForRentExemption.
  • Token accounts are typically created rent-exempt by the wallet that funds them.

Reading Account Data with getAccountInfo

getAccountInfo is the workhorse for reading any account's full state. It accepts an address and optional configuration: commitment, encoding (base58, base64, or base64+zstd), and dataSlice to fetch only a portion of the data. The response includes lamports, owner, executable, rentEpoch, and data as an array of [encodedData, encoding].

A common pitfall is that getAccountInfo returns null for accounts that do not exist, not an empty object. If you see null, the account has never been created or has been deleted. Also, the default encoding is base58, which is inefficient for large data; use base64 for program accounts or token accounts.

The following curl example fetches the account info for the SPL Token program itself (address TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA). Note that the data is large and base64-encoded.

curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getAccountInfo",
  "params": [
    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
    {
      "encoding": "base64",
      "commitment": "confirmed"
    }
  ]
}'

# Expected output (truncated):
# {
#   "jsonrpc": "2.0",
#   "result": {
#     "context": { "slot": 123456 },
#     "value": {
#       "data": ["base64string...", "base64"],
#       "executable": true,
#       "lamports": 1000000000,
#       "owner": "BPFLoader2111111111111111111111111111111111111",
#       "rentEpoch": 0
#     }
#   }
# }

SPL Token Accounts: The 165-Byte Layout and Deserialization

SPL Token accounts are accounts owned by the SPL Token program (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA). Their data is exactly 165 bytes and follows a fixed layout: mint (32 bytes), owner (32 bytes), amount (u64, little-endian), delegate (32 bytes, all zeros if none), state (1 byte: 0=uninitialized, 1=initialized, 2=frozen), is_native (1 byte), delegated_amount (u64), close_authority (32 bytes, optional).

To read the balance, you must deserialize the amount field at offset 64 (after mint and owner). Many SDKs provide helpers: @solana/spl-token has unpackAccount, and @solana/web3.js has AccountInfo but not token-specific parsing. The following Node.js example uses @solana/spl-token to fetch and parse a token account.

The SPL Token program source is the authoritative reference for the layout. For a quick manual check, you can use getAccountInfo and slice the data.

// npm install @solana/web3.js @solana/spl-token
import { Connection, PublicKey } from '@solana/web3.js';
import { getAccount, unpackAccount } from '@solana/spl-token';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const tokenAccountAddress = new PublicKey('YOUR_TOKEN_ACCOUNT_ADDRESS');

// Using getAccount (high-level)
const account = await getAccount(connection, tokenAccountAddress);
console.log('Balance:', account.amount.toString());

// Using unpackAccount (lower-level)
const info = await connection.getAccountInfo(tokenAccountAddress);
const parsed = unpackAccount(tokenAccountAddress, info);
console.log('Owner:', parsed.owner.toBase58());
console.log('Mint:', parsed.mint.toBase58());
console.log('Amount:', parsed.amount.toString());

Wallet vs Associated Token Account: Why Balances Can Be Zero

A wallet's SOL balance is stored in the wallet's system account. A wallet's token balance is not stored in the wallet account; it is stored in one or more separate token accounts that are owned by the SPL Token program. The most common token account is the associated token account (ATA), whose address is deterministically derived from the wallet and mint using findProgramAddress with seeds [wallet, TOKEN_PROGRAM_ID, mint].

If a user has never created an ATA for a particular mint, they may still own tokens in a manually-created token account, or they may have zero tokens. Therefore, querying getBalance on the wallet returns only SOL, and querying getAccountInfo on the wallet returns empty data. To find all token accounts for a wallet, use getTokenAccountsByOwner.

The ATA derivation formula is: findProgramAddress([owner, TOKEN_PROGRAM_ID, mint], TOKEN_PROGRAM_ID). The SPL Associated Token Account documentation explains this. The following example derives an ATA and fetches its balance.

// Node.js example to derive ATA and get balance
import { Connection, PublicKey } from '@solana/web3.js';
import { getAssociatedTokenAddress } from '@solana/spl-token';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const wallet = new PublicKey('YOUR_WALLET_ADDRESS');
const mint = new PublicKey('YOUR_MINT_ADDRESS');

const ata = await getAssociatedTokenAddress(mint, wallet);
console.log('ATA:', ata.toBase58());

const info = await connection.getAccountInfo(ata);
if (info === null) {
  console.log('ATA does not exist. User may have no tokens or uses a non-ATA token account.');
} else {
  const balance = await connection.getTokenAccountBalance(ata);
  console.log('Token balance:', balance.value.amount);
}

Token Holdings: getTokenAccountsByOwner, getTokenLargestAccounts, and getTokenSupply

To list all token accounts owned by a wallet, use getTokenAccountsByOwner. This method accepts the owner address and optional filters: mint (to filter by a specific token) and programId (to filter by token program, useful for token-2022). It returns an array of { pubkey, account } objects, where each account is a standard AccountInfo with base64 data. You must deserialize each to get the balance.

The method supports pagination via the before and limit parameters, where before is a token account pubkey cursor. This is essential for wallets with many token accounts.

For a mint's aggregate statistics, getTokenSupply returns the total supply, and getTokenLargestAccounts returns the top N token accounts by balance. These are useful for analytics but do not tell you which wallet owns a token account unless you also fetch the account's owner field.

The following curl example fetches all USDC token accounts for a wallet. Replace the wallet address with a real one.

curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getTokenAccountsByOwner",
  "params": [
    "WALLET_ADDRESS",
    {
      "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
    },
    {
      "encoding": "jsonParsed"
    }
  ]
}'

# Expected output (truncated):
# {
#   "jsonrpc": "2.0",
#   "result": {
#     "context": { "slot": 123456 },
#     "value": [
#       {
#         "pubkey": "TOKEN_ACCOUNT_ADDRESS",
#         "account": {
#           "data": {
#             "parsed": {
#               "info": {
#                 "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
#                 "owner": "WALLET_ADDRESS",
#                 "state": "initialized",
#                 "tokenAmount": {
#                   "amount": "1000000",
#                   "decimals": 6,
#                   "uiAmount": 1.0
#                 }
#               },
#               "type": "account"
#             },
#             "program": "spl-token",
#             "space": 165
#           },
#           "executable": false,
#           "lamports": 2039280,
#           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#           "rentEpoch": 0
#         }
#       }
#     ]
#   }
# }

Account Write Version and Encoding: Avoiding Common Pitfalls

When you fetch account data, the data field is returned as an array [encoded, encoding]. The encoding can be base58, base64, or base64+zstd. For large data, base58 is inefficient and may hit response size limits. Always use base64 for program accounts or token accounts.

Another pitfall is the account write version—a concept from the Solana runtime that tracks how many times an account has been written. This is not exposed in getAccountInfo but is relevant for transaction simulation and versioned transactions. For reading state, you generally don't need it, but be aware that some RPC responses include a version field in the context.

When using jsonParsed encoding (as in the example above), the RPC node automatically deserializes known token accounts, giving you a human-readable tokenAmount object. This is the easiest way to read token balances without manual deserialization. However, jsonParsed only works for accounts owned by known programs (like SPL Token). For custom programs, you must use base64 and deserialize yourself.

The Solana RPC documentation details the encoding options. OnFinality's API service supports these encodings across its endpoints.

  • Use base64 for large data to avoid base58 bloat.
  • Use jsonParsed for SPL Token accounts to get pre-deserialized balances.
  • For custom programs, fetch base64 and deserialize according to the program's layout.
  • Account data is returned as [data, encoding]; do not treat it as a plain string.

Troubleshooting Checklist: Why You See Zero or Empty Data

If you get null from getAccountInfo, the account does not exist. This is normal for an ATA that has never been created. If you get an account but the data is empty, you may be looking at a system account (wallet) rather than a token account. If you get a token account but the balance is zero, the account may be uninitialized or frozen.

Commitment level matters: processed may return data before finalization, while finalized ensures the state is canonical. For most reads, confirmed is a good default. If you see inconsistent results across calls, check your commitment.

Encoding mismatch is another common issue: if you request base58 but expect a JSON-parsed object, you'll get a string. Always match the encoding to your parsing logic.

Finally, remember that getBalance on a token mint returns the mint's lamport balance (the SOL used to fund the mint account), not the token supply. To get token supply, use getTokenSupply.

  • null account → does not exist; create it or check the address.
  • Empty data on a wallet → that's normal; token balances are in separate accounts.
  • Zero token balance → check if the token account is initialized and not frozen.
  • Wrong commitment → use confirmed or finalized for consistent reads.
  • Encoding mismatch → request jsonParsed for token accounts or parse base64 correctly.
  • getBalance on a mint → returns lamports, not token supply.

Limitations and Tradeoffs

Reading account data via RPC has inherent limitations. First, getAccountInfo returns the entire data buffer, which can be large for program accounts (e.g., the SPL Token program is over 100 KB). Fetching such data repeatedly can be inefficient; consider using dataSlice to fetch only the bytes you need.

Second, RPC providers often impose rate limits and payload size limits. The exact numbers are documented/varies by provider; OnFinality's RPC pricing page lists the plans, but specific caps are not published here. Always check your provider's documentation.

Third, getTokenAccountsByOwner can return a large number of accounts for a wallet with many tokens. Pagination is mandatory for production use. The method's limit parameter is capped by the provider (documented/varies by provider).

Finally, account data is only as fresh as the slot you query. For real-time applications, use WebSocket subscriptions (e.g., accountSubscribe) to monitor changes. OnFinality's Monitoring RPC endpoints and node health guide covers this.

  • Large data buffers can slow down responses; use dataSlice.
  • Provider rate limits and caps vary; check your plan.
  • Pagination is required for wallets with many token accounts.
  • For real-time updates, use WebSocket subscriptions instead of polling.

Next Steps and Further Reading

Now that you understand the account model and RPC methods, you can build reliable indexers and dApps. To go deeper, explore the Solana RPC endpoints and providers (RPC Assistant) to choose the best endpoint for your needs. For historical account state, see Reading Solana historical transaction data over RPC.

If you're building on other chains, the same principles apply: Accessing historical blockchain data covers general patterns, and State proofs with eth_getProof shows how Ethereum does it.

For a broader overview of Solana, visit the Solana network page. And don't forget to check the OnFinality Learn hub for more tutorials. If you need production-grade RPC access, review the API service and RPC pricing pages.

  • Try the examples with your own addresses and compare results.
  • Use a fill-in table to record your measurements: method, address, commitment, encoding, result, and notes.
  • Experiment with dataSlice to fetch only the amount field of a token account.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started