Token-2022 Transfer Fees: Why Some Accounts Won't Close
Seeing "invalid account data for instruction" or the program log TransferFeeInstruction: HarvestWithheldTokensToMint? Token-2022 accounts that use the Transfer Fee Extension can only be closed when their withheld fees are zero. This guide explains why and shows the exact instruction sequence to fix it.
Quick Summary
- Token-2022 adds transfer hooks, metadata, and optional fees on top of the legacy SPL Token Program.
- With transfer fees enabled, each token account can accumulate withheld fee amounts.
- Closing fails with errors like "An account can only be closed if its withheld fee balance is zero"until those withheld fees are harvested to the mint.
- Fix: run
createHarvestWithheldTokensToMintInstructionbeforecreateCloseAccountInstruction.
SPL Token vs Token-2022
| Feature | SPL Token (Legacy) | Token-2022 |
|---|---|---|
| Compatibility | Maximum | Backward-compatible |
| Transfer hooks | No | Yes |
| Confidential transfers | No | Yes |
| Transfer fees / royalties | No | Yes |
| Metadata extensions | Limited | Rich & extensible |
| Authority controls | Basic | Advanced |
| Best use | Simple tokens | Programmable / compliant tokens |
Why Closing Fails
Token-2022 accounts with the Transfer Fee Extension track withheld fees inside each token account. Until those fees are harvested to the mint's fee destination, the close instruction throws errors such as invalid account data for instruction and "An account can only be closed if its withheld fee balance is zero".
This is expected behavior, not a wallet bug. Harvesting resets the withheld fee balance to zero, making the account eligible for closure.
Fix: Harvest Then Close
Run createHarvestWithheldTokensToMintInstruction first, then close. The snippet below shows the minimal sequence using @solana/spl-token with the Token-2022 program ID.
import {
TOKEN_2022_PROGRAM_ID,
createHarvestWithheldTokensToMintInstruction,
createCloseAccountInstruction,
} from "@solana/spl-token";
const harvestIx = createHarvestWithheldTokensToMintInstruction(
mintPubkey,
[tokenAccount],
TOKEN_2022_PROGRAM_ID
);
const closeIx = createCloseAccountInstruction(
tokenAccount,
destinationWallet,
authority,
[],
TOKEN_2022_PROGRAM_ID
);
// Send harvestIx, wait for success, then send closeIx.
Once the harvest transaction lands, the account's withheld fee balance becomes zero and the close instruction will succeed. If you skip the harvest, you'll keep seeing TransferFeeInstruction: HarvestWithheldTokensToMint in logs when the close fails.
Workflow to Avoid Errors
- Detect if the mint uses the Transfer Fee Extension.
- For each empty account, include a harvest instruction (batched is fine).
- Confirm harvest landed; withheld fees move to the mint's fee destination.
- Issue
createCloseAccountInstructionfor the now-zeroed accounts. - Return the reclaimed SOL rent to the user's wallet.
Tools & Links
Reclaim SOL automatically batches harvest + close, so you don't hit the invalid account data for instruction error when cleaning up.
Using a cleanup tool keeps you from juggling extra instructions manually. Reclaim SOL already detects Token-2022 accounts with withheld fees, harvests them, and then closes them so you can reclaim rent without running into HarvestWithheldTokensToMint errors.
Keep learning
How to Close Token Accounts on Solana