Skip to main content

Building a Production-Ready Jupiter Swap Integration on Solana with Anchor

· 30 min read
Vadim Nicolai
Senior Software Engineer at Vitrifi

Jupiter swap integration architecture on Solana

TL;DR: This is a production swap execution engine that wraps Jupiter with guardrails, operational controls, and analytics-ready telemetry—not just a technical integration example.

  • Jupiter routes across 20+ venues for best execution
  • This guide adds an on-chain policy layer: fee collection, slippage caps, admin pause, and full auditability
  • Every swap is attributable, debuggable, and measurable via structured event telemetry
  • Designed for product teams who need: reliable execution, operational visibility, and supportability

Why integrate Jupiter programmatically?

The aggregator value proposition

Jupiter aggregates liquidity from:

  • CPAMM pools (Raydium, Orca)
  • CLMM venues (Orca Whirlpools, Raydium CLMM)
  • DLMM (Meteora)
  • PMM (Lifinity)
  • Order books (Phoenix, OpenBook)

For traders, this means:

  • Best execution: automatic routing finds optimal prices across venues
  • Reduced slippage: splits large orders across multiple pools
  • MEV protection: private routing options and advanced order types

Why wrap Jupiter in your own program?

Direct Jupiter API usage is simple, but wrapping it in an Anchor program enables:

FeatureDirect APIAnchor program wrapper
Fee collectionmanual logicon-chain enforcement
Platform brandingclient-side onlyprogram-owned config
Access controlnoneadmin-gated pause/update
ComposabilitylimitedCPI-friendly for other protocols
Audit trailoff-chainon-chain events
Slippage protectionclient-sideprogram-enforced

Policy layer: what is enforced where

Understanding enforcement boundaries is critical for security and UX:

ConcernEnforced on-chainEnforced in clientNotes
Slippage ceilingYesYesOn-chain cap prevents hostile clients from bypassing limits
Fee collectionYesNoMust be deterministic; client cannot skip or reduce fees
Quote freshnessNoYesClient refreshes quotes; include quote timestamp in intent
Route allowlist/denylistOptionalOptionalUseful for risk control (e.g., block suspicious pools)
Pause / emergency stopYesNoAdmin can halt swaps immediately for incidents
Compute budgetNoYesClient requests higher compute units for complex routes
Intent deduplicationNoOff-chainBackend checks intent_id before indexing

Key principle: On-chain enforces non-bypassable invariants (fees, caps, pauses). Client enforces UX optimizations (quote refresh, compute). Off-chain systems handle analytics and deduplication.


Where this fits in a product

This swap integration is a component in an operating system, not a standalone feature. Understanding the full lifecycle is critical for product reliability:

Workflow breakdown:

  1. UI → Quote: User selects tokens and amount; frontend requests quote from Jupiter API
  2. Quote → Intent: UI creates a swap intent (client-side idempotency key) with user parameters
  3. Intent → Execute: Program validates intent, enforces policy, executes swap via CPI to Jupiter
  4. Execute → Telemetry: On-chain event emitted with full context (intent ID, amounts, fees, route, timestamps)
  5. Telemetry → Indexer: Off-chain indexer writes structured event to database (Postgres, ClickHouse, etc.)
  6. Indexer → Backoffice: Dashboards show conversion funnels, failure rates, revenue; support uses intent IDs for debugging

Why this matters:

  • Idempotency: Intent IDs prevent double-swaps on retries
  • Attribution: Every swap traces back to user session, client version, marketing campaign
  • Debuggability: Support can search by intent ID, see full context in one query
  • Operability: Dashboards answer "Why did conversion drop 10%?" without guessing

Intent model: idempotency + attribution

In production systems, you need reliable execution and clean analytics. The intent model provides both.

What is a swap intent?

A SwapIntent captures user action before execution:

interface SwapIntent {
intent_id: string; // UUIDv4 or client-generated unique ID
wallet: PublicKey; // User wallet address
input_mint: PublicKey; // Source token
output_mint: PublicKey; // Destination token
amount_in: u64; // Input amount (lamports)
max_slippage_bps: u16; // Max acceptable slippage
created_at: i64; // Client timestamp
client_version: string; // App version (e.g., "web-v2.1.3")
metadata?: Record<string, string>; // Campaign ID, referrer, etc.
}

Client-side intent creation

import { v4 as uuidv4 } from 'uuid';

function createSwapIntent(params: {
wallet: PublicKey;
inputMint: PublicKey;
outputMint: PublicKey;
amountIn: number;
slippageBps: number;
}): SwapIntent {
return {
intent_id: uuidv4(),
wallet: params.wallet,
input_mint: params.inputMint,
output_mint: params.outputMint,
amount_in: params.amountIn,
max_slippage_bps: params.slippageBps,
created_at: Date.now(),
client_version: process.env.NEXT_PUBLIC_APP_VERSION || 'unknown',
metadata: {
campaign_id: getCampaignId(),
referrer: document.referrer,
},
};
}

Benefits of the intent model

1. Idempotency (no double-swaps):

// User clicks "Swap" → creates intent
const intent = createSwapIntent(params);

// Network error on first attempt
try {
await executeSwap(intent); // Fails
} catch (error) {
// User retries with SAME intent_id
await executeSwap(intent); // Succeeds
}

// Backend deduplicates by intent_id
// Only ONE swap executed, even with multiple transactions

2. Consistent analytics:

  • Every event has intent_id → join quote request, execution, and outcome
  • Measure quote-to-swap conversion accurately
  • Track funnel drop-off by stage

3. Clean support threads:

