Transfers & Delegates
SPL transfers move token balances between token accounts of the same mint. Delegates let a third party transfer up to an approved amount on the owner's behalf - the pattern behind DEX routers and escrow programs.
Search across all documentation pages
SPL transfers move token balances between token accounts of the same mint. Delegates let a third party transfer up to an approved amount on the owner's behalf - the pattern behind DEX routers and escrow programs.
# CLI amounts are UI amounts (whole tokens), never raw base units
spl-token transfer <MINT> <UI_AMOUNT> <RECIPIENT> --fund-recipient
# approve takes the SOURCE TOKEN ACCOUNT, not the mint
spl-token approve <TOKEN_ACCOUNT> <UI_AMOUNT> <DELEGATE_PUBKEY>use anchor_spl::token::{self, TransferChecked, Approve};
token::transfer_checked(cpi_ctx, amount, decimals)?;
token::approve(cpi_ctx, amount)?;When to reach for this:
# Alice sends 2.5 tokens - the CLI takes the UI amount and scales by mint decimals
spl-token transfer "$MINT" 2.5 <BOB_WALLET> --fund-recipient
# Alice approves the router to spend up to 1 token from HER token account.
# Arg 1 is the token account being delegated, not the mint.
ALICE_ATA=$(spl-token address --token "$MINT" --verbose | awk '/Associated token address/{print $NF}')
spl-token approve "$ALICE_ATA" 1 <ROUTER_DELEGATE_PUBKEY>use anchor_lang::prelude::*;
use anchor_spl::token::{self, Approve, Mint, Token, TokenAccount, TransferChecked};
#[derive(Accounts)]
pub struct TransferPayment<'info> {
pub mint: Account<'info, Mint>,
#[account(mut, token::mint = mint, token::authority = authority)]
pub from: Account<'info, TokenAccount>,
#[account(mut, token::mint = mint)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
pub fn transfer_payment(ctx: Context<TransferPayment>, amount: u64) -> Result<()> {
let decimals = ctx.accounts.mint.decimals;
let cpi = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
mint: ctx.accounts.mint.to_account_info(),
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
);
token::transfer_checked(cpi, amount, decimals)?;
Ok(())
}
#[derive(Accounts)]
pub struct ApproveRouter<'info> {
#[account(mut, token::authority = owner)]
pub owner_token: Account<'info, TokenAccount>,
/// CHECK: the pubkey being granted delegate rights; the Token program stores it verbatim.
pub delegate: UncheckedAccount<'info>,
pub owner: Signer<'info>,
pub token_program: Program<'info, Token>,
}
pub fn approve_router(ctx: Context<ApproveRouter>, amount: u64) -> Result<()> {
let cpi = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Approve {
to: ctx.accounts.owner_token.to_account_info(),
delegate: ctx.accounts.delegate.to_account_info(),
authority: ctx.accounts.owner.to_account_info(),
},
);
token::approve(cpi, amount)?;
Ok(())
}What this demonstrates:
token::mint = mint pins both sides to the same asset before any CPItransfer_checked passes the mint and asserts decimals on-chain; plain transfer is deprecatedapprove; revoke sets delegated amount back to zerosource.amount and credits destination.amount atomicallydelegate: Option<Pubkey>, delegated_amount: u64| Action | Effect |
|---|---|
approve | Sets delegate + delegated_amount ceiling |
transfer by delegate | Reduces delegated_amount and balance |
revoke | Clears delegate |
import { createSolanaRpc, address } from "@solana/kit";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
// Build transfer instructions via @solana-program/token helpers
// Always derive recipient ATA; fund if missingfrom.mint == to.mint on-chain and in clients.state field before UX promises.transfer - spl_token::instruction::transfer and anchor_spl::token::{transfer, Transfer} are deprecated, and Token-2022 rejects the unchecked variant on transfer-fee mints. Fix: use transfer_checked / TransferChecked everywhere.spl-token transfer <MINT> 2500000 sends 2,500,000 tokens, not 2.5. Fix: the CLI takes UI amounts; only instruction data takes raw base units.approve / revoke given the mint - both take the token account address, not the mint. Fix: derive the ATA first (spl-token address --token <MINT>).
| Alternative | Use When | Don't Use When |
|---|---|---|
transfer_checked | On-chain programs need decimals guard | Simple CLI transfers |
| Escrow PDA custody | Atomic swap/settlement | Simple wallet send |
| Token-2022 transfer hook | Policy on every move | Plain fungible payments |
A pubkey authorized to transfer up to delegated_amount from your token account without owning the account.
No. SPL program enforces delegated_amount ceiling.
spl-token revoke <TOKEN_ACCOUNT> (the token account address, not the mint) or a token::revoke CPI signed by the owner.
Small transaction fee only - no rent change on token account.
Yes. Router program PDAs or vaults commonly hold delegate role.
Includes expected decimals in instruction data - programs use it to prevent decimal confusion attacks.
Yes. Any amount up to source balance (or delegated ceiling for delegates).
No. Supply unchanged - only redistributes existing tokens.
Transaction fails unless you add create-ATA instruction or --fund-recipient.
Delegates are per token account (per wallet+mint ATA), not global per wallet.
Yes. Owner retains full control; delegate has additional limited authority.
Read token account delegate fields via RPC jsonParsed encoding.
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.
Reviewed by Chris St. John·Last updated Jul 19, 2026