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 createHarvestWithheldTokensToMintInstruction before createCloseAccountInstruction.

SPL Token vs Token-2022

FeatureSPL Token (Legacy)Token-2022
CompatibilityMaximumBackward-compatible
Transfer hooksNoYes
Confidential transfersNoYes
Transfer fees / royaltiesNoYes
Metadata extensionsLimitedRich & extensible
Authority controlsBasicAdvanced
Best useSimple tokensProgrammable / 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

  1. Detect if the mint uses the Transfer Fee Extension.
  2. For each empty account, include a harvest instruction (batched is fine).
  3. Confirm harvest landed; withheld fees move to the mint's fee destination.
  4. Issue createCloseAccountInstruction for the now-zeroed accounts.
  5. Return the reclaimed SOL rent to the user's wallet.

Tools & Links

Token-2022 Transfer Fee docs

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.