User: "My swap failed!"
Support: "Please send your transaction signature or intent ID"
User: "abc123-def456-..."
Support: [searches DB] → sees stale quote, invalid route, or slippage exceeded
Support: "Your quote expired. Please refresh and try again."

Implementation note: You don't need to store intents on-chain (expensive). Store them:

  • Client-side: Browser localStorage for retry logic
  • Off-chain DB: When user initiates swap, log intent to database
  • In events: Include intent_id in on-chain event for correlation

Architecture overview

Program structure (Anchor 0.32.1)

programs/token-swap/src/
├── state.rs # Account structures
│ └── JupiterConfig # 78 bytes: admin, fees, slippage, pause
├── constants.rs # Program IDs and seeds
│ ├── JUPITER_V6_PROGRAM_ID # JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4
│ └── JUPITER_CONFIG_SEED # "jupiter_config"
├── errors.rs # 8 custom error types
│ ├── InvalidAmount
│ ├── JupiterPaused
│ └── MinimumOutputNotMet
└── instructions/
├── init_jupiter_config.rs # Initialize config PDA
├── update_jupiter_config.rs # Admin updates
├── jupiter_swap.rs # Main swap execution
└── jupiter_route_swap.rs # Legacy route support

Account layout: JupiterConfig PDA

#[account]
pub struct JupiterConfig {
pub admin: Pubkey, // 32 bytes
pub fee_account: Pubkey, // 32 bytes
pub platform_fee_bps: u16, // 2 bytes (0-10000)
pub max_slippage_bps: u16, // 2 bytes (0-10000)
pub paused: bool, // 1 byte
pub bump: u8, // 1 byte
} // Total: 70 bytes (+ 8 discriminator = 78)

Design decisions:

  • u16 for BPS values (supports full 0-10000 range, 100% = 10000 BPS)
  • Platform fee ≤ 1000 BPS (10%) enforced at init/update
  • Max slippage ≤ 10000 BPS configurable per use case
  • Admin-controlled pause for emergency stops

Implementation deep-dive

1. Initialize configuration

#[derive(Accounts)]
pub struct InitJupiterConfig<'info> {
#[account(
init,
payer = admin,
space = 8 + 70,
seeds = [b"jupiter_config"],
bump
)]
pub config: Account<'info, JupiterConfig>,

#[account(mut)]
pub admin: Signer<'info>,

pub system_program: Program<'info, System>,
}

pub fn init_jupiter_config(
ctx: Context<InitJupiterConfig>,
fee_account: Pubkey,
platform_fee_bps: u16,
max_slippage_bps: u16,
) -> Result<()> {
require!(
platform_fee_bps <= 1000,
JupiterSwapError::InvalidPlatformFee
);
require!(
max_slippage_bps <= 10000,
JupiterSwapError::InvalidMaxSlippage
);

let config = &mut ctx.accounts.config;
config.admin = ctx.accounts.admin.key();
config.fee_account = fee_account;
config.platform_fee_bps = platform_fee_bps;
config.max_slippage_bps = max_slippage_bps;
config.paused = false;
config.bump = ctx.bumps.config;

Ok(())
}

Key validations:

  • Platform fee capped at 10% to prevent abuse
  • Max slippage configurable (typically 50-500 BPS for production)
  • PDA derivation ensures single config per program deployment

2. Execute Jupiter swap (CPI pattern)

#[derive(Accounts)]
pub struct JupiterSwap<'info> {
#[account(
seeds = [b"jupiter_config"],
bump = config.bump
)]
pub config: Account<'info, JupiterConfig>,

#[account(mut)]
pub user: Signer<'info>,

/// CHECK: Jupiter v6 program ID verified in instruction
pub jupiter_program: UncheckedAccount<'info>,

// Token accounts + remaining accounts for Jupiter routing
}

pub fn jupiter_swap<'info>(
ctx: Context<'_, '_, 'info, 'info, JupiterSwap<'info>>,
amount_in: u64,
minimum_amount_out: u64,
) -> Result<()> {
let config = &ctx.accounts.config;

// 1. Validate state
require!(!config.paused, JupiterSwapError::JupiterPaused);
require!(amount_in > 0, JupiterSwapError::InvalidAmount);

// 2. Verify Jupiter program ID
require_keys_eq!(
ctx.accounts.jupiter_program.key(),
JUPITER_V6_PROGRAM_ID.parse::<Pubkey>().unwrap(),
JupiterSwapError::InvalidJupiterProgram
);

// 3. Build CPI accounts (dynamically from remaining_accounts)
let mut accounts = Vec::new();
for account in ctx.remaining_accounts.iter() {
accounts.push(AccountMeta {
pubkey: *account.key,
is_signer: account.is_signer,
is_writable: account.is_writable,
});
}

// 4. Execute CPI to Jupiter
let swap_ix = Instruction {
program_id: ctx.accounts.jupiter_program.key(),
accounts,
data: /* Jupiter swap instruction data */,
};

invoke_signed(&swap_ix, ctx.remaining_accounts, &[])?;

// 5. Verify output amount (Token-2022 safe)
let output_amount = /* observe vault delta */;
require!(
output_amount >= minimum_amount_out,
JupiterSwapError::MinimumOutputNotMet
);

// 6. Collect platform fee
if config.platform_fee_bps > 0 {
let fee = (output_amount as u128)
.checked_mul(config.platform_fee_bps as u128)
.unwrap()
.checked_div(10000)
.unwrap() as u64;

// Transfer fee to platform account
}

// 7. Emit event for analytics
emit!(JupiterSwapEvent {
user: ctx.accounts.user.key(),
amount_in,
amount_out: output_amount,
platform_fee: fee,
});

Ok(())
}

