ALT Auto-Expansion — And the SOL Depletion Incident

The Address Lookup Table is working. Accounts compress from 32 bytes to single-byte indices. Transactions that once overflowed the 1,232-byte limit now fit comfortably. The arbitrage bot discovers cycles, builds instructions, submits bundles. The system is functional.

But it is also static. The lookup table contains exactly the addresses I registered during the initial setup — the pools, the token accounts, the program IDs that I knew about at deployment time. Every time a new pool enters the ecosystem, every time the bot discovers a new trading route, the addresses involved in that route are not in the table. The transaction falls back to full 32-byte addresses. The byte budget shrinks. Some cycles that should be executable become too large to fit.

The solution is obvious. The table needs to grow as the bot discovers new addresses. The expansion needs to happen automatically, without me sitting at the terminal adding entries by hand. The bot needs to manage its own lookup table.

This is where I build auto-expansion logic. And this is where I learn a lesson about automation that has nothing to do with Solana and everything to do with how software fails when it runs unsupervised.

The Phone Plan Illusion

Everyone who has ever signed up for a mobile phone plan understands the concept of an "unlimited" data allowance. You pay your monthly bill, the carrier says you have unlimited data, and you stream video and download podcasts without thinking about it. Unlimited means unlimited. Until you read the fine print and discover that "unlimited" actually means "we throttle your speed after a certain threshold" or "we deprioritize your traffic when the network is congested." The word unlimited creates a mental model in which you stop monitoring your usage. The fine print creates a reality in which monitoring your usage is exactly what you should be doing.

The initial balance check in my bot creates the exact same illusion.

At startup, the bot checks the wallet balance. Is there enough SOL to pay for transactions? Yes. The balance exceeds the minimum threshold by a comfortable margin. The check passes. The bot proceeds to its main loop: discover pools, detect arbitrage cycles, build transactions, submit bundles. Somewhere in that loop, I add the auto-expansion logic. When the bot encounters addresses that are not in the lookup table, it constructs an extend instruction, signs it, submits it. The table grows. The bot continues.

The check happens once. The spending happens many times. And I treat the initial check as though it grants permanent permission, the way you treat an "unlimited" data plan as though it grants permanent bandwidth. In both cases, the resource is finite. In both cases, the initial assurance creates a false sense of safety. In both cases, the depletion happens silently, one transaction at a time, until the threshold is crossed and everything stops.

What Auto-Expansion Actually Costs

Extending an Address Lookup Table is not free. Every extension is a transaction on the Solana network, and every transaction requires fees. Beyond the base transaction fee, extending a table means adding bytes to an on-chain account, and those additional bytes require additional rent-exempt deposit. The rent deposit is proportional to the number of bytes added. Each new address is 32 bytes. Each extension transaction that adds, say, ten addresses increases the table's storage by 320 bytes, and the rent-exempt minimum for those bytes must be transferred into the table account.

The individual cost is small. A single extension transaction costs a fraction of a SOL. It is the kind of expense that, in isolation, does not register. Like buying a single cup of coffee. Nobody worries about a single cup of coffee. But the bot is not buying a single cup of coffee. The bot is running a loop, and every iteration of the loop can trigger an extension, and every extension costs a fraction that accumulates.

This is exactly the autopay trap. You set up automatic payments for a streaming service, a gym membership, a cloud storage subscription, a news app. Each one is small. Each one is individually harmless. But they are all drawing from the same checking account, and none of them knows about the others, and nobody is adding up the total. One morning you check your balance and discover that twenty small charges have collectively done what no single charge could do alone: drained the account below the minimum.

The bot's extension logic behaves identically. Each extension is individually affordable. But the extensions do not coordinate with each other. There is no running total. There is no aggregate check. There is just a startup validation that says "balance sufficient" and a loop that says "extend, extend, extend" without ever re-asking the question.

The Discovery Loop

