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.
Recipe
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--decimals 6 \
--transfer-fee-basis-points 100 \
--transfer-fee-maximum-fee 1000000use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
// Use spl-token-2022 CPI helpers for harvest_withheld_tokens_to_mintWhen to reach for this:
- Protocol revenue on secondary transfers
- Tokenized real-world assets with issuer fees
- Loyalty tokens with redistribution mechanics
- Replacing brittle off-chain fee tracking
Working Example
# Create mint with 1% fee (100 bps), max 1 token raw fee cap
MINT=$(spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--decimals 6 \
--transfer-fee-basis-points 100 \
--transfer-fee-maximum-fee 1000000 | awk '/Creating token/{print $3}')
spl-token create-account "$MINT" --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
spl-token mint "$MINT" 10000000 --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
# Transfer 1_000_000 raw - recipient receives less; fee withheld
spl-token transfer "$MINT" 1000000 <RECIPIENT> --fund-recipient \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
spl-token display <RECIPIENT_ATA> --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEbWhat this demonstrates:
- Fee computed in basis points (100 bps = 1%) capped by
maximum_fee - Withheld amount stays in recipient or sender account per config until harvest
- Wallets must show net amount, not gross transfer input
Deep Dive
How It Works
- Transfer fee config stored in mint extension (bps, max fee, transfer fee authority)
- On
Transfer, program moves fee portion to withheld bucket on accounts HarvestWithheldTokensToMintaggregates withheld fees to mintWithdrawWithheldTokensFromMintsends collected fees to collector ATA
Fee Math
| Setting | Example | On 1_000_000 raw transfer |
|---|---|---|
| 100 bps | 1% | 10_000 fee if under max |
| max fee | 1_000_000 raw | Caps fee at 1 UI token (6 dec) |
TypeScript Notes
// When building transfers, fetch mint with extensions parsed
// Show: amount, estimated fee, net to recipient
// Include harvest instructions in treasury crank botsGotchas
- Users surprised by net receive - UX expects gross amount. Fix: display fee breakdown before sign.
- Withheld fees stuck in ATAs - no harvest crank. Fix: automate
harvest_withheldin keeper bot. - DEX incompatibility - routers may not pass extension accounts. Fix: verify Jupiter/Meteora Token-2022 support.
- Basis points overflow - max 10_000 bps (100%). Fix: validate at mint creation.
- Assuming classic transfer instruction - fee extension requires Token-2022 program. Fix: branch on mint owner.
Alternatives
| 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 |
FAQs
What are transfer fees?
Automatic fee deducted on Token-2022 transfers per mint extension config.
Who receives fees?
Transfer fee authority withdraws harvested fees to configured collector accounts.
What are basis points?
1 bps = 0.01%. 100 bps = 1% of transfer amount before max cap.
Can fees change after launch?
Transfer fee authority can update bps/max within authority rules - document governance.
Do fees apply to mint/burn?
Typically transfers only - verify extension spec for your mint config.
How do I harvest fees?
Crank harvest_withheld_tokens_to_mint then withdraw_withheld_tokens_from_mint instructions.
Do wallets support fee display?
Varies - major wallets improving Token-2022 support; test your target wallets.
Can fee be 0%?
Extension can exist with 0 bps - uncommon; omit extension if no fee needed.
Impact on AMM swaps?
Swaps are transfers - fees apply unless pool program handles exemption via hook.
Devnet testing?
Use same program ID on devnet with Surfpool 0.12.0 for local iteration.
Immutable fee config?
Some fields immutable at init - plan before mainnet mint.
Relation to transfer hooks?
Hooks add custom logic; transfer fee is native extension with simpler math.
Related
- Token-2022 Basics - program ID
- Transfer Hooks - custom transfer logic
- Token-2022 Best Practices - client checks
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.