Critical implementation details:

Token-2022 compatibility

// WRONG: Trusting instruction amount
let output_amount = minimum_amount_out;

// CORRECT: Observe vault delta
let vault_before = user_output_token.amount;
// ... execute swap ...
user_output_token.reload()?;
let output_amount = user_output_token.amount
.saturating_sub(vault_before);

Transfer fees and hooks mean you cannot trust amounts in instruction data.

Remaining accounts pattern

Jupiter requires dynamic account lists (routes vary by liquidity):

// Frontend passes all necessary accounts
const remainingAccounts = [
{ pubkey: userSourceToken, isSigner: false, isWritable: true },
{ pubkey: userDestToken, isSigner: false, isWritable: true },
// ... all intermediary pool accounts from Jupiter API
];

Program must accept remaining_accounts via:

pub fn jupiter_swap<'info>(
ctx: Context<'_, '_, 'info, 'info, JupiterSwap<'info>>,
// ^^^ lifetime annotation required for remaining_accounts

3. Update configuration (admin-only)

pub fn update_jupiter_config(
ctx: Context<UpdateJupiterConfig>,
new_admin: Option<Pubkey>,
new_fee_account: Option<Pubkey>,
new_platform_fee_bps: Option<u16>,
new_max_slippage_bps: Option<u16>,
new_paused: Option<bool>,
) -> Result<()> {
let config = &mut ctx.accounts.config;

// Validate admin
require_keys_eq!(
ctx.accounts.admin.key(),
config.admin,
JupiterSwapError::Unauthorized
);

// Optional updates (partial update pattern)
if let Some(admin) = new_admin {
config.admin = admin;
}
if let Some(fee_bps) = new_platform_fee_bps {
require!(fee_bps <= 1000, JupiterSwapError::InvalidPlatformFee);
config.platform_fee_bps = fee_bps;
}
// ... other optional fields

Ok(())
}

Partial update pattern: All fields optional → supports single-field updates without re-specifying everything.


Frontend integration

React hook: useJupiter

import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { PublicKey, VersionedTransaction } from '@solana/web3.js';
import { useProgram } from './useSwapProgram';

export function useJupiter() {
const { connection } = useConnection();
const wallet = useWallet();
const program = useProgram();

async function getQuote(params: {
inputMint: string;
outputMint: string;
amount: number;
slippageBps?: number;
}) {
const response = await fetch(
`https://quote-api.jup.ag/v6/quote?` +
new URLSearchParams({
inputMint: params.inputMint,
outputMint: params.outputMint,
amount: params.amount.toString(),
slippageBps: (params.slippageBps || 50).toString(),
})
);
return response.json();
}

async function getSwapInstructions(quote: any) {
const response = await fetch('https://quote-api.jup.ag/v6/swap-instructions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
quoteResponse: quote,
userPublicKey: wallet.publicKey!.toBase58(),
wrapAndUnwrapSol: true,
// Use versioned transactions for ALT support
useVersionedTransaction: true,
}),
});
return response.json();
}

async function executeSwapWithProgram(quote: any) {
if (!wallet.publicKey || !program) return;

const { swapInstruction } = await getSwapInstructions(quote);

// Get config PDA
const [configPda] = PublicKey.findProgramAddressSync(
[Buffer.from('jupiter_config')],
program.programId
);

// Build transaction via Anchor
const tx = await program.methods
.jupiterSwap(
new BN(quote.inAmount),
new BN(quote.outAmount)
)
.accounts({
config: configPda,
user: wallet.publicKey,
jupiterProgram: new PublicKey('JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4'),
})
.remainingAccounts(swapInstruction.accounts) // Dynamic routing accounts
.transaction();

// Handle Address Lookup Tables if present
if (swapInstruction.addressLookupTableAccounts?.length > 0) {
const lookupTables = await Promise.all(
swapInstruction.addressLookupTableAccounts.map((key: string) =>
connection.getAddressLookupTable(new PublicKey(key))
)
);

// Build versioned transaction
const message = new TransactionMessage({
payerKey: wallet.publicKey,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions: tx.instructions,
}).compileToV0Message(lookupTables.map(lt => lt.value!));

const versionedTx = new VersionedTransaction(message);
await wallet.sendTransaction(versionedTx, connection);
} else {
await wallet.sendTransaction(tx, connection);
}
}

return { getQuote, executeSwapWithProgram };
}

Key frontend considerations:

Address Lookup Tables (ALTs)

Complex Jupiter routes exceed the 1232-byte transaction limit. ALTs compress account lists:

// Without ALT: 32 bytes per account × 40 accounts = 1280 bytes (fails)
// With ALT: table reference + indices = ~50 bytes (works)

Use versioned transactions (v0) to support ALTs.

Quote freshness

Jupiter quotes expire quickly (10-30 seconds):

const quote = await getQuote(params);
// Wait too long...
await sleep(60000); // Quote now stale
await executeSwap(quote); // Likely fails with slippage error

Best practice: poll quotes every 5-10 seconds during user review.


UI component: JupiterSwap

'use client';

import { useState, useEffect } from 'react';
import { useJupiter } from '@/hooks/useJupiter';

const TOKENS = {
SOL: 'So11111111111111111111111111111111111111112',
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
JUP: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN',
};

