All articles
solanajupiterdefidexaggregatorliquidityperps

Jupiter: the aggregation layer of Solana DeFi

How Jupiter's smart order router, JLP vault, limit orders, DCA program, and LFG launchpad work — with real TypeScript examples for swaps, quotes, and the v6 API.

Share

Every Solana wallet that shows a swap button is almost certainly routing through Jupiter. By mid-2026 it handles the majority of Solana DEX volume — not because it runs its own AMM pools, but because it knows where every pool is and how to split an order across them to minimise price impact. That distinction matters for builders: Jupiter is infrastructure, not a venue.

This article covers the mechanics that matter to developers: how the router actually finds a route, how the JLP vault and perpetuals work, what the on-chain limit-order and DCA programs do, and how to call the v6 API end-to-end in TypeScript.

The routing problem

A naive swap — send USDC to Raydium, get SOL — is easy. The hard case is a long-tail token with thin liquidity on one pool. A large order against a single constant-product pool walks the price against you. Jupiter solves this with three mechanisms:

  • Multi-hop routing — instead of BONK → USDC directly, route BONK → SOL → USDC if that path offers better output.
  • Split routes — divide the input across multiple pools simultaneously. 60% through Raydium CLMM, 40% through Orca Whirlpool, merge the outputs.
  • Smart Order Router (SOR) — a graph search (roughly Dijkstra-weighted by expected output) run server-side in milliseconds over a live snapshot of all integrated pools.

The pool graph includes every major AMM: Raydium (v4 CPMM, CLMM), Orca (Whirlpools), Meteora (DLMM, stable pools), Phoenix (orderbook), Lifinity, Saros, and dozens of smaller venues. Jupiter indexes their accounts via a Geyser plugin and keeps the graph warm in memory. When you call the quote API, it's reading from that in-memory state — not making RPC calls per quote.

The routing output is a route plan: an ordered list of swap steps, each with an AMM address, input/output mints, and an expected output amount. The route plan becomes a versioned transaction via the swap API — assembled server-side with all required ALT (Address Lookup Table) references baked in, so the transaction fits within the 1232-byte limit even for complex split routes.

The v6 API: quote then swap

Jupiter exposes a public REST API at https://quote-api.jup.ag/v6. Two endpoints cover 99% of use cases: /quote and /swap.

/quote returns a route plan with expected output and fees./swap takes that route plan plus a user public key and returns a base64-encoded versioned transaction ready to sign and send.

typescript
import {
  Connection,
  Keypair,
  VersionedTransaction,
  PublicKey,
} from "@solana/web3.js";

const connection = new Connection("https://api.mainnet-beta.solana.com");

// Step 1: get a quote
const quoteResponse = await fetch(
  "https://quote-api.jup.ag/v6/quote?" +
    new URLSearchParams({
      inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
      outputMint: "So11111111111111111111111111111111111111112",   // SOL (wrapped)
      amount: "10000000", // 10 USDC (6 decimals)
      slippageBps: "50",  // 0.5%
    })
).then((r) => r.json());

console.log("Expected output (lamports):", quoteResponse.outAmount);
console.log("Price impact %:", quoteResponse.priceImpactPct);
console.log("Route plan:", quoteResponse.routePlan);

// Step 2: get the swap transaction
const { swapTransaction } = await fetch("https://quote-api.jup.ag/v6/swap", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    quoteResponse,
    userPublicKey: wallet.publicKey.toString(),
    wrapAndUnwrapSol: true, // auto wrap/unwrap native SOL
    dynamicComputeUnitLimit: true, // let Jupiter estimate CUs
    prioritizationFeeLamports: "auto", // let Jupiter set priority fee
  }),
}).then((r) => r.json());

// Step 3: deserialise, sign, send
const txBuf = Buffer.from(swapTransaction, "base64");
const tx = VersionedTransaction.deserialize(txBuf);
tx.sign([wallet]);

const sig = await connection.sendRawTransaction(tx.serialize(), {
  skipPreflight: false,
  maxRetries: 2,
});
await connection.confirmTransaction(sig, "confirmed");
console.log("Swap confirmed:", sig);

slippageBps vs. dynamicSlippage

slippageBps is a static tolerance — the transaction reverts if the actual output falls more than N basis points below the quoted output. Jupiter also exposes dynamicSlippage (pass { minBps: 10, maxBps: 300 } instead of a fixed bps value), which lets the router tighten slippage for liquid pairs and loosen it for thin markets. For most integrations, a static 50 bps is fine for stablecoin pairs; 100-200 bps for volatile long-tail tokens.

