Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana JIT Swap Routing with Raptor V1 in TypeScript
RaptorTypeScriptOctober 10, 202618 min readSolana Tracker

Solana JIT Swap Routing with Raptor V1 in TypeScript

Raptor V1 JIT routing in TypeScript: when jitRouting helps, why quotes expire, the top-level rule for swap-instructions, V0 vs V1 and error handling.

  • raptor,
  • swap-api,
  • jit-routing,
  • dex-aggregator,
  • typescript
Raptor Swap API›Solana RPC›Yellowstone gRPC›

Raptor V1 adds just-in-time (JIT) routing to the Solana swap API. A normal route fixes its pools when you call GET /quote. With JIT, the transaction carries several candidate pools and the Raptor program picks between them when the transaction executes, using the prices in those pools at that slot. The quote's minAmountOut is still the floor: if live prices can't meet it, the transaction fails rather than filling worse. This guide covers how jitRouting behaves in each endpoint, what changes when you compose your own instructions, how the V1 transaction format compares to V0, and how to classify the errors the API returns. Two things share the name: "Raptor V1" is the API release, and txVersion: V1 is a Solana transaction format that Raptor can now build. This guide says "Raptor V1" for the first and V1 in code style for the second. Every number below comes from the companion project running against mainnet.

If you have not used the API before, start with the Solana swap API guide, which walks through quoting, building, signing and sending. This article assumes that flow and focuses on what Raptor V1 changes.

When JIT routing helps

SituationSettingWhy
Most swaps from a wallet or botjitRouting=auto (default)Raptor decides per quote; larger orders get a full search
Large orders on pools that move every slotjitRouting=trueThe split follows prices at execution rather than the prices you quoted against
You need a LEGACY transactionjitRouting=falseJIT plans need lookup tables, which LEGACY does not have
Your program calls the swap through CPIjitRouting=false on /swap-instructionsA JIT swap must be a top-level instruction
You want a route that is identical every time you rebuild itjitRouting=falseA fixed route uses the pools and splits it was quoted with

The gain depends on order size and on how fast the pools move. On a small order through a single deep pool, a JIT and fixed quote often name the same venue and land within a basis point of each other. On a large order split across several pools during a volatile minute, re-pricing at execution can recover the movement a fixed route would lose. JIT does not make a bad quote good: it chooses the best of the candidate pools at execution time, and the candidates are picked when you quote.

How the three modes behave

jitRouting is a query parameter on GET /quote:

ValueBehavior
autoThe default. Raptor uses JIT when it improves the result. Orders of roughly $10,000 or more get a full search
trueAlways build a JIT route when one exists for the pair
falseNever use JIT. The route is fixed at quote time

A quote that uses JIT includes "jitRouting": true in its body. That flag is the only reliable signal: under auto, two quotes for the same pair can come back with and without JIT depending on the size and the pools at that moment, so read it from the response instead of the mode you requested.

The companion project quotes the same 25 SOL → USDC order three ways, back to back:

jitRouting  Expected out      Min out            Impact   Route     JIT route  Search  Round trip
----------  ----------------  -----------------  -------  --------  ---------  ------  ----------
auto        2,736.35961 USDC  2,722.677811 USDC  0.0000%  TesseraV  yes        fast    265 ms
true        2,736.35961 USDC  2,722.677811 USDC  0.0000%  TesseraV  yes        fast    121 ms
false       2,736.35961 USDC  2,722.677811 USDC  0.0000%  TesseraV  no         fast    55 ms

All three led with the same prop AMM and quoted the same amounts, and auto chose JIT for an order of this size. Identical quotes are common: a JIT quote's expected output is an estimate of a route that settles at execution, so the difference shows up in what lands, not in the quote. When the numbers do differ, part of the gap is price movement, because the quotes run one after another. To compare modes, measure real fills, as shown in "Check what actually landed" below. Round-trip times vary with load and cache state; the first request in a run usually pays for a cold cache.

The quote fields that matter

GET /quote returns base units as decimal strings. Parse them with BigInt, never Number, because 64-bit amounts lose precision as floats.

const quote = await raptor.quote({
  inputMint: SOL,
  outputMint: USDC,
  amount: 25_000_000_000n,
  slippageBps: 50,
  jitRouting: "auto",
});

const expected = BigInt(quote.amountOut);
const floor = BigInt(quote.minAmountOut); // enforced on chain, JIT or not
const usedJit = quote.jitRouting === true;
const venues = [...new Set(quote.routePlan.map((step) => step.dex))];

