Landing Raydium and Jupiter swaps

Programs19 min read

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.


the short version
  • 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 snipeSwap
Arriving lateYou get nothingYou fill at a worse price
Worst outcomeMissed opportunityExecuted at a bad price
Slippage exists toBound your entryProtect against MEV and drift
Compute costLow and predictableVariable, sometimes very
CompetitionEveryone, simultaneouslyUsually 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.

VenueTypical unitsWhy it varies
Raydium AMM v430k to 60kConstant product, essentially fixed
Raydium CLMM60k to 250kDepends on tick arrays crossed
Orca Whirlpool50k to 180kSame tick crossing dynamic
Meteora DLMM70k to 300kBin traversal, can be very wide
Jupiter, one hop80k to 200kUnderlying venue plus routing
Jupiter, two hops150k to 400kCompounds 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.

Do not set a CLMM compute limit from a single simulation. Measure across a range of sizes and market conditions, take a high percentile, and add 25 to 40 percent. A swap that fails on compute has cost you a fee and landed you nothing, which is the worst of both outcomes.

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.

lookup.ts
1import {
2 AddressLookupTableProgram, Connection, Keypair, PublicKey,
3 TransactionMessage, VersionedTransaction,
4} from "@solana/web3.js";
5
6/**
7 * Address lookup tables are the difference between a route that fits in a
8 * transaction and one that does not.
9 *
10 * A transaction is capped at 1,232 bytes. Every account referenced costs 32
11 * 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 route
15 * 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");
23
24 const [createIx, tableAddress] = AddressLookupTableProgram.createLookupTable({
25 authority: payer.publicKey,
26 payer: payer.publicKey,
27 recentSlot: slot,
28 });
29
30 // Extending is capped at roughly 30 addresses per instruction, so a large
31 // 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));
34
35 const extendIxs = chunks.map((chunk) =>
36 AddressLookupTableProgram.extendLookupTable({
37 payer: payer.publicKey,
38 authority: payer.publicKey,
39 lookupTable: tableAddress,
40 addresses: chunk,
41 }),
42 );
43
44 return { createIx, extendIxs, tableAddress };
45}
46
47/**
48 * A table is only usable one slot after the transaction that extended it
49 * 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.
A table is not usable until one slot after the transaction extending it lands. Build your tables well ahead of when you need them, ideally at startup, and never in the same breath as the swap that uses them.

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.

slippage.ts
1import { Connection, PublicKey } from "@solana/web3.js";
2
3/**
4 * Slippage tolerance is a risk decision, and treating it as a constant is how
5 * people lose money slowly.
6 *
7 * Too tight and you fail on chain, having paid the fee, and you retry into a
8 * market that has already moved. Too loose and you are handing a sandwich
9 * attacker a guaranteed profit: your maxSlippage IS their profit ceiling, and
10 * they will take exactly that much.
11 *
12 * The right tolerance comes from the volatility of the pair and the depth of
13 * the pool, not from a settings file.
14 */
15export function toleranceBps(opts: {
16 recentVolatilityBps: number; // realised move over your expected latency
17 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;
23
24 // Cover your own impact, plus the market moving while you are in flight.
25 const base = impactBps + opts.recentVolatilityBps;
26
27 // 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;
30
31 // The cap is not arbitrary: past a few percent you are underwriting an
32 // attacker rather than tolerating market movement.
33 return Math.min(500, Math.ceil(base * margin));
34}
35
36/** 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.

jupiter.ts
1/**
2 * Jupiter's quote endpoint, with the parameters that matter for landing rather
3 * 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));
16
17 // Each of these trades a little price for a lot of reliability.
18 //
19 // maxAccounts caps how many accounts the route may touch, which directly
20 // caps transaction size and compute. Left unbounded, the best-priced route
21 // is often one that will not fit or will not land.
22 url.searchParams.set("maxAccounts", "40");
23
24 // 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");
27
28 // Intermediate tokens add hops, accounts and failure modes.
29 url.searchParams.set("restrictIntermediateTokens", "true");
30
31 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}
35
36/**
37 * Build the swap transaction. Ask for the transaction, then take over the
38 * compute budget and the sending yourself: the defaults are tuned for a
39 * 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 work
53 }),
54 signal: AbortSignal.timeout(3_000),
55 });
56
57 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:

ParameterEffectCost
maxAccountsCaps transaction size and computeMay exclude the best-priced route
restrictIntermediateTokensAvoids exotic intermediate hopsSlightly worse quotes sometimes
onlyDirectRoutesSingle hop only, smallest and most reliableMaterially 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#

  1. Measure compute per venue, at a high percentile, with generous headroom on concentrated liquidity.
  2. Use lookup tables, built at startup, for any route touching more than a handful of accounts.
  3. Compute slippage from depth and volatility, and cap it so it cannot become an attacker’s payday.
  4. Bound routing with maxAccounts and restricted intermediates when you are racing.
  5. Price the fee against the pool accounts you write to.
  6. Keep the blockhash fresh in a background cache, never fetched inline.
  7. 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.

Stop guessing at delivery.

swqos.com forwards your signed transactions to the leader over staked connections held open and kept warm. One prepaid balance, a flat price per send.

Get an API key
keep reading