Transfer Fees

What Are Transfer Fees?

The Token Extension Program's TransferFeeConfig mint extension applies a fee to each transfer for that mint.

At transfer time:

  • The transferred amount is reduced by the configured fee
  • The withheld fee is tracked on the destination token account
  • Withheld fees can be withdrawn directly from token accounts or harvested to the mint
  • The withdraw_withheld_authority can move harvested fees to a fee receiver token account

Token Account Extensions

When a token account is initialized for a mint with TransferFeeConfig, the token account is initialized with TransferFeeAmount. When the token account is created through the Associated Token Program, the required account size is calculated and the token account is created with the required size and rent-exempt lamports.

How to Create a Transfer Fee Mint

To create a transfer fee mint:

  1. Calculate the mint account size and rent needed for the mint and the TransferFeeConfig extension.
  2. Create the mint account with CreateAccount, initialize TransferFeeConfig, and initialize the mint with InitializeMint.
  3. Create token accounts for the mint. TransferFeeAmount is automatically enabled for token accounts.
  4. Use TransferCheckedWithFee to transfer tokens and verify the expected fee. TransferChecked also works and automatically withholds the configured fee on the destination token account.
  5. Use WithdrawWithheldTokensFromAccounts to move withheld fees directly from token accounts to a fee receiver token account, signed by the mint's withdraw_withheld_authority.
  6. Use permissionless HarvestWithheldTokensToMint to move withheld fees from token accounts to the mint account, then use WithdrawWithheldTokensFromMint to withdraw the mint account's withheld fees to a fee receiver token account, signed by the mint's withdraw_withheld_authority.
  7. Use SetTransferFee to update the next transfer fee configuration, which takes effect starting two epochs later.

Calculate account size

Calculate the mint account size for the base mint plus the TransferFeeConfig extension. This is the size used in CreateAccount.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));

Calculate rent

Calculate rent using the size needed for the mint plus the TransferFeeConfig extension.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();

Create the mint account

Create the mint account with the calculated space and lamports.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
})
]);

Initialize TransferFeeConfig

Initialize the TransferFeeConfig extension on the mint.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
})
]);

Initialize the mint

Initialize the mint with InitializeMint in the same transaction.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);

Create token accounts and mint tokens

Create token accounts for the source, recipients, and fee receiver, then mint tokens to the source token account.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);

Transfer with TransferCheckedWithFee

Transfer tokens with TransferCheckedWithFee and verify the expected fee.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationAToken,
authority: client.payer,
amount: 200n,
decimals: 2,
fee: 3n
})
]);

Transfer with TransferChecked

Transfer tokens with TransferChecked. The configured fee is still withheld on the destination token account.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationAToken,
authority: client.payer,
amount: 200n,
decimals: 2,
fee: 3n
})
]);
await client.sendTransaction([
getTransferCheckedInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationBToken,
authority: client.payer,
amount: 200n,
decimals: 2
})
]);

Withdraw withheld fees from token accounts

Use WithdrawWithheldTokensFromAccounts to move withheld fees directly from token accounts to the fee receiver token account.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationAToken,
authority: client.payer,
amount: 200n,
decimals: 2,
fee: 3n
})
]);
await client.sendTransaction([
getTransferCheckedInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationBToken,
authority: client.payer,
amount: 200n,
decimals: 2
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromAccountsInstruction({
mint: mint.address,
feeReceiver: feeReceiverToken,
withdrawWithheldAuthority: client.payer,
numTokenAccounts: 1,
sources: [destinationAToken]
})
]);

Harvest withheld fees to the mint