ALTs in versioned transactions

Jupiter's swap transactions are always VersionedTransaction (Message v0), never legacy. The swap API response includes addressLookupTableAddresses in the route plan — these are the ALT accounts the transaction loads at execution time. You don't need to manage these yourself; the assembled transaction has them embedded. But if you're simulating or building on top of the swap tx, you need to load the ALT accounts before simulation:

typescript
import { AddressLookupTableAccount } from "@solana/web3.js";

// Extract ALT addresses from the deserialized message
const altAddresses =
  tx.message.addressTableLookups.map((l) => l.accountKey);

// Load them before simulating
const altAccounts = await Promise.all(
  altAddresses.map((addr) =>
    connection
      .getAddressLookupTable(addr)
      .then((res) => res.value as AddressLookupTableAccount)
  )
);

// Now you can decompose the full account list
const accountKeys = tx.message.getAccountKeys({
  addressLookupTableAccounts: altAccounts,
});

The quote response in detail

The /quote response carries more than just the output amount. Fields developers regularly need:

  • inAmount / outAmount — exact amounts in token base units (after all fees).
  • priceImpactPct — estimated price impact as a percentage. Anything above 1% on a mainstream pair is a signal to split or warn the user.
  • routePlan — array of swap steps. Each step has swapInfo (AMM label, input/output mints, fee amount) and percent (share of input routed through this leg).
  • contextSlot — the slot the quote was computed against. Use this to detect stale quotes: if it's more than ~10 slots old (≈5 seconds), re-fetch.
  • timeTaken — server-side routing compute time in seconds. Usually sub-100ms for common pairs.
typescript
// Inspect the route plan
for (const step of quoteResponse.routePlan) {
  const { swapInfo, percent } = step;
  console.log(
    `${percent}% via ${swapInfo.label}: ` +
    `${swapInfo.inputMint.slice(0,8)}… → ${swapInfo.outputMint.slice(0,8)}… ` +
    `(fee: ${swapInfo.feeAmount} ${swapInfo.feeMint.slice(0,8)}…)`
  );
}
// Example output:
// 60% via Raydium CLMM: EPjFWdd… → So11111… (fee: 600 EPjFWdd…)
// 40% via Orca Whirlpool: EPjFWdd… → So11111… (fee: 400 EPjFWdd…)

Referral fees and platform fees

Jupiter lets integrators collect a platform fee on each swap. You create a fee account (a token account owned by your referral program) and pass its address to the swap API. Jupiter deducts the fee from the output before delivering it to the user.

typescript
// Pass platform fee config to the swap endpoint
const { swapTransaction } = await fetch("https://quote-api.jup.ag/v6/swap", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    quoteResponse,
    userPublicKey: wallet.publicKey.toString(),
    feeAccount: "YOUR_FEE_TOKEN_ACCOUNT_ADDRESS", // output-mint ATA of your referral account
    // feeBps is set when you create the fee account on-chain, not here
    wrapAndUnwrapSol: true,
  }),
}).then((r) => r.json());

The fee is denominated in the output token. To receive fees in USDC regardless of output token, use the token-ledger feature or collect and swap separately. Fee accounts are created once via the Jupiter referral program on-chain — see the referral program docs.

Limit orders

Jupiter's limit-order program is a pure on-chain construct. A user creates a limit-order account specifying: input mint, output mint, input amount, minimum output amount, and an expiry. The order sits on-chain until a keeper finds a fill opportunity — typically when the spot price crosses the limit — and cranks the execution. The keeper earns a small fill fee.

Importantly, the keeper is permissionless: anyone can run one. Jupiter operates the primary keeper network, but the program is open. The limit is not a conditional that executes against Jupiter's own liquidity — it executes against the live AMM graph, exactly like a normal swap, at the moment the keeper fires.

typescript
import { createJupiterApiClient } from "@jup-ag/api";

const jupiterApi = createJupiterApiClient();

// Create a limit order: buy 1 SOL when the price drops to 100 USDC
// input: 100 USDC (6 decimals = 100_000_000)
// output minimum: 1 SOL (9 decimals = 1_000_000_000)
const { tx } = await jupiterApi.limitOrdersPost({
  owner: wallet.publicKey.toString(),
  inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
  outputMint: "So11111111111111111111111111111111111111112",  // SOL
  makingAmount: "100000000",   // 100 USDC in
  takingAmount: "1000000000",  // 1 SOL out minimum
  expiredAt: Math.floor(Date.now() / 1000) + 86400, // 24h expiry
});

