A bot that has worked for months starts reverting on one specific token. The instructions are identical, the balances are fine, and the slippage setting has not changed.
★The token uses the Token-2022 transfer fee extension, and the amount arriving is smaller than the amount sent.★
Where the Fee Is Taken
With classic SPL tokens, a transfer of 1,000 units delivers 1,000 units. With the transfer fee extension, the token program withholds a percentage inside the transfer itself:
you send 1,000
fee (1%) 10 ← ★withheld by the token program★
recipient gets 990 ← ★this is what the next step sees★
★The fee is not a separate instruction you could inspect. It happens inside the transfer.★ Nothing in your instruction list mentions it, which is why it is invisible until something downstream fails.
Two parameters define it:
transferFeeBasisPoints — the rate, in hundredths of a percent.
maximumFee — an absolute cap, so large transfers are not charged proportionally forever.
import { getTransferFeeConfig, getMint } from "@solana/spl-token";
const mint = await getMint(connection, mintAddress, "confirmed", TOKEN_2022_PROGRAM_ID);
const feeConfig = getTransferFeeConfig(mint);
if (feeConfig) {
const { transferFeeBasisPoints, maximumFee } = feeConfig.newerTransferFee;
}
★getTransferFeeConfig returning null means the mint has no fee extension★ — which is the common case, and why code that never checks works fine until it meets a token that does.
Why Your Minimum Output Fails
A swap encodes a minimum output amount, and the program enforces it on chain. ★The check runs against what actually arrives, not what you calculated.★
// ★Wrong: assumes the full amount arrives — no fee deducted.★
// const minOut = expectedOut * (10_000n - slippageBps) / 10_000n;
// ★Right: the fee comes off before the check.★
const fee = calculateFee(expectedOut, feeConfig);
const minOut = (expectedOut - fee) * (10_000n - slippageBps) / 10_000n;
Without that adjustment, a 1% transfer fee against a 1% slippage tolerance produces a revert every single time — the arriving amount is below your minimum by construction, and no amount of retrying changes it.
This is why the failure looks so strange. It is not intermittent, not congestion-related, and not fixed by a higher priority fee. ★It fails deterministically on one token and works on every other.★
If your fee math is right and transactions still miss, a free BoltTx key is one line to test the submission path.
Calculating the Fee
function calculateFee(amount: bigint, cfg): bigint {
const { transferFeeBasisPoints, maximumFee } = cfg.newerTransferFee;
const fee = (amount * BigInt(transferFeeBasisPoints)) / 10_000n;
return fee > maximumFee ? maximumFee : fee; // ★cap applies★
}
★Note newerTransferFee.★ The extension stores two configurations — the current one and a pending one that activates at a future epoch. Reading the wrong field gives you a rate that is not in effect yet.
const epoch = (await connection.getEpochInfo()).epoch;
const active = BigInt(epoch) >= cfg.newerTransferFee.epoch
? cfg.newerTransferFee
: cfg.olderTransferFee;
★A fee schedule can change at an epoch boundary.★ A bot that reads the config once at startup and caches it forever will use a stale rate after the switch — and that produces exactly the deterministic revert described above, appearing overnight on a token that worked yesterday.
The Instruction Changes Too
createTransferInstruction does not know about fees. The checked variant does:
import { createTransferCheckedWithFeeInstruction } from "@solana/spl-token";
const ix = createTransferCheckedWithFeeInstruction(
source, mint, destination, owner,
amount, // ★gross — what leaves the source★
decimals,
fee, // ★must match what the program computes★
[],
TOKEN_2022_PROGRAM_ID,
);
★The fee you pass must equal what the program calculates, or the instruction fails.★ It is a checked instruction — the value is an assertion, not an input. Computing it with a stale config is the same bug as above, surfacing at a different point.
What This Does to Your Accounting
★Gross and net diverge, and a bot that records one number is recording the wrong one.★
// ★Read what actually moved, not what you intended.★
const pre = tx.meta.preTokenBalances ?? [];
const post = tx.meta.postTokenBalances ?? [];
// The delta on the destination is net of fee.
Three places this matters:
Position sizing. You hold less than you bought. Sizing the exit from the entry amount tries to sell tokens that were never delivered.
P&L. The fee is a real cost that appears in neither your fee accounting nor your slippage accounting.
Round trips. ★Buy and sell both pay it★, so a 1% transfer fee is roughly 2% on a round trip — which can exceed every other cost combined.
Detect It Before You Trade It
async function checkTransferFee(connection, mintAddress) {
const info = await connection.getAccountInfo(mintAddress);
if (!info) return { exists: false };
// ★Owner tells you which token program — this is the first check.★
const is2022 = info.owner.equals(TOKEN_2022_PROGRAM_ID);
if (!is2022) return { is2022: false, hasFee: false };
const mint = await getMint(connection, mintAddress, "confirmed", TOKEN_2022_PROGRAM_ID);
const cfg = getTransferFeeConfig(mint);
return { is2022: true, hasFee: !!cfg, cfg };
}
★The mint's owner field is the cheapest possible check and answers the important question first.★ If the owner is the classic token program, none of this applies and you can skip the rest.
Make this part of your token safety check, alongside mint authority and freeze authority. A fee that is currently zero can be raised later by whoever holds the config authority — so "no fee today" is not the same as "no fee risk."
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★A transfer fee revert never gets you that number — it lands, reverts, and costs you the base fee.★ That makes it worth catching in your fee math rather than in production, since retrying cannot fix an arithmetic error.
Where BoltTx Fits
We handle submission. Fee extensions are a property of the token, and they are handled entirely in your instruction construction.
Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands. We never modify transaction contents, which includes never touching the amounts in your instructions.
You sign locally. We never hold funds and never sign. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.
Get a free API key. No monthly fee:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
FAQ
What is the Token-2022 transfer fee extension? A mint configuration that withholds a percentage of every transfer inside the token program itself. The recipient receives less than the sender sent, and no separate instruction reveals it.
Why does my swap revert on one specific token? Most likely a transfer fee. The amount arriving is below your minimum output because the fee was taken during the transfer, which makes the revert deterministic rather than intermittent.
How do I check if a token has a transfer fee?
Read the mint's owner field first — if it is not the Token-2022 program, there is no fee. If it is, call getTransferFeeConfig, which returns null when no fee extension is present.
How is the transfer fee calculated?
Amount multiplied by transferFeeBasisPoints divided by 10,000, capped at maximumFee. The cap means large transfers are not charged proportionally without limit.
What is the difference between newerTransferFee and olderTransferFee?
The extension stores a current and a pending configuration. Compare the current epoch against the epoch field to determine which one is actually in effect.
Can a transfer fee change after I start trading a token? Yes, at an epoch boundary, by whoever holds the config authority. A bot that caches the config at startup will use a stale rate and revert deterministically once the change takes effect.
How do I adjust slippage for a transfer fee? Subtract the fee from the expected output before applying your slippage tolerance. A 1% fee against a 1% tolerance reverts every time, because the arriving amount is below the minimum by construction.
Which instruction should I use for fee-bearing tokens?
createTransferCheckedWithFeeInstruction, passing the gross amount and the fee. The fee is asserted rather than applied, so a value computed from a stale config fails the instruction.
Does the transfer fee apply to both buys and sells? Yes. A round trip pays it twice, so a 1% fee is roughly 2% across a complete trade, which can exceed the combined cost of network fees and slippage.
Where does the withheld fee go? It accumulates in the recipient token accounts and can later be harvested to the mint and withdrawn by the withdraw authority. It leaves your position either way.
Will getTokenAccountBalance show the fee? No. It shows the resulting balance, which is already net of any fee. To see what was withheld, compare pre and post token balances in the transaction meta.
Does a transfer fee affect my position tracking? Yes. You receive less than you bought, so sizing an exit from the entry amount attempts to sell tokens that were never delivered. Track the net amount from the transaction meta.
Do aggregators handle transfer fees automatically? Many do, but do not assume it. Verify against a fee-bearing token in a small trade before relying on it, since the failure mode is a deterministic revert rather than a warning.
Is a transfer fee the same as a tax token? It is the standard-level version of the same idea. Older tax tokens implemented this in custom program logic, while Token-2022 makes it a mint extension enforced by the token program.
Should I avoid tokens with transfer fees? Not necessarily, but price them in. The fee is a real cost on both legs, and a strategy with thin margins may not survive it once the round trip is accounted for.
How do I add this to my token safety checks? Check the mint owner and the fee config alongside mint authority and freeze authority. Note that a zero fee today can be raised later, so the config authority matters as much as the current rate.