Responsible team: Reclaim / MarketWhale · Reviewed:

Token-2022 guide 4 min read

Why some Token-2022 accounts won't close

An empty token balance can hide withheld transfer fees. Here is what must move before the account can release its SOL.

The short answer

For a token account with transfer fees, its withheld fee balance must be zero before it closes. Harvesting those fees to the mint clears that condition. The account still needs the correct closing authority and must pass any other extension checks.

Zero tokens can still leave fees behind.

Token-2022 supports optional extensions. When a mint uses the Transfer Fee extension, a transfer withholds part of the transferred tokens on the receiving account. Those fees sit in a separate balance from the tokens the account owner can spend.

Sending or selling the spendable tokens can therefore leave an account showing zero tokens while it still has withheld fees. The account's TransferFeeAmount extension must also be empty before closure. The program reports AccountHasWithheldTransferFees when this condition fails. See the extension's closure check.

These fees are denominated in the mint's token. They are separate from the SOL held for account storage, Solana's transaction fee, and Reclaim's service charges.

Harvest the fees, then close the account.

HarvestWithheldTokensToMint moves withheld fees from the token account into the mint's withheld balance. Anyone can request this harvest; it needs no signature from the token owner or fee-withdrawal authority.

Two balances, two destinations

First · harvest token fees

Account's withheld tokens

To: Mint's withheld balance

Then · close an eligible account

Account's SOL balance

To: Your wallet in Reclaim

Harvesting does not pay those tokens to your wallet. Withdrawing fees is a separate operation, signed by the mint's configured withdraw-withheld authority. You do not need that withdrawal to happen before your token account closes. Solana's transfer-fee documentation explains both operations.

Reclaim adds a harvest instruction before closing accounts it identifies as having the transfer-fee extension. Its close instruction sends the account's SOL to your wallet, with Reclaim's charges included separately in the transaction. Review the fees and eligibility before signing.

Check the account before building instructions.

Use the Token-2022 program ID for both instructions. Verify that the token account and mint belong to that program, the account references this mint, the mint has TransferFeeConfig, and the account has TransferFeeAmount. Include harvest only for matching accounts with that extension.

The ordinary token balance must be zero. For closure, use the configured close authority if one exists; otherwise use the account owner. This @solana/spl-token 0.4.x example builds instructions for a single signing authority:

import {
  TOKEN_2022_PROGRAM_ID,
  createHarvestWithheldTokensToMintInstruction,
  createCloseAccountInstruction,
} from "@solana/spl-token";

// Supply verified PublicKeys from your application.
const instructions = [
  createHarvestWithheldTokensToMintInstruction(
    mint, [tokenAccount], TOKEN_2022_PROGRAM_ID
  ),
  createCloseAccountInstruction(
    tokenAccount, solDestination, closeAuthority,
    [], TOKEN_2022_PROGRAM_ID
  ),
];

Add these instructions in order to a transaction. The fee payer and close authority must sign; multisig authorities require their signer list. Simulate before submitting, then check confirmation.

If closing still fails, inspect the failing instruction and full program logs. A generic invalid-account-data error does not prove withheld fees are the cause. A log naming HarvestWithheldTokensToMint only shows that the instruction ran; a source account can be skipped with a logged harvest error. See the harvest processor.

Harvesting cannot fix the wrong closing authority or other extension conditions. Confidential-transfer balances and confidential withheld fees have their own closure checks. The Token-2022 close processor requires the SOL destination to be the token account owner when CPI Guard is enabled and closure runs through another program.