export function JupiterSwap() {
const { getQuote, executeSwapWithProgram } = useJupiter();
const [inputMint, setInputMint] = useState(TOKENS.SOL);
const [outputMint, setOutputMint] = useState(TOKENS.USDC);
const [amount, setAmount] = useState('1.0');
const [quote, setQuote] = useState<any>(null);
const [loading, setLoading] = useState(false);

// Auto-refresh quote every 10 seconds
useEffect(() => {
const interval = setInterval(async () => {
if (amount && parseFloat(amount) > 0) {
const q = await getQuote({
inputMint,
outputMint,
amount: parseFloat(amount) * 1e9, // Convert to lamports
slippageBps: 50,
});
setQuote(q);
}
}, 10000);
return () => clearInterval(interval);
}, [inputMint, outputMint, amount]);

const handleSwap = async () => {
setLoading(true);
try {
await executeSwapWithProgram(quote);
// Success notification
} catch (error) {
console.error('Swap failed:', error);
} finally {
setLoading(false);
}
};

return (
<div className="swap-card">
<div className="input-section">
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
<select value={inputMint} onChange={(e) => setInputMint(e.target.value)}>
<option value={TOKENS.SOL}>SOL</option>
<option value={TOKENS.USDC}>USDC</option>
<option value={TOKENS.USDT}>USDT</option>
<option value={TOKENS.JUP}>JUP</option>
</select>
</div>

<div className="output-section">
<div className="estimated-output">
{quote ? (
<>
<span className="amount">{(quote.outAmount / 1e6).toFixed(6)}</span>
<span className="price-impact">
Price impact: {(quote.priceImpactPct * 100).toFixed(2)}%
</span>
</>
) : (
<span className="loading">Fetching quote...</span>
)}
</div>
<select value={outputMint} onChange={(e) => setOutputMint(e.target.value)}>
<option value={TOKENS.USDC}>USDC</option>
<option value={TOKENS.USDT}>USDT</option>
<option value={TOKENS.SOL}>SOL</option>
<option value={TOKENS.JUP}>JUP</option>
</select>
</div>

<button
onClick={handleSwap}
disabled={loading || !quote}
className="swap-button"
>
{loading ? 'Swapping...' : 'Swap'}
</button>

{quote && (
<div className="route-info">
<div>Route: {quote.routePlan?.map((r: any) => r.swapInfo.label).join(' → ')}</div>
<div>Min output: {((quote.outAmount * 0.995) / 1e6).toFixed(6)} (0.5% slippage)</div>
</div>
)}
</div>
);
}

UX enhancements:

  • Real-time quote updates (auto-refresh)
  • Price impact warnings (greater than 5% highlighted)
  • Route visualization (which venues are used)
  • Minimum output calculation (slippage tolerance display)

Testing strategy

Unit tests (29 tests across 7 modules)

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_config_validation() {
// Valid config
let config = JupiterConfig {
admin: Pubkey::new_unique(),
fee_account: Pubkey::new_unique(),
platform_fee_bps: 50, // 0.5%
max_slippage_bps: 100, // 1%
paused: false,
bump: 255,
};
assert!(config.is_valid());

// Invalid platform fee
let bad_config = JupiterConfig {
platform_fee_bps: 10001, // > 100%
..config
};
assert!(!bad_config.is_valid());
}

#[test]
fn test_fee_calculation() {
let output = 1_000_000; // 1 USDC (6 decimals)
let fee_bps = 30; // 0.3%

let fee = (output as u128)
.checked_mul(fee_bps as u128)
.unwrap()
.checked_div(10000)
.unwrap() as u64;

assert_eq!(fee, 300); // 0.003 USDC
}

#[test]
fn test_overflow_protection() {
// u64::MAX * fee_bps should not panic
let result = (u64::MAX as u128)
.checked_mul(100)
.unwrap()
.checked_div(10000);

assert!(result.is_some());
}
}

Integration tests (18 tests)

#[tokio::test]
async fn test_jupiter_swap_e2e() {
let program = setup_program().await;
let config_pda = /* derive config */;

// 1. Initialize config
program.methods()
.initJupiterConfig(
fee_account,
50, // 0.5% platform fee
100, // 1% max slippage
)
.accounts(/* ... */)
.rpc()
.await
.unwrap();

// 2. Execute swap
let swap_result = program.methods()
.jupiterSwap(1_000_000_000, 950_000) // 1 SOL → min 0.95 USDC
.accounts(/* ... */)
.remaining_accounts(/* Jupiter route accounts */)
.rpc()
.await;

assert!(swap_result.is_ok());

// 3. Verify fee collection
let fee_account_balance = /* check fee account */;
assert!(fee_account_balance > 0);
}

#[tokio::test]
async fn test_slippage_protection() {
let program = setup_program().await;

// Intentionally set min_out higher than achievable
let result = program.methods()
.jupiterSwap(1_000_000, 999_999_999) // Impossible min_out
.accounts(/* ... */)
.rpc()
.await;

// Should fail with MinimumOutputNotMet
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"MinimumOutputNotMet"
);
}

Test coverage breakdown:

ModuleTestsFocus
state.rs5Validation logic, size calculations
constants.rs5Program ID parsing, seed lengths
errors.rs2Error code existence
init_jupiter_config.rs4Init validation, boundary cases
update_jupiter_config.rs7Partial updates, admin transfer
jupiter_swap.rs5Event emission, calculations
jupiter_rust_tests.rs18End-to-end flows, security

Production deployment checklist

Pre-deployment validation

