Solana Account Data: How Programs Store State On-Chain (2026 Guide)

Tools Mentioned in This Article
Compare features, read reviews, and check live health scores on MadeOnSol.

Compare features, read reviews, and check live health scores on MadeOnSol.

Multi-chain RPC provider with free and paid Solana nodes — shared and dedicated endpoints for dApps, bots, and analytics

High-performance Solana RPC node infrastructure for developers and traders

Decentralized RPC network providing fast, resilient Solana node access

Fast and affordable Solana RPC node provider with unlimited requests

Full node RPC access for Solana and 80+ blockchains with simple API keys

Learn how to build a Solana token analytics dashboard that combines real-time price feeds, holder data, trading volume, and liquidity metrics into a single view using TypeScript and the MadeOnSol API.

Solana validators prune old data aggressively, making historical analysis difficult. Learn how to access Solana historical data using BigQuery, Flipside, Dune, Helius DAS API, and archival services — with practical query examples.

Learn how to set up Solana webhooks for real-time blockchain alerts. Compare Helius, QuickNode, Shyft, and Triton webhook providers, with TypeScript code examples for wallet monitoring, NFT sales alerts, and payment confirmation.
Build with MadeOnSol API
KOL tracking, deployer intelligence, DEX streaming, and webhooks. Free API key — 200 requests/day.
import { MadeOnSol } from "madeonsol";
// Get a free API key at madeonsol.com/developer
const client = new MadeOnSol({ apiKey: "msk_your_key" });
const { trades } = await client.kol.feed({ limit: 10 });
const { alerts } = await client.deployer.alerts({ limit: 5 });
const { tools } = await client.tools.search({ q: "trading" });Get weekly Solana ecosystem insights delivered to your inbox.
On Solana, everything is an account. Unlike Ethereum where smart contracts have their own internal storage, Solana separates code from data entirely. Programs (smart contracts) are stateless executables, and all state lives in separate accounts that programs own and control.
Understanding the Solana account model is fundamental if you want to build on-chain programs, parse indexer data, or make sense of what explorers like SolanaFM and Solscan are showing you. This guide breaks down exactly how Solana account data works, from the raw byte layout to practical deserialization examples.
Solana's architecture treats every piece of on-chain data as an account. Your wallet is an account. A token balance is an account. A deployed program is an account. Even the metadata describing an NFT is an account.
This design has important consequences:
This is fundamentally different from Ethereum, where a smart contract's storage is part of the contract itself. On Solana, you need to pass every account a program will read or write as part of the transaction instruction.
Every Solana account has exactly five fields:
| Field | Type | Description |
|---|---|---|
lamports | u64 | Balance in lamports (1 SOL = 1,000,000,000 lamports) |
data | byte array | Arbitrary data stored by the owning program |
owner | Pubkey | The program that owns this account and can modify its data |
executable | bool | Whether this account contains executable program code |
rent_epoch | u64 | The epoch at which this account will next owe rent |
The data field is where things get interesting. Its contents are entirely program-defined -- Solana itself does not impose any structure on account data. A token account stores balances and mint info. A governance proposal stores voting parameters. The layout depends entirely on the program that owns the account.
For more on the rent mechanism and how it affects account creation costs, see our guide to Solana rent.
The simplest accounts are system accounts -- regular wallet addresses. These are owned by the System Program (11111111111111111111111111111111) and have an empty data field. Their lamports field holds the wallet's SOL balance.
When you deploy a program on Solana, the runtime creates an account with executable: true. The account's data contains the compiled BPF bytecode. Program accounts are owned by the BPF Loader program.
Programs themselves cannot store mutable state. They must create separate data accounts to hold state.
SPL token balances live in dedicated token accounts, owned by the Token Program (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA). Each token account stores a fixed 165-byte data structure:
| Offset | Size | Field |
|---|---|---|
| 0 | 32 | mint (which token) |
| 32 | 32 | owner (wallet that controls this token account) |
| 64 | 8 | amount (token balance as u64) |
| 72 | 4 | delegate option |
| 76 | 32 | delegate pubkey |
| 108 | 1 | state (initialized, frozen) |
| 109 | 4 | is_native option |
| 113 | 8 | is_native amount |
| 121 | 8 | delegated_amount |
| 129 | 4 | close_authority option |
| 133 | 32 | close_authority pubkey |
This fixed layout is why tools and indexers can efficiently parse token balances across millions of accounts. For a deeper dive into SPL tokens, see our SPL tokens guide.
PDAs are accounts whose address is deterministically derived from a program ID and a set of seeds. They do not have a corresponding private key, which means only the owning program can sign for them.
PDAs are the backbone of on-chain state management. A program uses seeds to create predictable addresses for its data accounts -- for example, a lending protocol might derive a user's position account from the user's wallet address and the market ID.
import { PublicKey } from "@solana/web3.js";
const [pda, bump] = PublicKey.findProgramAddressSync(
[
Buffer.from("user_position"),
userWallet.toBuffer(),
marketId.toBuffer(),
],
programId
);
console.log("PDA address:", pda.toBase58());
console.log("Bump seed:", bump);
The bump seed is a single byte that ensures the derived address falls off the ed25519 curve (meaning no private key exists for it). Programs store this bump in the account data so they can re-derive the address efficiently.
Since account data is just raw bytes, programs need a serialization format to encode structured data. Two formats dominate Solana development.
Borsh is the standard serialization format for native Solana programs. It produces compact, deterministic binary output with a fixed layout -- no field names, no delimiters, just packed bytes in a defined order.
A Rust struct like this:
pub struct UserProfile {
pub authority: Pubkey, // 32 bytes
pub display_name: String, // 4 bytes length + UTF-8 bytes
pub score: u64, // 8 bytes
pub is_verified: bool, // 1 byte
}
Gets serialized as: 32 bytes for the pubkey, then 4 bytes for the string length, then the string bytes, then 8 bytes for the u64, then 1 byte for the bool. No padding, no alignment -- just sequential fields.
Anchor simplifies account serialization by adding an 8-byte discriminator at the start of every account. This discriminator is derived from the account struct name and lets programs verify they are reading the correct account type.
#[account]
pub struct GameState {
pub player: Pubkey,
pub score: u64,
pub level: u8,
}
Anchor automatically generates the serialization code and prepends the discriminator. The on-chain data layout becomes: 8-byte discriminator + 32-byte pubkey + 8-byte u64 + 1-byte u8.
The most fundamental way to read account data is getAccountInfo:
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const accountPubkey = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
const accountInfo = await connection.getAccountInfo(accountPubkey);
if (accountInfo) {
console.log("Owner:", accountInfo.owner.toBase58());
console.log("Lamports:", accountInfo.lamports);
console.log("Data length:", accountInfo.data.length);
console.log("Executable:", accountInfo.executable);
}
The returned accountInfo.data is a Buffer containing the raw bytes. To make sense of it, you need to know the account layout.
The SPL token library provides built-in deserialization:
import { Connection, PublicKey } from "@solana/web3.js";
import { getAccount, getMint } from "@solana/spl-token";
const connection = new Connection("https://api.mainnet-beta.solana.com");
// Read a token account
const tokenAccountPubkey = new PublicKey("YOUR_TOKEN_ACCOUNT_ADDRESS");
const tokenAccount = await getAccount(connection, tokenAccountPubkey);
console.log("Mint:", tokenAccount.mint.toBase58());
console.log("Owner:", tokenAccount.owner.toBase58());
console.log("Amount:", tokenAccount.amount.toString());
// Read the mint to get decimals
const mintInfo = await getMint(connection, tokenAccount.mint);
const uiAmount = Number(tokenAccount.amount) / Math.pow(10, mintInfo.decimals);
console.log("Human-readable balance:", uiAmount);
When working with custom program accounts, you often need to deserialize manually:
import { Connection, PublicKey } from "@solana/web3.js";
import { deserialize, Schema } from "borsh";
class GameState {
player: Uint8Array;
score: bigint;
level: number;
constructor(fields: { player: Uint8Array; score: bigint; level: number }) {
this.player = fields.player;
this.score = fields.score;
this.level = fields.level;
}
}
const schema: Schema = new Map([
[
GameState,
{
kind: "struct",
fields: [
["player", [32]],
["score", "u64"],
["level", "u8"],
],
},
],
]);
const connection = new Connection("https://api.mainnet-beta.solana.com");
const accountInfo = await connection.getAccountInfo(
new PublicKey("ACCOUNT_ADDRESS")
);
if (accountInfo) {
// Skip 8-byte Anchor discriminator if applicable
const data = accountInfo.data.slice(8);
const gameState = deserialize(schema, GameState, Buffer.from(data));
console.log("Score:", gameState.score.toString());
console.log("Level:", gameState.level);
}
Combining PDA derivation with account reading:
import { Connection, PublicKey } from "@solana/web3.js";
const PROGRAM_ID = new PublicKey("YOUR_PROGRAM_ID");
const connection = new Connection("https://api.mainnet-beta.solana.com");
// Derive the PDA
const [configPda] = PublicKey.findProgramAddressSync(
[Buffer.from("global_config")],
PROGRAM_ID
);
// Read the account
const accountInfo = await connection.getAccountInfo(configPda);
if (accountInfo) {
console.log("Config account found at:", configPda.toBase58());
console.log("Data length:", accountInfo.data.length, "bytes");
console.log("Owner:", accountInfo.owner.toBase58());
// Deserialize based on your program's schema
}
If you use APIs from providers like Helius or indexing platforms, the data you receive is ultimately derived from Solana account data. Understanding the account model helps you in several ways:
getProgramAccounts lets you fetch all accounts owned by a specific program with optional filters on the data bytes -- but you need to know the byte offsets to filter effectively.The getProgramAccounts call is particularly powerful for building dashboards and analytics tools:
const accounts = await connection.getProgramAccounts(programId, {
filters: [
{ dataSize: 165 }, // Only accounts with exactly 165 bytes (token accounts)
{
memcmp: {
offset: 32, // Owner field offset in token account layout
bytes: walletAddress.toBase58(),
},
},
],
});
console.log("Found", accounts.length, "token accounts for wallet");
This is how wallets and portfolio trackers find all token balances for a given address -- by filtering token accounts where the owner field matches the wallet.
For more on reading and interpreting on-chain activity, see our guide to reading Solana transactions.
A Solana account can hold up to 10 MB of data, though accounts are typically much smaller. SPL token accounts are exactly 165 bytes, and most program data accounts range from a few hundred bytes to a few kilobytes. Larger accounts cost more in rent-exempt lamports, so developers are incentivized to keep account data compact. You can use realloc to resize accounts after creation, up to the 10 MB limit.
Program Derived Addresses are accounts whose address is deterministically computed from a program ID and a set of seeds, rather than generated from a keypair. The key difference is that no private key exists for a PDA, so only the owning program can sign transactions on its behalf. This makes PDAs ideal for holding program-controlled funds and state. Programs use PDAs to create predictable, findable addresses for user-specific or global state without requiring the user to provide an account address.
All Solana accounts must maintain a minimum lamport balance to be rent-exempt, meaning they persist on-chain indefinitely. If an account falls below the rent-exempt threshold, the runtime will eventually deallocate it. The required balance depends on the account's data size -- roughly 0.00089 SOL per kilobyte. When creating accounts in your programs, you calculate this via getMinimumBalanceForRentExemption. For a full breakdown of rent costs and strategies to minimize them, see our Solana rent guide.
Block explorers like SolanaFM and Solscan automatically decode account data for well-known programs including the Token Program, Token-2022, and popular DeFi protocols. Paste any account address into the explorer and it will show you the parsed fields. For Anchor programs, SolanaFM can decode accounts using the program's IDL (Interface Definition Language) if it has been published on-chain. For completely custom programs without a published IDL, you would need to deserialize the raw bytes yourself using the program's source code as reference.