Transfer Fees
The Transfer Fee extension lets Token-2022 mints charge a fee on every transfer, withheld in token accounts until harvested to a fee collector. It enables on-chain revenue without custom transfer programs.
Search across all documentation pages
The Transfer Fee extension lets Token-2022 mints charge a fee on every transfer, withheld in token accounts until harvested to a fee collector. It enables on-chain revenue without custom transfer programs.
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--decimals 6 \
--transfer-fee-basis-points 100 \
--transfer-fee-maximum-fee 1--transfer-fee-maximum-fee is a UI amount, not raw base units - 1 caps the fee at one token, 1000000 would cap it at a million.
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
// Use spl-token-2022 CPI helpers for harvest_withheld_tokens_to_mint.
// Transfers on a fee mint must use transfer_checked - Token-2022 rejects
// the unchecked Transfer instruction on mints carrying a transfer fee.When to reach for this:
# Create mint with 1% fee (100 bps) and a 1-token fee cap.
# All spl-token amounts here are UI amounts, not raw base units.
MINT=$(spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--decimals 6 \
--transfer-fee-basis-points 100 \
--transfer-fee-maximum-fee 1 | awk '/Creating token/{print $3}')
spl-token create-account "$MINT" --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
spl-token mint "$MINT" 10 --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
# Transfer 1 token (raw 1_000_000) - recipient receives less; fee withheld.
# The CLI issues TransferChecked, which is required on a fee mint.
spl-token transfer "$MINT" 1 <RECIPIENT> --fund-recipient \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
spl-token display <RECIPIENT_ATA> --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEbWhat this demonstrates:
maximum_feeTransferChecked, the program moves the fee portion to a withheld bucket on the accounts. Token-2022 rejects the deprecated unchecked Transfer on a fee mint, because it cannot verify decimals for the fee mathHarvestWithheldTokensToMint aggregates withheld fees to mintWithdrawWithheldTokensFromMint sends collected fees to collector ATA| Setting | Example | On 1_000_000 raw transfer |
|---|---|---|
| 100 bps | 1% | 10_000 fee if under max |
| max fee | CLI --transfer-fee-maximum-fee 1 (UI) = 1_000_000 raw | Caps fee at 1 UI token (6 dec) |
// When building transfers, fetch mint with extensions parsed
// Show: amount, estimated fee, net to recipient
// Include harvest instructions in treasury crank botsharvest_withheld in keeper bot.spl_token::instruction::* builders call check_program_account and return IncorrectProgramId for the Token-2022 id, and Token-2022 rejects the unchecked Transfer on fee mints regardless. Fix: branch on mint owner, then use spl_token_2022::instruction::transfer_checked (or anchor_spl::token_interface, which dispatches over both programs).--transfer-fee-maximum-fee and every mint/transfer amount are UI amounts. Fix: pass 1, not 1000000, for a one-token cap.| Alternative | Use When | Don't Use When |
|---|---|---|
| Transfer hook custom fee | Complex fee logic | Simple bps fee suffices |
| Treasury tax in your program | Full control in one program | Standard wallet transfers outside your UI |
| Classic Token + off-chain fee | Maximum compatibility | Need enforceable on-chain fee |
Automatic fee deducted on Token-2022 transfers per mint extension config.
Transfer fee authority withdraws harvested fees to configured collector accounts.
1 bps = 0.01%. 100 bps = 1% of transfer amount before max cap.
Transfer fee authority can update bps/max within authority rules - document governance.
Typically transfers only - verify extension spec for your mint config.
Crank harvest_withheld_tokens_to_mint then withdraw_withheld_tokens_from_mint instructions.
Varies - major wallets improving Token-2022 support; test your target wallets.
Extension can exist with 0 bps - uncommon; omit extension if no fee needed.
Swaps are transfers - fees apply unless pool program handles exemption via hook.
Use same program ID on devnet with Surfpool 0.12.0 for local iteration.
Some fields immutable at init - plan before mainnet mint.
Hooks add custom logic; transfer fee is native extension with simpler math.
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 16, 2026