Jupiter (Solana) — provider dossier
Provider id jupiter, displayed as “Jupiter (Metis)”. Adapter: packages/swap/src/providers/jupiter.ts
(+ jupiter-dexes.ts) for price discovery. Execution is enabled since 2026-11-10 (V5) for one transaction shape only —
see §10 (packages/core/src/swap-sol.ts, services/api/src/swap-exec-sol.ts). The quote adapter itself is unchanged
and still classifies its raw answer as opaque; the API marks the route executable and the verification happens when a
swap is prepared. §1–§9 below describe the provider as surveyed before execution; where §10 supersedes them it says so.
Everything below was checked on 2026-11-10 against the official documentation (https://developers.jup.ag/docs, which
https://dev.jup.ag redirects to; every page has a .md twin) and against the live API. Statements marked (measured)
come from live answers, the rest from the cited page. Statements marked (pinned by test) are asserted on the recorded
fixtures in packages/swap/test/jupiter.test.ts.
1. Which API
| API | Base URL | Status (docs) | Used here |
|---|---|---|---|
Swap API V2 — Router: GET /build |
https://api.jup.ag/swap/v2 |
current; Metis on-chain routing only, no Jupiter swap fee, returns quote + raw instructions | yes |
Swap API V2 — Meta-Aggregator: GET /order + POST /execute |
same | current, recommended by Jupiter; Metis + JupiterZ RFQ + Dflow + OKX; Jupiter platform fee; transaction cannot be modified and is landed by Jupiter | no (see §9) |
Swap API V1 (“Metis Swap API”): GET /quote, POST /swap, POST /swap-instructions |
https://api.jup.ag/swap/v1 |
“no longer actively maintained and has been superseded by Swap V2”; still answers keyless (measured) | no |
lite-api.jup.ag |
— | being phased out: “replaced by keyless requests on api.jup.ag”, rate limit reduced progressively until retired; still answers (measured) |
no |
quote-api.jup.ag/v6 |
— | gone (connection refused, measured) | no |
Sources: Swap overview, Build, GET /build reference, Metis → Router migration, V1 quote reference, Developer Platform migration.
Keys. None required: “a request with no x-api-key header is treated as keyless”. The adapter sends x-api-key only
if JUPITER_API_KEY is set (a free key raises the limit to 1 request/s; paid tiers 10/50/150). The wallet did not sign up
for anything.
Rate limits (Rate Limits). Keyless: 0.5 request/s; documented as
30 per 60-second sliding window, measured as 5 requests per ~10-second sliding window (the 6th answers
429 {"code":429,"message":"[API Gateway] Too many requests"}; no lock-out afterwards). All main endpoints share one
bucket (/build, V1 /quote, /program-id-to-label, Price, Tokens…). /swap/v2/order and /execute showed a separate
20-request bucket (measured header). Headers on 200 and 429: x-ratelimit-remaining, x-ratelimit-current,
x-ratelimit-reset (Unix seconds when one slot frees). The runtime must therefore run this provider at
JUPITER_KEYLESS_POLICY (ratePerSec: 0.5, burst: 4), not the default 2 requests/s.
Latency (measured, from Québec, keyless, 2026-11-10): 40–140 ms per /build call, median ≈ 60–80 ms, answers of
14–68 kB (the instructions and lookup tables dominate).
2. The quote call
GET https://api.jup.ag/swap/v2/build?inputMint=<mint>&outputMint=<mint>&amount=<base units>&taker=<wallet>&slippageBps=<0..10000>/buildis ExactIn only (noswapMode);takeris required and must be a valid public key (no balance needed).- Native SOL is the wrapped-SOL mint
So11111111111111111111111111111111111111112;wrapAndUnwrapSoldefaults totrue, so the answer contains the wrap (input) or unwrap (output) instructions. - Parameters the adapter does not send (defaults apply):
mode=fast,dexes/excludeDexes,maxAccounts(64),platformFeeBps+feeAccount,payer,destinationTokenAccount/nativeDestinationAccount,blockhashSlotsToExpiry(150),tipAmount,computeUnitPricePercentile,forJitoBundle,transactionVersion(0),slippageBps=rtse. restrictIntermediateTokensandonlyDirectRoutesexist only on V1/quote;/buildhas no equivalent (it chooses intermediate tokens dynamically). The closest levers aremaxAccountsanddexes.
Answer fields and how the adapter maps them:
| Field | Meaning | Mapping |
|---|---|---|
inputMint, outputMint, inAmount, swapMode, slippageBps |
echo of the request | must equal the request, else schema error |
outAmount (string, base units) |
expected output | amountOut (BigInt from the string) |
otherAmountThreshold |
“minimum output amount after slippage” = outAmount − floor(outAmount × slippageBps / 10 000) (measured on every sample) |
minAmountOut, taken as given but only if within [floor(out × (10 000 − bps) / 10 000), out] |
priceImpactPct |
decimal ratio string ("0.001" = 0.1 %) |
priceImpactBps = ratio × 10 000 (2 decimals) |
routePlan[] |
legs: swapInfo{ammKey,label,inputMint,outputMint,inAmount,outAmount}, bps (share of the amount entering that leg's input mint; canonical in V2), percent (legacy) |
one RouteStep; via = labels; contracts = aggregator program (router), DEX programs of the labels (program), pool accounts ammKey (vault) |
computeBudgetInstructions |
one SetComputeUnitPrice (micro-lamports/CU); no compute-unit limit |
network-fee ceiling (see §6) |
setupInstructions, swapInstruction, cleanupInstruction, otherInstructions, tipInstruction, addressesByLookupTableAddress |
the transaction material (§3) | validated structurally, kept in raw |
blockhashWithMetadata{blockhash[32], lastValidBlockHeight, fetchedAt} |
recent blockhash, valid ~150 slots | expiresAt = min(now, fetchedAt) + 30 s |
transactionVersion |
0 |
must be 0 |
- Platform fees.
/buildhas noplatformFeefield unless an integrator fee is requested (platformFeeBps+feeAccount, taken inside the swap instruction). The adapter requests none and rejects an answer carrying one. On V1/quotethe field isplatformFee: {amount, feeBps} | null; on/orderit isfeeBps,feeMint,platformFee. - Validity. Jupiter states none for the price. The only hard limit is the blockhash (
lastValidBlockHeight, ≈ 150 slots ≈ 60 s); the adapter uses the contract's conservative 30 s. - Duration. None given →
estimatedSeconds: null(a Solana transaction confirms in seconds, but that is not the provider's figure). - No route =
HTTP 400 {"error":"No routes found"}(dust amounts, unknown mints) or{"error":"Insufficient liquidity"}(size) → empty array. Other 400s are failures:Invalid amount,Invalid taker,inputMint cannot be same as outputMint, a Zod error object for out-of-range parameters, and — observed once during the smoke run —{"error":"500: {\"error\":\"Oracle price out of date. Pair temporarily unavailable\"}"}(transient, the retry seconds later succeeded).
The route may pass through mints that are not in the wallet registry (USD1, cbBTC, mSOL… seen live) and may split
across parallel legs. Because the whole route is one atomic instruction and the intermediates never stay in the wallet,
the quote exposes a single swap step from the requested input to the requested output; the legs stay in raw.routePlan.
The adapter checks that the legs leaving the input mint sum to inAmount and the legs reaching the output mint sum to
outAmount.
What the adapter refuses locally (empty array, no HTTP call): recipient ≠ sender (a /build swap pays the taker's own
token account), a sender that is not a Solana address, non-positive or > u64 amounts, slippage that is not an integer in
0..10 000, any asset outside the registry.
3. Transaction shape (what execution would have to sign)
/build returns instructions, not a transaction. The integrator assembles, in this order
(Common Instructions): compute-budget instructions
(plus a SetComputeUnitLimit it must add itself after simulation) → setupInstructions → swapInstruction →
cleanupInstruction → otherInstructions → tipInstruction; fee payer = taker; lifetime = the returned blockhash.
- Versioned transaction, v0, with address lookup tables.
addressesByLookupTableAddressmaps each ALT address to its full address list, so no RPC fetch is needed to compile — but it also means the ALT content comes from Jupiter: a verifier must either fetch the tables from its own RPC or treat the listed addresses as untrusted input and check the resolved accounts. Recorded answers used 1 to 5 tables and 24 to 81 accounts in the swap instruction (pinned by test).transactionVersion=1requests a Solana v1 transaction (SIMD-0385: 4 096 bytes, ≤ 64 inline accounts, no ALT, compute budget on the message config,@solana/kit≥ 8) — Transaction Versions. Legacy transactions are not offered by V2 (asLegacyTransactionwas V1). - Wrap / unwrap. SOL input:
ATA.CreateIdempotent(taker's wSOL account) +System.Transferof exactlyinAmountlamports into it +Token.SyncNative; cleanupToken.CloseAccount(wSOL account → taker). SOL output: cleanupToken.CloseAccountof the wSOL account back to the taker (pinned by test). - Token-account creation.
ATA.CreateIdempotent(instruction tag 1), payer and owner = taker, for the destination mint — under the Token-2022 program for PYUSD/USDG — and, on some multi-hop routes, also for intermediate mints (a taker USDC and USDT account were created on a recorded SOL → PYUSD route). Each new account costs ≈ 0.00204 SOL of rent paid by the taker and is left open (rent recoverable by closing it). Jupiter does not report this cost. - Compute budget. Only the price is given (50th percentile of recent fees by default); the limit must come from the
integrator's own
simulateTransaction(docs: 1.2 × consumed, max 1 400 000) and the price should be clamped — Compute Units.
Legacy / alternative endpoints (documented, not used)
- V1
POST /swap/v1/swap{quoteResponse, userPublicKey, wrapAndUnwrapSol, useSharedAccounts, feeAccount, prioritizationFeeLamports, asLegacyTransaction, destinationTokenAccount, nativeDestinationAccount, dynamicComputeUnitLimit, dynamicSlippage, …}→{swapTransaction (base64, unsigned), lastValidBlockHeight, prioritizationFeeLamports}. V1POST /swap/v1/swap-instructions(same body) →computeBudgetInstructions, setupInstructions, swapInstruction, cleanupInstruction, otherInstructions, addressLookupTableAddresses(addresses only: the tables must be fetched by RPC). V2/build= V1/quote+/swap-instructionsin one call, with V2 instructions. - V2
GET /swap/v2/order(withtaker) →{transaction (base64, unsigned, v0 or v1), requestId, router, outAmount, feeBps, feeMint, lastValidBlockHeight | expireAt, errorCode…}; thenPOST /swap/v2/execute {signedTransaction, requestId}→{status: "Success"|"Failed", signature, code, totalInputAmount, inputAmountResult, outputAmountResult, totalOutputAmount}— Order & Execute.
4. Programs involved
| Program | Address | Role | Source |
|---|---|---|---|
| Jupiter v6 swap aggregator | JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 |
the swap instruction of every recorded answer | named in the Jupiter docs (JupiterZ RFQ v2); IDL in jup-ag/jupiter-cpi-swap-example (same address); audits: Offside Labs 2024 and 2025 |
| System | 11111111111111111111111111111111 |
SOL transfer into the wSOL account | Solana |
| SPL Token | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
SyncNative, CloseAccount, classic mints | Solana |
| Token-2022 | TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb |
PYUSD, USDG | Solana |
| Associated Token Account | ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL |
CreateIdempotent | Solana |
| Compute Budget | ComputeBudget111111111111111111111111111111 |
priority-fee price | Solana |
AMM programs. The route plan names venues by label; the program behind each label comes from Jupiter's own
GET /swap/v2/program-id-to-label (reference):
110 programs / 105 labels on 2026-11-10, snapshot in jupiter-dexes.ts and in the fixture program-id-to-label.json
(a test checks they agree and that, for every recorded leg, a program of its label really is among the swap instruction's
accounts). Venues seen in recorded and smoke routes, with their program ids from that endpoint:
| Label | Program | Label | Program |
|---|---|---|---|
| Whirlpool (Orca) | whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc |
Raydium CLMM | CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK |
| Raydium CP | CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C |
Raydium | 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 |
| Meteora DLMM | LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo |
Meteora | Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB |
| Manifest | MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms |
PancakeSwap | HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq |
| GoonFi V2 | goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE |
BisonFi | BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi |
| ZeroFi | ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY |
Kipseli | 3TK9D8aoBFYjYZtKCjciPrVrRStsnvo7KmpcJqDavpaU |
| Byreal | REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2 |
Scorch | ojh19ojaKduoJZuaJADhcVGp4xt1TcdAvZmpVsCorch |
| Deriverse | DRVSpZ2YUYYKgZP8XtLhAGtT1zYSCKzeHfb4DgRnrgqD |
JupLend AMM | jupZ4m2GqUCJ5iueMfzQf8khFfH31d4XAQt3RzCT9Vd |
| Saber | SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ |
DefiTuna | fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9 |
| 1DEX | DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm |
Perena | NUMERUNsFCP3kuNmWZuXtm1AaQCPj9uw6Guv2Ekoi5P |
| Token Swap | SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8 |
Orca V1 | DjVE6JNiYqPL2QXyCUUh8rNjHrbz9hXHNYt99MQ59qw1 |
The source for every one of these addresses is Jupiter's endpoint, not each DEX's own documentation: an execution phase
that allowlists AMM programs should confirm each against its publisher. The set is open-ended (Jupiter lists new venues
continuously; many of the busiest ones — GoonFi, BisonFi, ZeroFi, Kipseli, Scorch — are proprietary market-maker programs
with no public source), so an allowlist of AMMs will either reject many best routes (restrict with dexes=) or be
permanently behind.
5. What an isolated signer could verify by itself
Recipient and minimum output are not free-form: they sit at fixed places in the swap instruction. Observed on every
recorded answer and pinned by test (structure of the recorded instructions):
swapInstruction.programId= the aggregator; Anchor discriminatorbb64facc31c4af14(route_v2) ord19853937cfed8e9(shared_accounts_route_v2, preceded by a 1-byte authority id).- Data, little-endian, after the discriminator (and id):
in_amount u64,quoted_out_amount u64,slippage_bps u16,platform_fee_bps u16(0), a thirdu16(0, positive-slippage fee), then the route-plan vector (u32length = number of legs; per leg a venue enum,bps u16, input index, output index). So the instruction carries the quoted output and the slippage, and the program enforcesquoted_out × (1 − slippage)(error 6001SlippageToleranceExceededin the IDL);otherAmountThresholditself is not in the data. - Accounts:
route_v2→[0]taker (only signer),[1]taker's source token account,[2]taker's destination token account,[3]input mint,[4]output mint;shared_accounts_route_v2→[1]taker,[2]source,[5]destination,[6]input mint,[7]output mint. Source and destination are the taker's associated token accounts (derived with the mint's own token program), which the signer can recompute. - Setup/cleanup contain only:
ATA.CreateIdempotentfor the taker's own accounts,System.Transfertaker → taker's wSOL account for exactlyinAmount,Token.SyncNative,Token.CloseAccountwSOL → taker.
Caveat (resolved in §10.2): the V2 instructions (route_v2, shared_accounts_route_v2) are not in the IDL file
published on GitHub (it stops at route / shared_accounts_route, whose layout differs: route plan first,
platform_fee_bps u8), but they are in the program's on-chain IDL, which confirms the layout above.
So a signer could rebuild the compute-budget, ATA, wrap and unwrap instructions itself, and check in the swap
instruction: program id, discriminator, input amount, quoted output, slippage, zero platform fee, taker as sole signer,
source/destination = its own ATAs, mints. It cannot rebuild or meaningfully check the remaining 20–75 accounts and the
venue-specific route data (pools, vaults, oracles, proprietary AMMs): a hostile answer could still route through a
malicious program placed among them. That is why the class is opaque, not verified.
Post-conditions verifiable by simulation (own RPC, simulateTransaction with replaceRecentBlockhash, requesting the
post-state of the taker's accounts): (1) no error; (2) the taker's input balance decreases by exactly inAmount (plus
fees and rent when the input is SOL); (3) the destination token account — or lamports, for SOL — increases by at least
minAmountOut; (4) no other token account owned by the taker loses balance, and none gets a new delegate or owner;
(5) lamport decrease ≤ fee + rent for the accounts created; (6) every writable signer-owned account is one of the expected
ATAs; (7) compute units consumed, to set the limit. Simulation is advisory (state can change before landing); the
on-chain slippage check is what binds. A Token-2022 mint with a non-zero transfer fee
would make (3) differ from the quote: the mint's fee configuration has to be read on-chain by the wallet (not checked in
this phase).
Token-2022 (PYUSD, USDG). The Swap docs make no statement about Token-2022 on /build (their only mention, “Supports
SPL and Token2022 tokens”, concerns referral fees on /order). That Jupiter routes both mints is established by live
answers (measured, both directions, against SOL, USDC and each other): the routes go through Manifest, Whirlpool and
others, the swap instruction references the Token-2022 program, and the destination account is created under it (pinned
by test). No Token-2022 swap was executed.
Approvals: none. Solana has no allowance step; the taker signs the transaction that moves its own tokens.
6. Fees
- Jupiter: none on
/build. (On/order: 0 bps pegged pairs/Jupiter tokens, 2 bps SOL↔stable, 5 bps LST↔stable, 10 bps otherwise, 50 bps for tokens < 24 h old, shown asfeeBps/feeMint.) - Integrator fee: optional
platformFeeBps+feeAccounton/build(any token account of the integrator); not used. - AMM / LP fees: inside the quoted
outAmount; V2 route plans no longer itemise them. - Network: 5 000 lamports per signature + priority fee (
price × compute-unit limit). The quote reports onenetworkFeeItem, estimated, a ceiling:5 000 + ceil(price × 1 400 000 / 10⁶)lamports — typically 6 000–18 000 lamports (< 0.002 USD). Real cost is lower once the limit is simulated. Not included: rent for token accounts the swap creates (≈ 0.00204 SOL each, sometimes for intermediate mints, see §3) — the largest real cost for a first swap. /submitlanding (optional): a Jupiter tip of at least 0.001 SOL per transaction.
7. Sending, status and tracking
/buildtransactions are sent through the wallet's own RPC (sendTransaction) and tracked withgetSignatureStatuses/ blockhash expiry (lastValidBlockHeight); Jupiter offers no status endpoint for them (“/buildtransactions cannot use/execute”).- Optional landing service:
POST https://tx.jup.ag(JSON-RPCsendTransaction, base64) — requires an API key and a tip ≥ 1 000 000 lamports to one of 16 tip accounts (tipAmounton/buildadds the instruction); send-only, no confirmation queries — Transaction Submission. /orderpath:POST /executelands, polls and returns the final status with the executed amounts (codes: 0 success, −1 unknownrequestId, −1000 failed to land, −1005 expired, −2003 RFQ quote expired…).- Every response carries
x-api-gateway-request-id(the id to quote to support). API status: https://status.jup.ag. Breaking changes are announced on Discord / Telegramjup_dev.
Refunds. Not applicable: a swap is one atomic transaction. If the minimum is not met or any leg fails, the whole
transaction reverts and only the network fee is spent (atomic: true, refundable: true). An unsent or expired
transaction moves nothing.
8. Terms of use (SDK & API License Agreement)
https://developers.jup.ag/docs/legal/sdk-api-license-agreement — points that constrain this wallet:
- §2.3 labelling: the specific API used must be shown prominently to end users; anything other than Ultra must be labelled “Metis”. The provider name is “Jupiter (Metis)”; the UI must keep it.
- §3.2 f: no access “for competitive analysis” and no dissemination of “performance information (including uptime, response time and/or benchmarks)”. The latency figures above and the provider statistics of the runtime must stay internal.
- §3.2 g: no combining API content “with content obtained through scraping or any other means outside the API”. A multi-provider comparison screen and the CoinGecko check in the smoke script sit close to this clause — to be confirmed with Jupiter (info@jup.ag, §2.4) before a public release.
- §3.2 d/e: no resale of API content; no usage beyond “reasonable request volume” — keep to the rate limit.
- §5.2 / §12: no availability guarantee; Jupiter may change or stop the API without notice.
- §2.5 / §7.3: the licensee is responsible for sanctions and KYC/AML compliance towards its users.
9. Not fitted / open points
/order(Meta-Aggregator) is deliberately not used: its transaction cannot be modified, it is co-signed and landed by Jupiter or a market maker, and withouttakerit returnsslippageBps: 0withotherAmountThreshold = outAmount(no guaranteed minimum). It often quotes slightly better (RFQ) but charges the platform fee.- The quote needs the wallet's real address (
taker), so it is sent to Jupiter with every price check. - A third-party recipient is possible on
/build(destinationTokenAccount/nativeDestinationAccount) but would need ATA derivation in the adapter; refused for now. - The contract's one-step-per-hop model does not fit split routes through unregistered mints; one step is reported.
- Fixtures are re-recorded with
npx tsx scripts/record-swap-jupiter-fixtures.mts(the 429 and the stale-oracle answers were captured separately); live check:npx tsx scripts/smoke-swap-jupiter.mts.
10. Execution (V5, 2026-11-10)
Scope: swaps between SOL and the SPL tokens of the wallet registry on Solana, exact input, output to the wallet itself.
Proof: npx tsx scripts/sim-jupiter.mts (mainnet simulation only; nothing was ever sent). Statements marked
(simulated) come from that script's output on 2026-11-10; (on-chain) from account data read on
api.mainnet-beta.solana.com and solana-rpc.publicnode.com; (docs) from the cited page.
10.1 Sources read
- GET /build reference (parameters
taker,slippageBps,transactionVersion,wrapAndUnwrapSol,platformFeeBps/feeAccount,payer,destinationTokenAccount,nativeDestinationAccount,maxAccounts,dexes/excludeDexes,tipAmount,computeUnitPricePercentile), Build, Common Instructions, Transaction Versions (v0 and v1), Compute Units, Rate Limits, SDK & API License Agreement. The V1 parametersuserPublicKey,asLegacyTransaction,useSharedAccounts,dynamicComputeUnitLimit,prioritizationFeeLamportsbelong toPOST /swap/v1/swap[-instructions]("no longer actively maintained") and are not parameters of/build; passinguseSharedAccounts=trueto/buildhad no visible effect (measured). - The aggregator program
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4(on-chain): owned byBPFLoaderUpgradeab1e11111111111111111111111, ProgramData4Ec7ZxZS6Sbdg5UGSLHbAnM7GQHp2eFd4KYWRexAipQT, last deployed at slot 462802085, executable 2 892 224 bytes, SHA-25653d53f447f594158a259cbdca1f5e613caf40bc14ce759ffd5953580dc2af8dd, upgrade authorityCvQZZ23qYDWF2RUpxYJ8y9K4skmuvYEEjH7fK58jtipQ(a system-owned account with no data; whether a multisig stands behind it was not established). Identical on both endpoints. - Its on-chain Anchor IDL (account
C88XWfp26heEmDkmfSzeXP7Fd7GQJ2j9dDTUsyiZbUTa, zlib JSON, 36 kB, IDL authority9u9iZBWqGsp5hXBxkVZtBTuLSGNAG9gEQLgpuVw39ASg). No source code of the program was found or read: everything about its behaviour below comes from the IDL and from simulation.
10.2 The instructions (on-chain IDL)
| Instruction | Discriminator | Arguments (little-endian) | Fixed accounts |
|---|---|---|---|
route_v2 |
bb64facc31c4af14 |
in_amount u64, quoted_out_amount u64, slippage_bps u16, platform_fee_bps u16, positive_slippage_bps u16, route_plan Vec<RoutePlanStepV2> |
[0] user_transfer_authority (signer), [1] user_source_token_account, [2] user_destination_token_account, [3] source_mint, [4] destination_mint, [5] source_token_program, [6] destination_token_program, [7] destination_token_account (optional), [8] event_authority, [9] program |
shared_accounts_route_v2 |
d19853937cfed8e9 |
id u8, then the same |
[0] program_authority, [1] user_transfer_authority (signer), [2] source_token_account, [3] program_source_token_account, [4] program_destination_token_account, [5] destination_token_account, [6] source_mint, [7] destination_mint, [8] source_token_program, [9] destination_token_program, [10] event_authority D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf, [11] program |
Also present and never signed: route, route_with_token_ledger, shared_accounts_route, exact_out_route,
shared_accounts_exact_out_route, exact_out_route_v2, shared_accounts_exact_out_route_v2, route_with_attestation,
shared_accounts_route_with_attestation, and the housekeeping instructions. RoutePlanStepV2 = { swap: Swap (enum of venues), bps u16, input_index u8, output_index u8 }; the plan stays opaque to the signer. Derived and pinned by test on
every recorded answer: program authority = PDA ["authority", id]; the program's own token accounts [3], [4] = its
associated token accounts for the two mints.
Minimum output (simulated). With the real output A of a still pool: quoted = A, 0 bps succeeds; quoted = A + 1, 0 bps fails with 6001 SlippageToleranceExceeded; at 1 bp a quoted amount whose exact minimum is A + 0.48 fails. The
program therefore enforces ceil(quoted × (10 000 − bps) / 10 000) at the destination account (the same figure as the
API's otherAmountThreshold). The signer uses floor(…), a lower bound of it; the approved minimum shown to the owner
is that floor (one base unit under Jupiter's figure when the product is not whole).
10.3 Why only shared_accounts_route_v2 is signed
In route_v2 the venue programs of each leg are invoked with the wallet's signature and take their input straight from
whatever accounts the route lists. The program does not tie those legs to the declared source account: a genuine
USDT → SOL route_v2, re-declared as "USDC → SOL, source = the wallet's USDC account" by editing accounts [1] and
[3] only, executes and debits 20 USDT (simulated). A signer that checks the declared accounts of a route_v2
would be approving something else. Beyond that, route_v2 lends the wallet's signature to the venue programs of the route
(about 110 are listed by Jupiter, several without public source), with accounts chosen by the route builder. No offline
check makes that comparable to the EVM path, so route_v2 is refused whatever it declares.
In shared_accounts_route_v2 the declared source account is bound: the same re-declaration fails inside the aggregator
with the token program's "Account not associated with this Mint" (simulated), i.e. the aggregator itself moves the
input out of the declared account, and the simulated debit is exactly in_amount. In the routes Jupiter builds this way
the legs name the program authority and the program's own token accounts, not the wallet. The signer additionally
requires that neither the wallet nor its source/destination account appears among the route's accounts, so that no
venue can be handed the wallet's signature (Jupiter does build some shared-accounts routes with the wallet inside a leg —
seen in routes that included Kipseli, Perena or Bonkswap legs — those are refused too).
Consequence and deviation. /build answers direct routes (the usual case for SOL ↔ USDC and other liquid pairs) with
route_v2, and has no documented parameter to ask for the other form. The API therefore rewrites a route_v2
instruction into shared_accounts_route_v2 (jupiterRouteToShared: same route plan; discriminator and id byte; the
wallet and its token accounts inside the legs replaced by the program authority and the program's own token accounts,
including for intermediate mints) and keeps it only if the exact bytes simulate successfully. This is not a documented
Jupiter feature; it relies on the two instructions sharing their route format, which held for the routes met in the proof
runs (most recorded pairs were rewritten) and fails cleanly — simulation error, then another route is requested without the
venues already tried, at most three /build calls — for venues that need the wallet inside a leg. The signer does not care
who built the instruction: it checks the result.
10.4 Lookup tables: none
A v0 transaction refers to most accounts through address lookup tables whose content the signer cannot read offline; the
API would have to supply it, and a table can be created by anyone. Rather than reason about what may hide behind a table,
the wallet uses Solana v1 transactions (SIMD-0385, live on
mainnet: confirmed v1 Jupiter transactions were read back from both RPC endpoints): 4 096 bytes, at most 64 accounts,
all inline, compute budget in the message header. /build?transactionVersion=1 returns instructions for it. The signer
refuses any message whose first byte is not 0x81 — legacy and v0 alike — so there is no table to trust. With every
account visible, the signer also refuses a transaction that references any other registry token account of the wallet.
Recorded messages: 20–61 accounts, 845–2 290 bytes. Routes that need more than 64 accounts cannot be expressed and are not
offered.
Wire format (hand-written in swap-sol.ts, cross-checked against @solana/kit 8.6 in tests): 0x81, header (3 bytes),
config mask u32 (must be 0x0f: priority fee, compute-unit limit, loaded-accounts data limit; no heap request), blockhash,
instruction count, account count, accounts, config values (u64 lamports, u32, u32), instruction headers, payloads;
the transaction is the message followed by the 64-byte signature. The node accepts the signer's signature and rejects it
with one bit flipped (simulated with sigVerify: true).
10.5 What the signer guarantees before signing (verifyJupiterSwap)
From the message bytes alone, with the intent the owner approved by passkey (which commits to the SHA-256 of those bytes):
- v1 message, exact length, no trailing bytes, unique accounts; one required signature, the fee payer, = the wallet; the blockhash is the approved one and not zero.
- Compute budget: limit and priority fee equal the approved ones (
limit ≤ 400 000, fee =ceil(limit × price / 10⁶)); 5 000 + priority fee + rent bound ≤ the approved fee ceiling, itself ≤ the policy ceiling for Solana (0.003 SOL). - Instruction list, in this order and nothing else:
ATA.CreateIdempotentfor the wallet's own associated token account of the input or output mint (payer and owner = wallet) · when SOL is sold,System.Transferwallet → its own wSOL account for exactly the approved amount thenToken.SyncNative· one Jupiter instruction · when SOL is sold or bought,Token.CloseAccountof the wallet's wSOL account to the wallet. No other program, no ComputeBudget instruction, no transfer, approve, setAuthority or close for anyone else. - Jupiter instruction: pinned program id;
shared_accounts_route_v2only; the twelve fixed accounts are exactly the derived ones (program authority forid, wallet, wallet's source ATA, program's source and destination accounts, wallet's destination ATA, the two registry mints — wSOL for SOL —, their token programs, event authority, program);in_amount= approved amount;floor(quoted_out × (10 000 − slippage_bps) / 10 000)≥ approved minimum;platform_fee_bps = 0;positive_slippage_bps = 0; route accounts contain neither the wallet nor its two accounts. - Both assets are registry assets on Solana and differ;
to = from; policy limits as for a send (per operation, per day, fee ceiling, approval window, halt); one signature per proposal.
Anything else is refused: see the mutations in the proof script and in the unit tests. Several of them would have executed on-chain (another destination account, a halved minimum, a doubled input, an extra transfer, approve or third-party close) — the checks are what stops them.
10.6 Residual trust (what the signer cannot prove)
- The aggregator program's own behaviour. Its source was not read. That it takes exactly
in_amountfrom the declared source and reverts unless the declared destination receives the minimum is established by its IDL, by mainnet simulation of the recorded pairs, and by the failing mutations above — not by reading code, unlike the KyberSwap router. In particular, that the aggregator never passes the wallet's signature to a venue when the wallet is absent from the route accounts follows from how its routes are built and from Solana's rule that a program can only pass on accounts it lists; it was not read in code. - Upgradeability. The program can be replaced by its upgrade authority at any time; a replaced program could do
anything the wallet's signature allows on the accounts named in the transaction. Mitigation:
JUPITER_PIN(ProgramData address, last-deploy slot, upgrade authority, executable hash). The API refuses to prepare a swap unless the ProgramData header shows the pinned slot and authority on both RPC endpoints, and re-hashes the 2.9 MB executable on both at first use and every six hours. This is an API-side guard: the signer cannot see the chain. An upgrade landing between preparation and inclusion (seconds) is not covered. After any upgrade swaps stop until the new deployment is reviewed and the pin changed in code. - The route between source and destination: venues, pools, intermediate mints are not checked (as with KyberSwap's executor data). The worst a hostile route can do, if the program behaves as observed, is deliver only the approved minimum.
- Rent. A new token account costs a deposit set by the protocol (measured 0.00148844 SOL for a 165-byte account on
2026-11-10). The signer bounds it with constants (
RENT_TOKEN_ACCOUNT2 039 280 lamports; 2 498 640 for a Token-2022 account of up to 231 bytes, both at the historical rent rate). The deposit stays in the wallet's own token account. If the protocol raised rent above these bounds, the API's simulation would refuse the swap; the signer's figure would no longer be a bound. - Token-2022 (PYUSD, USDG): handled, with an API-side guard. Accounts are derived with the Token-2022 program and the instruction must name it. The mint's extensions are read on both endpoints before preparing: a non-zero transfer fee or a transfer hook program → refused. The signer cannot see the mint. PYUSD simulated in both directions; USDG not.
- Other wallet accounts the signer cannot name: token accounts of the wallet that are not the associated account of a registry mint are not recognised by address.
- The API and the RPC endpoints for everything that is not in the message: balances, simulation, confirmation.
10.7 API life cycle
POST /api/v1/swap/proposals with network: "solana" → kind: 'swap' proposal in the existing table and worker.
Prepare: genesis hash on both endpoints; program pin; wallet is a plain system account; Token-2022 guard; balance;
/build (v1, no fee, no tip, no referral; an answer carrying a platform fee, a tip, lookup tables, another amount or mint
is refused); arrangement/rewrite; the signer's own verification; simulation of the exact bytes (sigVerify: false,
own blockhash) which must succeed, spend exactly the amount, deliver ≥ the minimum, cost exactly the computed fee and at
most the rent bound, and show no other token account of the wallet (nor its lamports) losing value. Then passkey approval
over the intent hash, signature, and a second simulation of the signed bytes with the signature checked before
sendTransaction (existing relay to both endpoints, existing rebroadcast: always the same bytes, hence the same signature
— a retry can never become a second swap). A blockhash that is no longer valid before sending → failed without sending;
not included before lastValidBlockHeight → failed, nothing moved. Outcome = the wallet's real balance change from the
confirmed transaction's pre/postBalances and pre/postTokenBalances (maxSupportedTransactionVersion: 1). Audit:
swap.prepared, swap.not_sent, swap.settled, plus the usual transfer.*. One operation in flight per network, as for
sends.
Costs shown to the owner before approval: network fee (5 000 lamports + priority fee; Jupiter's suggested price, capped at 2 lamports per compute unit and lowered if the fee ceiling requires), the deposit of a new token account when one is created, the approved ceiling.
10.8 Licence and rate limits in execution
- §2.3 label: every place a Jupiter route or swap is shown says "Jupiter (Metis)" (route cards, execute button,
review sheet, API
swap.detail.label). - Rate limit: execution calls
/buildat most once every 2.1 s and at most three times per swap, in the same keyless bucket as quotes (0.5 request/s); a 429 is reported to the owner, not retried in a loop.JUPITER_API_KEYis used if set. - No integrator fee, no referral, no tip are requested, and an answer or instruction carrying one is refused.
- The rewrite of §10.3 changes the instruction Jupiter returned. The licence text read does not address it, and this use was not confirmed with Jupiter.
- §3.2 f/g (no benchmarks, no mixing with other sources) still applies to anything derived from the API; the simulation figures in this file and the script output stay internal.
10.9 Verified / not verified
Verified (2026-11-10): on-chain IDL and program data on two endpoints; v1 transactions accepted by both endpoints
(simulation, and confirmed ones read back); swaps prepared by the real API code for public funded addresses, signed by the
real signer code with that address standing in for the wallet, the signed bytes simulated successfully with outputs ≥ the
verified minimum (USDC→SOL with and without an existing wSOL account, SOL→USDC twice, USDC→JUP creating the token account,
JUP→USDC, USDC→USDT, USDC→PYUSD, PYUSD→USDC, USDC→BONK); signature format with a fresh unfunded key (sigVerify: true);
the minimum-output rule; the refusals; the route_v2 re-declaration.
Not verified: any transaction actually sent, landed or confirmed on Solana (so: real inclusion, priority-fee adequacy,
the worker's confirmation path against a live cluster, the outcome read from a real confirmed swap of this wallet);
solana-test-validator was not available on this machine and was not installed; the program's source; who controls the
upgrade authority; behaviour of the rewrite for venues not met in the proof runs; Token-2022 mints with an active fee or
hook (refused, not exercised on-chain); USDG; routes above 400 000 compute units (refused); the web screens in a browser;
Jupiter's position on the rewrite.