Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana Swap API: Quote, Build and Send Swaps in TypeScript
RaptorTypeScriptOctober 8, 202615 min readSolana Tracker

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

Use the Raptor Solana swap API from TypeScript: get a routed quote, build a V0 transaction, sign it locally, send it over Jet TPU and track confirmation.

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

The Raptor Solana swap API turns a token swap into four calls: GET /quote returns the best route and expected output for your exact size, POST /swap builds an unsigned transaction for your wallet, you sign it locally, then POST /send-transaction submits it through Yellowstone Jet TPU and GET /transaction/:signature tells you when it confirmed. There is no API key and the hosted endpoint is free. This guide runs the whole flow in TypeScript with the error handling a bot needs.

A price feed tells you what a token last traded at. A swap API tells you what you will receive for a specific amount on a specific route right now, and hands you the transaction that executes exactly that. On thin pools and fresh launches the two numbers can be far apart, which is why execution code should never start from a spot price.

When to use a Solana swap API

NeedUseWhy
Show a price on screenData API price endpoints (guide)One cached request, no wallet, no route
Decide whether a trade is worth itGET /quoteIncludes size, route, slippage and price impact
Execute a swapPOST /swap, sign, POST /send-transactionTransaction is built server side, signed by you, landed over Jet TPU
Add your own instructions, or chain a buy and a sell in one transactionPOST /swap-instructionsReturns raw instructions plus the address lookup tables to load
Keep a quote fresh while a button is on screenWS /stream or WS /stream/swapPush updates instead of polling

Routing yourself means indexing every pool type you want to reach: Raydium AMM, CLMM and CPMM, Meteora DLMM and DAMM, Orca Whirlpools, the Pump.fun curve and PumpSwap, several newer bonding curves and prop AMMs. The Raptor Swap API does that server side and splits an order across pools and up to four hops. If you would rather run it yourself, the same binary is downloadable and takes flags for DEX filtering, worker threads and an RPC rate limit; it needs an RPC URL and, for Jet sending, a Yellowstone gRPC endpoint.

The endpoints

MethodPathPurpose
GET/quoteRoute, expected output, worst-case output, price impact
POST/swapUnsigned transaction for a wallet, built from a quote
POST/swap-instructionsThe same swap as instructions and lookup tables
POST/quote-and-swapQuote and build in one request
POST/send-transactionSubmit a signed transaction over Jet TPU with background retries
GET/transaction/:signaturepending, confirmed, failed or expired, with slot and latency
WS/streamLive quote updates for a subscription
WS/stream/swapLive quotes plus a prebuilt transaction, re-sent after 10 idle slots

The product page lists https://raptor.solanatracker.io; the docs examples use https://raptor-beta.solanatracker.io. Both answered identical quotes while this guide was written. The docs state that Raptor is currently free with no rate limits, so there is nothing to authenticate.

Run the example

Create a project, install the Solana web3 library and a TypeScript runner, and keep secrets in .env:

npm init -y && npm pkg set type=module
npm install @solana/web3.js@^1.99.0 bs58@^6.0.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts
RAPTOR_BASE_URL=https://raptor.solanatracker.io
INPUT_MINT=So11111111111111111111111111111111111111112
OUTPUT_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
AMOUNT=10000000
SLIPPAGE_BPS=dynamic
PRIORITY_FEE=medium
WALLET_PUBLIC_KEY=
WALLET_SECRET_KEY=
EXECUTE=false

Without a wallet the script only quotes. With WALLET_PUBLIC_KEY it also builds the transaction, which is a safe dry run. Only with a base58 secret key and EXECUTE=true does it sign and send on mainnet, so use a dedicated bot wallet with a small balance.

