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
| Need | Use | Why |
|---|---|---|
| Show a price on screen | Data API price endpoints (guide) | One cached request, no wallet, no route |
| Decide whether a trade is worth it | GET /quote | Includes size, route, slippage and price impact |
| Execute a swap | POST /swap, sign, POST /send-transaction | Transaction is built server side, signed by you, landed over Jet TPU |
| Add your own instructions, or chain a buy and a sell in one transaction | POST /swap-instructions | Returns raw instructions plus the address lookup tables to load |
| Keep a quote fresh while a button is on screen | WS /stream or WS /stream/swap | Push 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
| Method | Path | Purpose |
|---|---|---|
| GET | /quote | Route, expected output, worst-case output, price impact |
| POST | /swap | Unsigned transaction for a wallet, built from a quote |
| POST | /swap-instructions | The same swap as instructions and lookup tables |
| POST | /quote-and-swap | Quote and build in one request |
| POST | /send-transaction | Submit a signed transaction over Jet TPU with background retries |
| GET | /transaction/:signature | pending, confirmed, failed or expired, with slot and latency |
| WS | /stream | Live quote updates for a subscription |
| WS | /stream/swap | Live 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,amountOutandminAmountOutare base units as strings. USDC has 6 decimals and SOL has 9, so11357124is 11.357124 USDC. Parse withBigInt; aNumbersilently loses precision on large token supplies.minAmountOutis the on-chain floor. If the pool moves so that the fill would be lower, the transaction fails instead of filling badly.slippageBpsis the resolved value. WithslippageBps=dynamicthe 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.routePlanlists one step per hop and pool. A split route has several steps at the same hop whosepercentvalues add up to 100. Each step names theprogramId,dexandpool.priceImpactis the router's estimate for your size. The reference example shows0.5and the docs guide multiplies the value by 100 before display. Live quotes returned0.0058for 0.01 SOL and0.1044for 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.feeAmountis the fee taken by the pools on the route, not your platform fee. Your fee is reported separately underplatformFee.contextSlotis the slot the quote was computed against, andtimeTakenis the routing time in seconds.- Live responses carry fields the reference does not list yet, such as
quoteIdandsearchMode. Type the object with an index signature and send the whole quote back to/swapunchanged.
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.
| Field | Default | Notes |
|---|---|---|
txVersion | V0 | V0 or LEGACY, case sensitive. v0 returns a plain-text 422 |
wrapUnwrapSol | true | Wraps SOL in setup instructions and unwraps leftover wSOL in cleanup. Set false only if you manage a long-lived wSOL account |
priorityFee | none | A 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 |
maxPriorityFee | none | Cap applied when a level resolves to a fee |
computeUnitPriceMicroLamports | none | Exact compute unit price when you want to set the fee yourself |
computeUnitLimit | none | Override the compute budget, useful when combining two swaps |
tipAccount, tipLamports | none | Optional SOL tip to a validator or relay account |
feeAccount, feeBps, feeFromInput, chargeBps | none | Platform 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