Use HarvestWithheldTokensToMint to move withheld fees from token accounts to the mint account's withheld accumulator.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationAToken,
authority: client.payer,
amount: 200n,
decimals: 2,
fee: 3n
})
]);
await client.sendTransaction([
getTransferCheckedInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationBToken,
authority: client.payer,
amount: 200n,
decimals: 2
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromAccountsInstruction({
mint: mint.address,
feeReceiver: feeReceiverToken,
withdrawWithheldAuthority: client.payer,
numTokenAccounts: 1,
sources: [destinationAToken]
})
]);
await client.sendTransaction([
getHarvestWithheldTokensToMintInstruction({
mint: mint.address,
sources: [destinationBToken]
})
]);

Withdraw withheld fees from the mint

Use WithdrawWithheldTokensFromMint to move harvested fees from the mint account to the fee receiver token account.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationAToken,
authority: client.payer,
amount: 200n,
decimals: 2,
fee: 3n
})
]);
await client.sendTransaction([
getTransferCheckedInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationBToken,
authority: client.payer,
amount: 200n,
decimals: 2
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromAccountsInstruction({
mint: mint.address,
feeReceiver: feeReceiverToken,
withdrawWithheldAuthority: client.payer,
numTokenAccounts: 1,
sources: [destinationAToken]
})
]);
await client.sendTransaction([
getHarvestWithheldTokensToMintInstruction({
mint: mint.address,
sources: [destinationBToken]
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromMintInstruction({
mint: mint.address,
feeReceiver: feeReceiverToken,
withdrawWithheldAuthority: client.payer
})
]);

Update the next transfer fee

Use SetTransferFee to update the next transfer fee configuration.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
transferFeeBasisPoints: 150,
maximumFee: 10n
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationAToken,
authority: client.payer,
amount: 200n,
decimals: 2,
fee: 3n
})
]);
await client.sendTransaction([
getTransferCheckedInstruction({
source: sourceToken,
mint: mint.address,
destination: destinationBToken,
authority: client.payer,
amount: 200n,
decimals: 2
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromAccountsInstruction({
mint: mint.address,
feeReceiver: feeReceiverToken,
withdrawWithheldAuthority: client.payer,
numTokenAccounts: 1,
sources: [destinationAToken]
})
]);
await client.sendTransaction([
getHarvestWithheldTokensToMintInstruction({
mint: mint.address,
sources: [destinationBToken]
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromMintInstruction({
mint: mint.address,
feeReceiver: feeReceiverToken,
withdrawWithheldAuthority: client.payer
})
]);
await client.sendTransaction([
getSetTransferFeeInstruction({
mint: mint.address,
transferFeeConfigAuthority: client.payer,
transferFeeBasisPoints: 250,
maximumFee: 25n
})
]);

Calculate account size

Calculate the mint account size for the base mint plus the TransferFeeConfig extension. This is the size used in CreateAccount.

Calculate rent

Calculate rent using the size needed for the mint plus the TransferFeeConfig extension.

Create the mint account

Create the mint account with the calculated space and lamports.

Initialize TransferFeeConfig

Initialize the TransferFeeConfig extension on the mint.

Initialize the mint

Initialize the mint with InitializeMint in the same transaction.

Create token accounts and mint tokens

Create token accounts for the source, recipients, and fee receiver, then mint tokens to the source token account.

Transfer with TransferCheckedWithFee

Transfer tokens with TransferCheckedWithFee and verify the expected fee.

Transfer with TransferChecked

Transfer tokens with TransferChecked. The configured fee is still withheld on the destination token account.

Withdraw withheld fees from token accounts

Use WithdrawWithheldTokensFromAccounts to move withheld fees directly from token accounts to the fee receiver token account.

Harvest withheld fees to the mint

Use HarvestWithheldTokensToMint to move withheld fees from token accounts to the mint account's withheld accumulator.

Withdraw withheld fees from the mint

Use WithdrawWithheldTokensFromMint to move harvested fees from the mint account to the fee receiver token account.

Update the next transfer fee

Use SetTransferFee to update the next transfer fee configuration.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));

Instruction Order

InitializeTransferFeeConfig must come before InitializeMint. CreateAccount, InitializeTransferFeeConfig, and InitializeMint must be included in the same transaction.

Source Reference

ItemDescriptionSource
TransferFeeConfigMint extension that stores the fee authorities, accumulated withheld amount, and the older and newer transfer fee configurations.Source
TransferFeeFee configuration struct stored in TransferFeeConfig that defines the epoch when the fee takes effect, the maximum fee, and the fee in basis points.Source
TransferFeeAmountToken account extension that stores the amount withheld during transfers for a token account.Source
TransferFeeInstruction::InitializeTransferFeeConfigInstruction that initializes transfer-fee settings before InitializeMint.Source
TransferFeeInstruction::TransferCheckedWithFeeTransfer instruction that verifies the caller-supplied fee matches the mint's current transfer-fee config.Source
TransferFeeInstruction::WithdrawWithheldTokensFromAccountsInstruction that moves withheld fees directly from token accounts to a fee receiver token account, signed by the mint's withdraw_withheld_authority.Source
TransferFeeInstruction::HarvestWithheldTokensToMintPermissionless instruction that moves withheld fees from token accounts to the mint account's withheld accumulator.Source
TransferFeeInstruction::WithdrawWithheldTokensFromMintInstruction that withdraws the mint account's withheld fees to a fee receiver token account, signed by the mint's withdraw_withheld_authority.Source
TransferFeeInstruction::SetTransferFeeInstruction that updates the newer transfer fee configuration, which takes effect starting two epochs later.Source
process_initialize_transfer_fee_configProcessor logic that initializes the TransferFeeConfig extension on an uninitialized mint and sets both the older and newer transfer fee configurations to the initial fee.Source
process_withdraw_withheld_tokens_from_accountsProcessor logic that validates withdraw_withheld_authority and moves withheld fees from token accounts to a destination token account.Source
process_harvest_withheld_tokens_to_mintProcessor logic that harvests withheld fees from token accounts to the mint account's withheld accumulator.Source
process_withdraw_withheld_tokens_from_mintProcessor logic that validates withdraw_withheld_authority and moves the mint account's withheld amount to a destination token account.Source
process_set_transfer_feeProcessor logic that validates the transfer-fee config authority and updates the newer transfer fee configuration to take effect starting two epochs later.Source
get_required_init_account_extensionsUsed to automatically add token account extensions when token accounts are initialized based on extensions enabled on the mint. For TransferFeeConfig mints, it adds TransferFeeAmount.Source

Typescript

The Kit example below uses explicit Token-2022 instructions. Legacy examples using @solana/web3.js and @solana/spl-token are included for reference.

Kit

Instructions
import {
lamports,
createClient,
generateKeyPairSigner,
unwrapOption
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
extension,
fetchMint,
fetchToken,
findAssociatedTokenPda,
getCreateAssociatedTokenInstructionAsync,
getHarvestWithheldTokensToMintInstruction,
getInitializeMintInstruction,
getInitializeTransferFeeConfigInstruction,
getMintSize,
getMintToCheckedInstruction,
getSetTransferFeeInstruction,
getTransferCheckedWithFeeInstruction,
getWithdrawWithheldTokensFromAccountsInstruction,
getWithdrawWithheldTokensFromMintInstruction,
isExtension,
TOKEN_2022_PROGRAM_ADDRESS
} from "@solana-program/token-2022";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer, // Account funding account creation.
newAccount: mint, // New mint account to create.
lamports: mintRent, // Lamports funding the mint account rent.
space: mintSpace, // Account size in bytes for the mint plus TransferFeeConfig.
programAddress: TOKEN_2022_PROGRAM_ADDRESS // Program that owns the mint account.
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address, // Mint account that stores the TransferFeeConfig extension.
transferFeeConfigAuthority: client.payer.address, // Authority allowed to update the transfer fee later.
withdrawWithheldAuthority: client.payer.address, // Value stored in the mint's `withdraw_withheld_authority` field.
transferFeeBasisPoints: 150, // Transfer fee in basis points.
maximumFee: 10n // Maximum fee charged on each transfer.
}),
getInitializeMintInstruction({
mint: mint.address, // Mint account to initialize.
decimals: 2, // Number of decimals for the token.
mintAuthority: client.payer.address, // Authority allowed to mint new tokens.
freezeAuthority: client.payer.address // Authority allowed to freeze token accounts.
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken, // Token account sending the transfer.
mint: mint.address, // Mint with the transfer fee configuration.
destination: destinationAToken, // Token account receiving the transfer.
authority: client.payer, // Signer approving the transfer.
amount: 200n, // Token amount in base units.
decimals: 2, // Decimals defined on the mint.
fee: 3n // Expected transfer fee for this transfer.
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromAccountsInstruction({
mint: mint.address, // Mint with the transfer fee configuration.
feeReceiver: feeReceiverToken, // Token account receiving the withdrawn fees.
withdrawWithheldAuthority: client.payer, // Signer matching the mint's `withdraw_withheld_authority`.
numTokenAccounts: 1, // Number of token accounts listed in `sources`.
sources: [destinationAToken] // Token accounts to withdraw withheld fees from.
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken, // Token account sending the transfer.
mint: mint.address, // Mint with the transfer fee configuration.
destination: destinationBToken, // Token account receiving the transfer.
authority: client.payer, // Signer approving the transfer.
amount: 200n, // Token amount in base units.
decimals: 2, // Decimals defined on the mint.
fee: 3n // Expected transfer fee for this transfer.
})
]);
await client.sendTransaction([
getHarvestWithheldTokensToMintInstruction({
mint: mint.address, // Mint that collects harvested withheld fees.
sources: [destinationBToken] // Token accounts to harvest withheld fees from.
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromMintInstruction({
mint: mint.address, // Mint storing harvested withheld fees.
feeReceiver: feeReceiverToken, // Token account receiving withdrawn fees.
withdrawWithheldAuthority: client.payer // Signer matching the mint's `withdraw_withheld_authority`.
}),
getSetTransferFeeInstruction({
mint: mint.address, // Mint whose next transfer fee configuration is updated.
transferFeeConfigAuthority: client.payer, // Signer authorized to update the transfer fee later.
transferFeeBasisPoints: 250, // New transfer fee in basis points.
maximumFee: 25n // New maximum fee for the next transfer fee configuration.
})
]);
const destinationAAccount = await fetchToken(client.rpc, destinationAToken);
const destinationBAccount = await fetchToken(client.rpc, destinationBToken);
const feeReceiverAccount = await fetchToken(client.rpc, feeReceiverToken);
const mintAccount = await fetchMint(client.rpc, mint.address);
const transferFeeConfig = (
unwrapOption(mintAccount.data.extensions) ?? []
).find((item) => isExtension("TransferFeeConfig", item));
console.log("Mint Address:", mint.address);
console.dir(
{
destinationA: {
amount: destinationAAccount.data.amount,
extensions: destinationAAccount.data.extensions
},
destinationB: {
amount: destinationBAccount.data.amount,
extensions: destinationBAccount.data.extensions
},
feeReceiver: {
amount: feeReceiverAccount.data.amount,
extensions: feeReceiverAccount.data.extensions
},
transferFeeConfig
},
{ depth: null }
);
Console
Click to execute the code.

Web3.js

Instructions
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
sendAndConfirmTransaction,
SystemProgram,
Transaction
} from "@solana/web3.js";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
ExtensionType,
createAssociatedTokenAccountInstruction,
createHarvestWithheldTokensToMintInstruction,
createInitializeMintInstruction,
createInitializeTransferFeeConfigInstruction,
createMintToCheckedInstruction,
createSetTransferFeeInstruction,
createTransferCheckedWithFeeInstruction,
createWithdrawWithheldTokensFromAccountsInstruction,
createWithdrawWithheldTokensFromMintInstruction,
getAccount,
getAssociatedTokenAddressSync,
getMint,
getMintLen,
getTransferFeeAmount,
getTransferFeeConfig,
TOKEN_2022_PROGRAM_ID
} from "@solana/spl-token";
const connection = new Connection("http://localhost:8899", "confirmed");
const latestBlockhash = await connection.getLatestBlockhash();
const feePayer = Keypair.generate();
const recipientA = Keypair.generate();
const recipientB = Keypair.generate();
const feeReceiver = Keypair.generate();
const airdropSignature = await connection.requestAirdrop(
feePayer.publicKey,
5 * LAMPORTS_PER_SOL
);
await connection.confirmTransaction({
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: airdropSignature
});
const mint = Keypair.generate();
const transferFeeConfigExtensions = [ExtensionType.TransferFeeConfig];
const mintLen = getMintLen(transferFeeConfigExtensions);
const mintRent = await connection.getMinimumBalanceForRentExemption(mintLen);
const sourceToken = getAssociatedTokenAddressSync(
mint.publicKey,
feePayer.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const destinationAToken = getAssociatedTokenAddressSync(
mint.publicKey,
recipientA.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const destinationBToken = getAssociatedTokenAddressSync(
mint.publicKey,
recipientB.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const feeReceiverToken = getAssociatedTokenAddressSync(
mint.publicKey,
feeReceiver.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
SystemProgram.createAccount({
fromPubkey: feePayer.publicKey, // Account funding account creation.
newAccountPubkey: mint.publicKey, // New mint account to create.
lamports: mintRent, // Lamports funding the mint account rent.
space: mintLen, // Account size in bytes for the mint plus TransferFeeConfig.
programId: TOKEN_2022_PROGRAM_ID // Program that owns the mint account.
}),
createInitializeTransferFeeConfigInstruction(
mint.publicKey, // Mint account that stores the TransferFeeConfig extension.
feePayer.publicKey, // Authority allowed to update the transfer fee later.
feePayer.publicKey, // Value stored in the mint's `withdraw_withheld_authority` field.
150, // Transfer fee in basis points.
10n, // Maximum fee charged on each transfer.
TOKEN_2022_PROGRAM_ID // Token program that owns the mint.
),
createInitializeMintInstruction(
mint.publicKey, // Mint account to initialize.
2, // Number of decimals for the token.
feePayer.publicKey, // Authority allowed to mint new tokens.
feePayer.publicKey, // Authority allowed to freeze token accounts.
TOKEN_2022_PROGRAM_ID // Program that owns the mint account.
)
),
[feePayer, mint],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
sourceToken,
feePayer.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
destinationAToken,
recipientA.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
destinationBToken,
recipientB.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
feeReceiverToken,
feeReceiver.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createMintToCheckedInstruction(
mint.publicKey,
sourceToken,
feePayer.publicKey,
1_000,
2,
[],
TOKEN_2022_PROGRAM_ID
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createTransferCheckedWithFeeInstruction(
sourceToken, // Token account sending the transfer.
mint.publicKey, // Mint with the transfer fee configuration.
destinationAToken, // Token account receiving the transfer.
feePayer.publicKey, // Signer approving the transfer.
200n, // Token amount in base units.
2, // Decimals defined on the mint.
3n, // Expected transfer fee for this transfer.
[], // Additional multisig signers.
TOKEN_2022_PROGRAM_ID // Token program that processes the transfer.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createWithdrawWithheldTokensFromAccountsInstruction(
mint.publicKey, // Mint with the transfer fee configuration.
feeReceiverToken, // Token account receiving the withdrawn fees.
feePayer.publicKey, // Signer matching the mint's `withdraw_withheld_authority`.
[], // Additional multisig signers.
[destinationAToken], // Token accounts to withdraw withheld fees from.
TOKEN_2022_PROGRAM_ID // Token program that processes the withdraw.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createTransferCheckedWithFeeInstruction(
sourceToken, // Token account sending the transfer.
mint.publicKey, // Mint with the transfer fee configuration.
destinationBToken, // Token account receiving the transfer.
feePayer.publicKey, // Signer approving the transfer.
200n, // Token amount in base units.
2, // Decimals defined on the mint.
3n, // Expected transfer fee for this transfer.
[], // Additional multisig signers.
TOKEN_2022_PROGRAM_ID // Token program that processes the transfer.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createHarvestWithheldTokensToMintInstruction(
mint.publicKey, // Mint that collects harvested withheld fees.
[destinationBToken], // Token accounts to harvest withheld fees from.
TOKEN_2022_PROGRAM_ID // Token program that processes the harvest.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createWithdrawWithheldTokensFromMintInstruction(
mint.publicKey, // Mint storing harvested withheld fees.
feeReceiverToken, // Token account receiving withdrawn fees.
feePayer.publicKey, // Signer matching the mint's `withdraw_withheld_authority`.
[], // Additional multisig signers.
TOKEN_2022_PROGRAM_ID // Token program that processes the withdraw.
),
createSetTransferFeeInstruction(
mint.publicKey, // Mint whose next transfer fee configuration is updated.
feePayer.publicKey, // Authority allowed to update the transfer fee later.
[], // Additional multisig signers.
250, // New transfer fee in basis points.
25n, // New maximum fee for the next transfer fee configuration.
TOKEN_2022_PROGRAM_ID // Token program that owns the mint.
)
),
[feePayer],
{ commitment: "confirmed" }
);
const destinationAAccount = await getAccount(
connection,
destinationAToken,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
const destinationBAccount = await getAccount(
connection,
destinationBToken,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
const feeReceiverAccount = await getAccount(
connection,
feeReceiverToken,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
const mintAccount = await getMint(
connection,
mint.publicKey,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
console.log("Mint Address:", mint.publicKey.toBase58());
console.log("Destination A Amount:", destinationAAccount.amount.toString());
console.log(
"Destination A Transfer Fee Amount:",
getTransferFeeAmount(destinationAAccount)
);
console.log("Destination B Amount:", destinationBAccount.amount.toString());
console.log(
"Destination B Transfer Fee Amount:",
getTransferFeeAmount(destinationBAccount)
);
console.log("Fee Receiver Amount:", feeReceiverAccount.amount.toString());
console.log(
"Fee Receiver Transfer Fee Amount:",
getTransferFeeAmount(feeReceiverAccount)
);
console.log("Transfer Fee Config:", getTransferFeeConfig(mintAccount));
Console
Click to execute the code.

Rust

Rust
use anyhow::Result;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
};
use solana_system_interface::instruction::create_account;
use spl_associated_token_account_interface::{
address::get_associated_token_address_with_program_id,
instruction::create_associated_token_account,
};
use spl_token_2022_interface::{
extension::{
transfer_fee::{
instruction::{
harvest_withheld_tokens_to_mint, initialize_transfer_fee_config,
set_transfer_fee, transfer_checked_with_fee,
withdraw_withheld_tokens_from_accounts, withdraw_withheld_tokens_from_mint,
},
TransferFeeAmount, TransferFeeConfig,
},
BaseStateWithExtensions, ExtensionType, StateWithExtensions,
},
instruction::{initialize_mint, mint_to_checked},
state::{Account, Mint},
ID as TOKEN_2022_PROGRAM_ID,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = RpcClient::new_with_commitment(
String::from("http://localhost:8899"),
CommitmentConfig::confirmed(),
);
let fee_payer = Keypair::new();
let recipient_a = Keypair::new();
let recipient_b = Keypair::new();
let fee_receiver = Keypair::new();
let decimals = 2;
let airdrop_signature = client
.request_airdrop(&fee_payer.pubkey(), 5_000_000_000)
.await?;
loop {
let confirmed = client.confirm_transaction(&airdrop_signature).await?;
if confirmed {
break;
}
}
let mint = Keypair::new();
let mint_space =
ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::TransferFeeConfig])?;
let mint_rent = client
.get_minimum_balance_for_rent_exemption(mint_space)
.await?;
let mint_blockhash = client.get_latest_blockhash().await?;
let mint_transaction = Transaction::new_signed_with_payer(
&[
create_account(
&fee_payer.pubkey(), // Account funding account creation.
&mint.pubkey(), // New mint account to create.
mint_rent, // Lamports funding the mint account rent.
mint_space as u64, // Account size in bytes for the mint plus TransferFeeConfig.
&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.
),
initialize_transfer_fee_config(
&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.
&mint.pubkey(), // Mint account that stores the TransferFeeConfig extension.
Some(&fee_payer.pubkey()), // Authority allowed to update the transfer fee later.
Some(&fee_payer.pubkey()), // Value stored in the mint's `withdraw_withheld_authority` field.
150, // Transfer fee in basis points.
10, // Maximum fee charged on each transfer.
)?,
initialize_mint(
&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.
&mint.pubkey(), // Mint account to initialize.
&fee_payer.pubkey(), // Authority allowed to mint new tokens.
Some(&fee_payer.pubkey()), // Authority allowed to freeze token accounts.
decimals, // Number of decimals for the token.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer, &mint],
mint_blockhash,
);
client.send_and_confirm_transaction(&mint_transaction).await?;
let source_token_address = get_associated_token_address_with_program_id(
&fee_payer.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let destination_a_token_address = get_associated_token_address_with_program_id(
&recipient_a.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let destination_b_token_address = get_associated_token_address_with_program_id(
&recipient_b.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let fee_receiver_token_address = get_associated_token_address_with_program_id(
&fee_receiver.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let setup_blockhash = client.get_latest_blockhash().await?;
let setup_transaction = Transaction::new_signed_with_payer(
&[
create_associated_token_account(
&fee_payer.pubkey(),
&fee_payer.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
create_associated_token_account(
&fee_payer.pubkey(),
&recipient_a.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
create_associated_token_account(
&fee_payer.pubkey(),
&recipient_b.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
create_associated_token_account(
&fee_payer.pubkey(),
&fee_receiver.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
mint_to_checked(
&TOKEN_2022_PROGRAM_ID,
&mint.pubkey(),
&source_token_address,
&fee_payer.pubkey(),
&[],
1_000,
decimals,
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
setup_blockhash,
);
client.send_and_confirm_transaction(&setup_transaction).await?;
let first_transfer_blockhash = client.get_latest_blockhash().await?;
let first_transfer_transaction = Transaction::new_signed_with_payer(
&[
transfer_checked_with_fee(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the transfer.
&source_token_address, // Token account sending the transfer.
&mint.pubkey(), // Mint with the transfer fee configuration.
&destination_a_token_address, // Token account receiving the transfer.
&fee_payer.pubkey(), // Signer approving the transfer.
&[], // Additional multisig signers.
200, // Token amount in base units.
decimals, // Decimals defined on the mint.
3, // Expected transfer fee for this transfer.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
first_transfer_blockhash,
);
client
.send_and_confirm_transaction(&first_transfer_transaction)
.await?;
let withdraw_accounts_blockhash = client.get_latest_blockhash().await?;
let withdraw_accounts_transaction = Transaction::new_signed_with_payer(
&[
withdraw_withheld_tokens_from_accounts(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the withdraw.
&mint.pubkey(), // Mint with the transfer fee configuration.
&fee_receiver_token_address, // Token account receiving withdrawn fees.
&fee_payer.pubkey(), // Signer matching the mint's `withdraw_withheld_authority`.
&[], // Additional multisig signers.
&[&destination_a_token_address], // Token accounts to withdraw withheld fees from.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
withdraw_accounts_blockhash,
);
client
.send_and_confirm_transaction(&withdraw_accounts_transaction)
.await?;
let second_transfer_blockhash = client.get_latest_blockhash().await?;
let second_transfer_transaction = Transaction::new_signed_with_payer(
&[
transfer_checked_with_fee(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the transfer.
&source_token_address, // Token account sending the transfer.
&mint.pubkey(), // Mint with the transfer fee configuration.
&destination_b_token_address, // Token account receiving the transfer.
&fee_payer.pubkey(), // Signer approving the transfer.
&[], // Additional multisig signers.
200, // Token amount in base units.
decimals, // Decimals defined on the mint.
3, // Expected transfer fee for this transfer.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
second_transfer_blockhash,
);
client
.send_and_confirm_transaction(&second_transfer_transaction)
.await?;
let harvest_blockhash = client.get_latest_blockhash().await?;
let harvest_transaction = Transaction::new_signed_with_payer(
&[
harvest_withheld_tokens_to_mint(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the harvest.
&mint.pubkey(), // Mint that collects harvested withheld fees.
&[&destination_b_token_address], // Token accounts to harvest withheld fees from.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
harvest_blockhash,
);
client.send_and_confirm_transaction(&harvest_transaction).await?;
let finalize_blockhash = client.get_latest_blockhash().await?;
let finalize_transaction = Transaction::new_signed_with_payer(
&[
withdraw_withheld_tokens_from_mint(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the withdraw.
&mint.pubkey(), // Mint storing harvested withheld fees.
&fee_receiver_token_address, // Token account receiving withdrawn fees.
&fee_payer.pubkey(), // Signer matching the mint's `withdraw_withheld_authority`.
&[], // Additional multisig signers.
)?,
set_transfer_fee(
&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.
&mint.pubkey(), // Mint whose next transfer fee configuration is updated.
&fee_payer.pubkey(), // Authority allowed to update the transfer fee later.
&[], // Additional multisig signers.
250, // New transfer fee in basis points.
25, // New maximum fee for the next transfer fee configuration.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
finalize_blockhash,
);
client.send_and_confirm_transaction(&finalize_transaction).await?;
let destination_a_account = client.get_account(&destination_a_token_address).await?;
let destination_a_state = StateWithExtensions::<Account>::unpack(&destination_a_account.data)?;
let destination_b_account = client.get_account(&destination_b_token_address).await?;
let destination_b_state = StateWithExtensions::<Account>::unpack(&destination_b_account.data)?;
let fee_receiver_account = client.get_account(&fee_receiver_token_address).await?;
let fee_receiver_state = StateWithExtensions::<Account>::unpack(&fee_receiver_account.data)?;
let mint_account = client.get_account(&mint.pubkey()).await?;
let mint_state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;
println!("Mint: {}", mint.pubkey());
println!("Destination A Balance: {}", destination_a_state.base.amount);
println!(
"Destination A Transfer Fee Amount: {:#?}",
destination_a_state.get_extension::<TransferFeeAmount>()?
);
println!("Destination B Balance: {}", destination_b_state.base.amount);
println!(
"Destination B Transfer Fee Amount: {:#?}",
destination_b_state.get_extension::<TransferFeeAmount>()?
);
println!("Fee Receiver Balance: {}", fee_receiver_state.base.amount);
println!(
"Fee Receiver Transfer Fee Amount: {:#?}",
fee_receiver_state.get_extension::<TransferFeeAmount>()?
);
println!(
"Transfer Fee Config: {:#?}",
mint_state.get_extension::<TransferFeeConfig>()?
);
Ok(())
}
Console
Click to execute the code.

Is this page helpful?

Table of Contents

Edit Page

Managed by

© 2026 Solana Foundation.
All rights reserved.
Get connected