All articles
Smart Contracts·March 22, 2026·6 min read

Address Lookup Tables: Fitting 20-Leg Arbs in One Solana Tx

A practical guide to solana address lookup tables: build, extend, and warm ALTs so a 20-leg Jupiter/Meteora arb fits under the 1232-byte v0 tx limit.

A 20-hop route through Jupiter and Meteora can touch 40-plus accounts, and a legacy Solana transaction dies long before you get there. Every account key you reference is 32 raw bytes in the message, and the whole serialized transaction has to fit in 1232 bytes — the MTU-derived cap the network enforces. Do the arithmetic: 64-byte signature, a handful of instructions, and maybe 30 account keys, and you blow past the limit around hop eight. Address Lookup Tables (ALTs) are the fix, and they're the difference between a route that lands and one your bundler silently drops.

This isn't a compute-budget problem. You can request 1.4M CU all day and it won't buy you a single extra byte. ALTs attack the serialization limit instead: they let a v0 transaction reference an account by a 1-byte index into an on-chain table rather than inlining its 32-byte pubkey. That's a 32x compression on every account you can move into a table.

What actually gets smaller

A v0 message carries two kinds of account references. Static keys still live in the message header at 32 bytes each — signers, the fee payer, and any account that has to be writable-and-signed or that a program treats as an address-space anchor. Everything else can be looked up: the message stores the ALT's address once (32 bytes) plus a list of 1-byte indices into it.

So the win is per-account, not per-table. If your 20-leg arb references 35 distinct accounts and 30 of them are lookup-eligible, you're trading 30 × 32 = 960 bytes of inline keys for one table address plus 30 index bytes — roughly 62 bytes. That's the headroom that makes the route fit.

Two hard limits worth memorizing:

  • A single ALT holds at most 256 addresses.
  • A single transaction can reference at most 256 accounts total across all tables and static keys.

Big AMM routes rarely need a second table for capacity — they need it for organization, or because different tables were warmed at different times. More on warming below.

Building and extending a table

The lifecycle is create, extend, then use. Creating a table returns its address, derived from the authority and a recent slot. You then push addresses into it with extend instructions. Each extend is its own transaction and — this is the gotcha that costs people a block — a freshly extended table is not usable in the same slot it was created or extended. The runtime requires the table to be one slot old before a v0 tx can dereference it. Build ahead of time; never build-and-fire in the hot path.

import {
  AddressLookupTableProgram,
  Transaction, sendAndConfirmTransaction,
} from "@solana/web3.js";

const slot = await connection.getSlot("finalized");
const [createIx, tableAddr] = AddressLookupTableProgram.createLookupTable({
  authority: payer.publicKey,
  payer: payer.publicKey,
  recentSlot: slot,
});

// Extend in chunks. ~20-30 keys per extend keeps the extend tx itself
// under 1232 bytes — the extend payload is also a normal transaction.
const extendIx = AddressLookupTableProgram.extendLookupTable({
  lookupTable: tableAddr,
  authority: payer.publicKey,
  payer: payer.publicKey,
  addresses: poolAccounts.slice(0, 25),
});

await sendAndConfirmTransaction(connection, new Transaction().add(createIx, extendIx), [payer]);

Note the comment: the extend instruction ships the new addresses as raw pubkeys, so a single extend can't add more than ~30 keys before the extend transaction itself overflows. Chunk your extends. For a 200-address pool table that's seven or eight extend transactions, and each one needs the previous to confirm if you care about ordering.

If you're designing the on-chain accounts these tables point at, the seed layout matters as much as the ALT does — the PDA seed design work for multi-vault trading programs directly shapes which accounts end up lookup-eligible versus stuck as static signers.

Warming: the part everyone skips

"Warming" an ALT means getting its account into the leader's cache so that when your v0 transaction arrives, the validator can dereference the indices without a cold account load stalling the pipeline. A cold table doesn't fail — it just resolves slower, and on a congested slot slower can mean your tx misses the leader window entirely.

Practical warming, in order of what actually helps:

  • Keep the table long-lived. Reuse one table across thousands of transactions instead of minting fresh ones. A hot table stays in caches naturally.
  • Prefetch on your side. Call getAddressLookupTable before you build, so your client isn't the thing waiting on the RPC round-trip. You need the resolved addresses locally anyway to construct the MessageV0.
  • Land transactions through the table steadily. The table's cache residency tracks usage. A table you touch every few slots stays warm; one you use once an hour is cold every time.

Don't overthink warming into a cargo cult. The dominant win is table longevity plus client-side prefetch. If your bot rebuilds its ALT on every restart, fix that first.

Assembling the v0 transaction

Once the table is a slot old and prefetched, construction is mechanical: compile a MessageV0, hand it the resolved lookup table accounts, and let web3.js figure out which keys go static and which get indexed.

const lut = (await connection.getAddressLookupTable(tableAddr)).value!;

const msg = new TransactionMessage({
  payerKey: payer.publicKey,
  recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
  instructions: routeIxs,          // your 20-leg Jupiter/Meteora route
}).compileToV0Message([lut]);       // pass every table you reference

const tx = new VersionedTransaction(msg);
tx.sign([payer]);

compileToV0Message takes an array — pass multiple tables when one route spans pools you warmed separately. The compiler only indexes an account if it appears in a supplied table and isn't forced static. If a key you expected to compress is still inline, it's almost always because the instruction marks it as a signer, or it's your fee payer.

Where it bites in production

Three failure modes I've watched eat real capital:

  1. Stale index after a table edit. ALTs can be deactivated and closed, and indices shift if you rebuild rather than append. Only ever append to a live table. Never reorder.
  2. Route accounts that aren't in the table. Jupiter's quote can return pools you didn't pre-add. Your table needs to cover the union of accounts across the routes you'll actually take, or the tx falls back to inlining and overflows. Diff the quote's account list against your table before signing.
  3. Assuming ALTs relax account count limits. They don't touch the 256-account ceiling or the locking rules. A 20-leg arb that writes to 40 accounts still contends for 40 write locks. ALTs make it fit; they don't make it cheaper to land.

That third point is why ALTs pair naturally with tight account-state design. If you're squeezing an on-chain orderbook or a shared vault into as few writable accounts as possible, zero-copy account layouts for on-chain orderbook state cut both the write-lock surface and the number of distinct keys you have to look up in the first place. And if any of those tables sit under a program you deployed, get the account-resolution logic in front of a second pair of eyes — a focused smart-contract review or audit catches the deactivate-and-reuse bug long before mainnet does.

The mental model that keeps you out of trouble: a legacy transaction inlines every key; a v0 transaction inlines the signers and indexes the rest. Design your program so the accounts that must be signers are few, keep everything else in a long-lived, warm table, and a 20-hop route stops being a serialization problem and goes back to being a plain latency problem. If you're building the programs and off-chain infrastructure behind those routes, our Solana smart-contract development team handles exactly this account-layout-and-ALT plumbing so the bytes are never what stops your fill.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#solana#address lookup tables#arbitrage bots#v0 transactions#jupiter