const limitOrderTx = VersionedTransaction.deserialize(
  Buffer.from(tx, "base64")
);
limitOrderTx.sign([wallet]);
const sig = await connection.sendRawTransaction(limitOrderTx.serialize());
console.log("Limit order created:", sig);

To cancel: call the cancel instruction with the order account address. To query open orders for a wallet, use the limit-orders GET endpoint:

typescript
// Fetch open limit orders for a wallet
const orders = await jupiterApi.getLimitOrders({
  wallet: wallet.publicKey.toString(),
  inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
});
console.log("Open orders:", orders.map(o => ({
  account: o.account,
  makingAmount: o.account.makingAmount,
  takingAmount: o.account.takingAmount,
  expiredAt: o.account.expiredAt,
})));

Dollar-cost averaging (DCA)

The Jupiter DCA program lets a user schedule recurring swaps on-chain. A DCA account holds the full input allocation and executes one swap per interval until the allocation is exhausted or the account is closed. No off-chain scheduler is required — keepers crank each cycle.

Parameters: input token, output token, total input amount, amount per cycle, cycle frequency (in seconds), minimum output per cycle (optional price floor), and a start time offset. The program is useful for treasury diversification, yield-farming entry positions, and any flow that benefits from time-averaging.

typescript
import { DCA, Network } from "@jup-ag/dca-sdk";
import { PublicKey } from "@solana/web3.js";

const dca = new DCA(connection, Network.MAINNET);

// DCA: spend 100 USDC total, buying SOL in 10 USDC chunks every 6 hours
const { tx } = await dca.createDCA({
  payer: wallet.publicKey,
  user: wallet.publicKey,
  inAmount: BigInt(100_000_000),        // 100 USDC total
  inAmountPerCycle: BigInt(10_000_000), // 10 USDC per cycle
  cycleSecondsApart: BigInt(6 * 3600),  // every 6 hours
  inputMint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
  outputMint: new PublicKey("So11111111111111111111111111111111111111112"),
  minOutAmountPerCycle: null, // no floor — fill at market
  maxOutAmountPerCycle: null,
  startAt: null, // start immediately
});

// Sign and send
tx.sign([wallet]);
const sig = await connection.sendRawTransaction(tx.serialize());
console.log("DCA account created:", sig);

// Query active DCAs for a wallet
const dcaAccounts = await dca.getCurrentByUser(wallet.publicKey);
console.log("Active DCA positions:", dcaAccounts.length);

The JLP vault and perpetuals

Jupiter Perpetuals is a peer-to-pool perpetuals exchange. The counterparty to every trade is the Jupiter Liquidity Pool (JLP)— a single multi-asset vault holding BTC, ETH, SOL, USDC, and USDT. JLP holders are the house: they earn trading fees, borrow fees, and liquidation proceeds. They take the other side of PnL.

JLP is not an AMM. The vault composition targets are set by Jupiter governance (e.g., 20% SOL, 30% USDC, etc.) and enforced via fee incentives — adding an underweight asset costs less; removing it costs more. There is no bonding curve for deposits/withdrawals: the share price is the AUM divided by total JLP supply, marked to market by on-chain keeper oracles (Pyth price feeds).

How borrowing works in perpetuals

When a trader opens a leveraged long on SOL, they don't receive SOL. Instead, the program records a position and locks a proportional amount of SOL in the JLP vault as collateral backing. The trader pays a borrow fee — an hourly rate on the locked notional — which accrues to JLP holders. The position is marked to market using the Pyth price feed; when the mark price moves against the trader past the liquidation threshold (typically when collateral covers less than 10% of notional), a keeper liquidates the position and the collateral flows to JLP.

This means JLP is net-short volatility in aggregate: JLP holders profit when traders lose and collect borrow fees regardless of direction. In trending markets where traders win, JLP underperforms. This is the fundamental risk of providing liquidity to a perpetuals venue.

Reading JLP state

typescript
// Jupiter Perpetuals program ID
const PERP_PROGRAM_ID = new PublicKey(
  "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"
);

// JLP mint
const JLP_MINT = new PublicKey(
  "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4"
);

// Fetch JLP mint info to compute circulating supply
import { getMint } from "@solana/spl-token";
const jlpMint = await getMint(connection, JLP_MINT);
console.log("JLP supply:", Number(jlpMint.supply) / 1e6);

// The on-chain pool state account can be fetched via the program's
// getProgramAccounts with the pool discriminator — or more practically,
// use Jupiter's stats API:
const stats = await fetch(
  "https://stats.jup.ag/info"
).then(r => r.json());
console.log("JLP AUM USD:", stats.tvl);