CheckWhy it mattersHow to verify
Program ID fixedReproducible buildsanchor build --verifiable
Upgrade authorityImmutability post-auditsolana program set-upgrade-authority <program_id> --final
Config adminEmergency controlsMultisig or DAO-controlled
Platform fee ≤ 1%Competitive with alternativesReview platform_fee_bps
Max slippage reasonableProtect usersTypically 50-500 BPS
Pause mechanism testedKill-switch worksIntegration test coverage
Token-2022 testedFee-on-transfer handlingTest with USDT (transfer fees)

Deployment steps

# 1. Build verifiable program
anchor build --verifiable

# 2. Deploy to devnet
anchor deploy --provider.cluster devnet

# 3. Initialize config (via multisig in production)
anchor run initialize-config --provider.cluster devnet

# 4. Verify deployment
solana program show <program_id>

# 5. Audit & security review
# (Use Sec3, OtterSec, or similar)

# 6. Deploy to mainnet
anchor deploy --provider.cluster mainnet-beta

# 7. Initialize mainnet config
anchor run initialize-config --provider.cluster mainnet-beta

# 8. Set upgrade authority to final
solana program set-upgrade-authority <program_id> --final

Operational runbook: support & incident handling

When swaps fail or users contact support, you need immediate answers. This runbook maps symptoms to root causes.

Required debug information

Every support ticket needs:

  • Transaction signature (if swap was attempted)
  • Intent ID (from client logs or user session)
  • Wallet address
  • Input/output mints
  • Timestamp (when issue occurred)

Failure classification matrix

SymptomRoot causeDiagnosisImmediate mitigation
"Slippage tolerance exceeded"Quote stale OR low liquidityCheck quote_age_seconds in eventShorten quote refresh interval to 5-10s
"Transaction simulation failed"Compute budget exceededCheck route complexity (hops >3)Bump compute units to 400k for complex routes
"Account not found"ALT missing or not loadedCheck addressLookupTableAccounts in txnEnsure ALTs created and extended with pool accounts
"Insufficient funds"User balance < amount + feesCheck wallet balance vs amount_inShow clear error: "Need X SOL for fees"
"Custom program error: 0x1770"Token-2022 transfer feeCheck if token has transfer fee extensionUse vault delta verification (not instruction data)
"Transaction timeout"Network congestionCheck priority fee paidIncrease priority fee dynamically (use Helius API)
"Invalid instruction data"Jupiter program upgradedCheck program version mismatchUpdate Jupiter program ID constant
Swap succeeds but user didn't receive full amountToken-2022 fee-on-transferCompare quote vs actual receivedDocument this in UI ("Receives ~X after fees")

Incident response playbook

Scenario 1: Sudden spike in failures (greater than 10% failure rate)