import { Keypair, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";

const RAPTOR = (process.env.RAPTOR_BASE_URL ?? "https://raptor.solanatracker.io").replace(/\/+$/, "");
const SOL = "So11111111111111111111111111111111111111112";
const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

type RouteStep = { dex: string; pool: string; inputMint: string; outputMint: string; amountIn: string; amountOut: string; percent: number };
type Quote = {
  amountIn: string; amountOut: string; minAmountOut: string; slippageBps: number; priceImpact?: number;
  swapUsdValue?: string; contextSlot: number; routePlan: RouteStep[]; [key: string]: unknown;
};
type Swap = { swapTransaction: string; lastValidBlockHeight: number; prioritizationFeeLamports?: number };
type Sent = { signature: string; success: boolean };
type Status = { status: "pending" | "confirmed" | "failed" | "expired"; slot?: number; latency_ms?: number; error?: string };

async function raptor<T>(path: string, body?: unknown): Promise<T> {
  const res = await fetch(`${RAPTOR}${path}`, {
    method: body ? "POST" : "GET",
    headers: body ? { "content-type": "application/json" } : {},
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(15_000),
  });
  const text = await res.text();
  if (!res.ok) {
    // 400 and 404 bodies are JSON { error, code }; 422 validation errors are plain text.
    let message = text.trim().slice(0, 200);
    try { message = (JSON.parse(text) as { error?: string }).error ?? message; } catch { /* not JSON */ }
    throw new Error(`${path.split("?")[0]} failed with HTTP ${res.status}: ${message}`);
  }
  return JSON.parse(text) as T;
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function main(): Promise<void> {
  const inputMint = process.env.INPUT_MINT ?? SOL;
  const outputMint = process.env.OUTPUT_MINT ?? USDC;
  const amount = BigInt(process.env.AMOUNT ?? "10000000"); // base units: 0.01 SOL
  const signer = process.env.WALLET_SECRET_KEY ? Keypair.fromSecretKey(bs58.decode(process.env.WALLET_SECRET_KEY)) : undefined;
  const wallet = signer?.publicKey.toBase58() ?? process.env.WALLET_PUBLIC_KEY;

  // 1. Quote. Amounts come back as base-unit strings; keep them as BigInt.
  const query = new URLSearchParams({
    inputMint, outputMint, amount: amount.toString(), slippageBps: process.env.SLIPPAGE_BPS ?? "dynamic", maxHops: "4",
  });
  const quote = await raptor<Quote>(`/quote?${query}`);
  const impact = typeof quote.priceImpact === "number" ? `${quote.priceImpact.toFixed(4)}%` : "n/a";
  console.log(`out ${quote.amountOut} (min ${quote.minAmountOut}) at ${quote.slippageBps} bps, impact ${impact}, slot ${quote.contextSlot}`);
  for (const step of quote.routePlan) console.log(`  ${step.dex} ${step.pool} ${step.percent}%`);
  if (!wallet) return;

  // 2. Build an unsigned V0 transaction. Pass the quote back exactly as received.
  const swap = await raptor<Swap>("/swap", {
    userPublicKey: wallet, quoteResponse: quote, txVersion: "V0", wrapUnwrapSol: true,
    priorityFee: process.env.PRIORITY_FEE ?? "medium",
  });
  const tx = VersionedTransaction.deserialize(Buffer.from(swap.swapTransaction, "base64"));
  console.log(`built: priority fee ${swap.prioritizationFeeLamports ?? "n/a"} lamports, valid until block height ${swap.lastValidBlockHeight}`);
  if (!signer || process.env.EXECUTE !== "true") return;

  // 3. Sign locally. The secret key never leaves this process.
  tx.sign([signer]);

  // 4. Send through Jet TPU. Raptor retries in the background until it confirms or expires.
  const sent = await raptor<Sent>("/send-transaction", { transaction: Buffer.from(tx.serialize()).toString("base64") });
  console.log(`sent ${sent.signature}`);

  // 5. Track. Stop on a final status; a retry after "expired" needs a fresh quote and a new transaction.
  const deadline = Date.now() + 60_000;
  while (Date.now() < deadline) {
    const status = await raptor<Status>(`/transaction/${sent.signature}`);
    if (status.status !== "pending") {
      console.log(`${status.status} at slot ${status.slot ?? "n/a"}, ${status.latency_ms ?? "n/a"} ms${status.error ? `, error: ${status.error}` : ""}`);
      return;
    }
    await sleep(1_000);
  }
  console.log("no final status after 60 s; check the signature on an explorer before retrying");
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exitCode = 1;
});

The companion project splits this into a typed client with per-request timeouts, capped backoff on quotes and status reads, a tolerant status poller, validated environment variables and a readable route table.

What a quote contains

A trimmed live response for 0.1 SOL into USDC:

{
  "inputMint": "So11111111111111111111111111111111111111112",
  "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  "amountIn": "100000000",
  "amountOut": "11357124",
  "minAmountOut": "11300338",
  "feeAmount": "200000",
  "priceImpact": 0.1044,
  "slippageBps": 50,
  "routePlan": [
    { "dex": "Crema", "pool": "DV569UDdnjkYWJDnpJfJZE4HyzYKYyRGowtdPQrFUZpm", "amountIn": "100000000", "amountOut": "11357124", "percent": 100 }
  ],
  "swapUsdValue": "11.36",
  "platformFee": { "feeBps": 0, "feeFromInput": false, "chargeBps": 0 },
  "contextSlot": 454529718,
  "timeTaken": 0.0049
}
  • amountIn, amountOut and minAmountOut are base units as strings. USDC has 6 decimals and SOL has 9, so 11357124 is 11.357124 USDC. Parse with BigInt; a Number silently loses precision on large token supplies.
  • minAmountOut is the on-chain floor. If the pool moves so that the fill would be lower, the transaction fails instead of filling badly.
  • slippageBps is the resolved value. With slippageBps=dynamic the router picks a number from volatility and route complexity and reports it here; in testing it returned 100 bps for small SOL to USDC swaps and more for a split route.
  • routePlan lists one step per hop and pool. A split route has several steps at the same hop whose percent values add up to 100. Each step names the programId, dex and pool.
  • priceImpact is the router's estimate for your size. The reference example shows 0.5 and the docs guide multiplies the value by 100 before display. Live quotes returned 0.0058 for 0.01 SOL and 0.1044 for 0.1 SOL on the same pool, which scales like a percentage, not a fraction, so the code above prints it as a percentage unchanged. Check it against your own fills before showing it to users.
  • feeAmount is the fee taken by the pools on the route, not your platform fee. Your fee is reported separately under platformFee.
  • contextSlot is the slot the quote was computed against, and timeTaken is the routing time in seconds.
  • Live responses carry fields the reference does not list yet, such as quoteId and searchMode. Type the object with an index signature and send the whole quote back to /swap unchanged.

Build options

POST /swap takes userPublicKey and quoteResponse plus optional controls. The fields below come from the build reference and were exercised against the hosted endpoint.

FieldDefaultNotes
txVersionV0V0 or LEGACY, case sensitive. v0 returns a plain-text 422
wrapUnwrapSoltrueWraps SOL in setup instructions and unwraps leftover wSOL in cleanup. Set false only if you manage a long-lived wSOL account
priorityFeenoneA level string: min, low, auto, medium, high, veryHigh, turbo, unsafeMax. A JSON number is rejected with a 422; a numeric string such as "5000" is accepted as an exact lamport value
maxPriorityFeenoneCap applied when a level resolves to a fee
computeUnitPriceMicroLamportsnoneExact compute unit price when you want to set the fee yourself
computeUnitLimitnoneOverride the compute budget, useful when combining two swaps
tipAccount, tipLamportsnoneOptional SOL tip to a validator or relay account
feeAccount, feeBps, feeFromInput, chargeBpsnonePlatform fee controls, mirrored from the quote

The response is swapTransaction (base64, unsigned), lastValidBlockHeight, contextSlot and prioritizationFeeLamports. Raptor also adds idempotent associated token account creation for any input, output or intermediate mint the wallet lacks, so a first-time buyer does not need a separate setup transaction.

Fee levels resolve from current network conditions. For one 0.01 SOL swap on the day this guide was written, medium resolved to a priority fee of 8,800 lamports, high to 44,000 and unsafeMax to 880,000. Treat those as an illustration of the scale between levels, not as constants.

Production pitfalls

Quotes and transactions expire. A built transaction is valid until lastValidBlockHeight. If you wait for a user to confirm and the height passes, the send fails or the tracker reports expired. Rebuild from a fresh quote at that point. Never patch an old transaction.

Resend the same bytes, never a rebuilt transaction, while a send is pending. Raptor's sender already retries in the background. If your own code also retries, resend the identical signed transaction; a rebuilt one has a new signature and can fill twice.

Dynamic slippage is a floor, not a promise. dynamic widens the tolerance on volatile routes so transactions land, which also means a worse worst-case fill. Bots with tight margins should pass a fixed slippageBps and accept more failures, then compare minAmountOut against their own threshold before signing.

Price impact depends on route search. A quote for a very small amount can route through a pool that would never absorb a real order. Quote the size you intend to trade. If the impact is high, lower the size or restrict dexes and maxHops to pools you trust.

Check the token before you trade it. A clean quote says nothing about mint authority, holder concentration or a creator who dumps at graduation. Gate automated buys on a rug check, and when you trade on launch events, re-quote after the event rather than trusting data that was true when the alert fired (graduation guide).

Errors come in two shapes. Bad parameters return JSON with error and code on a 400; validation failures such as a missing userPublicKey or lowercase txVersion return a plain-text 422. A 503 from /quote means the router is still indexing, which is worth a retry with backoff; a 503 from /send-transaction means the sender is disabled and no retry will help.

Confirmed is not finalized. The tracker reports confirmed at the confirmed commitment level. If your accounting needs finality, or you want to read the resulting balances, use your own Solana RPC endpoint for the follow-up read. For a bot that sizes the next trade on the last fill, a dedicated node keeps that read off a shared queue.

Keep the secret key where the signature happens. For user wallets, deserialize the transaction in the browser and let the wallet adapter sign. For bots, load the keypair on the host that runs the loop. Raptor only ever sees the public key and the signed bytes.

Self-hosting changes the failure modes. The binary takes --enable-websocket for streams and --enable-yellowstone-jet for sending, with RPC_URL and YELLOWSTONE_ENDPOINT as environment variables. Jet sending needs a Yellowstone gRPC endpoint, and route quality depends on your RPC keeping pool state current.

Platform fees

To monetise swaps, pass feeBps (up to 1000, which is 10%) and feeAccount on the quote. The quote's platformFee object echoes the settings so you can verify them before building. feeFromInput takes the fee from the input token instead of the output. chargeBps adds a charge on positive slippage, the difference between the expected and the actual fill. Send the same fee fields to /swap.

Streaming quotes

For a swap button that stays open, subscribe over WebSocket instead of polling /quote. Send one JSON message per subscription and handle the server's subscribed, quote, unsubscribed, pong and error messages:

import WebSocket from "ws";

const ws = new WebSocket(`${RAPTOR.replace(/^http/, "ws")}/stream`);
ws.on("open", () => ws.send(JSON.stringify({
  type: "subscribe", id: "sol-usdc", inputMint: SOL, outputMint: USDC, amount: 100000000, slippageBps: "dynamic", maxHops: 2,
})));
ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString()) as { type: string; id?: string; data?: Quote; error?: string };
  if (msg.type === "quote" && msg.data) console.log(`${msg.id}: ${msg.data.amountOut} (slot ${msg.data.contextSlot})`);
  if (msg.type === "error") console.error(msg.error);
});

