The SPL Token program in practice
Most of what makes Solana useful flows through one program: the SPL Token Program. USDC, USDT, every project's governance token, every meme coin, every staking receipt, every wrapped asset, every position token from every protocol. The same program manages all of them. You've already called it twice through CPI without seeing its formal shape. This lecture is the formal shape: what state lives in its accounts, what instructions it accepts, and what authorities gate what actions. Once you have these pieces, working with tokens in your own programs becomes mechanical.
The Token Program is just a program
The first thing to understand about the SPL Token Program is that there is nothing special about it. It is a Solana program, deployed at a fixed and well-known address (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA), with the same shape as any program you've written. It has accounts that store its state. It exposes a set of instructions. Anyone can call those instructions from a wallet or from another program via CPI. The runtime treats it like any other program.
What makes it foundational is that everyone agreed to use it. Rather than each token contract on Solana writing its own state machine for tracking balances and approving transfers, the entire ecosystem standardized on one program. The Token Program owns the accounts that store every token's supply and every wallet's holdings. Every program that touches tokens does so by CPI-ing into this one well-tested codebase.
Two account types matter. A Mint account describes a token. A TokenAccount describes one holder's balance of a token. Almost every interaction with the Token Program boils down to those two account types and how a handful of instructions relate them.
Mint and TokenAccount: the central split
Think of a Mint as a currency itself. The U.S. dollar has rules about who can print new ones, how many decimal places it uses, and what its total circulation is. A Mint account stores the equivalent rules for one specific token: its total supply, its decimal precision, the authority allowed to print more, and the authority allowed to freeze holdings.
A TokenAccount is one person's bank account holding that currency. It references the mint, names an owner, and tracks a balance. Alice's USDC account holds 100 USDC, Bob's USDC account holds 50 USDC, and a protocol's vault PDA holds 1,000 USDC. Three accounts, three balances, all denominated in the same Mint.
The relationship is one-to-many. One mint, many token accounts referencing it. The mint is created once, by whoever issues the token. The token accounts are created on demand, one per wallet per mint, as users come to hold the token for the first time. Most wallets you'd recognize hold dozens of token accounts: one for each different token in the wallet.
A common new-developer confusion is conflating these two layers. People will say "transfer 50 USDC from Alice to Bob" and assume Alice's wallet directly holds tokens. It doesn't. Alice's wallet owns a token account, and that token account holds the balance. To transfer, the Token Program updates two token accounts. The mint, and Alice's wallet, are unchanged. Keep this split clear from the start and the rest of the lecture lands easily.
Decimals: the gotcha that bites first
A balance stored in a token account is not a display value. It's a raw integer scaled by ten to the power of the mint's decimal precision. USDC has 6 decimals, which means a stored balance of 100,000,000 represents 100.000000 USDC when displayed. To take a user-friendly input like "I want to send 100 USDC" and turn it into the right transfer amount, you multiply by 10^6 to get 100,000,000. To display a balance from the chain, you divide by 10^6.
The decimals are stored on the mint rather than on the token account. So to convert correctly between raw and display values, you need to know which mint a balance belongs to. SOL uses 9 decimals, so 1 SOL displays as a balance of 1,000,000,000 lamports under the hood. Most fungible tokens use 6 or 9. NFTs typically use 0 decimals, since you can't have half of one.
This is the single most common source of off-by-a-million bugs in early Solana code. A new developer reads a balance, treats it as a display value, multiplies it by some factor in their handler, and ends up moving the wrong amount of tokens. The fix is discipline: every amount that touches the Token Program is a raw integer. Conversion to and from display values happens only at the edges, in your frontend or in your test setup, never inside program logic.
The four instructions you'll use
The Token Program has dozens of instructions, but for everyday work you'll reach for four of them constantly: Transfer, MintTo, Burn, and CloseAccount.
Transfer is what you call to move tokens. Pass the source and destination token accounts, plus the authority that's allowed to spend from the source. The authority must sign. In simple cases the authority is the user's wallet, the same wallet that owns the source token account. In program-controlled cases the authority is a PDA, and your program signs for it using invoke_signed. Both work the same way to the Token Program.
MintTo creates new tokens. Pass the mint, the destination token account that will receive them, and the mint authority. The mint authority must sign. The mint's supply field increases. This is how new tokens enter circulation. A protocol that issues its own token uses MintTo, gated by a PDA mint authority, to distribute initial supply or ongoing rewards.
Burn is the inverse. Pass a token account, the mint, and the authority that owns the account. The authority must sign. The token account's balance decreases, and the mint's supply decreases by the same amount. Burning is permanent. Users burn tokens to redeem them for something else in a wrapped-asset protocol, to remove voting power, or to retire stale receipts.
CloseAccount closes a token account that has a zero balance and refunds the rent deposit to a destination. The owner of the account must sign. This matters because every token account costs about 0.002 SOL in rent. Users who've accumulated many dust accounts can reclaim that rent by closing each empty one. Protocols often close their own PDAs' token accounts when they're no longer needed, to keep the chain tidy and reclaim rent.
There are initialization instructions too. InitializeMint creates a new mint with chosen decimals and authorities. InitializeAccount creates a new token account for a specific mint and owner. In Anchor programs, both are usually wrapped by the init constraint plus the right type, so you rarely call them directly.
Tokens involve three distinct authority concepts, and developers who blur them tend to write incorrect access control. Each one gates a different action, lives on a different account, and has a different scope.
The mint authority lives on a Mint account and controls one thing: who can call MintTo against that mint. Set it to a wallet, that wallet can mint new tokens. Set it to a PDA your program controls, your program can mint via invoke_signed. Set it to None, and the supply is permanently fixed. The supply-frozen case is one-way. Once mint authority is None, you cannot reinstate it. The supply you have is the supply you'll always have.
The freeze authority also lives on a Mint and controls FreezeAccount, an instruction that locks a specific token account from doing transfers. Most tokens you'll work with have no freeze authority, since most token issuers don't want the ability to seize holdings. Regulated tokens like real-world-asset representations often do. If a mint has a freeze authority, every holder is taking on the implicit risk that their balance could be frozen.
The token account owner lives on each individual TokenAccount and controls what can be done with that account's balance: Transfer, Burn, and CloseAccount all require the owner to sign. This is the authority your program will interact with most often, because every time you move tokens, somebody is signing as the source account's owner. Most of your CPIs to the Token Program are about getting the right owner to sign for the right token account.
The split between mint authorities and token account owners is what people miss. Holding the mint authority doesn't give you the ability to move other people's tokens, only to create new ones. Holding ownership of a token account doesn't give you the ability to mint new tokens, only to spend that one balance. They're independent powers.
Working with tokens in Anchor
The anchor_spl crate gives you typed Rust bindings for everything above. The Mint type, the TokenAccount type, and CPI helpers for each instruction. You add it to your Cargo.toml, import the pieces you need, and you can declare token accounts in your Accounts struct alongside your own program's accounts.
use anchor_spl::token::{Mint, Token, TokenAccount};
use anchor_spl::associated_token::AssociatedToken;
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub depositor: Signer<'info>,
pub mint: Account<'info, Mint>,
#[account(mut, token::mint = mint, token::authority = depositor)]
pub user_account: Account<'info, TokenAccount>,
#[account(mut, token::mint = mint, token::authority = vault_pda)]
pub vault_account: Account<'info, TokenAccount>,
/// CHECK: this is a PDA, validated by seeds constraint
#[account(seeds = [b"vault"], bump)]
pub vault_pda: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
}Notice the token::mint = ... and token::authority = ... constraints. These are Anchor checks that the token account in question is for the right mint and owned by the right authority. Adding them means a malicious caller can't slip in a token account for the wrong token or one they don't own. The Token Program would eventually catch most such mistakes, but Anchor catches them earlier with cleaner error messages.
A final note on Token-2022. There's a newer version of the Token Program with the same conceptual model but extra features: transfer fees, transfer hooks, interest-bearing accounts, metadata pointers, and more. Adoption is growing but classic SPL Token still dominates by a wide margin, and most ecosystem tooling assumes it. The conceptual split into Mints and TokenAccounts, and the four core instructions, work identically in both. Token-2022 is its own topic worth its own treatment when you get there.
Everything above is foundational. When you write a program that holds funds, you're writing accounts of types you've now seen. When you check whether a withdrawal is authorized, you're checking the token account's owner. When you mint protocol rewards, you're calling MintTo with a mint authority your program controls. The patterns repeat. Internalize this once and you'll recognize the shape of every token-touching protocol on Solana.