26 min read
Overview
Building a Swap UI on Solana usually means stitching together token discovery, quoting, slippage handling, transaction building, sending, retries, and status updates. That complexity makes it easy to ship something that works, but breaks under real user conditions.
Jupiter Ultra Swap simplifies this by giving you a clean execution path. You get a quote to preview the swap, then request an order that returns a transaction-ready payload for the user to sign, and finally execute the signed transaction.
By the end of this guide, you’ll have a working token swap UI on Solana powered by Jupiter Ultra API, where users can pick tokens, get a quote, sign once, and execute a swap end-to-end.
What You Will Do
You’ll wire up a quote-first flow, restrict swaps to the verified token list only, and connect the Ultra order/sign/execute lifecycle so your Swap UI can reliably complete swaps and display the final signature.
- Clone and run the companion sample app
- Load and enforce the verified token list so users can only swap verified tokens
- Add a quote-first step so users can preview expected output before committing
- Integrate the Ultra swap lifecycle: Order → Sign → Execute → Status
- Validate success and common failure paths
What You Will Need
This guide assumes you have a basic understanding of building TypeScript dApps, DeFi, and working with Solana wallets.
If you need a refresher, refer to:
You will also need:
- Small amount of
mainnet-betaSOL for fees - Small amount of tokens on mainnet to swap
- Jupiter API key
- Quicknode Solana RPC endpoint
Jupiter Ultra Swap is only available on Solana mainnet-beta. That means if you follow this guide and execute swaps, you’ll be trading real tokens and paying real network fees, and you could lose value due to price movement, slippage, or selecting the wrong token pair.
Run Sample Application
To get started, clone the sample application repository and open the solana/jupiter-ultra-swap folder.
git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/solana/jupiter-ultra-swap
npm install
npm run dev
Once you have it running locally, take a quick look through the codebase to familiarize yourself with the structure, then come back to this guide to walk through each integration step in detail and understand how the Jupiter Ultra API methods work together.
Understand Jupiter Ultra Swap Lifecycle
Instead of building and sending a swap transaction yourself, you ask Ultra for an order which includes a transaction-ready payload. The user signs it with their wallet and sends it back to Jupiter Ultra to execute.
Here’s the lifecycle your Swap UI follows in this guide:
Restrict Token Selection To Verified Tokens
To keep the UX safer and predictable, you build token selection from a verified-only set. Jupiter’s Tokens API v2 supports querying tokens with the verified tag.
/tokens/v2/tag?query=verified retrieves the list of verified tokens available for swapping. The response is filtered to show only verified tokens for safety.
Request An Ultra Order
Before the user commits, you fetch a quote to show expected output and swap conditions. Ultra returns an unsigned swap transaction. Your UI then prompts the connected wallet to sign the returned transaction payload.
/ultra/v1/order is called when the user clicks Swap to generate an unsigned transaction with the requestId for execution. This endpoint returns pricing information, route details, and a base64-encoded transaction ready for signing.
Execute Through Ultra And Track Status
The signed transaction is sent to Jupiter’s infrastructure, which includes handling slippage, priority fees, and transaction landing, and returns execution status so your UI can show progress and completion.
/ultra/v1/execute is called after the user signs the transaction in their wallet. This endpoint submits the signed transaction along with the requestId from the order response.
Optional UX Helpers
Ultra also exposes supporting endpoints that can improve the UI experience, such as token search, holdings, and token warnings. These aren’t required for the core swap lifecycle, but they’re commonly used to make a swap UI feel more complete and safer.
/ultra/v1/holdings/{walletAddress} retrieves token balances for a wallet address, including both native SOL and SPL tokens.
Sample Swap UI
This sample application is a Swap UI wired around a quote → order → sign → execute → status lifecycle. The UI components and state are already in place in modules that call the Jupiter APIs and coordinate the swap flow.
This application uses several Jupiter API endpoints to enable token swaps, fetch token lists, and retrieve wallet balances. All API calls are proxied through Next.js API routes (like /api/quote, /api/execute, /api/balances, and /api/tokens) instead of calling Jupiter's API directly from the client. This proxy pattern keeps your Jupiter API key secure on the server where it can't be exposed in the client-side code and gives you control over request handling, rate limiting, and error management.
Get Balances
The application uses Jupiter Ultra's holdings/{walletAddress} method to fetch wallet balances. When a wallet connects the application calls Jupiter Ultra API key to retrieve the wallet's native SOL and SPL tokens, with balances aggregated per mint. These balances are used to display available tokens next to swap inputs, filter the token selector to tokens with non-zero balances, validate sufficient balance before swaps, and enable the "Max" button to fill the input with the full balance.
/**
* Fetch token balances using Jupiter Ultra API via API route
* Returns both SOL and SPL token balances for a wallet
* @param walletAddress - Wallet address to fetch balances for
* @param signal - Optional AbortSignal to cancel the request
*/
export async function fetchTokenBalances(
walletAddress: string,
signal?: AbortSignal
): Promise<TokenBalance[]> {
if (!walletAddress) {
return [];
}
try {
const response = await fetch(`/api/balances?walletAddress=${encodeURIComponent(walletAddress)}`, {
cache: "no-store",
signal,
});
if (!response.ok) {
return [];
}
const balances: TokenBalance[] = await response.json();
return balances;
} catch (error) {
// Don't throw if request was aborted
if (error instanceof Error && error.name === "AbortError") {
throw error;
}
console.error("Error fetching token balances:", error);
return [];
}
}
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import { fetchTokenBalances } from "@/lib/jupiter";
import type { TokenBalance } from "@/lib/types";
export function useTokenBalances() {
const { publicKey } = useWallet();
const [balances, setBalances] = useState<TokenBalance[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Track current publicKey to prevent stale updates in refreshBalances
const publicKeyRef = useRef<string | null>(null);
useEffect(() => {
// Update ref to track current publicKey
publicKeyRef.current = publicKey?.toBase58() || null;
// Reset balances when publicKey changes
setBalances([]);
setError(null);
if (!publicKey) {
setLoading(false);
return;
}
// Create AbortController to cancel in-flight requests
const abortController = new AbortController();
let isCancelled = false;
const currentPublicKey = publicKey.toBase58();
setLoading(true);
const fetchBalances = async () => {
try {
const tokenBalances = await fetchTokenBalances(
currentPublicKey,
abortController.signal
);
// Check if request was cancelled or publicKey changed before updating state
if (isCancelled || publicKeyRef.current !== currentPublicKey) {
return;
}
setBalances(tokenBalances);
setError(null);
} catch (err) {
// Don't update state if request was aborted
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
return;
}
// Check again if cancelled or publicKey changed before setting error
if (isCancelled || publicKeyRef.current !== currentPublicKey) {
return;
}
setError(err instanceof Error ? err.message : "Failed to fetch balances");
console.error("Error fetching balances:", err);
} finally {
// Only update loading state if not cancelled and publicKey hasn't changed
if (!isCancelled && publicKeyRef.current === currentPublicKey) {
setLoading(false);
}
}
};
fetchBalances();
// Cleanup: abort in-flight requests when publicKey changes
return () => {
isCancelled = true;
abortController.abort();
};
}, [publicKey]); // Only recreate when publicKey changes
const refreshBalances = useCallback(async () => {
if (!publicKey) {
setBalances([]);
return;
}
const currentPublicKey = publicKey.toBase58();
const abortController = new AbortController();
let isCancelled = false;
setLoading(true);
setError(null);
try {
const tokenBalances = await fetchTokenBalances(
currentPublicKey,
abortController.signal
);
// Check if request was cancelled or publicKey changed before updating state
if (isCancelled || publicKeyRef.current !== currentPublicKey) {
return;
}
setBalances(tokenBalances);
setError(null);
} catch (err) {
// Don't update state if request was aborted
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
return;
}
// Check again if cancelled or publicKey changed before setting error
if (isCancelled || publicKeyRef.current !== currentPublicKey) {
return;
}
setError(err instanceof Error ? err.message : "Failed to fetch balances");
console.error("Error fetching balances:", err);
} finally {
// Only update loading state if not cancelled and publicKey hasn't changed
if (!isCancelled && publicKeyRef.current === currentPublicKey) {
setLoading(false);
}
}
}, [publicKey]);
const getBalance = useCallback((mint: string): number => {
const balance = balances.find((b) => b.mint === mint);
return balance ? balance.balance / Math.pow(10, balance.decimals) : 0;
}, [balances]); // Only recreate when balances change
return {
balances,
loading,
error,
refreshBalances,
getBalance,
};
}
With wallet balances loaded, you'll next need to fetch the verified token list to populate the swap token selectors.
Get Token List (verified)
The application uses Jupiter's GET /tokens/v2/tag?query=verified to fetch verified tokens. The token list populates both token selector dropdowns (From and To), enabling users to choose swap tokens. This ensures users only see legitimate tokens and can select any verified token as the swap destination.
/**
* Fetch token list from Jupiter Token API v2 via API route
* Uses the verified tag endpoint to get all verified tokens
* Maps v2 API response format to our Token interface
*/
export async function fetchTokenList(): Promise<Token[]> {
try {
const response = await fetch("/api/tokens", {
cache: "no-store",
});
if (!response.ok) {
// Try to parse error message from response
let errorMessage = `Failed to fetch tokens: ${response.statusText}`;
try {
const errorData = await response.json();
if (errorData.error) {
errorMessage = errorData.error;
}
} catch {
// If response is not JSON, use default message
}
throw new Error(errorMessage);
}
const tokens: Token[] = await response.json();
console.log(`Successfully loaded ${tokens.length} verified tokens`);
return tokens;
} catch (error) {
console.error("Error fetching token list:", error);
// Re-throw the error so it can be handled by the hook
throw error;
}
}
"use client";
import { useState, useEffect } from "react";
import { fetchTokenList } from "@/lib/jupiter";
import type { Token } from "@/lib/types";
const COMMON_TOKENS: Token[] = [
{
address: "So11111111111111111111111111111111111111112",
symbol: "SOL",
name: "Solana",
decimals: 9,
},
{
address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
symbol: "USDC",
name: "USD Coin",
decimals: 6,
},
{
address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
symbol: "USDT",
name: "Tether USD",
decimals: 6,
},
];
export function useTokenList() {
const [tokens, setTokens] = useState<Token[]>(COMMON_TOKENS);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadTokens = async () => {
setLoading(true);
setError(null);
try {
const tokenList = await fetchTokenList();
// Merge common tokens with fetched list, prioritizing common tokens
const commonAddresses = new Set(COMMON_TOKENS.map((t) => t.address));
const otherTokens = tokenList.filter((t) => !commonAddresses.has(t.address));
setTokens([...COMMON_TOKENS, ...otherTokens]);
} catch (err) {
console.error("Error loading token list:", err);
setError(err instanceof Error ? err.message : "Failed to load tokens");
// Fallback to common tokens only
setTokens(COMMON_TOKENS);
} finally {
setLoading(false);
}
};
loadTokens();
}, []);
const searchTokens = (query: string): Token[] => {
if (!query) return tokens.slice(0, 20); // Return first 20 tokens
const lowerQuery = query.toLowerCase();
return tokens.filter(
(token) =>
token.symbol.toLowerCase().includes(lowerQuery) ||
token.name.toLowerCase().includes(lowerQuery) ||
token.address.toLowerCase().includes(lowerQuery)
);
};
return {
tokens,
loading,
error,
searchTokens,
};
}
After users select tokens and enter an amount, the next step is fetching a quote to show them what they'll receive before executing the swap.
Get Swap Quote
The quote step displays the expected output and route details before committing to a swap. Quote retrieval is implemented in a hook and updates a quote state object for the UI to render.
export interface JupiterQuoteResponse {
inputMint: string;
outputMint: string;
inAmount: string;
outAmount: string;
otherAmountThreshold: string;
swapMode: string;
slippageBps: number;
priceImpactPct: string;
routePlan: any[];
_ultraTransaction?: string; // Ultra API: base64 encoded transaction
_ultraRequestId?: string; // Ultra API: request ID for execute endpoint
}
export interface JupiterSwapResponse {
swapTransaction: string; // base64 encoded transaction
lastValidBlockHeight: number;
priorityFeeLamports: string;
_ultraOrder?: boolean; // Flag for Ultra API
_ultraRequestId?: string; // Request ID for Ultra execute endpoint
}
/**
* Get swap quote from Jupiter Ultra API via API route
* @param inputMint - Address of the token to swap from
* @param outputMint - Address of the token to swap to
* @param amount - Amount in smallest units (e.g., lamports for SOL)
* @param slippageBps - Slippage tolerance in basis points (50 = 0.5%)
* @param taker - Optional wallet address (required for Ultra API to generate transaction)
* @param signal - Optional AbortSignal to cancel the request
*/
export async function getSwapQuote(
inputMint: string,
outputMint: string,
amount: number,
slippageBps: number = 50,
taker?: string,
signal?: AbortSignal
): Promise<JupiterQuoteResponse> {
// Build query parameters
const params = new URLSearchParams({
inputMint,
outputMint,
amount: Math.floor(amount).toString(),
slippageBps: slippageBps.toString(),
});
if (taker) {
params.append("taker", taker);
}
const response = await fetch(`/api/quote?${params.toString()}`, {
signal,
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Request failed: ${response.statusText}`);
}
const quote = await response.json();
console.log("Jupiter Ultra API Response:", quote);
return quote as JupiterQuoteResponse;
}
export function useQuote(
fromToken: Token | null,
toToken: Token | null,
amount: string
) {
const { publicKey } = useWallet();
const [quoteInfo, setQuoteInfo] = useState<QuoteInfo>(createEmptyQuoteInfo());
useEffect(() => {
// Reset quote info when dependencies change
setQuoteInfo(createEmptyQuoteInfo());
// Don't fetch if required values are missing
if (
!fromToken ||
!toToken ||
!amount ||
!publicKey ||
fromToken.address === toToken.address
) {
return;
}
const amountNum = parseFloat(amount);
if (amountNum <= 0 || isNaN(amountNum)) {
return;
}
// Set loading state
setQuoteInfo(createEmptyQuoteInfo(true));
// Create AbortController to cancel in-flight requests
const abortController = new AbortController();
let isCancelled = false;
// Debounce: wait 500ms before fetching to avoid too many API calls
const timeoutId = setTimeout(async () => {
// Check if already cancelled before starting fetch
if (isCancelled) {
return;
}
try {
// Convert amount to smallest unit (e.g., lamports for SOL)
const amountInSmallestUnit = Math.floor(
amountNum * Math.pow(10, fromToken.decimals)
);
// Fetch quote from Jupiter Ultra API with abort signal
const quote = await getSwapQuote(
fromToken.address,
toToken.address,
amountInSmallestUnit,
50, // 0.5% slippage tolerance
publicKey.toBase58(),
abortController.signal
);
// Check if request was cancelled before updating state
if (isCancelled) {
return;
}
// Convert output amount back to readable format
// Use BigInt to preserve precision when parsing large amounts (e.g., tokens with 18 decimals)
const outAmountBigInt = BigInt(quote.outAmount);
const decimalsBigInt = BigInt(10 ** toToken.decimals);
// For display, multiply by 10^6 to preserve 6 decimal places, then divide
// This preserves precision better than converting both to Number first
const displayPrecision = BigInt(1000000); // 10^6 for 6 decimal places
const scaledAmount = (outAmountBigInt * displayPrecision) / decimalsBigInt;
const outAmountNative = (Number(scaledAmount) / Number(displayPrecision)).toFixed(6);
// Calculate exchange rate
const exchangeRate =
amountNum > 0
? (parseFloat(outAmountNative) / amountNum).toFixed(6)
: "0";
// Extract route labels
const routeLabels = extractRouteLabels(quote.routePlan || []);
// Update quote info with results
setQuoteInfo({
outAmount: outAmountNative,
priceImpactPct: quote.priceImpactPct || "0",
slippageBps: quote.slippageBps || 50,
exchangeRate,
routeCount: routeLabels.length,
routeLabels,
loading: false,
error: null,
});
} catch (err) {
// Don't update state if request was aborted
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
return;
}
// Check again if cancelled before setting error
if (isCancelled) {
return;
}
setQuoteInfo({
...createEmptyQuoteInfo(),
error: err instanceof Error ? err.message : "Failed to fetch quote",
});
}
}, 500);
// Cleanup: cancel timeout and abort in-flight requests
return () => {
isCancelled = true;
clearTimeout(timeoutId);
abortController.abort();
};
}, [fromToken, toToken, amount, publicKey]);
return quoteInfo;
}
Sign Swap Transaction
Then the user clicks Swap, the application requests an Ultra order, receives the unsigned transaction payload, prompts the connected wallet to sign, and then serializes it back into a base64 payload suitable for submission.
export interface JupiterUltraOrderResponse {
inAmount: string;
outAmount: string;
priceImpactPct?: string; // May be called priceImpact in some responses
priceImpact?: string; // Alternative field name
transaction: string; // base64 encoded transaction (empty if no taker or insufficient funds)
requestId: string; // Request ID for execute endpoint
swapMode: string;
slippageBps: number;
routePlan?: any[];
error?: string; // Error message if any
errorCode?: number; // Error code if any
}
/**
* Get swap transaction from order
* For Ultra API, the transaction is already included in the order response
*/
export async function getSwapTransaction(
quote: JupiterQuoteResponse,
userPublicKey: string
): Promise<JupiterSwapResponse> {
if (!(quote as any)._ultraTransaction) {
throw new Error("Transaction not found in quote");
}
return {
swapTransaction: (quote as any)._ultraTransaction,
lastValidBlockHeight: 0,
priorityFeeLamports: "0",
_ultraOrder: true,
_ultraRequestId: (quote as any)._ultraRequestId,
} as any;
}
Once the user approves the quote and clicks Swap, the application retrieves the transaction from the order and prompts the wallet to sign it.
Execute Swap Transaction
Execution is handled by sending the signed transaction back to Jupiter Ultra along with the request identifier from the order response. When execute succeeds, the application captures the returned signature and moves into a success state so the UI can display the outcome.
export interface JupiterUltraExecuteResponse {
signature: string;
status: string;
}
//**
* Execute Ultra swap order via API route
* Sends the signed transaction to Jupiter Ultra API for execution
*/
export async function executeUltraSwap(
signedTransaction: string,
requestId?: string
): Promise<JupiterUltraExecuteResponse> {
const response = await fetch("/api/execute", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
signedTransaction,
requestId,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Request failed: ${response.statusText}`);
}
return await response.json();
}
"use client";
import { useState, useRef } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import {
getSwapQuote,
getSwapTransaction,
executeUltraSwap,
} from "@/lib/jupiter";
import type { Token, SwapStatus } from "@/lib/types";
/**
* Hook to manage swap execution flow
* Handles: quote → transaction → signing → execution
*/
export function useSwap() {
const { publicKey, signTransaction } = useWallet();
const [status, setStatus] = useState<SwapStatus>("idle");
const [error, setError] = useState<string | null>(null);
const [txSignature, setTxSignature] = useState<string | null>(null);
const [estimatedOutput, setEstimatedOutput] = useState<string | null>(null);
// Synchronous guard to prevent concurrent swap executions
// This ref is updated immediately (synchronously), unlike state which is batched
const isExecutingRef = useRef(false);
const executeSwap = async (
fromToken: Token,
toToken: Token,
amount: number
) => {
if (!publicKey || !signTransaction) {
throw new Error("Wallet not connected");
}
// Synchronous guard: prevent concurrent executions
// This check happens immediately, before any async operations or state updates
if (isExecutingRef.current) {
throw new Error("Swap already in progress");
}
// Set guard synchronously to prevent race conditions
isExecutingRef.current = true;
setStatus("quoting");
setError(null);
setTxSignature(null);
try {
// Step 1: Convert amount to smallest unit (e.g., lamports for SOL)
const amountInSmallestUnit = Math.floor(
amount * Math.pow(10, fromToken.decimals)
);
// Step 2: Get swap quote from Jupiter Ultra API
const quote = await getSwapQuote(
fromToken.address,
toToken.address,
amountInSmallestUnit,
50, // 0.5% slippage tolerance
publicKey.toBase58()
);
// Store estimated output for display
// Use BigInt to preserve precision when parsing large amounts (e.g., tokens with 18 decimals)
const outAmountBigInt = BigInt(quote.outAmount);
const decimalsBigInt = BigInt(10 ** toToken.decimals);
// For display, multiply by 10^6 to preserve 6 decimal places, then divide
// This preserves precision better than converting both to Number first
const displayPrecision = BigInt(1000000); // 10^6 for 6 decimal places
const scaledAmount = (outAmountBigInt * displayPrecision) / decimalsBigInt;
setEstimatedOutput(
(Number(scaledAmount) / Number(displayPrecision)).toFixed(6)
);
// Step 3: Get transaction from quote (Ultra API includes it in quote)
setStatus("signing");
const swapResponse = await getSwapTransaction(quote, publicKey.toBase58());
// Step 4: Deserialize and sign the transaction
// NOTE: This is the ONLY place using @solana/web3.js v1. Required because:
// - @solana/wallet-adapter-react expects v1 VersionedTransaction for signTransaction()
// - Jupiter Ultra API returns transactions in v1 format
// - @solana/kit (v2) uses a different transaction model incompatible with wallet adapters
const { VersionedTransaction } = await import("@solana/web3.js");
const transaction = VersionedTransaction.deserialize(
Buffer.from(swapResponse.swapTransaction, "base64")
);
const signedTransaction = await signTransaction(transaction);
// Step 5: Execute the signed transaction via Jupiter Ultra API
setStatus("executing");
const signedBuffer = signedTransaction.serialize();
const executeResponse = await executeUltraSwap(
Buffer.from(signedBuffer).toString("base64"),
(swapResponse as any)._ultraRequestId
);
// Success!
setTxSignature(executeResponse.signature);
setStatus("success");
} catch (err) {
setStatus("error");
setError(err instanceof Error ? err.message : "Swap failed");
throw err;
} finally {
// Always reset the guard, even if an error occurred
isExecutingRef.current = false;
}
};
const reset = () => {
setStatus("idle");
setError(null);
setTxSignature(null);
setEstimatedOutput(null);
// Reset the execution guard when manually resetting
isExecutingRef.current = false;
};
return {
executeSwap,
status,
error,
txSignature,
estimatedOutput,
reset,
};
}
With the transaction signed and executed, the swap is complete and the UI displays the transaction signature, allowing users to verify their swap on-chain. This completes the full Ultra swap lifecycle: quote → order → sign → execute.
Ultra vs. Metis
Ultra is Jupiter's newer way to integrate swaps. Choose Ultra when you want the cleanest, fastest app integration and you’re happy delegating more of the swap execution complexity to Jupiter.
Metis is the original, lower-level routing engine that exposes more granular control over swap construction and execution. Choose Metis API when you want maximum control for assembling raw swap instructions, adding custom instructions/CPI, or owning your own transaction sending and confirmation strategy.
As of late 2025, Jupiter has transitioned Metis into an independent public good while continuing best-effort support and maintenance.
Wrapping Up
Your Swap UI is ready to handle the complexity of token discovery, routing, slippage handling, and transaction management into a streamlined swap experience powered by Jupiter Ultra, while the clean lifecycle of quote → order → sign → execute gives you a solid foundation to extend.
Frequently Asked Questions
What is the Jupiter Ultra API?
The Jupiter Ultra API is a DEX aggregator on Solana that provides optimal swap routes across multiple liquidity sources for token swaps.
What is Jupiter Ultra Swap and how does it work?
Jupiter Ultra Swap is Jupiter's simplified API for integrating token swaps into Solana dApps. Instead of manually building swap transactions, Ultra provides a streamlined lifecycle where you request a quote to preview swap details, receive a pre-built transaction payload ready for signing, making it easier to build reliable swap UIs that work under real-world conditions.
What's the difference between Jupiter Ultra and Metis API?
Jupiter Ultra is the newer, higher-level integration option designed for developers who want the fastest, cleanest app integration and are comfortable delegating swap execution complexity to Jupiter. Metis is the original lower-level routing engine that provides granular control over swap construction, custom instructions, CPI calls, and transaction sending strategies.
Is Jupiter Ultra Swap available on Solana testnet or only mainnet?
Jupiter Ultra Swap is only available on Solana mainnet-beta. This means any swaps executed using Ultra will trade real tokens and incur real network fees. Developers should be aware that following this guide and executing swaps will involve actual financial transactions, with potential for value loss due to price movement, slippage, or incorrect token pair selection.
What do I need to integrate Jupiter Ultra Swap into my Solana dApp?
To integrate Jupiter Ultra Swap, you'll need a Jupiter API key, a Quicknode Solana RPC endpoint, basic understanding of TypeScript dApp development, and small amounts of mainnet SOL and tokens for testing.
What are verified tokens and how does Jupiter Ultra use them?
Verified tokens are tokens that have been validated by Jupiter's token verification system, ensuring they meet certain legitimacy and safety criteria. Jupiter Ultra uses Jupiter's Tokens API v2 to filter by status and fetch only verified tokens for your swap UI, protecting users from scams, fake tokens, and malicious contracts.
Resources
- Jupiter Ultra Swap Demo App
- Jupiter Ultra Swap API Developer Docs
- Jupiter Tokens API V2 (Beta)
- Metis Solana Trading API Guide
- Quicknode Metis Jupiter Swap API Add-on
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.