Two Raptor V1 fields deserve attention:

  • quoteId points at a short-lived execution plan held by the API instance that produced the quote. /swap and /swap-instructions use it to rebuild exactly that route.
  • searchMode reports fast or deep. Send searchMode=deep to search more routes at the cost of a slower quote.

Treat the quote as opaque when you pass it back. Fields your type definition does not name still have to reach /swap byte for byte. Editing any value, including minAmountOut, makes the build fail with a 422 stale-quote error, as shown in the error table below.

Quotes expire in seconds

The execution plan behind a quote lives for a few seconds. Build straight after you quote. If the build reports an expired plan, quote again rather than retrying the same body. The companion project wraps that into one helper:

async function quoteAndBuild<T>(mode: JitMode, build: (quote: QuoteResponse) => Promise<T>) {
  for (let attempt = 0; ; attempt++) {
    const quote = await raptor.quote({ inputMint, outputMint, amount, slippageBps, jitRouting: mode });
    try {
      return { quote, result: await build(quote) };
    } catch (error) {
      const stale = error instanceof RaptorError && error.kind === "stale-quote";
      if (!stale || attempt > 0) throw error;
    }
  }
}

One retry is enough. If a fresh quote goes stale again before it reaches the build, the problem is the gap between your calls (a slow signer prompt, a UI that waits for a click) and should be fixed there. In a UI, keep the quote fresh with the /stream WebSocket while the button is on screen, then quote and build once more when the user confirms.

Self-hosting changes one detail. The plan is held in memory by the instance that quoted, so a build that a load balancer sends to a different instance comes back as Quote expired, modified or unavailable. Pin the quote and the build to the same instance, or run one instance per client.

/swap keeps the quote's route

POST /swap builds a complete, unsigned transaction. On this endpoint, leaving jitRouting out or setting it to auto keeps whatever the quote chose, so a JIT quote builds a JIT transaction and a fixed quote builds a fixed one. Set the mode on the quote, not the build.

const built = await raptor.buildSwap({
  userPublicKey: wallet.publicKey.toBase58(),
  quoteResponse: quote,
  txVersion: "V0",
  wrapUnwrapSol: true,
  priorityFee: "medium",
});

For most integrations this is all V1 requires: set jitRouting on the quote (or rely on auto), keep txVersion at V0, and keep signing and sending as before.

/swap-instructions and the top-level rule

POST /swap-instructions returns raw instructions plus the lookup tables to load, so you can add your own instructions to the transaction. Here JIT is opt-in: the endpoint returns an ordinary swap unless the request body carries jitRouting: true. That default protects integrations that call the swap from their own on-chain program, because a JIT swap cannot run under CPI.

When the response includes topLevelOnly: true, two rules apply:

  1. swapInstruction must be the last instruction in the transaction.
  2. It must be called directly by the transaction, not invoked from another program.

Break either rule and the Raptor program rejects the transaction with InvalidJitPlan (error 6026). The companion project's compose() enforces the order and refuses a combination it cannot satisfy:

function compose(res: SwapInstructionsResponse, extra: TransactionInstruction[]) {
  // This example requests neither a tip nor a token ledger, so their placement is not guessed.
  if (res.tipInstruction || res.tokenLedgerInstruction) throw new Error("Unexpected tip or token ledger instruction");
  const head = [...res.computeBudgetInstructions, ...res.setupInstructions].map(toInstruction);
  const cleanup = res.cleanupInstruction ? [toInstruction(res.cleanupInstruction)] : [];
  if (res.topLevelOnly && cleanup.length) {
    throw new Error("A JIT swap came with a cleanup instruction, which would break the swap-last rule");
  }
  return [...head, ...extra, toInstruction(res.swapInstruction), ...cleanup];
}

toInstruction converts the API's JSON instruction (program ID, account metas, base64 data) into a web3.js TransactionInstruction. Your own instructions go before the swap. Anything that has to run after it (an unwrap, a transfer of the proceeds, an assertion on the output account) cannot share a transaction with a JIT swap. Build those with a fixed route, or move them to a second transaction.

If the response includes a quoteResponse, use its amounts rather than the ones you sent. It reflects the instructions that were actually built.

JIT instructions are much larger

A JIT swap names every candidate pool and the accounts each one needs. The companion project builds the same 25 SOL order both ways, adds a one-line memo, and compiles a V0 message against the returned lookup tables:

