Reading time: 12 minutes Difficulty: Intermediate Last updated: January 2026
TL;DR
- Jupiter processes 93.6% of Solana DEX aggregator volume and routes $49B+ monthly in trades
- Two API options: Ultra API (recommended, managed execution) and Swap API (full control)
- Quote endpoint finds optimal routes across 20+ DEXs in under 15ms
- Slippage error
0x1771is the most common failure - use dynamic slippage with 300 BPS cap - Priority fees directly impact landing rates - budget 0.01 SOL for high-priority swaps
What You'll Build
By the end of this tutorial, you'll have a working TypeScript module that:
- Fetches optimal swap quotes from Jupiter
- Executes token swaps with proper error handling
- Implements retry logic for failed transactions
- Handles priority fees for consistent landing rates
We'll swap SOL to USDC as our example, but the code works for any SPL token pair.
Prerequisites
Before starting, make sure you have:
- Node.js 18+ installed
- A Solana wallet with some SOL for testing (devnet works fine)
- Basic understanding of async/await and TypeScript
- Familiarity with Solana transactions (helpful but not required)
You don't need prior Jupiter experience. We'll cover everything from scratch.
Project Setup
Create a new directory and initialize the project:
mkdir jupiter-swap-demo && cd jupiter-swap-demo
npm init -yInstall the required dependencies:
npm install @solana/web3.js@1.98.0 @jup-ag/api@6.0.48Add TypeScript support:
npm install -D typescript @types/node ts-node
npx tsc --initNote: Thelite-api.jup.agendpoint is being deprecated on January 31, 2026. Useapi.jup.agfor production applications.
Create a tsconfig.json with these settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"resolveJsonModule": true
},
"include": ["src/**/*"]
}Understanding Jupiter's API Architecture
Jupiter offers two distinct APIs for swaps. Choosing the right one matters.
Ultra API (Recommended)
The Ultra API handles everything for you:
- Managed transaction execution - Jupiter's infrastructure sends and confirms
- Built-in retry logic - Automatic retries on transient failures
- MEV protection - Reduced sandwich attack exposure
- Simpler integration - Two endpoints:
/orderand/execute
Use Ultra API when you want reliable swaps without managing transaction infrastructure.
Swap API (Full Control)
The Swap API gives you raw transaction bytes:
- Custom instructions - Add your own instructions to the transaction
- CPI integration - Call Jupiter from your own programs
- Broadcasting control - Choose your own RPC and priority fee strategy
- DEX selection - Include or exclude specific AMMs
Use Swap API when building complex DeFi applications that need transaction-level control.
For this tutorial, we'll implement both approaches so you can choose what fits your use case.
Step 1: Getting a Quote
Every swap starts with a quote. The quote tells you:
- How many output tokens you'll receive
- The route through various DEXs
- Price impact and fees
- The minimum amount after slippage
Token Mint Addresses
Solana tokens are identified by their mint addresses. Here are the common ones:
// src/constants.ts
export const TOKENS = {
SOL: "So11111111111111111111111111111111111111112", // Wrapped SOL
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
USDT: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT
BONK: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // BONK
} as const;
// Token decimals - needed for amount calculations
export const DECIMALS = {
SOL: 9,
USDC: 6,
USDT: 6,
BONK: 5,
} as const;Fetching a Quote
The quote endpoint accepts amounts in the token's smallest unit (lamports for SOL, micro-units for USDC).
// src/quote.ts
import { TOKENS, DECIMALS } from "./constants";
interface QuoteResponse {
inputMint: string;
inAmount: string;
outputMint: string;
outAmount: string;
otherAmountThreshold: string;
swapMode: string;
slippageBps: number;
priceImpactPct: string;
routePlan: RoutePlan[];
contextSlot: number;
timeTaken: number;
}
interface RoutePlan {
ammKey: string;
label: string;
inputMint: string;
outputMint: string;
inAmount: string;
outAmount: string;
feeAmount: string;
feeMint: string;
percent: number;
}
export async function getQuote(
inputMint: string,
outputMint: string,
amountInSmallestUnit: number,
slippageBps: number = 50
): Promise<QuoteResponse> {
const params = new URLSearchParams({
inputMint,
outputMint,
amount: amountInSmallestUnit.toString(),
slippageBps: slippageBps.toString(),
restrictIntermediateTokens: "true", // Stick to liquid tokens for stability
});
const response = await fetch(
`https://api.jup.ag/swap/v1/quote?${params}`
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Quote failed: ${error.error || response.statusText}`);
}
return response.json();
}Converting Human-Readable Amounts
Nobody thinks in lamports. Here's a helper to convert SOL amounts:
// src/utils.ts
export function toSmallestUnit(amount: number, decimals: number): number {
return Math.floor(amount * Math.pow(10, decimals));
}
export function fromSmallestUnit(amount: string, decimals: number): number {
return parseInt(amount) / Math.pow(10, decimals);
}Putting It Together
Let's fetch a quote for swapping 0.1 SOL to USDC:
// src/demo-quote.ts
import { getQuote } from "./quote";
import { toSmallestUnit, fromSmallestUnit } from "./utils";
import { TOKENS, DECIMALS } from "./constants";
async function main() {
const solAmount = 0.1; // 0.1 SOL
const lamports = toSmallestUnit(solAmount, DECIMALS.SOL);
console.log(`Fetching quote for ${solAmount} SOL -> USDC...`);
const quote = await getQuote(
TOKENS.SOL,
TOKENS.USDC,
lamports,
50 // 0.5% slippage
);
const outputUsdc = fromSmallestUnit(quote.outAmount, DECIMALS.USDC);
const minOutput = fromSmallestUnit(quote.otherAmountThreshold, DECIMALS.USDC);
console.log(`\nQuote received in ${quote.timeTaken.toFixed(3)}s`);
console.log(`Expected output: ${outputUsdc.toFixed(2)} USDC`);
console.log(`Minimum output: ${minOutput.toFixed(2)} USDC`);
console.log(`Price impact: ${quote.priceImpactPct}%`);
console.log(`\nRoute:`);
quote.routePlan.forEach((hop, i) => {
console.log(` ${i + 1}. ${hop.label} (${hop.percent}%)`);
});
}
main().catch(console.error);Run it:
npx ts-node src/demo-quote.tsYou'll see output like:
Fetching quote for 0.1 SOL -> USDC...
Quote received in 0.012s
Expected output: 16.19 USDC
Minimum output: 16.11 USDC
Price impact: 0%
Route:
1. Meteora DLMM (100%)Pro tip: The contextSlot in the response tells you which Solana slot the quote was generated for. Quotes become stale quickly - execute within 30 seconds for best results.Step 2: Executing the Swap
Now let's turn that quote into actual tokens. We'll cover both API approaches.
Method A: Ultra API (Recommended)
The Ultra API is the simplest path to production-ready swaps.
// src/swap-ultra.ts
import { Keypair, VersionedTransaction } from "@solana/web3.js";
import { TOKENS, DECIMALS } from "./constants";
import { toSmallestUnit } from "./utils";
interface OrderResponse {
transaction: string; // Base64-encoded transaction
requestId: string; // Needed for execution
inAmount: string;
outAmount: string;
otherAmountThreshold: string;
swapType: string;
priceImpactPct: string;
}
interface ExecuteResponse {
status: "Success" | "Failed";
signature: string;
slot?: string;
error?: string;
code?: number;
inputAmountResult?: string;
outputAmountResult?: string;
}
export async function swapWithUltraApi(
wallet: Keypair,
inputMint: string,
outputMint: string,
amountInSmallestUnit: number
): Promise<ExecuteResponse> {
// Step 1: Get order (includes the transaction)
const orderParams = new URLSearchParams({
inputMint,
outputMint,
amount: amountInSmallestUnit.toString(),
taker: wallet.publicKey.toString(),
});
const orderResponse: OrderResponse = await (
await fetch(`https://api.jup.ag/ultra/v1/order?${orderParams}`)
).json();
if (!orderResponse.transaction) {
throw new Error("Failed to get order transaction");
}
console.log(`Order received. Expected: ${orderResponse.outAmount}`);
// Step 2: Deserialize and sign the transaction
const transactionBuffer = Buffer.from(orderResponse.transaction, "base64");
const transaction = VersionedTransaction.deserialize(transactionBuffer);
transaction.sign([wallet]);
// Step 3: Serialize the signed transaction
const signedTransaction = Buffer.from(transaction.serialize()).toString("base64");
// Step 4: Execute via Jupiter's infrastructure
const executeResponse: ExecuteResponse = await (
await fetch("https://api.jup.ag/ultra/v1/execute", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
signedTransaction,
requestId: orderResponse.requestId,
}),
})
).json();
return executeResponse;
}Method B: Swap API (Full Control)
Use this when you need to customize the transaction or use your own RPC.
// src/swap-api.ts
import {
Connection,
Keypair,
VersionedTransaction,
SendTransactionError,
} from "@solana/web3.js";
interface SwapResponse {
swapTransaction: string;
lastValidBlockHeight: number;
prioritizationFeeLamports?: number;
}
export async function swapWithSwapApi(
connection: Connection,
wallet: Keypair,
quoteResponse: any // The quote from Step 1
): Promise<string> {
// Step 1: Get the swap transaction
const swapResponse: SwapResponse = await (
await fetch("https://api.jup.ag/swap/v1/swap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
quoteResponse,
userPublicKey: wallet.publicKey.toString(),
dynamicComputeUnitLimit: true, // Optimize compute units
dynamicSlippage: {
maxBps: 300, // Cap at 3% to prevent MEV
},
prioritizationFeeLamports: {
priorityLevelWithMaxLamports: {
maxLamports: 10_000_000, // 0.01 SOL max priority fee
priorityLevel: "veryHigh",
},
},
}),
})
).json();
if (!swapResponse.swapTransaction) {
throw new Error("Failed to get swap transaction");
}
// Step 2: Deserialize, sign, and serialize
const transactionBuffer = Buffer.from(swapResponse.swapTransaction, "base64");
const transaction = VersionedTransaction.deserialize(transactionBuffer);
transaction.sign([wallet]);
const signedTransaction = transaction.serialize();
// Step 3: Send with retries
const signature = await connection.sendRawTransaction(signedTransaction, {
maxRetries: 5,
skipPreflight: false,
});
console.log(`Transaction sent: ${signature}`);
// Step 4: Confirm the transaction
const confirmation = await connection.confirmTransaction(
{
signature,
blockhash: transaction.message.recentBlockhash,
lastValidBlockHeight: swapResponse.lastValidBlockHeight,
},
"finalized"
);
if (confirmation.value.err) {
throw new Error(
`Transaction failed: ${JSON.stringify(confirmation.value.err)}`
);
}
return signature;
}Warning: Never use skipPreflight: true unless you understand the risks. Preflight simulation catches many errors before they cost you transaction fees.Step 3: Error Handling and Retries
Solana transactions fail. A lot. Network congestion, slippage exceeded, insufficient compute - the list goes on. Robust error handling separates production code from demos.
Common Error Codes
Error Code | Meaning | Solution
0x1771(6001) → Slippage exceeded → Increase slippage or retry with fresh quote
`0x1772` (6002) | Insufficient funds | Check wallet balance
`0x1778` (6008) | Invalid route | Get a new quote
`6023` | Custom program error | Usually stale quote, retry
Implementing Retry Logic
Here's a production-grade retry wrapper:
// src/retry.ts
interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
}
const DEFAULT_CONFIG: RetryConfig = {
maxAttempts: 3,
baseDelayMs: 1000,
maxDelayMs: 5000,
};
export async function withRetry<T>(
operation: () => Promise<T>,
shouldRetry: (error: Error) => boolean,
config: RetryConfig = DEFAULT_CONFIG
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
const isRetryable = shouldRetry(lastError);
const isLastAttempt = attempt === config.maxAttempts;
if (!isRetryable || isLastAttempt) {
throw lastError;
}
// Exponential backoff with jitter
const delay = Math.min(
config.baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 500,
config.maxDelayMs
);
console.log(
`Attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError!;
}
export function isRetryableSwapError(error: Error): boolean {
const message = error.message.toLowerCase();
// Retryable conditions
const retryablePatterns = [
"0x1771", // Slippage exceeded
"blockhash", // Expired blockhash
"timeout", // Network timeout
"429", // Rate limited
"503", // Service unavailable
"network", // Network error
];
// Non-retryable conditions (don't waste attempts)
const nonRetryablePatterns = [
"insufficient", // Insufficient funds
"0x1772", // Insufficient balance (Jupiter specific)
"invalid signature", // Bad signature
];
if (nonRetryablePatterns.some((p) => message.includes(p))) {
return false;
}
return retryablePatterns.some((p) => message.includes(p));
}Complete Swap with Retries
Now let's combine everything into a robust swap function:
// src/robust-swap.ts
import { Connection, Keypair } from "@solana/web3.js";
import { getQuote } from "./quote";
import { swapWithSwapApi } from "./swap-api";
import { withRetry, isRetryableSwapError } from "./retry";
import { TOKENS, DECIMALS } from "./constants";
import { toSmallestUnit, fromSmallestUnit } from "./utils";
interface SwapParams {
inputToken: keyof typeof TOKENS;
outputToken: keyof typeof TOKENS;
amount: number; // Human-readable amount
slippageBps?: number;
}
export async function executeSwap(
connection: Connection,
wallet: Keypair,
params: SwapParams
): Promise<string> {
const { inputToken, outputToken, amount, slippageBps = 100 } = params;
const inputMint = TOKENS[inputToken];
const outputMint = TOKENS[outputToken];
const inputDecimals = DECIMALS[inputToken];
const outputDecimals = DECIMALS[outputToken];
const amountInSmallestUnit = toSmallestUnit(amount, inputDecimals);
return withRetry(
async () => {
// Fresh quote for each attempt
console.log(`\nFetching fresh quote...`);
const quote = await getQuote(
inputMint,
outputMint,
amountInSmallestUnit,
slippageBps
);
const expectedOutput = fromSmallestUnit(quote.outAmount, outputDecimals);
console.log(`Expected output: ${expectedOutput.toFixed(4)} ${outputToken}`);
// Execute the swap
console.log(`Executing swap...`);
const signature = await swapWithSwapApi(connection, wallet, quote);
console.log(`\nSwap successful!`);
console.log(`View transaction: https://solscan.io/tx/${signature}`);
return signature;
},
isRetryableSwapError,
{ maxAttempts: 3, baseDelayMs: 2000, maxDelayMs: 10000 }
);
}Common Pitfalls
Let's save you hours of debugging. These are the mistakes every Jupiter developer makes at least once.
1. Stale Quotes
Problem: You fetch a quote, user reviews it for 2 minutes, then you try to execute.
What happens: Transaction fails with mysterious errors. Prices have moved. The route may no longer be optimal.
Solution: Always fetch a fresh quote immediately before execution:
// Bad: Using old quote
const quote = await getQuote(...);
await showConfirmationDialog(); // User takes 2 minutes
await executeSwap(quote); // Likely fails
// Good: Fresh quote at execution time
await showConfirmationDialog();
const freshQuote = await getQuote(...);
await executeSwap(freshQuote);2. Wrong Amount Units
Problem: You pass 1 expecting 1 SOL, but the API interprets it as 1 lamport (0.000000001 SOL).
What happens: Swap succeeds but you get essentially nothing.
Solution: Always convert to smallest units:
// Bad: 1 SOL becomes 1 lamport
const amount = 1;
// Good: 1 SOL = 1,000,000,000 lamports
const amount = toSmallestUnit(1, 9);3. Ignoring Price Impact
Problem: Swapping large amounts through illiquid pools.
What happens: You lose significant value to price impact. A 1% impact on $100K is $1,000 gone.
Solution: Check priceImpactPct before executing:
const quote = await getQuote(...);
const impact = parseFloat(quote.priceImpactPct);
if (impact > 1) {
throw new Error(`Price impact too high: ${impact}%. Consider smaller swaps.`);
}4. Hardcoded Slippage
Problem: Using 0.5% slippage for all swaps.
What happens: Low slippage fails during volatility. High slippage loses money during stability.
Solution: Use dynamic slippage or adjust based on token liquidity:
function getRecommendedSlippage(inputToken: string, amount: number): number {
// Stable pairs need less slippage
const stableTokens = [TOKENS.USDC, TOKENS.USDT];
if (stableTokens.includes(inputToken)) {
return 10; // 0.1% for stables
}
// Large swaps need more slippage
if (amount > 10) {
return 200; // 2% for large swaps
}
return 100; // 1% default
}5. No Timeout Handling
Problem: Transaction confirmation hangs indefinitely.
What happens: Your application freezes. Users refresh and resubmit. Chaos.
Solution: Implement confirmation timeouts:
async function confirmWithTimeout(
connection: Connection,
signature: string,
timeoutMs: number = 60000
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const status = await connection.getSignatureStatus(signature);
if (status.value?.confirmationStatus === "finalized") {
return;
}
if (status.value?.err) {
throw new Error(`Transaction failed: ${JSON.stringify(status.value.err)}`);
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`Transaction confirmation timeout after ${timeoutMs}ms`);
}Complete Working Example
Here's everything together in one runnable file:
// src/index.ts
import {
Connection,
Keypair,
VersionedTransaction,
clusterApiUrl,
} from "@solana/web3.js";
import * as fs from "fs";
// === CONSTANTS ===
const TOKENS = {
SOL: "So11111111111111111111111111111111111111112",
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
} as const;
const DECIMALS = { SOL: 9, USDC: 6 } as const;
// === UTILITIES ===
function toSmallestUnit(amount: number, decimals: number): number {
return Math.floor(amount * Math.pow(10, decimals));
}
function fromSmallestUnit(amount: string, decimals: number): number {
return parseInt(amount) / Math.pow(10, decimals);
}
// === QUOTE ===
interface QuoteResponse {
inputMint: string;
inAmount: string;
outputMint: string;
outAmount: string;
otherAmountThreshold: string;
slippageBps: number;
priceImpactPct: string;
routePlan: { label: string; percent: number }[];
}
async function getQuote(
inputMint: string,
outputMint: string,
amount: number,
slippageBps: number
): Promise<QuoteResponse> {
const params = new URLSearchParams({
inputMint,
outputMint,
amount: amount.toString(),
slippageBps: slippageBps.toString(),
restrictIntermediateTokens: "true",
});
const res = await fetch(`https://api.jup.ag/swap/v1/quote?${params}`);
if (!res.ok) throw new Error(`Quote failed: ${res.statusText}`);
return res.json();
}
// === SWAP ===
async function executeSwap(
connection: Connection,
wallet: Keypair,
quote: QuoteResponse
): Promise<string> {
// Get swap transaction
const swapRes = await fetch("https://api.jup.ag/swap/v1/swap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
quoteResponse: quote,
userPublicKey: wallet.publicKey.toString(),
dynamicComputeUnitLimit: true,
dynamicSlippage: { maxBps: 300 },
prioritizationFeeLamports: {
priorityLevelWithMaxLamports: {
maxLamports: 5_000_000,
priorityLevel: "high",
},
},
}),
});
const { swapTransaction, lastValidBlockHeight } = await swapRes.json();
if (!swapTransaction) throw new Error("No swap transaction returned");
// Sign and send
const tx = VersionedTransaction.deserialize(
Buffer.from(swapTransaction, "base64")
);
tx.sign([wallet]);
const signature = await connection.sendRawTransaction(tx.serialize(), {
maxRetries: 3,
});
// Confirm
const confirmation = await connection.confirmTransaction(
{ signature, blockhash: tx.message.recentBlockhash, lastValidBlockHeight },
"finalized"
);
if (confirmation.value.err) {
throw new Error(`Failed: ${JSON.stringify(confirmation.value.err)}`);
}
return signature;
}
// === MAIN ===
async function main() {
// Load wallet (replace with your path)
const walletPath = process.env.WALLET_PATH || "~/.config/solana/id.json";
const secretKey = JSON.parse(
fs.readFileSync(walletPath.replace("~", process.env.HOME!), "utf-8")
);
const wallet = Keypair.fromSecretKey(new Uint8Array(secretKey));
// Connect to mainnet (use devnet for testing)
const connection = new Connection(
process.env.RPC_URL || clusterApiUrl("mainnet-beta"),
"confirmed"
);
console.log(`Wallet: ${wallet.publicKey.toString()}`);
// Check balance
const balance = await connection.getBalance(wallet.publicKey);
console.log(`Balance: ${balance / 1e9} SOL\n`);
// Swap 0.01 SOL to USDC
const solAmount = 0.01;
const lamports = toSmallestUnit(solAmount, DECIMALS.SOL);
console.log(`Getting quote for ${solAmount} SOL -> USDC...`);
const quote = await getQuote(TOKENS.SOL, TOKENS.USDC, lamports, 100);
const expectedUsdc = fromSmallestUnit(quote.outAmount, DECIMALS.USDC);
console.log(`Expected output: ${expectedUsdc.toFixed(4)} USDC`);
console.log(`Price impact: ${quote.priceImpactPct}%`);
console.log(`Route: ${quote.routePlan.map((r) => r.label).join(" -> ")}\n`);
console.log("Executing swap...");
const signature = await executeSwap(connection, wallet, quote);
console.log(`\nSwap complete!`);
console.log(`Transaction: https://solscan.io/tx/${signature}`);
}
main().catch(console.error);Run with:
WALLET_PATH=~/.config/solana/id.json npx ts-node src/index.tsNext Steps
You now have working Jupiter swap integration. Here's where to go from here:
- Add a UI - Build a React frontend with wallet adapters
- Implement limit orders - Jupiter supports trigger orders for better prices
- Add DCA - Dollar-cost averaging with Jupiter's recurring API
- Monitor positions - Track swaps with Jupiter's portfolio API
- Go multi-chain - Jupiter now supports cross-chain swaps
Useful Resources
- Jupiter Developer Docs - Official documentation
- Jupiter Discord - Community support
- Solana Cookbook - General Solana development
- QuickNode Jupiter Guide - Additional transaction tips