Here is how the auto-expansion logic works in practice. The bot maintains a list of known addresses — the ones already registered in the lookup table. As the bot scans the on-chain state for new trading opportunities, it encounters pool accounts, token vaults, and program addresses that it has not seen before. These addresses go into a queue. When the queue reaches a certain size, or when enough time has passed since the last extension, the bot builds an extend transaction, adds the queued addresses to the table, and clears the queue.

The logic is straightforward. It is also relentless. The Solana ecosystem produces new pools constantly. AMMs create new liquidity pools for new token pairs. Concentrated liquidity protocols open new positions. New DEX programs deploy and attract liquidity. The bot's discovery mechanism surfaces these addresses, and the auto-expansion mechanism adds them. The loop does not pause. The loop does not ask whether extending is still affordable. The loop trusts that the initial balance check was sufficient.

In a quiet market, this works. A few new pools per hour. A few extension transactions per day. The cost is negligible.

In an active market — when new tokens are launching, when liquidity is migrating between protocols, when the DeFi ecosystem is churning — the discovery rate accelerates. Dozens of new addresses per hour. Extension transactions firing every few minutes. Each one small. Each one eating into the same finite balance. The initial check said there was enough. The initial check did not anticipate this volume.

Below the Floor

There is a critical threshold in any on-chain operation, and it is not a soft limit. It is a cliff. A wallet needs SOL to pay transaction fees. The fee itself is small, but it is non-negotiable. If the wallet balance drops below the minimum required to pay for a single transaction, the wallet cannot transact. It cannot pay fees because it does not have enough SOL. It cannot acquire more SOL through a swap because executing the swap requires paying a fee. It cannot close accounts to recover rent deposits because closing an account is a transaction that requires a fee.

This is the turnstile problem. You are riding the subway, and your transit card runs low. Not empty — low. Just below the fare threshold. You swipe at the turnstile and it rejects you. You cannot get on the train. You cannot ride one stop to the station with the reload kiosk. You cannot do anything except stand there, blocked, until someone hands you cash or you walk to a kiosk outside the system. The card has value on it — just not enough to do the one thing you need to do. The system does not offer partial fares. It does not extend credit. The threshold is binary: above it, you move; below it, you are stuck.

On-chain, the consequence is identical but worse. A transit card just needs a reload. A wallet below the minimum balance threshold needs someone to send it SOL from the outside. If this is an automated bot running on a server, there is no "someone." There is no automatic mechanism to refill the wallet. The bot is stuck. Transactions queue up and fail. Arbitrage opportunities pass by. The bot watches profitable cycles appear and disappear because it cannot afford the fraction of a SOL needed to submit a single transaction.

And here is the part that makes it truly painful: the wallet is not empty. There is SOL in it. There might be a meaningful amount of SOL locked in rent deposits across various accounts. But that SOL is illiquid — it is committed to keeping those accounts alive. The freely available balance, the part that can be used to pay fees, has been eaten away by dozens of small extension transactions, each one individually harmless, collectively devastating.

It is the gas tank warning light. You see the low fuel indicator come on, and you think: I can make it to the next station, it is only five miles away. Except the next station is closed. And the one after that is ten miles further. And now you are coasting on fumes with no options. The warning light was not the problem. Ignoring the warning light was not even the problem. The problem was not checking the gauge during the drive.

The Assumption That Kills

The root cause is not a bug in the extension logic. The extension logic works correctly. It discovers addresses, builds transactions, extends the table. Every individual component does exactly what it is supposed to do. The root cause is a design assumption: that initial validation is sufficient for ongoing operation.

This assumption shows up everywhere in software, not just in on-chain bots. A web server checks disk space at startup and logs a confirmation. Then it writes logs for six months without checking again, and the disk fills up, and the server crashes at 3 AM on a Saturday. A backup script verifies that the target drive is mounted before starting the first backup. Then it runs nightly for a year without re-checking, and one day the drive is unmounted for maintenance, and the script silently writes backups to a local directory until that fills up too.