Immediate actions:

  1. Check Solana network status (https://status.solana.com)

  2. Verify Jupiter API health (https://status.jup.ag)

  3. Query last 100 failures by error_code:

    SELECT error_code, COUNT(*) as count
    FROM swap_failed_events
    WHERE timestamp > NOW() - INTERVAL '1 hour'
    GROUP BY error_code
    ORDER BY count DESC;
  4. If error_code=1005 (compute exceeded): Bump compute budget globally

  5. If error_code=1002 (stale quote): Reduce quote refresh interval

  6. If widespread network issue: Enable pause toggle via admin

Scenario 2: User reports "missing tokens"

Debug flow:

  1. Get transaction signature → check on Solscan/Explorer
  2. Verify transaction succeeded (success or failure)
  3. If succeeded:
    • Check JupiterSwapEvent.amount_out
    • Compare to user's token account balance
    • Check for Token-2022 transfer fees (some tokens deduct on transfer)
  4. If failed:
    • Check JupiterSwapFailedEvent.error_code
    • Map to human-readable explanation
    • Guide user on fix (refresh quote, increase slippage, etc.)

Scenario 3: Revenue suddenly drops

Diagnostic queries:

-- Check if swap volume dropped or fee collection failed
SELECT
DATE_TRUNC('hour', timestamp) as hour,
COUNT(*) as swap_count,
SUM(amount_in) as total_volume,
SUM(platform_fee) as revenue
FROM swap_events
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY hour
ORDER BY hour DESC;

Possible causes:

  • Fee collection logic broken (check program logs)
  • Users bypassing your wrapper (check if they're using Jupiter directly)
  • Platform fee set to 0 accidentally (check config PDA)

Proactive monitoring alerts

Set up alerts for:

  • Failure rate greater than 5% for 10 minutes
  • Quote-to-swap conversion less than 70% (indicates UX friction)
  • Median execution latency greater than 30 seconds (quote staleness)
  • Zero swaps for 15 minutes (system down or paused)
  • Platform fee revenue drops greater than 50% hour-over-hour

Common pitfalls and solutions (reference)

1. Transaction size limits (exceeded max accounts)

Problem: Complex Jupiter routes require 30-50 accounts, exceeding transaction limits.

Solution: Use Address Lookup Tables (ALTs)

// Create ALT during program initialization
const [lookupTable, _] = AddressLookupTableProgram.createLookupTable({
authority: admin,
payer: admin,
recentSlot: await connection.getSlot(),
});

// Add frequently-used accounts
await connection.sendTransaction(
AddressLookupTableProgram.extendLookupTable({
lookupTable,
authority: admin,
payer: admin,
addresses: [USDC_MINT, USDT_MINT, /* common pools */],
})
);

2. Slippage errors on mainnet (works on devnet)

Problem: Mainnet has higher volatility and MEV, causing more slippage failures.

Solution: Dynamic slippage based on liquidity

function calculateSlippage(quote: JupiterQuote) {
const priceImpact = quote.priceImpactPct;

if (priceImpact < 0.01) return 50; // 0.5% for deep liquidity
if (priceImpact < 0.05) return 100; // 1% for medium liquidity
return 500; // 5% for low liquidity
}

3. Token-2022 fee-on-transfer not accounted

Problem: USDT (SPL Token-2022 with transfer fees) results in less than expected amounts.

Solution: Always observe vault deltas

let before = token_account.amount;
// ... execute transfer ...
token_account.reload()?;
let actual_received = token_account.amount.saturating_sub(before);

4. Quote expiration (stale routes)

Problem: User reviews swap for 60 seconds, quote becomes stale, transaction fails.

Solution: Auto-refresh quotes

useEffect(() => {
const interval = setInterval(refreshQuote, 10000); // Every 10s
return () => clearInterval(interval);
}, [inputMint, outputMint, amount]);

5. Insufficient compute budget

Problem: Complex routes run out of compute units (200k default).

Solution: Request higher compute budget

use solana_program::compute_budget::ComputeBudgetInstruction;

// Add as first instruction in transaction
let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000);

Performance optimization

Account lookup optimization

// Inefficient: Multiple account lookups
for account in ctx.remaining_accounts.iter() {
let data = account.try_borrow_data()?;
// Process...
}

// Efficient: Single borrow per account
let accounts: Vec<_> = ctx.remaining_accounts
.iter()
.map(|a| (a.key(), a.try_borrow_data()))
.collect();

Fee calculation (avoid division)

// Slower: Division
let fee = (amount * fee_bps) / 10000;

// Faster: Shift (if fee_bps is power of 2)
// For 0.5% (50 bps): multiply by 1/200 = right shift by ~8
// Not always applicable, but pattern to consider

Frontend quote batching

// Sequential quotes (slower)
const quote1 = await getQuote({ inputMint: SOL, outputMint: USDC, amount: 1e9 });
const quote2 = await getQuote({ inputMint: SOL, outputMint: USDT, amount: 1e9 });

// Parallel quotes (faster)
const [quote1, quote2] = await Promise.all([
getQuote({ inputMint: SOL, outputMint: USDC, amount: 1e9 }),
getQuote({ inputMint: SOL, outputMint: USDT, amount: 1e9 }),
]);

Monitoring and analytics

On-chain events (CRM/ops-grade telemetry)

Your event schema defines what you can measure and debug. Make it comprehensive:

#[event]
pub struct JupiterSwapEvent {
// Attribution
pub intent_id: [u8; 16], // UUID bytes for client-side correlation
pub user: Pubkey, // Wallet address
pub client_version: [u8; 32], // App version (e.g., "web-v2.1.3\0\0...")

// Swap details
pub input_mint: Pubkey,
pub output_mint: Pubkey,
pub amount_in: u64,
pub amount_out: u64,
pub platform_fee: u64,

// Execution context
pub quote_timestamp: i64, // When quote was generated (detect staleness)
pub execution_timestamp: i64, // When swap executed
pub route_hash: u64, // Hash of route plan (fingerprint venues used)
pub slippage_bps_requested: u16, // User-requested slippage
pub slippage_bps_effective: u16, // Actual slippage observed

// Operational data
pub compute_units_consumed: u64, // For performance tuning
pub priority_fee_paid: u64, // MEV/congestion analysis
}

#[event]
pub struct JupiterSwapFailedEvent {
// Attribution (same as success event)
pub intent_id: [u8; 16],
pub user: Pubkey,
pub client_version: [u8; 32],

// Failure context
pub input_mint: Pubkey,
pub output_mint: Pubkey,
pub amount_in: u64,
pub minimum_amount_out: u64,

// Diagnostic data
pub error_code: u32, // Mapped to human-readable reasons
pub program_error: Option<String>, // Anchor error details
pub quote_age_seconds: i64, // How old was the quote?
pub timestamp: i64,
}

Why these fields matter:

  • intent_id: Join client logs, backend DB, and on-chain events for full trace
  • client_version: Identify bugs introduced in specific releases
  • quote_timestamp vs execution_timestamp: Measure latency, detect stale quotes
  • route_hash: Identify which venue combinations succeed/fail most
  • slippage_bps_effective: Measure if users are over-allocating slippage tolerance
  • compute_units_consumed: Optimize compute budgets dynamically
  • error_code: Build dashboards showing top failure reasons

Failure telemetry (first-class error logging)

Most teams only emit success events. Failures are more valuable for ops:

pub fn jupiter_swap<'info>(
ctx: Context<'_, '_, 'info, 'info, JupiterSwap<'info>>,
intent_id: [u8; 16],
amount_in: u64,
minimum_amount_out: u64,
) -> Result<()> {
// ... validation and swap execution ...

// If swap fails, emit failure event BEFORE returning error
if let Err(e) = execute_jupiter_cpi(&ctx, amount_in) {
emit!(JupiterSwapFailedEvent {
intent_id,
user: ctx.accounts.user.key(),
client_version: get_client_version(),
input_mint: ctx.accounts.input_mint.key(),
output_mint: ctx.accounts.output_mint.key(),
amount_in,
minimum_amount_out,
error_code: map_error_to_code(&e),
program_error: Some(e.to_string()),
quote_age_seconds: calculate_quote_age(),
timestamp: Clock::get()?.unix_timestamp,
});
return Err(e);
}

// Success: emit success event
emit!(JupiterSwapEvent { /* ... */ });
Ok(())
}

Error code mapping (for structured dashboards):

fn map_error_to_code(error: &anchor_lang::error::Error) -> u32 {
match error {
JupiterSwapError::MinimumOutputNotMet => 1001,
JupiterSwapError::StaleQuote => 1002,
JupiterSwapError::JupiterPaused => 1003,
JupiterSwapError::InsufficientLiquidity => 1004,
JupiterSwapError::ComputeBudgetExceeded => 1005,
JupiterSwapError::ALTMissing => 1006,
JupiterSwapError::Token2022TransferFee => 1007,
_ => 9999, // Unknown error
}
}

Dashboard-ready KPI definitions

These metrics map directly to SQL queries and BI dashboards:

KPIFormulaQuery exampleTarget
Quote → Swap conversioncompleted_swaps / quote_requestsSELECT COUNT(DISTINCT intent_id) FROM swaps / COUNT(*) FROM quotesgreater than 80%
Intent completion ratecompleted_intents / created_intentsSELECT successful / total FROM intent_summarygreater than 90%
Failure rate by reasonfailures(reason=X) / total_attemptsSELECT error_code, COUNT(*) / total FROM failures GROUP BY error_codeless than 5% overall
Median execution latencymedian(event_time - intent_creation_time)SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY latency) FROM swapsless than 15s
Revenue (USD)SUM(platform_fee * token_price_usd)SELECT SUM(f.amount * p.price) FROM fees f JOIN prices p ON f.mint = p.mintTrack growth
Route health scoresuccess_rate_per_route_fingerprintSELECT route_hash, COUNT(*) successes / total FROM swaps GROUP BY route_hashgreater than 95% per route
Effective slippageAVG((quote_out - actual_out) / quote_out * 10000)SELECT AVG((quoted - actual) / quoted * 10000) FROM swapsless than 50 BPS
Repeat user rateusers_with_2plus_swaps / total_usersSELECT COUNT(DISTINCT user) FROM (SELECT user, COUNT(*) c FROM swaps GROUP BY user HAVING c >= 2)greater than 40%

