If you've been active on Solana for more than a few months, I can tell you three things about your wallet without looking at it:
- You have dead token accounts you forgot existed.
- If you've ever traded on pump.fun, there's probably an unclaimed reward sitting in a program you never interacted with.
- Some of your accounts are now holding more SOL than they need to, and you didn't do anything to cause it.
None of this is a bug. It's a side effect of how Solana's storage model works — and once you understand the mechanism, it's actually a pretty elegant piece of design that most people just never get exposed to. Let's go through it.
1. Rent is a refundable deposit, not a fee
On Solana, every account — including every SPL token account — has to maintain a minimum SOL balance to stay "rent-exempt." This isn't a subscription. It's a bond: SOL locked against the account's existence, refunded in full the moment you close it.
const rentExemptReserve = await connection.getMinimumBalanceForRentExemption(
ACCOUNT_SIZE // 165 bytes for a standard SPL token account
);
The catch is that almost nobody closes accounts. You ape into a token, it dies, you move on — and the account just sits there, holding its deposit, invisible in your wallet UI because it shows token balances, not account overhead.
Multiply that by every token you've ever touched, every NFT mint you tested, every airdrop you claimed once and ignored, and you're looking at real, non-trivial SOL parked across dozens of accounts doing nothing.
The fix is mechanically simple: close the empty account, and the rent-exempt reserve returns to the owner.
createCloseAccountInstruction(
accountPubkey,
walletPubkey, // destination for reclaimed lamports
walletPubkey, // authority
[],
programId // TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID
)
The hard part was never the mechanism — it's that nobody's wallet surfaces which of your dozens of accounts are safe to close, or bothers to batch it for you.
2. Rent reduction created a second, weirder category of surplus
Solana recently rolled out a multi-phase rent reduction (SIMD-0437), lowering the lamports-per-byte cost of on-chain storage. Good for the network. But it created an interesting edge case: accounts funded under the old, higher rent requirement are now sitting on more SOL than the new minimum demands.
This is different from a dead/empty account — these can be accounts you're actively using right now, still holding real token balances, just overfunded relative to the current rent-exempt floor.
To handle this without forcing people to close accounts they're still using, the Token Program shipped a dedicated instruction: WithdrawExcessLamports.
const WITHDRAW_EXCESS_LAMPORTS_DISCRIMINATOR = 38;
function buildWithdrawExcessLamportsInstruction(
accountPubkey: PublicKey,
destinationPubkey: PublicKey,
authorityPubkey: PublicKey,
programId: PublicKey
): TransactionInstruction {
return new TransactionInstruction({
programId,
keys: [
{ pubkey: accountPubkey, isSigner: false, isWritable: true },
{ pubkey: destinationPubkey, isSigner: false, isWritable: true },
{ pubkey: authorityPubkey, isSigner: true, isWritable: false },
],
data: Buffer.from([WITHDRAW_EXCESS_LAMPORTS_DISCRIMINATOR]),
});
}
It only skims the surplus above the rent-exempt minimum — it can't drop the account below that floor, and it doesn't touch token balances or close anything. Quiet, surgical, and completely invisible unless you go looking for it.
One implementation detail worth flagging if you're building this yourself: don't assume ACCOUNT_SIZE (165 bytes) for every account when computing the rent-exempt minimum. Token-2022 accounts can carry extensions (transfer fees, interest-bearing config, etc.) that make them larger than the base size — so their actual rent-exempt minimum is higher. Use the account's real space:
const uniqueSpaces = [...new Set(accounts.map((a) => a.account.space))];
const rentBySpace = new Map(
await Promise.all(
uniqueSpaces.map(async (space) => [
space,
await connection.getMinimumBalanceForRentExemption(space),
] as const)
)
);
Get this wrong for a Token-2022 account with extensions, and you'll either overstate the claimable excess (bad UX, failed transactions) or leave money on the table.
3. Unclaimed pump.fun rewards
Separate from rent entirely: if you've traded on pump.fun, there's a decent chance you've accrued cashback or fee rewards sitting in a claim program tied to your wallet. Same story if you've launched a token and generated creator fees. The rewards exist on-chain. The mechanism to claim them exists. What's usually missing is the interface — most people have no idea the claim program is there, let alone that they have a balance in it.
4. The gas fee problem nobody talks about
Here's the part that makes all of the above useless for a meaningful chunk of users: claiming costs a transaction fee. If your wallet is down to the SOL equivalent of pocket lint — which, statistically, if you have forty dead accounts, it probably is — you're stuck. You have reclaimable value. You can't afford the fee to reclaim it.
The fix is a sponsor pattern: a separate fee-payer keypair covers the transaction fee when the user's wallet balance is too low, while the user's wallet remains the required signer/authority on every actual account instruction (close, withdraw-excess, claim). This matters for a reason beyond convenience — it means the sponsor never gains control over the user's funds. It only pays gas. The user still has to authorize every instruction that touches their accounts.
const walletBalanceLamports = await connection.getBalance(walletPubkey);
const shouldUseSponsor = !!feeSponsor && walletBalanceLamports < 5000;
const payerPubkey = shouldUseSponsor ? feeSponsor.publicKey : walletPubkey;
const message = new TransactionMessage({
payerKey: payerPubkey,
recentBlockhash: blockhash,
instructions: allInstructions,
}).compileToV0Message();
const tx = new VersionedTransaction(message);
if (shouldUseSponsor) {
tx.sign([feeSponsor]); // sponsor signs as fee payer only
}
// wallet still has to sign as authority on close/withdraw instructions —
// this happens client-side via wallet.signTransaction / signAllTransactions
The transaction still requires the wallet's signature as authority on every account-touching instruction — the sponsor's signature only covers the fee-payer role. So the user retains full custody and consent over what happens to their accounts; they just don't need to own SOL to pay for the privilege of reclaiming SOL.
Putting it together
None of these three things — dead account rent, rent-reduction surplus, unclaimed pump.fun rewards — are secrets or exploits. They're all just consequences of how Solana's account model works, sitting in the gap between "technically claimable" and "someone built the tooling to surface and batch it."
That gap is what claimyoursols exists to close: scan a wallet, find every account leaking value across all three categories, batch the reclaim transactions, and sponsor the gas if the wallet can't otherwise afford to claim its own money back.
If you're building something similar, the mechanisms above (getMinimumBalanceForRentExemption batched by unique space, WithdrawExcessLamports, and a fee-sponsor pattern that never takes authority over user accounts) are the core pieces worth getting right.
Top comments (0)