Landing Raydium and Jupiter swaps
A swap that arrives late does not just miss, it can execute at a price you would not have accepted. This covers the compute profile of Raydium AMM and CLMM swaps, how Jupiter routing changes the risk, how to set slippage that protects you without causing needless failures, and why address lookup tables matter more than people think.
- A late swap does not simply miss. It can execute at a price you would not have accepted, which is worse than failing.
- CLMM swaps have wildly variable compute cost because tick crossings are unpredictable. Budget generously here.
- Address lookup tables are often what decides whether a route fits in 1,232 bytes at all.
- Your slippage tolerance is an attacker's profit ceiling. Set it from volatility and depth, not from a constant.
Swaps are a different problem from launch snipes. In a snipe, arriving late means you get nothing, which is disappointing but bounded. In a swap, arriving late means you execute anyway, at whatever the price has become while you were in flight.
That asymmetry changes almost every decision, and it is the thread running through everything below.
Why a swap is not a snipe#
| Launch snipe | Swap | |
|---|---|---|
| Arriving late | You get nothing | You fill at a worse price |
| Worst outcome | Missed opportunity | Executed at a bad price |
| Slippage exists to | Bound your entry | Protect against MEV and drift |
| Compute cost | Low and predictable | Variable, sometimes very |
| Competition | Everyone, simultaneously | Usually only sandwichers |
The practical upshot: for swaps, reliability matters more than raw speed. A swap that lands 50ms later but always lands beats one that is faster and fails a fifth of the time, because each failure costs you a fee and re-entry into a moved market.
The compute profile#
Swap venues differ enormously, and the variance is what catches people out.
| Venue | Typical units | Why it varies |
|---|---|---|
| Raydium AMM v4 | 30k to 60k | Constant product, essentially fixed |
| Raydium CLMM | 60k to 250k | Depends on tick arrays crossed |
| Orca Whirlpool | 50k to 180k | Same tick crossing dynamic |
| Meteora DLMM | 70k to 300k | Bin traversal, can be very wide |
| Jupiter, one hop | 80k to 200k | Underlying venue plus routing |
| Jupiter, two hops | 150k to 400k | Compounds across venues |
Concentrated liquidity is the source of the variance. A swap staying inside one tick array is cheap; one crossing several does substantially more work, and how many it crosses depends on trades that happen between your simulation and your execution.
Address lookup tables#
A transaction is capped at 1,232 bytes and every referenced account costs 32 bytes inline. A two-hop route touching 30 accounts spends nearly a kilobyte before any instruction data.
Lookup tables replace each key with a one-byte index.
1import {2 AddressLookupTableProgram, Connection, Keypair, PublicKey,3 TransactionMessage, VersionedTransaction,4} from "@solana/web3.js";56/**7 * Address lookup tables are the difference between a route that fits in a8 * transaction and one that does not.9 *10 * A transaction is capped at 1,232 bytes. Every account referenced costs 3211 * bytes inline. A two-hop Jupiter route can reference 30 or more accounts,12 * which is nearly a kilobyte before you have added a single instruction.13 *14 * A lookup table replaces each 32-byte key with a 1-byte index. On a route15 * with 30 accounts that is roughly 930 bytes reclaimed.16 */17export async function createLookupTable(18 rpc: Connection,19 payer: Keypair,20 addresses: PublicKey[],21) {22 const slot = await rpc.getSlot("finalized");2324 const [createIx, tableAddress] = AddressLookupTableProgram.createLookupTable({25 authority: payer.publicKey,26 payer: payer.publicKey,27 recentSlot: slot,28 });2930 // Extending is capped at roughly 30 addresses per instruction, so a large31 // table needs several transactions.32 const chunks: PublicKey[][] = [];33 for (let i = 0; i < addresses.length; i += 30) chunks.push(addresses.slice(i, i + 30));3435 const extendIxs = chunks.map((chunk) =>36 AddressLookupTableProgram.extendLookupTable({37 payer: payer.publicKey,38 authority: payer.publicKey,39 lookupTable: tableAddress,40 addresses: chunk,41 }),42 );4344 return { createIx, extendIxs, tableAddress };45}4647/**48 * A table is only usable one slot after the transaction that extended it49 * lands. Build it well ahead of when you need it, not in the same breath.50 */51export async function loadTable(rpc: Connection, address: PublicKey) {52 const { value } = await rpc.getAddressLookupTable(address);53 if (!value) throw new Error("lookup table not found or not yet active");54 return value;55}
Beyond simply fitting, they help in ways that compound:
- Smaller transactions are cheaper to load, so compute drops too.
- Smaller packets are marginally more likely to survive a congested path.
- Routes that would otherwise be impossible become available.
Slippage that protects you#
On an AMM, your slippage tolerance is not a convenience setting. It is the maximum profit you are offering to anyone willing to sandwich you, and they will take precisely that much.
1import { Connection, PublicKey } from "@solana/web3.js";23/**4 * Slippage tolerance is a risk decision, and treating it as a constant is how5 * people lose money slowly.6 *7 * Too tight and you fail on chain, having paid the fee, and you retry into a8 * market that has already moved. Too loose and you are handing a sandwich9 * attacker a guaranteed profit: your maxSlippage IS their profit ceiling, and10 * they will take exactly that much.11 *12 * The right tolerance comes from the volatility of the pair and the depth of13 * the pool, not from a settings file.14 */15export function toleranceBps(opts: {16 recentVolatilityBps: number; // realised move over your expected latency17 poolDepthUsd: number;18 tradeSizeUsd: number;19 competitive: boolean; // are you racing anyone for this fill?20}): number {21 // Price impact from your own size against the pool.22 const impactBps = (opts.tradeSizeUsd / opts.poolDepthUsd) * 10_000;2324 // Cover your own impact, plus the market moving while you are in flight.25 const base = impactBps + opts.recentVolatilityBps;2627 // In a race, failing is worse than paying a little more. Outside a race,28 // there is no reason to widen the target on your own back.29 const margin = opts.competitive ? 1.5 : 1.15;3031 // The cap is not arbitrary: past a few percent you are underwriting an32 // attacker rather than tolerating market movement.33 return Math.min(500, Math.ceil(base * margin));34}3536/** Turn basis points into the minimum-out figure the program wants. */37export function minimumOut(quotedOut: bigint, bps: number): bigint {38 return (quotedOut * BigInt(10_000 - bps)) / 10_000n;39}
The reasoning behind each term:
- Your own price impact is computable from your size against pool depth. It is not slippage in the risk sense, it is arithmetic, and it must be covered or you fail every time.
- Recent volatility over your expected latency is the market genuinely moving while you are in flight. Faster delivery shrinks this term directly, which is the clearest place where latency turns into money.
- The competitive margin reflects that in a race, failing costs more than paying slightly more.
- The cap is the important one. Past a few percent you have stopped tolerating market movement and started underwriting an attacker.
Notice the second term: cutting delivery latency lets you tighten slippage safely, which reduces your MEV exposure on every trade. That is a more durable benefit than winning any individual race.
Jupiter routing trade-offs#
Jupiter finds the best-priced route. Best-priced and most-likely-to-land are not the same thing, and the default parameters optimise for the first.
1/**2 * Jupiter's quote endpoint, with the parameters that matter for landing rather3 * than for the headline price.4 */5export async function quote(params: {6 inputMint: string;7 outputMint: string;8 amount: bigint;9 slippageBps: number;10}) {11 const url = new URL("https://quote-api.jup.ag/v6/quote");12 url.searchParams.set("inputMint", params.inputMint);13 url.searchParams.set("outputMint", params.outputMint);14 url.searchParams.set("amount", params.amount.toString());15 url.searchParams.set("slippageBps", String(params.slippageBps));1617 // Each of these trades a little price for a lot of reliability.18 //19 // maxAccounts caps how many accounts the route may touch, which directly20 // caps transaction size and compute. Left unbounded, the best-priced route21 // is often one that will not fit or will not land.22 url.searchParams.set("maxAccounts", "40");2324 // Direct routes only, when you are racing. A two-hop route quoting 0.1%25 // better is worse than a direct route that lands.26 url.searchParams.set("onlyDirectRoutes", "false");2728 // Intermediate tokens add hops, accounts and failure modes.29 url.searchParams.set("restrictIntermediateTokens", "true");3031 const res = await fetch(url, { signal: AbortSignal.timeout(2_000) });32 if (!res.ok) throw new Error(`quote failed: ${res.status}`);33 return res.json();34}3536/**37 * Build the swap transaction. Ask for the transaction, then take over the38 * compute budget and the sending yourself: the defaults are tuned for a39 * generic wallet, not for a sender that has measured anything.40 */41export async function buildSwap(quoteResponse: unknown, userPublicKey: string) {42 const res = await fetch("https://quote-api.jup.ag/v6/swap", {43 method: "POST",44 headers: { "Content-Type": "application/json" },45 body: JSON.stringify({46 quoteResponse,47 userPublicKey,48 wrapAndUnwrapSol: true,49 // We set our own budget from measured figures.50 dynamicComputeUnitLimit: false,51 prioritizationFeeLamports: 0,52 asLegacyTransaction: false, // v0 so lookup tables work53 }),54 signal: AbortSignal.timeout(3_000),55 });5657 if (!res.ok) throw new Error(`swap build failed: ${res.status}`);58 const { swapTransaction } = await res.json();59 return Buffer.from(swapTransaction, "base64");60}
Three parameters do the work:
| Parameter | Effect | Cost |
|---|---|---|
maxAccounts | Caps transaction size and compute | May exclude the best-priced route |
restrictIntermediateTokens | Avoids exotic intermediate hops | Slightly worse quotes sometimes |
onlyDirectRoutes | Single hop only, smallest and most reliable | Materially worse price on thin pairs |
Do the arithmetic rather than choosing by instinct. If a two-hop route quotes 0.15% better but lands 12% less often, and a failure costs you a fee plus re-entry into a moved market, the direct route wins comfortably. On a thin pair where the direct route quotes 3% worse, it does not.
Also note the build call disables Jupiter’s own compute and fee handling. Its defaults are tuned for a generic wallet. If you have measured your units and you are pricing your fee from live per-account data, you know more than the default does. That method is in the priority fees guide.
When to skip the aggregator#
Going direct to a pool is worth it when:
- You always trade the same pair, so routing tells you nothing new.
- You are competing for a fill and every hop is risk.
- You need the smallest possible transaction.
- You cannot afford the quote call on your critical path, which is 50 to 200ms of somebody else’s API.
Stay with the aggregator when:
- You trade varied pairs where routing genuinely finds better prices.
- Liquidity is fragmented and a single pool would give you a bad fill.
- You are not racing anyone and price is what matters.
A common hybrid: quote through the aggregator on a slower cadence to learn where liquidity lives, and execute directly against the pool it names.
Pool contention#
Every swap on a pool writes to that pool’s accounts, so swaps on the same pool cannot execute in parallel. On a hot pair, you are serialised against everyone else regardless of what you paid.
What follows from that:
- Price your fee against the pool accounts specifically, not against a network median.
- Watch for the per-account compute ceiling on genuinely hot pools; it throttles everyone.
- Splitting a large trade across pools is a real strategy, and it also reduces price impact.
A checklist#
- Measure compute per venue, at a high percentile, with generous headroom on concentrated liquidity.
- Use lookup tables, built at startup, for any route touching more than a handful of accounts.
- Compute slippage from depth and volatility, and cap it so it cannot become an attacker’s payday.
- Bound routing with
maxAccountsand restricted intermediates when you are racing. - Price the fee against the pool accounts you write to.
- Keep the blockhash fresh in a background cache, never fetched inline.
- Measure landing rate per venue. They differ a great deal, and the aggregate hides it.
That last one is the habit that pays for itself. Landing rate is not one number; it is a number per venue, per size and per market condition. Measuring your real landing rate covers how to instrument it so the answer is trustworthy.