LFG Launchpad

LFG (Launchpad For Growth — or "Let's F***ing Go", depending who you ask) is Jupiter's community-gated token launch mechanism. It combines two phases:

  1. Vote phase — JUP holders vote on which projects get a launch slot in the upcoming cohort. Voting weight is proportional to locked JUP. Projects submit an application; the community vets it. This is social layer, not smart contract — there's no on-chain vote program enforcing outcome; it's a signalling round that Jupiter team uses as input.
  2. Liquidity bootstrapping pool (LBP) phase — the approved project launches via a time-weighted auction pool. Starting price is high; it decreases over the auction period (usually 48h) as weight shifts from project token to SOL/USDC. Buyers who enter late pay lower prices; buyers who enter early subsidise price discovery. The LBP contract prevents bots from sweeping at open because the price starts intentionally expensive.

From a builder perspective: if you want to launch a token on LFG, the entry point is the governance forum, not a smart contract call. The LBP execution is handled by Jupiter's Meteora-style DLMM-adjacent pool contract — you don't deploy it yourself. Post-launch, the project is expected to seed a Raydium or Orca liquidity pool with proceeds.

JUP token and governance

JUP is the governance token. Voting happens via a locked-staking model: you lock JUP for a chosen duration (up to 3 years) and receive voting power proportional to amount × lock duration. Votes govern: fee parameters, LFG cohort decisions, ecosystem grants from the JUP DAO treasury, and major protocol changes.

JUP is also used for merit rewards — the Jupiter team distributes periodic airdrops to active on-chain participants (swappers, LFG participants, governance voters). The airdrop eligibility is computed off-chain against on-chain activity snapshots.

Integrators don't need JUP to use the swap API. It only matters if you're building a governance dashboard or a staking UI on top of the vote-escrow program.

Using the @jup-ag/api SDK

Jupiter maintains a first-party TypeScript SDK that wraps the REST API and provides typed request/response objects. It covers swaps, limit orders, and price lookups.

bash
npm install @jup-ag/api
typescript
import { createJupiterApiClient } from "@jup-ag/api";

const jupiter = createJupiterApiClient({
  basePath: "https://quote-api.jup.ag", // default
});

// Quote
const quote = await jupiter.quoteGet({
  inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  outputMint: "So11111111111111111111111111111111111111112",
  amount: 10_000_000,
  slippageBps: 50,
  onlyDirectRoutes: false,
  asLegacyTransaction: false, // always false for v0
});

// Swap transaction
const { swapTransaction } = await jupiter.swapPost({
  swapRequest: {
    quoteResponse: quote,
    userPublicKey: wallet.publicKey.toString(),
    wrapAndUnwrapSol: true,
    dynamicComputeUnitLimit: true,
    prioritizationFeeLamports: {
      autoMultiplier: 2, // 2× the estimated priority fee
    },
  },
});

const tx = VersionedTransaction.deserialize(
  Buffer.from(swapTransaction, "base64")
);
tx.sign([wallet]);
await connection.sendRawTransaction(tx.serialize());

Price API

Jupiter also exposes a price API — a fast, low-latency endpoint that returns the current USD price of any token by mint address, derived from the live routing graph rather than a separate oracle. Useful for portfolio displays.

typescript
// Fetch USD prices for multiple mints
const mints = [
  "So11111111111111111111111111111111111111112",   // SOL
  "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
  "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // BONK
].join(",");

const { data } = await fetch(
  `https://price.jup.ag/v6/price?ids=${mints}`
).then((r) => r.json());

for (const [mint, info] of Object.entries(data as Record<string, any>)) {
  console.log(`${info.mintSymbol}: $${info.price}`);
}
// SOL: $142.50
// USDC: $1.00
// BONK: $0.00002341

The price API computes price as the output of a small reference swap (default 1 unit of input) against the routing graph. It's not a TWAP — it's a spot price from current pool states. For TWAP or manipulation-resistant price feeds, use Pyth instead.

Token list and strict mode

Jupiter maintains a curated token list (the "strict" list) of tokens that pass a basic vetting bar: non-zero on-chain liquidity, no obvious mint authority risk, legitimate metadata. By default the quote API routes through any token regardless of list status — including unverified mints. If you're building a consumer app, filter to strict-listed tokens in your UI to protect users from swap-front-run scams on fake mint addresses.

typescript
// Fetch the verified token list
const strictTokens = await fetch(
  "https://token.jup.ag/strict"
).then((r) => r.json());

// Also available: the all-tokens list (includes unverified)
// "https://token.jup.ag/all"