Each quote message carries the same object as GET /quote under data. /stream/swap additionally needs userPublicKey and returns a swap message with quote, swapTransaction and lastValidBlockHeight, and re-sends the latest transaction after 10 slots without a change so the one you hold does not expire. Add reconnect with backoff and resubscribe on open, as with any WebSocket feed.

FAQ

Is the Raptor Solana swap API free?

Yes. The hosted endpoint needs no account or API key and the docs state it is currently free with no rate limits. The binary is also free to self-host. The older Solana Tracker Swap API at swap-v2.solanatracker.io is a separate product that charges a platform fee on each swap.

Which DEXes does Raptor route across?

Raydium AMM, CLMM, CPMM and LaunchLab; Meteora DLMM, Dynamic AMM, DAMM v2, Curve and DBC; Orca Whirlpool and Whirlpool v2; the Pump.fun bonding curve and PumpSwap; the Heaven, MoonIt and Boopfun curves; several prop AMMs; PancakeSwap v3 and FluxBeam. Restrict the set with the dexes parameter.

How do I charge my users a fee on swaps?

Pass feeBps and feeAccount on the quote and again on the swap build. The maximum is 1000 bps. Set feeFromInput to collect in the input token. The quote's platformFee object shows what will be charged.

What should I do when the status is expired?