The pattern is always the same. A precondition is verified once, at the beginning. The system then enters a loop that depends on that precondition remaining true. But the loop itself changes the conditions. Each iteration consumes a resource, alters a state, or depends on an external factor that can change independently. The initial check becomes stale. The system operates on a cached assumption. And the gap between the assumption and reality widens with every iteration until something breaks.

In the arbitrage bot, the precondition is "the wallet has sufficient SOL." The loop is "discover pools, extend table, submit bundles." Each iteration of the loop spends SOL. The precondition becomes less true with every iteration. But nobody re-checks.

The Fix: Check Before Every Transaction

The solution is not complicated. It is almost embarrassingly simple. Before every transaction that costs SOL — every extension, every bundle submission, every account creation — check the balance. If the balance is below the minimum threshold, do not submit the transaction. Stop the expansion. Log a warning. Continue operating with the addresses already in the table rather than risk depleting the wallet.

This is the equivalent of checking your bank balance before every autopay charge. Not once a month when you reconcile your statement. Before every charge. The discipline is annoying. It adds overhead. It means an RPC call before every extension transaction to fetch the current balance. It means adding conditional logic to a code path that was previously unconditional. It means accepting that some extensions will be skipped, that some addresses will not be added to the table, that the table will be less complete than it could be.

But "less complete" is infinitely better than "unable to transact." A bot with a smaller lookup table can still operate. It falls back to full 32-byte addresses for some accounts. It might not be able to execute certain large cycles. It loses some opportunities. But it stays alive. It can still pay fees. It can still submit bundles for the routes it does have. It can still generate revenue.

A bot that depleted its wallet cannot do anything. It is dead until manual intervention revives it.

The balance check before every transaction is a safety guard, and safety guards exist precisely for situations where the system is operating autonomously. When a human is at the keyboard, the human can notice the balance dropping and stop the process. When the bot is running unattended at 2 AM, the bot is the only entity that can protect itself. If the bot does not check, nobody checks.

The Guard Logic

The implementation adds a balance verification step to the transaction submission pipeline. Not just for extension transactions — for everything. Every code path that submits a transaction to the network now passes through a guard that fetches the current balance and compares it against the minimum threshold. If the balance is above the threshold, the transaction proceeds. If the balance is at or below the threshold, the transaction is rejected before it ever reaches the network.

The guard also implements graduated responses. Well above the threshold, the bot operates normally — discovering, extending, submitting. As the balance approaches the threshold, the bot enters a conservation mode. It stops extending the table. It stops creating new accounts. It continues submitting revenue-generating transactions — the arbitrage bundles themselves — because those transactions are the ones that replenish the balance. The bot prioritizes survival over growth.

Below the threshold, everything stops. The bot logs an alert, ceases all on-chain activity, and waits. It does not attempt to "optimize" its way out of the situation. It does not try to close accounts to recover rent deposits, because that would require transactions that cost fees. It sits idle, preserving what balance remains, until the operator intervenes.

This is not clever. It is not elegant. It is the financial equivalent of the low fuel shutoff on a furnace — when the gas pressure drops below the minimum, the furnace does not try to burn what little gas is left. It shuts off. Because running on insufficient fuel is worse than not running at all.

Loops Lie About Stability

The deeper lesson is not about SOL balances or lookup tables. It is about the nature of loops in automated systems.

A loop looks stable. It repeats the same code, produces the same kind of output, follows the same control flow. Iteration 1 looks like iteration 100 looks like iteration 10,000. The structure suggests uniformity. The structure lies.

Every iteration of a loop that interacts with the real world changes the world. A loop that reads from a queue changes the queue length. A loop that writes to a database changes the disk usage. A loop that submits transactions changes the wallet balance. The world at iteration N is not the world at iteration 0. The preconditions that were true at iteration 0 may not be true at iteration N. And if the loop does not re-verify those preconditions, it is operating on a fantasy — a snapshot of reality that was accurate once and grows more fictional with every iteration.

This is why defensive programming matters most in loops. Not in initialization code, where everything is fresh and verified. Not in cleanup code, where everything is winding down. In loops. In the code that runs a thousand times, that interacts with external systems, that spends resources, that assumes the world has not changed since the last time it checked.