Swap   Accounts  Writable  Lookup tables  Ixs with memo  Top level only  V0 size
-----  --------  --------  -------------  -------------  --------------  -----------------------------------
JIT    51        32        11             9              yes             1338 B, 16 static keys, over by 106
fixed  20        10        5              9              no              881 B, 12 static keys

Accounts and Writable count unique addresses in the swap instruction. The JIT plan touched 51 accounts and its instructions needed 11 lookup tables. Even with those tables, the compiled transaction was 1338 bytes, 106 over Solana's 1232-byte packet limit, so it could not be sent, and it stays over (1270 bytes) without the memo. The fixed route fit with room to spare. In the same run, /swap built a JIT quote for the same order as a 942-byte V0 transaction using 5 lookup tables (next section). When you compose the transaction yourself, keeping it under the limit is your job.

Measure before you sign. serialize() in @solana/web3.js 1.x is not a size check: it only throws when the message alone overflows a 1232-byte buffer, so a transaction slightly over the limit serializes fine and is rejected later by the network. The companion project computes the size from the compiled message instead:

const compactLength = (n: number) => (n < 0x80 ? 1 : n < 0x4000 ? 2 : 3);

function v0Size(message: MessageV0): number {
  const signers = message.header.numRequiredSignatures;
  const keys = message.staticAccountKeys.length;
  let size = compactLength(signers) + 64 * signers + 1 + 3 + compactLength(keys) + 32 * keys + 32;
  size += compactLength(message.compiledInstructions.length);
  for (const ix of message.compiledInstructions) {
    size += 1 + compactLength(ix.accountKeyIndexes.length) + ix.accountKeyIndexes.length;
    size += compactLength(ix.data.length) + ix.data.length;
  }
  size += compactLength(message.addressTableLookups.length);
  for (const lookup of message.addressTableLookups) {
    size += 32 + compactLength(lookup.writableIndexes.length) + lookup.writableIndexes.length;
    size += compactLength(lookup.readonlyIndexes.length) + lookup.readonlyIndexes.length;
  }
  return size;
}

The layout is the signature count and signatures, the version byte, the three header bytes, the static keys, the blockhash, the instructions and the lookups. If a composed JIT transaction is over 1232 bytes, you have three options: drop your extra instructions to the minimum, fall back to jitRouting: false for this build, or let /swap build the transaction and add nothing.

V0 vs V1 transactions

txVersion accepts V0 (the default), V1 and LEGACY, and is case-sensitive: v0 is rejected with a 422.

The companion project builds the same JIT quote as V0 and V1:

txVersion  Size    First byte  Static keys  Lookup tables  Instructions  ≤ 1232 B
---------  ------  ----------  -----------  -------------  ------------  --------
V0         942 B   0x01        11           5              8             yes
V1         2064 B  0x81        52           0              7             no

What the bytes show:

  • The layout is different. A V0 transaction starts with the signature count (0x01 for one signer). The V1 build starts with 0x81, a version prefix, so it is not a V0 transaction with a different flag.
  • No lookup tables. Every account is a static key: 52 here, against 11 static keys plus 5 tables for V0. You skip the lookup-table fetch, but every account costs 32 bytes.
  • It is larger than the old packet limit. At 2064 bytes it only works where the larger V1 size is accepted. Check that your RPC or sender supports V1 before switching.
  • One instruction fewer. The V0 build carries three compute budget instructions (unit limit, unit price and a heap frame request); the V1 build leaves out the heap frame request.
  • Your signer has to support it. @solana/web3.js 1.99 can deserialize the bytes but cannot sign a V1 message, and older 1.x releases cannot parse it at all. Wallets and SDKs add support on their own schedules.

Because of the last point, the companion project executes with V0. To read a landed V1 transaction over RPC, pass maxSupportedTransactionVersion: 1 to getTransaction. With the common value 0, the node returns an error for V1 transactions rather than the transaction.

LEGACY and venues without JIT

LEGACY transactions have no lookup tables and a 1232-byte limit, so a JIT plan almost never fits. Quote with jitRouting=false when you need LEGACY, for example for a wallet that only signs legacy messages.

JIT is available on every venue Raptor routes except Heaven, SolFi V1, BisonFi and Quantum. A route through one of those uses a fixed hop for that leg. Routing covers 71 DEXes as of Raptor V1, including 47 added in this release, such as Sanctum Infinity, Perena, Stabble CLMM, MetaDAO, Invariant and Crema, alongside prop AMMs like HumidiFi, Tessera and SolFi V2.