// Build a lookup map by mint
const strictMints = new Set(strictTokens.map((t: any) => t.address));

// Guard: only quote if both mints are verified
function isSafeSwap(inputMint: string, outputMint: string): boolean {
  return strictMints.has(inputMint) && strictMints.has(outputMint);
}

Common integration patterns

Swap widget embed

For the simplest integration, Jupiter provides an embeddable React swap widget via @jup-ag/terminal. It handles quote polling, transaction building, wallet adapter integration, and error states. Add it to any React app in a few lines:

typescript
import { useEffect } from "react";

// Load the terminal bundle (do this once at app root)
useEffect(() => {
  import("@jup-ag/terminal").then(({ init }) => {
    init({
      displayMode: "integrated",
      integratedTargetId: "jupiter-terminal",
      endpoint: "https://api.mainnet-beta.solana.com",
      strictTokenList: true,
      defaultExplorer: "SolanaFM",
      // Lock to specific mints if needed:
      // fixedInputMint: true,
      // defaultInputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    });
  });
}, []);

// Render target
return <div id="jupiter-terminal" style={{ width: "100%", minHeight: 500 }} />;

Polling for quote freshness

Quotes go stale in a few seconds on volatile pairs. A reasonable pattern for a custom UI: poll every 3 seconds, cancel in-flight requests on unmount, and show a stale indicator if the contextSlot is more than 15 slots old (≈6 seconds).

typescript
import { useEffect, useRef, useState } from "react";

function useJupiterQuote(inputMint: string, outputMint: string, amount: number) {
  const [quote, setQuote] = useState<any>(null);
  const abortRef = useRef<AbortController | null>(null);

  useEffect(() => {
    if (!amount) return;

    const poll = async () => {
      abortRef.current?.abort();
      abortRef.current = new AbortController();
      try {
        const res = await fetch(
          `https://quote-api.jup.ag/v6/quote?` +
            new URLSearchParams({
              inputMint,
              outputMint,
              amount: amount.toString(),
              slippageBps: "50",
            }),
          { signal: abortRef.current.signal }
        );
        const q = await res.json();
        setQuote(q);
      } catch (e: any) {
        if (e.name !== "AbortError") console.error(e);
      }
    };

    poll();
    const interval = setInterval(poll, 3000);
    return () => {
      clearInterval(interval);
      abortRef.current?.abort();
    };
  }, [inputMint, outputMint, amount]);

  return quote;
}

Rate limits and self-hosting

The public API at quote-api.jup.ag is rate-limited per IP. For production integrations with significant volume, Jupiter offers a self-hostable version of the quote API called Jupiter Self-Hosted (previously "Metis"). It runs as a Docker container, maintains its own in-memory pool graph via a Geyser connection to your own RPC node, and exposes the same REST interface. Contact Jupiter for access. Alternatively, some RPC providers (Helius, Triton) bundle a hosted Jupiter endpoint with their plans.

Error handling reference

The swap transaction can fail at the RPC layer or at program execution. Common failure modes:

  • Slippage exceeded (0x1771 or SlippageToleranceExceeded) — the pool moved between quote and execution. Retry with a fresh quote or widen slippage.
  • Insufficient output amount — similar to slippage but comes from the Jupiter swap program's own output check.
  • Compute budget exceeded — rare when using dynamicComputeUnitLimit: true; can happen on very long split routes. Jupiter caps routes to avoid this, but retry with onlyDirectRoutes: true as a fallback.
  • ALT not found — the address lookup table used in the transaction has been closed. This shouldn't happen with fresh quotes; if it does, the quote API returned a stale route plan.

What Jupiter is not

A few common misunderstandings worth clearing up:

  • Jupiter does not run its own AMM pools (except JLP for perpetuals). It routes through other protocols' pools.
  • The JLP vault is not a yield vault you can arbitrarily LP into with any token. It holds a fixed set of assets and has its own deposit/ withdrawal mechanics gated by the perps program.
  • LFG is not a permissionless launchpad. You can't call a contract to create an LBP slot; the team curates it.
  • The price API is not a reliable oracle for on-chain use. It's for display only. Use Pyth or Switchboard for on-chain price feeds.

Where to go from here

The Jupiter Station docs are the authoritative reference. The jupiter-quote-api-node repo has runnable examples. For the DCA SDK, see jup-ag/dca-sdk. The Jupiter Discord is active and the team responds to builder questions in the #developer channel.

Keep reading

Get new articles in your inbox

Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.

Jupiter: the aggregation layer of Solana DeFi | devrels.xyz