The Solana closeAccount instruction: complete developer reference
The Solana closeAccount instruction: complete developer reference
This is the technical reference for closeAccount — the SPL Token / Token-2022 instruction that returns the rent-exempt deposit from a token account back to a chosen destination. If you're building a rent-recovery tool, a wallet cleanup feature, or debugging a "cannot close" error, this page has the details.
Instruction discriminator
Both SPL Token and Token-2022 use the same 1-byte discriminator:
// SPL Token program
pub const CLOSE_ACCOUNT_DISCRIMINATOR: u8 = 9;
The instruction data is exactly 1 byte: [9]. No arguments.
Accounts (in order)
| Index | Account | Signer | Writable | Purpose |
|---|---|---|---|---|
| 0 | Token account to close | No | Yes | The ATA (or arbitrary token account) being closed |
| 1 | Rent destination | No | Yes | Where the reclaimed SOL goes (usually the owner's main wallet) |
| 2 | Owner authority | Yes | No | The signer authorized to close the account |
| 3+ | Multisig signers | Yes | No | Only if the account is owned by a multisig |
Preconditions
The instruction fails unless:
- Balance is 0 — the account's token balance must be exactly zero. Otherwise:
0x0011 / TokenError::NonNativeHasBalance. - Account is initialized — must be an actual token account, not a raw account.
- Owner authorization — the signer must match the account's owner field (or be a multisig with quorum).
- Not a wrapped SOL account with balance — WSOL accounts with balance must be handled specially (see below).
Program IDs
// Legacy SPL Token
pub const TOKEN_PROGRAM_ID: Pubkey =
pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
// Token-2022
pub const TOKEN_2022_PROGRAM_ID: Pubkey =
pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
The exact same discriminator ([9]) works for both programs — but the transaction must reference the correct program ID for each account. Sending a Token-2022 close to the legacy Token program (or vice versa) fails silently on some SDKs, or with an Incorrect program id error on others.
Rust (native / on-chain)
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
pubkey::Pubkey,
};
use spl_token::instruction::close_account;
pub fn close_ata(
token_program: &Pubkey,
ata: &Pubkey,
destination: &Pubkey,
owner: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let ix = close_account(
token_program,
ata,
destination,
owner,
&[], // no multisig signers
)?;
invoke(&ix, accounts)?;
Ok(())
}
TypeScript (client-side, @solana/web3.js)
import {
Connection,
Keypair,
Transaction,
sendAndConfirmTransaction,
PublicKey,
} from '@solana/web3.js';
import {
createCloseAccountInstruction,
TOKEN_PROGRAM_ID,
TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';
async function closeEmptyAta(
connection: Connection,
wallet: Keypair,
ataAddress: PublicKey,
programId: PublicKey = TOKEN_PROGRAM_ID
): Promise<string> {
const ix = createCloseAccountInstruction(
ataAddress, // account to close
wallet.publicKey, // destination for rent
wallet.publicKey, // authority
[], // multisig signers
programId // TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID
);
const tx = new Transaction().add(ix);
return await sendAndConfirmTransaction(connection, tx, [wallet]);
}
Batching (fit as many closes as possible in one tx)
Solana caps transactions at 1232 bytes. Each closeAccount instruction costs ~40 bytes (3 accounts × 32 bytes + overhead, deduplicated by the tx encoding). Realistic batch: 12 close instructions per transaction with room for a compute budget instruction.
import { Transaction, ComputeBudgetProgram } from '@solana/web3.js';
async function batchCloseAtas(
connection: Connection,
wallet: Keypair,
ataAddresses: PublicKey[]
): Promise<string[]> {
const CHUNK_SIZE = 12;
const txids: string[] = [];
for (let i = 0; i < ataAddresses.length; i += CHUNK_SIZE) {
const chunk = ataAddresses.slice(i, i + CHUNK_SIZE);
const tx = new Transaction();
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }));
for (const ata of chunk) {
tx.add(createCloseAccountInstruction(ata, wallet.publicKey, wallet.publicKey));
}
const sig = await sendAndConfirmTransaction(connection, tx, [wallet]);
txids.push(sig);
}
return txids;
}
Python (solders + solana-py)
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey
from solders.instruction import Instruction, AccountMeta
from solders.transaction import Transaction
from solders.message import Message
TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
CLOSE_DISC = bytes([9])
def build_close_ix(ata: Pubkey, destination: Pubkey, owner: Pubkey) -> Instruction:
accounts = [
AccountMeta(ata, is_signer=False, is_writable=True),
AccountMeta(destination, is_signer=False, is_writable=True),
AccountMeta(owner, is_signer=True, is_writable=False),
]
return Instruction(TOKEN_PROGRAM, CLOSE_DISC, accounts)
Wrapped SOL (special case)
Wrapped SOL (WSOL) accounts are token accounts holding wrapped native SOL. Closing them has an extra behavior: instead of returning just the rent-exempt deposit, closeAccount returns rent + token balance as native SOL to the destination.
This is intentional — it's the "unwrap" mechanism. If you have 5 WSOL in a WSOL ATA and close it, you get 5 SOL + 0.00204 SOL rent back.
Common errors
| Error code | Meaning | Fix |
|---|---|---|
0x0011 (17) |
Non-native has balance | Balance is not zero — transfer or burn tokens first |
0x0004 (4) |
Owner mismatch | The signer isn't the account owner |
0xC (12) |
Account not initialized | Not a valid token account |
| Program 0x0 | Incorrect program id | You sent Token-2022 tx to legacy Token program (or vice versa) |
| Tx too large | > 1232 bytes | Reduce batch size (12 closes max per tx) |
Under the hood: what happens on close
The runtime does exactly:
- Verify signer matches owner
- Verify balance == 0
- Transfer the account's lamports (rent + any accumulated balance) to destination
- Wipe the account's data
- Mark the account as system-owned again (data length = 0)
The account is genuinely closed — it can be re-created later (with fresh rent deposit) if needed.
Ecosystem tools that batch closeAccount
- jup.ag/close — Jupiter's built-in, single wallet
- sol-incinerator.com — burns worthless tokens + closes accounts
- SolDuster — multi-wallet batching, Token-2022 native (solduster.com)
- spl-token CLI —
spl-token close-account <ADDR>