Sample dashboard SQL (Postgres):

-- Real-time conversion funnel
WITH funnel AS (
SELECT
COUNT(DISTINCT q.intent_id) as quotes,
COUNT(DISTINCT s.intent_id) as swaps,
COUNT(DISTINCT CASE WHEN s.success = true THEN s.intent_id END) as successful
FROM quote_requests q
LEFT JOIN swap_events s ON q.intent_id = s.intent_id
WHERE q.created_at > NOW() - INTERVAL '1 hour'
)
SELECT
quotes,
swaps,
successful,
ROUND(100.0 * swaps / NULLIF(quotes, 0), 2) as quote_to_swap_pct,
ROUND(100.0 * successful / NULLIF(swaps, 0), 2) as success_rate
FROM funnel;

-- Top failure reasons (last 24 hours)
SELECT
CASE error_code
WHEN 1001 THEN 'Slippage exceeded'
WHEN 1002 THEN 'Stale quote'
WHEN 1005 THEN 'Compute budget exceeded'
WHEN 1006 THEN 'ALT missing'
WHEN 1007 THEN 'Token-2022 transfer fee'
ELSE 'Unknown'
END as failure_reason,
COUNT(*) as occurrences,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) as percentage
FROM swap_failed_events
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY error_code
ORDER BY occurrences DESC;

-- Revenue by token pair (last 7 days)
SELECT
s.input_mint,
s.output_mint,
COUNT(*) as swap_count,
SUM(s.platform_fee) as total_fee_tokens,
SUM(s.platform_fee * p.price_usd) as revenue_usd
FROM swap_events s
JOIN token_prices p ON s.output_mint = p.mint
WHERE s.timestamp > NOW() - INTERVAL '7 days'
GROUP BY s.input_mint, s.output_mint
ORDER BY revenue_usd DESC
LIMIT 10;

Grafana/Metabase integration:

  • Create alerts on conversion rate drop (greater than 10% decrease)
  • Dashboard panels: conversion funnel, failure reasons pie chart, revenue time series
  • User cohort analysis: new vs returning users by swap count

Indexing with Helius/Hellomoon

// Subscribe to program logs
const connection = new Connection(HELIUS_RPC_URL);

connection.onLogs(
programId,
(logs) => {
if (logs.logs.some(log => log.includes('JupiterSwapEvent'))) {
// Parse event and store in database
const event = parseJupiterSwapEvent(logs);
await db.swaps.insert(event);
}
},
'confirmed'
);

Cost analysis

Transaction costs (mainnet, Dec 2024)

OperationCompute unitsTypical cost (SOL)Notes
Init config~5,0000.000005One-time
Simple swap (1 hop)~50,0000.00005Direct pool
Complex swap (3+ hops)~200,0000.0002Multi-route
Update config~3,0000.000003Admin only

Priority fees: Add 0.00001-0.0001 SOL during congestion for faster inclusion.

Platform revenue model

Example with 0.3% platform fee:

  • User swaps 100 SOL → USDC
  • Jupiter finds route yielding 9,500 USDC
  • Platform collects: 9,500 × 0.003 = 28.5 USDC
  • User receives: 9,471.5 USDC