The transaction was not confirmed before its block height passed. Nothing was executed. Request a new quote, build a new transaction and send that. Do not resubmit the old bytes, which will be rejected for an expired blockhash.

Can I get the instructions instead of a whole transaction?

Yes. POST /swap-instructions returns setupInstructions, swapInstruction, cleanupInstruction, computeBudgetInstructions and addressLookupTableAddresses. Load the lookup tables and assemble your own versioned transaction; the address lookup table guide covers resolving them over RPC.

Does a quote guarantee the output amount?

No. amountOut is the expected fill at the quoted slot. The transaction guarantees minAmountOut by failing if the fill would be worse. Anything between the two is normal slippage.

References

  • Raptor Swap API overview
  • Build and send Solana swaps with Raptor (guide)
  • Get swap quote
  • Build swap transaction
  • Build swap instructions
  • Send a signed transaction
  • Get transaction status
  • Transactions and Jet TPU
  • /stream WebSocket
  • Raptor binary releases
  • @solana/web3.js on npm
  • bs58 on npm

Companion project

The companion project quotes any pair, prints the route table, builds the transaction as a dry run for WALLET_PUBLIC_KEY, and signs, sends and tracks it only when WALLET_SECRET_KEY and EXECUTE=true are set.

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

Yellowstone gRPC Account Subscribe: Stream Token Balances
Infrastructure

Yellowstone gRPC Account Subscribe: Stream Token Balances

Read more
Solana Liquidity Events API: LP History and Live Stream
Data API

Solana Liquidity Events API: LP History and Live Stream

Read more
Solana Tracker Data API SDK 0.5: Migration Notes
Updates

Solana Tracker Data API SDK 0.5: Migration Notes

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