Classify errors before you retry

The API returns JSON error bodies for some failures and plain text for others, notably request-body parse errors. Retrying everything wastes requests on errors that will never succeed, and retrying nothing gives up on a network blip. The companion project reduces every failure to one of four kinds:

type FailureKind = "no-route" | "stale-quote" | "bad-request" | "transient";

function classify(status: number | undefined, message: string): FailureKind {
  if (/no (multi-hop )?route/i.test(message)) return "no-route";
  if (/expired|modified or unavailable/i.test(message)) return "stale-quote";
  if (status !== undefined && status >= 400 && status < 500 && status !== 429) return "bad-request";
  return "transient";
}

The order matters. A stale quote comes back as 500 Execution plan expired when it is seconds old and as 422 Quote expired, modified or unavailable when it is older or edited, so the message has to be checked before the status. Any other 4xx except 429 is a bad request; 429, 5xx and network errors are transient. Triggering each case against mainnet:

Case                  HTTP  Kind         Action                   Message
--------------------  ----  -----------  -----------------------  ------------------------------------------------
Zero amount           400   bad-request  fix the request          Invalid amount: must be greater than 0
Unlisted output mint  422   no-route     change the pair or size  Failed to get quote: No multi-hop route found
Edited minAmountOut   422   stale-quote  quote again              quoteResponse: Quote expired, modified or unava…
Lowercase txVersion   422   bad-request  fix the request          txVersion: unknown variant `v0`, expected one o…

In a bot loop:

  • stale-quote: quote again, then build. Do not resend the old body.
  • no-route: skip this order or change its size. Retrying the same request returns the same answer until liquidity changes.
  • bad-request: fix the code and alert. It will not succeed on retry.
  • transient: back off with jitter and retry quotes and builds. Never auto-retry POST /send-transaction: a timeout there does not mean the transaction failed. Read the signature from the signed transaction before you send, so you can track it whatever /send-transaction returns.

GET /transaction/:signature returns 404 until the tracker has seen the signature, which can lag the send by a moment. Tolerate a few 404s before you treat the signature as unknown.

Run the example

The companion project runs four inspection steps and one optional execution step. Nothing is signed unless you ask for it.

cp .env.example .env
npm install
npm start

No API key is needed. Without a WALLET_PUBLIC_KEY the builds use a throwaway address, which is enough to inspect routes, sizes and errors. The default order is 25 SOL → USDC, large enough that JIT and fixed routes differ. Set SOLANA_RPC_URL to your own endpoint if the public one rate-limits you; the project uses it for mint decimals and lookup-table accounts.

To send a real swap, set WALLET_SECRET_KEY, an explicit AMOUNT and EXECUTE=true. Use a dedicated wallet with a small balance. The script runs the four inspection steps first, then quotes in JIT_MODE, builds V0, signs locally, sends, tracks the signature to a final status, and compares what landed with what was quoted.

Check what actually landed

The amount you receive is the only honest measure of a route. Once a transaction lands, GET /transaction/:signature returns the Raptor program's events, and SwapEvent.parsed.amountOut is the output that was actually transferred:

const signature = bs58.encode(tx.signatures[0]!); // known before sending
// ...send, then:
const final = await trackUntilFinal(signature);
const swapEvent = final?.events?.find((event) => event.name === "SwapEvent")?.parsed;
if (swapEvent?.amountOut !== undefined) {
  const received = BigInt(swapEvent.amountOut as string | number);
  console.log(`vs quote: ${bpsDelta(received, BigInt(quote.amountOut))}`);
  console.log(`vs floor: ${bpsDelta(received, BigInt(quote.minAmountOut))}`);
}

Log both deltas for every fill, tagged with the mode and whether the quote used JIT. After a few hundred fills you have your own answer to whether true beats auto for your order sizes and pairs, which is worth more than any single comparison.

Production pitfalls

Setting jitRouting on the build instead of the quote. /swap builds the route the quote chose. To switch between JIT and fixed, quote again with the mode you want rather than reusing the old quote.

Assuming /swap-instructions follows the quote. It returns an ordinary swap unless the body says jitRouting: true, even for a JIT quote. The response's topLevelOnly tells you which you got.

Appending instructions after a JIT swap. The swap must be last. An unwrap or transfer placed after it fails the whole transaction with InvalidJitPlan.