The initial check is necessary. Nobody disputes that. You have to verify preconditions before starting. But the initial check is also the least useful check, because it occurs at the moment when conditions are most likely to be favorable. The system just started. Resources are available. Connections are fresh. State is clean. Of course the check passes. The valuable checks are the ones that happen later, mid-loop, when resources have been consumed and connections have aged and state has accumulated and the system is running on the dregs of whatever was plentiful at startup.

The Autopay Principle

I start thinking about this pattern as the Autopay Principle. You set up autopay for your phone bill, your insurance, your subscriptions. At setup time, you verify that the checking account has sufficient funds. The verification passes. Everything is fine. Then three months later, five new subscriptions have been added, an unexpected charge has hit the account, and the next autopay attempt bounces — insufficient funds. The bank charges an overdraft fee. The service charges a late fee. The cascade begins.

The failure is not that you set up autopay. Autopay is useful. The failure is that the verification at setup time was treated as permanent. The checking account balance at month one is not the checking account balance at month four. But the autopay system does not know that. It does not check. It trusts the initial state.

Every automated system that consumes a finite resource faces this same risk. The initial check is the easiest and least important check. The mid-loop check is the hardest and most important check. And the natural tendency is to do the easy one and skip the hard one, because the easy one passes and creates confidence, and the hard one adds complexity and slows things down.

The auto-expansion incident teaches me to invert that tendency. Skip the easy check if you must — but never skip the hard one.

What Changes in the Bot

The bot now treats balance verification as a per-transaction concern, not a per-session concern. Every on-chain action checks its preconditions immediately before execution. Not a minute before. Not at the start of the loop iteration. Immediately before. Between the moment the transaction is constructed and the moment it is submitted, the guard fires.

The cost is one additional RPC call per transaction. In a system where latency matters, where single-digit milliseconds determine whether a bundle lands or not, an extra RPC call is a real cost. It adds network round-trip time. It adds a failure mode — the RPC call itself can fail, and now the bot needs logic to handle a failed balance check.

I accept the cost. The alternative is worse. The alternative is a bot that operates blind, spending SOL it may not have, racing toward a cliff it cannot see. The RPC call is the rearview mirror check before changing lanes. Yes, it takes a fraction of a second. Yes, you could skip it and probably be fine. But the one time you are not fine, you are in a wreck.

The guard also maintains a local counter of SOL spent since the last confirmed balance fetch. This is a defense against RPC latency — if the bot submits five transactions in rapid succession, each one costing fees, the guard does not rely solely on the RPC balance (which may not yet reflect the latest transactions). It subtracts the estimated cost of pending transactions from the last known balance. The estimate may be slightly off. That is fine. The guard errs on the side of caution. Better to reject a transaction that could have succeeded than to approve a transaction that cannot.

The General Rule

In any loop that consumes a finite resource, re-verify the resource at every iteration. Not because the check is expensive — it is cheap compared to running dry. Not because the check is interesting — it is the most boring code in the system. Because the check is the only thing standing between continuous operation and unrecoverable failure.

Initial validation is necessary but not sufficient. It is the starting line, not the finish line. The system must keep checking, keep verifying, keep asking the question: do I still have what I need to do what I am about to do?

The alternative is the gas tank on empty, the transit card below the fare, the checking account overdrawn. The alternative is a system that started with everything it needed and consumed it all without noticing, one small transaction at a time, until it had nothing left and no way to get more.

The bot checks before every transaction now. Every single one. And the check is the most important line of code in the entire system — not because it does anything clever, but because it prevents everything clever from becoming worthless.

Disclaimer

This article is for informational and educational purposes only and does not constitute financial, investment, legal, or professional advice. Content is produced independently and supported by advertising revenue. While we strive for accuracy, this article may contain unintentional errors or outdated information. Readers should independently verify all facts and data before making decisions. Company names and trademarks are referenced for analysis purposes under fair use principles. Always consult qualified professionals before making financial or legal decisions.