At 1M SOL monthly volume (current mid-tier DEX):

  • Revenue: **30,000/month(assuming0.330,000/month** (assuming 0.3% fee, 100 SOL)
  • Competitive with 0.01-0.1% range most aggregators use

Future enhancements

1. Limit orders via DCA (Dollar-Cost Averaging)

Jupiter DCA allows scheduled swaps:

// Create DCA order
const dcaOrder = await program.methods
.createDcaOrder({
inputMint: SOL,
outputMint: USDC,
amountPerSwap: 10 * 1e9, // 10 SOL
interval: 3600, // 1 hour
totalAmount: 1000 * 1e9, // 1000 SOL total
})
.rpc();

2. MEV protection via private routing

Integrate Jupiter's private RPC:

const quote = await getQuote({
inputMint,
outputMint,
amount,
// Use private RPC to avoid frontrunning
rpcUrl: 'https://private.jup.ag/rpc',
});

3. Cross-chain swaps (via Wormhole)

Jupiter integrates Wormhole for cross-chain swaps:

pub fn cross_chain_swap(
ctx: Context<CrossChainSwap>,
destination_chain: u16, // e.g., 1 = Ethereum
amount: u64,
) -> Result<()> {
// Swap SOL → USDC on Solana
// Bridge USDC to Ethereum via Wormhole
// Swap USDC → ETH on Ethereum
Ok(())
}

4. Liquidity aggregation metrics

Show users why Jupiter found better price:

interface RouteBreakdown {
venue: string;
percentage: number; // % of trade routed through this venue
priceImpact: number;
}

const breakdown: RouteBreakdown[] = [
{ venue: 'Raydium CLMM', percentage: 60, priceImpact: 0.12 },
{ venue: 'Orca Whirlpool', percentage: 30, priceImpact: 0.08 },
{ venue: 'Meteora DLMM', percentage: 10, priceImpact: 0.05 },
];

Comparison: direct Jupiter API vs program wrapper

AspectDirect APIProgram wrapper (this guide)
Implementation time2 hours1 week (with tests)
Fee collectionmanual off-chainautomatic on-chain
Composabilitylimitedfull CPI support
Audit surfacenoneprogram code
Brandingclient-side onlyon-chain enforcement
Access controlnoneadmin-gated
Event logsnoneon-chain events
Upgrade pathN/Aversioned program

When to use direct API:

  • Quick prototypes
  • Personal tools
  • No fee collection needed

When to use program wrapper:

  • Platform/product launch
  • Need fee revenue
  • Want composability with other protocols
  • Require audit for institutional users

Resources

Official documentation

Code examples

Community


Production integration checklist

Use this checklist before deploying to mainnet:

Pre-deployment

  • Intent ID propagated end-to-end: Client generates UUID → passed to program → included in events
  • Client version tracking: App version captured in all events for release correlation
  • Quote refresh mechanism: Auto-refresh every 10s; warn user if quote >30s old
  • Slippage calculation: Dynamic slippage based on liquidity depth (not hardcoded)
  • Quote staleness guard: Validate quote_timestamp on backend before execution

Smart contract

  • CPI remaining accounts tested: Verified with 2-hop, 3-hop, and 5-hop routes
  • Token-2022 vault delta validation: Output amount observed from token account change, not instruction data
  • ALT/v0 transaction support: Complex routes (>20 accounts) tested with Address Lookup Tables
  • Platform fee collection: Verified fees transfer to correct account on every swap
  • Slippage enforcement: On-chain max slippage cap cannot be bypassed by client
  • Pause toggle works: Admin can halt swaps; verified in integration tests
  • Overflow protection: All fee calculations use checked_mul / checked_div

Telemetry & observability

  • Success events comprehensive: Include intent_id, route_hash, slippage_effective, compute_units
  • Failure events captured: Emit SwapFailedEvent with error_code before returning errors
  • Error code mapping: All program errors map to documented reason codes (1001-1007+)
  • Event indexer running: Helius/Hellomoon/custom indexer writes events to database
  • Dashboard metrics live: Conversion rate, failure breakdown, revenue tracking operational

Operational readiness

  • Support runbook documented: Team knows how to classify failures by error code
  • Admin key security: Stored in hardware wallet or multisig (not hot wallet)
  • Monitoring alerts configured: Failure rate greater than 5%, conversion less than 70%, revenue drops
  • Incident response plan: Who to contact for Jupiter API issues, network outages
  • User-facing error messages: Map error codes to helpful guidance (e.g., "Quote expired. Refresh and retry.")

Testing

  • Mainnet-like environment: Tested on devnet with realistic routes and tokens
  • Token-2022 tokens tested: USDT (transfer fee), BONK (token extensions)
  • High-compute routes tested: 5+ hop routes with compute budget adjustments
  • Failure scenarios tested: Stale quote, slippage exceeded, insufficient balance
  • End-to-end user flow: Quote → Intent → Execute → Event → Dashboard (full trace)

Deployment

  • Verifiable build: anchor build --verifiable succeeds; hash matches deployed program
  • Config PDA initialized: Admin, fee account, platform fee, max slippage set correctly
  • Frontend pointing to correct program: Program ID hardcoded matches deployed address
  • Upgrade authority finalized: Set to multisig or --final after audit

Conclusion

Building a Jupiter integration on Solana requires:

  1. Solid Anchor fundamentals (PDAs, CPIs, account validation)
  2. Token-2022 awareness (vault deltas, not instruction amounts)
  3. Transaction size management (ALTs for complex routes)
  4. Comprehensive testing (unit + integration + E2E)
  5. Production-grade monitoring (events, metrics, alerts)

The result is a composable, fee-collecting swap infrastructure that leverages Jupiter's best-in-class routing while maintaining control over user experience and revenue.


Key takeaways:

  • Jupiter abstracts away liquidity fragmentation (20+ venues)
  • Program wrappers enable fee collection and composability
  • Token-2022 compatibility is non-negotiable in 2024+
  • Address Lookup Tables are essential for complex routes
  • Testing prevents costly mainnet bugs (each fix costs 0.5-1 SOL)

Ship safe.