Composing without measuring. A JIT instruction set can push a transaction past 1232 bytes even with lookup tables. Compute the size before you ask a user to sign.

Editing the quote. Any changed field, including a "safer" minAmountOut, makes the build fail. Set slippage with slippageBps on the quote instead.

Holding quotes. A quote that waited for a confirmation dialog is usually stale. Quote and build after the user confirms, not before.

Switching to V1 blindly. It needs a signer and a send path that support it. V0 remains the default for good reason.

Spreading quote and build across self-hosted instances. The plan lives on the instance that quoted. Route both calls to the same one.

Upgrading without updating the host. Since the Raptor V1 release the docs list only https://raptor.solanatracker.io and wss://raptor.solanatracker.io, so move any hardcoded beta URL to them.

FAQ

What is JIT routing on Solana?

A route whose final pool choice happens when the transaction executes, not when it is quoted. The transaction includes several candidate pools, and the Raptor program prices them at that slot and splits the order across the best ones.

Is JIT routing on by default?

Yes. jitRouting defaults to auto, which uses JIT when Raptor expects it to improve the result.

Can JIT fill below my minimum?

No. minAmountOut is enforced on chain for every route. If live prices can't meet it, the transaction fails.

Can my program call a JIT swap through CPI?

No. A JIT swap must be a top-level instruction and the last one in the transaction. Request jitRouting: false from /swap-instructions when your program needs to CPI into the swap.

What does error 6026 mean?

InvalidJitPlan: the JIT swap was not the last instruction, or it was invoked through CPI.

Should I use V1 transactions?

Only if your signer and send path support them. Raptor's V1 builds put every account inline without lookup tables and run past 1232 bytes. V0 works everywhere and carries JIT routes.

Why does my quote fail when I build it?

The execution plan lasted only a few seconds, or the quote was modified on the way to /swap. Quote again and build immediately, passing the quote object unchanged.

Does JIT cost more?

A JIT swap touches more accounts than a fixed route (51 against 20 in the example above), so set your priority fee as you would for any multi-pool route. There is no extra API charge: the docs list Raptor as currently free, with no API key.

References

  • Raptor overview and JIT routing
  • Get swap quote
  • Build swap transaction
  • Build swap instructions
  • Send a signed transaction
  • Get transaction status
  • Transactions and Jet TPU
  • Raptor binary releases
  • @solana/web3.js on npm
  • bs58 on npm

Companion project

The companion project compares the three jitRouting modes on one order, builds and measures JIT and fixed instruction sets, builds the same quote as V0 and V1, classifies each V1 error, and with EXECUTE=true sends a real swap and compares the landed SwapEvent with the quote.

cp .env.example .env && npm install && npm start
View source on GitHub›Run in StackBlitz›

Runnable Node.js project — clone from GitHub, add keys to .env, then npm start. Use a private local environment for credentials. Check the companion package version before running.

Related Guides

Solana Swap API: Quote, Build and Send Swaps in TypeScript
Raptor

Solana Swap API: Quote, Build and Send Swaps in TypeScript

Read more
Yellowstone gRPC Examples: 18 TypeScript Recipes for Solana
Infrastructure

Yellowstone gRPC Examples: 18 TypeScript Recipes for Solana

Read more
Yellowstone gRPC Account Subscribe: Stream Token Balances
Infrastructure

Yellowstone gRPC Account Subscribe: Stream Token Balances

Read more

Products

  • Data API
  • Pump.fun API
  • Solana RPC
  • Dedicated Nodes
  • Yellowstone gRPC
  • Raptor Swap API
  • Enterprise

Trading

  • Swap
  • Latest Tokens
  • Trending
  • Top Gainers
  • Memescope
  • Whale Watch
  • KOL Tracker

Tools

  • Wallet Tracker
  • Rugcheck
  • PnL Leaderboard
  • KOLScan
  • Axiom Leaderboard
  • Photon Leaderboard
  • Bloom Leaderboard
  • FOMO Leaderboard
  • GMGN Leaderboard
  • Pump.fun App Leaderboard
  • Terminal Leaderboard
  • Platform Compare
  • My Positions
  • Teams

Resources

  • Developer Guides
  • Blog
  • Documentation
  • API Reference
  • Status
  • Affiliate Program — 25% recurring, uncapped
Solana TrackerSolana Tracker© 2026
Terms of ServicePrivacy PolicyContact