SPB Git

Repository Public

jupiter.md 41.4 KB 474 lines markdown

1# Jupiter (Solana) — provider dossier23Provider id `jupiter`, displayed as **“Jupiter (Metis)”**. Adapter: `packages/swap/src/providers/jupiter.ts`4(+ `jupiter-dexes.ts`) for price discovery. **Execution is enabled since 2026-11-10 (V5)** for one transaction shape only —5see **§10** (`packages/core/src/swap-sol.ts`, `services/api/src/swap-exec-sol.ts`). The quote adapter itself is unchanged6and still classifies its raw answer as `opaque`; the API marks the route executable and the verification happens when a7swap is prepared. §1–§9 below describe the provider as surveyed before execution; where §10 supersedes them it says so.89Everything below was checked on **2026-11-10** against the official documentation (`https://developers.jup.ag/docs`, which10`https://dev.jup.ag` redirects to; every page has a `.md` twin) and against the live API. Statements marked **(measured)**11come from live answers, the rest from the cited page. Statements marked **(pinned by test)** are asserted on the recorded12fixtures in `packages/swap/test/jupiter.test.ts`.1314## 1. Which API1516| API | Base URL | Status (docs) | Used here |17|---|---|---|---|18| 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** |19| 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) |20| 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 |21| `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 |22| `quote-api.jup.ag/v6` | — | gone (connection refused, measured) | no |2324Sources: [Swap overview](https://developers.jup.ag/docs/swap), [Build](https://developers.jup.ag/docs/swap/build),25[GET /build reference](https://developers.jup.ag/docs/api-reference/swap/build),26[Metis → Router migration](https://developers.jup.ag/docs/swap/migration/metis-to-build),27[V1 quote reference](https://developers.jup.ag/docs/api-reference/swap/v1/quote),28[Developer Platform migration](https://developers.jup.ag/docs/portal/migration).2930**Keys.** None required: “a request with no `x-api-key` header is treated as keyless”. The adapter sends `x-api-key` only31if `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 up32for anything.3334**Rate limits** ([Rate Limits](https://developers.jup.ag/docs/portal/rate-limits)). Keyless: 0.5 request/s; documented as3530 per 60-second sliding window, **measured as 5 requests per ~10-second sliding window** (the 6th answers36`429 {"code":429,"message":"[API Gateway] Too many requests"}`; no lock-out afterwards). All main endpoints share one37bucket (`/build`, V1 `/quote`, `/program-id-to-label`, Price, Tokens…). `/swap/v2/order` and `/execute` showed a separate3820-request bucket (measured header). Headers on 200 and 429: `x-ratelimit-remaining`, `x-ratelimit-current`,39`x-ratelimit-reset` (Unix seconds when one slot frees). The runtime must therefore run this provider at40`JUPITER_KEYLESS_POLICY` (`ratePerSec: 0.5, burst: 4`), **not** the default 2 requests/s.4142**Latency (measured, from Québec, keyless, 2026-11-10):** 40–140 ms per `/build` call, median ≈ 60–80 ms, answers of4314–68 kB (the instructions and lookup tables dominate).4445## 2. The quote call4647```48GET https://api.jup.ag/swap/v2/build?inputMint=<mint>&outputMint=<mint>&amount=<base units>&taker=<wallet>&slippageBps=<0..10000>49```5051- `/build` is **ExactIn only** (no `swapMode`); `taker` is **required** and must be a valid public key (no balance needed).52- Native SOL is the **wrapped-SOL mint** `So11111111111111111111111111111111111111112`; `wrapAndUnwrapSol` defaults to53  `true`, so the answer contains the wrap (input) or unwrap (output) instructions.54- Parameters the adapter does **not** send (defaults apply): `mode=fast`, `dexes`/`excludeDexes`, `maxAccounts` (64),55  `platformFeeBps`+`feeAccount`, `payer`, `destinationTokenAccount`/`nativeDestinationAccount`, `blockhashSlotsToExpiry`56  (150), `tipAmount`, `computeUnitPricePercentile`, `forJitoBundle`, `transactionVersion` (`0`), `slippageBps=rtse`.57- `restrictIntermediateTokens` and `onlyDirectRoutes` exist **only on V1 `/quote`**; `/build` has no equivalent (it58  chooses intermediate tokens dynamically). The closest levers are `maxAccounts` and `dexes`.5960Answer fields and how the adapter maps them:6162| Field | Meaning | Mapping |63|---|---|---|64| `inputMint`, `outputMint`, `inAmount`, `swapMode`, `slippageBps` | echo of the request | must equal the request, else `schema` error |65| `outAmount` (string, base units) | expected output | `amountOut` (BigInt from the string) |66| `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]` |67| `priceImpactPct` | decimal **ratio** string (`"0.001"` = 0.1 %) | `priceImpactBps = ratio × 10 000` (2 decimals) |68| `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`) |69| `computeBudgetInstructions` | one `SetComputeUnitPrice` (micro-lamports/CU); **no** compute-unit limit | network-fee ceiling (see §6) |70| `setupInstructions`, `swapInstruction`, `cleanupInstruction`, `otherInstructions`, `tipInstruction`, `addressesByLookupTableAddress` | the transaction material (§3) | validated structurally, kept in `raw` |71| `blockhashWithMetadata{blockhash[32], lastValidBlockHeight, fetchedAt}` | recent blockhash, valid ~150 slots | `expiresAt = min(now, fetchedAt) + 30 s` |72| `transactionVersion` | `0` | must be `0` |7374- **Platform fees.** `/build` has no `platformFee` field unless an integrator fee is requested (`platformFeeBps` +75  `feeAccount`, taken inside the swap instruction). The adapter requests none and **rejects** an answer carrying one.76  On V1 `/quote` the field is `platformFee: {amount, feeBps} | null`; on `/order` it is `feeBps`, `feeMint`, `platformFee`.77- **Validity.** Jupiter states none for the price. The only hard limit is the blockhash (`lastValidBlockHeight`,78  ≈ 150 slots ≈ 60 s); the adapter uses the contract's conservative 30 s.79- **Duration.** None given → `estimatedSeconds: null` (a Solana transaction confirms in seconds, but that is not the80  provider's figure).81- **No route** = `HTTP 400 {"error":"No routes found"}` (dust amounts, unknown mints) or82  `{"error":"Insufficient liquidity"}` (size) → empty array. Other 400s are failures: `Invalid amount`, `Invalid taker`,83  `inputMint cannot be same as outputMint`, a Zod error object for out-of-range parameters, and — observed once during the84  smoke run — `{"error":"500: {\"error\":\"Oracle price out of date. Pair temporarily unavailable\"}"}` (transient, the85  retry seconds later succeeded).8687The route may pass through mints that are **not** in the wallet registry (USD1, cbBTC, mSOL… seen live) and may split88across parallel legs. Because the whole route is one atomic instruction and the intermediates never stay in the wallet,89the quote exposes a single `swap` step from the requested input to the requested output; the legs stay in `raw.routePlan`.90The adapter checks that the legs leaving the input mint sum to `inAmount` and the legs reaching the output mint sum to91`outAmount`.9293What the adapter refuses locally (empty array, no HTTP call): `recipient ≠ sender` (a `/build` swap pays the taker's own94token account), a sender that is not a Solana address, non-positive or > u64 amounts, slippage that is not an integer in950..10 000, any asset outside the registry.9697## 3. Transaction shape (what execution would have to sign)9899`/build` returns **instructions, not a transaction**. The integrator assembles, in this order100([Common Instructions](https://developers.jup.ag/docs/swap/build/common-instructions)): compute-budget instructions101(plus a `SetComputeUnitLimit` it must add itself after simulation) → `setupInstructions` → `swapInstruction` →102`cleanupInstruction` → `otherInstructions` → `tipInstruction`; fee payer = taker; lifetime = the returned blockhash.103104- **Versioned transaction, v0, with address lookup tables.** `addressesByLookupTableAddress` maps each ALT address to105  its full address list, so no RPC fetch is needed to compile — but it also means **the ALT content comes from Jupiter**:106  a verifier must either fetch the tables from its own RPC or treat the listed addresses as untrusted input and check the107  resolved accounts. Recorded answers used 1 to 5 tables and 24 to 81 accounts in the swap instruction (pinned by test).108  `transactionVersion=1` requests a Solana v1 transaction (SIMD-0385: 4 096 bytes, ≤ 64 inline accounts, no ALT, compute109  budget on the message config, `@solana/kit` ≥ 8) — [Transaction Versions](https://developers.jup.ag/docs/swap/advanced/transaction-versions).110  Legacy transactions are not offered by V2 (`asLegacyTransaction` was V1).111- **Wrap / unwrap.** SOL input: `ATA.CreateIdempotent` (taker's wSOL account) + `System.Transfer` of exactly `inAmount`112  lamports into it + `Token.SyncNative`; cleanup `Token.CloseAccount` (wSOL account → taker). SOL output: cleanup113  `Token.CloseAccount` of the wSOL account back to the taker (pinned by test).114- **Token-account creation.** `ATA.CreateIdempotent` (instruction tag 1), payer and owner = taker, for the destination115  mint — under the **Token-2022** program for PYUSD/USDG — and, on some multi-hop routes, **also for intermediate mints**116  (a taker USDC and USDT account were created on a recorded SOL → PYUSD route). Each new account costs ≈ 0.00204 SOL of117  rent paid by the taker and is left open (rent recoverable by closing it). Jupiter does not report this cost.118- **Compute budget.** Only the price is given (50th percentile of recent fees by default); the limit must come from the119  integrator's own `simulateTransaction` (docs: 1.2 × consumed, max 1 400 000) and the price should be clamped —120  [Compute Units](https://developers.jup.ag/docs/swap/advanced/compute-units).121122### Legacy / alternative endpoints (documented, not used)123124- V1 `POST /swap/v1/swap` `{quoteResponse, userPublicKey, wrapAndUnwrapSol, useSharedAccounts, feeAccount,125  prioritizationFeeLamports, asLegacyTransaction, destinationTokenAccount, nativeDestinationAccount,126  dynamicComputeUnitLimit, dynamicSlippage, …}` → `{swapTransaction (base64, unsigned), lastValidBlockHeight,127  prioritizationFeeLamports}`. V1 `POST /swap/v1/swap-instructions` (same body) → `computeBudgetInstructions,128  setupInstructions, swapInstruction, cleanupInstruction, otherInstructions, addressLookupTableAddresses` (addresses only:129  the tables must be fetched by RPC). V2 `/build` = V1 `/quote` + `/swap-instructions` in one call, with V2 instructions.130- V2 `GET /swap/v2/order` (with `taker`) → `{transaction (base64, unsigned, v0 or v1), requestId, router, outAmount,131  feeBps, feeMint, lastValidBlockHeight | expireAt, errorCode…}`; then `POST /swap/v2/execute {signedTransaction,132  requestId}` → `{status: "Success"|"Failed", signature, code, totalInputAmount, inputAmountResult, outputAmountResult,133  totalOutputAmount}` — [Order & Execute](https://developers.jup.ag/docs/swap/order-and-execute).134135## 4. Programs involved136137| Program | Address | Role | Source |138|---|---|---|---|139| Jupiter v6 swap aggregator | `JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4` | the swap instruction of every recorded answer | named in the Jupiter docs ([JupiterZ RFQ v2](https://developers.jup.ag/docs/swap/routing/rfq/v2/streaming)); IDL in [jup-ag/jupiter-cpi-swap-example](https://github.com/jup-ag/jupiter-cpi-swap-example/blob/main/cpi-swap-program/idls/jupiter_aggregator.json) (same address); audits: [Offside Labs 2024 and 2025](https://developers.jup.ag/docs/resources/audits) |140| System | `11111111111111111111111111111111` | SOL transfer into the wSOL account | Solana |141| SPL Token | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` | SyncNative, CloseAccount, classic mints | Solana |142| Token-2022 | `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | PYUSD, USDG | Solana |143| Associated Token Account | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` | CreateIdempotent | Solana |144| Compute Budget | `ComputeBudget111111111111111111111111111111` | priority-fee price | Solana |145146**AMM programs.** The route plan names venues by `label`; the program behind each label comes from Jupiter's own147`GET /swap/v2/program-id-to-label` ([reference](https://developers.jup.ag/docs/api-reference/swap/program-id-to-label)):148110 programs / 105 labels on 2026-11-10, snapshot in `jupiter-dexes.ts` and in the fixture `program-id-to-label.json`149(a test checks they agree and that, for every recorded leg, a program of its label really is among the swap instruction's150accounts). Venues seen in recorded and smoke routes, with their program ids from that endpoint:151152| Label | Program | Label | Program |153|---|---|---|---|154| Whirlpool (Orca) | `whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc` | Raydium CLMM | `CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK` |155| Raydium CP | `CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C` | Raydium | `675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8` |156| Meteora DLMM | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Meteora | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` |157| Manifest | `MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms` | PancakeSwap | `HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq` |158| GoonFi V2 | `goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE` | BisonFi | `BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi` |159| ZeroFi | `ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY` | Kipseli | `3TK9D8aoBFYjYZtKCjciPrVrRStsnvo7KmpcJqDavpaU` |160| Byreal | `REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2` | Scorch | `ojh19ojaKduoJZuaJADhcVGp4xt1TcdAvZmpVsCorch` |161| Deriverse | `DRVSpZ2YUYYKgZP8XtLhAGtT1zYSCKzeHfb4DgRnrgqD` | JupLend AMM | `jupZ4m2GqUCJ5iueMfzQf8khFfH31d4XAQt3RzCT9Vd` |162| Saber | `SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ` | DefiTuna | `fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9` |163| 1DEX | `DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm` | Perena | `NUMERUNsFCP3kuNmWZuXtm1AaQCPj9uw6Guv2Ekoi5P` |164| Token Swap | `SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8` | Orca V1 | `DjVE6JNiYqPL2QXyCUUh8rNjHrbz9hXHNYt99MQ59qw1` |165166The source for every one of these addresses is Jupiter's endpoint, not each DEX's own documentation: an execution phase167that allowlists AMM programs should confirm each against its publisher. The set is open-ended (Jupiter lists new venues168continuously; many of the busiest ones — GoonFi, BisonFi, ZeroFi, Kipseli, Scorch — are proprietary market-maker programs169with no public source), so an allowlist of AMMs will either reject many best routes (restrict with `dexes=`) or be170permanently behind.171172## 5. What an isolated signer could verify by itself173174Recipient and minimum output are **not** free-form: they sit at fixed places in the swap instruction. Observed on every175recorded answer and pinned by test (`structure of the recorded instructions`):176177- `swapInstruction.programId` = the aggregator; Anchor discriminator `bb64facc31c4af14` (`route_v2`) or178  `d19853937cfed8e9` (`shared_accounts_route_v2`, preceded by a 1-byte authority id).179- Data, little-endian, after the discriminator (and id): `in_amount u64`, `quoted_out_amount u64`, `slippage_bps u16`,180  `platform_fee_bps u16` (0), a third `u16` (0, positive-slippage fee), then the route-plan vector (`u32` length = number181  of legs; per leg a venue enum, `bps u16`, input index, output index). So the instruction carries the **quoted output and182  the slippage**, and the program enforces `quoted_out × (1 − slippage)` (error 6001 `SlippageToleranceExceeded` in the183  IDL); `otherAmountThreshold` itself is not in the data.184- Accounts: `route_v2` → `[0]` taker (only signer), `[1]` taker's source token account, `[2]` taker's destination token185  account, `[3]` input mint, `[4]` output mint; `shared_accounts_route_v2` → `[1]` taker, `[2]` source, `[5]` destination,186  `[6]` input mint, `[7]` output mint. Source and destination are the taker's **associated token accounts** (derived with187  the mint's own token program), which the signer can recompute.188- Setup/cleanup contain only: `ATA.CreateIdempotent` for the taker's own accounts, `System.Transfer` taker → taker's wSOL189  account for exactly `inAmount`, `Token.SyncNative`, `Token.CloseAccount` wSOL → taker.190191**Caveat (resolved in §10.2):** the V2 instructions (`route_v2`, `shared_accounts_route_v2`) are not in the IDL file192published on GitHub (it stops at `route` / `shared_accounts_route`, whose layout differs: route plan first,193`platform_fee_bps u8`), but they **are in the program's on-chain IDL**, which confirms the layout above.194195So a signer could **rebuild** the compute-budget, ATA, wrap and unwrap instructions itself, and **check** in the swap196instruction: program id, discriminator, input amount, quoted output, slippage, zero platform fee, taker as sole signer,197source/destination = its own ATAs, mints. It **cannot** rebuild or meaningfully check the remaining 20–75 accounts and the198venue-specific route data (pools, vaults, oracles, proprietary AMMs): a hostile answer could still route through a199malicious program placed among them. That is why the class is `opaque`, not `verified`.200201**Post-conditions verifiable by simulation** (own RPC, `simulateTransaction` with `replaceRecentBlockhash`, requesting the202post-state of the taker's accounts): (1) no error; (2) the taker's input balance decreases by exactly `inAmount` (plus203fees and rent when the input is SOL); (3) the destination token account — or lamports, for SOL — increases by at least204`minAmountOut`; (4) no other token account owned by the taker loses balance, and none gets a new delegate or owner;205(5) lamport decrease ≤ fee + rent for the accounts created; (6) every writable signer-owned account is one of the expected206ATAs; (7) compute units consumed, to set the limit. Simulation is advisory (state can change before landing); the207on-chain slippage check is what binds. A Token-2022 mint with a non-zero transfer fee208would make (3) differ from the quote: the mint's fee configuration has to be read on-chain by the wallet (not checked in209this phase).210211**Token-2022 (PYUSD, USDG).** The Swap docs make no statement about Token-2022 on `/build` (their only mention, “Supports212SPL and Token2022 tokens”, concerns referral fees on `/order`). That Jupiter routes both mints is established by live213answers (measured, both directions, against SOL, USDC and each other): the routes go through Manifest, Whirlpool and214others, the swap instruction references the Token-2022 program, and the destination account is created under it (pinned215by test). No Token-2022 swap was executed.216217Approvals: none. Solana has no allowance step; the taker signs the transaction that moves its own tokens.218219## 6. Fees220221- **Jupiter:** none on `/build`. (On `/order`: 0 bps pegged pairs/Jupiter tokens, 2 bps SOL↔stable, 5 bps LST↔stable,222  10 bps otherwise, 50 bps for tokens < 24 h old, shown as `feeBps`/`feeMint`.)223- **Integrator fee:** optional `platformFeeBps` + `feeAccount` on `/build` (any token account of the integrator); not used.224- **AMM / LP fees:** inside the quoted `outAmount`; V2 route plans no longer itemise them.225- **Network:** 5 000 lamports per signature + priority fee (`price × compute-unit limit`). The quote reports **one226  `network` FeeItem, estimated, a ceiling**: `5 000 + ceil(price × 1 400 000 / 10⁶)` lamports — typically 6 000–18 000227  lamports (< 0.002 USD). Real cost is lower once the limit is simulated. **Not included:** rent for token accounts the228  swap creates (≈ 0.00204 SOL each, sometimes for intermediate mints, see §3) — the largest real cost for a first swap.229- `/submit` landing (optional): a Jupiter tip of at least 0.001 SOL per transaction.230231## 7. Sending, status and tracking232233- `/build` transactions are sent through the wallet's **own RPC** (`sendTransaction`) and tracked with234  `getSignatureStatuses` / blockhash expiry (`lastValidBlockHeight`); Jupiter offers **no** status endpoint for them235  (“`/build` transactions cannot use `/execute`”).236- Optional landing service: `POST https://tx.jup.ag` (JSON-RPC `sendTransaction`, base64) — **requires an API key** and a237  tip ≥ 1 000 000 lamports to one of 16 tip accounts (`tipAmount` on `/build` adds the instruction); send-only, no238  confirmation queries — [Transaction Submission](https://developers.jup.ag/docs/transaction/submit).239- `/order` path: `POST /execute` lands, polls and returns the final status with the executed amounts (codes: 0 success,240  −1 unknown `requestId`, −1000 failed to land, −1005 expired, −2003 RFQ quote expired…).241- Every response carries `x-api-gateway-request-id` (the id to quote to support). API status: <https://status.jup.ag>.242  Breaking changes are announced on Discord / Telegram `jup_dev`.243244**Refunds.** Not applicable: a swap is one atomic transaction. If the minimum is not met or any leg fails, the whole245transaction reverts and only the network fee is spent (`atomic: true`, `refundable: true`). An unsent or expired246transaction moves nothing.247248## 8. Terms of use (SDK & API License Agreement)249250<https://developers.jup.ag/docs/legal/sdk-api-license-agreement> — points that constrain this wallet:251252- **§2.3 labelling:** the specific API used must be shown prominently to end users; anything other than Ultra must be253  labelled **“Metis”**. The provider name is “Jupiter (Metis)”; the UI must keep it.254- **§3.2 f:** no access “for competitive analysis” and no dissemination of “performance information (including uptime,255  response time and/or benchmarks)”. The latency figures above and the provider statistics of the runtime must stay256  internal.257- **§3.2 g:** no combining API content “with content obtained through scraping or any other means outside the API”. A258  multi-provider comparison screen and the CoinGecko check in the smoke script sit close to this clause — **to be259  confirmed with Jupiter (info@jup.ag, §2.4) before a public release.**260- **§3.2 d/e:** no resale of API content; no usage beyond “reasonable request volume” — keep to the rate limit.261- **§5.2 / §12:** no availability guarantee; Jupiter may change or stop the API without notice.262- **§2.5 / §7.3:** the licensee is responsible for sanctions and KYC/AML compliance towards its users.263264## 9. Not fitted / open points265266- **`/order` (Meta-Aggregator) is deliberately not used:** its transaction cannot be modified, it is co-signed and landed267  by Jupiter or a market maker, and without `taker` it returns `slippageBps: 0` with `otherAmountThreshold = outAmount`268  (no guaranteed minimum). It often quotes slightly better (RFQ) but charges the platform fee.269- The quote needs the wallet's real address (`taker`), so it is sent to Jupiter with every price check.270- A third-party recipient is possible on `/build` (`destinationTokenAccount` / `nativeDestinationAccount`) but would need271  ATA derivation in the adapter; refused for now.272- The contract's one-step-per-hop model does not fit split routes through unregistered mints; one step is reported.273- Fixtures are re-recorded with `npx tsx scripts/record-swap-jupiter-fixtures.mts` (the 429 and the stale-oracle answers274  were captured separately); live check: `npx tsx scripts/smoke-swap-jupiter.mts`.275276## 10. Execution (V5, 2026-11-10)277278Scope: swaps between **SOL and the SPL tokens of the wallet registry** on Solana, exact input, output to the wallet itself.279Proof: `npx tsx scripts/sim-jupiter.mts` (mainnet simulation only; nothing was ever sent). Statements marked280**(simulated)** come from that script's output on 2026-11-10; **(on-chain)** from account data read on281`api.mainnet-beta.solana.com` and `solana-rpc.publicnode.com`; **(docs)** from the cited page.282283### 10.1 Sources read284285- [GET /build reference](https://developers.jup.ag/docs/api-reference/swap/build) (parameters `taker`, `slippageBps`,286  `transactionVersion`, `wrapAndUnwrapSol`, `platformFeeBps`/`feeAccount`, `payer`, `destinationTokenAccount`,287  `nativeDestinationAccount`, `maxAccounts`, `dexes`/`excludeDexes`, `tipAmount`, `computeUnitPricePercentile`),288  [Build](https://developers.jup.ag/docs/swap/build),289  [Common Instructions](https://developers.jup.ag/docs/swap/build/common-instructions),290  [Transaction Versions (v0 and v1)](https://developers.jup.ag/docs/swap/advanced/transaction-versions),291  [Compute Units](https://developers.jup.ag/docs/swap/advanced/compute-units),292  [Rate Limits](https://developers.jup.ag/docs/portal/rate-limits),293  [SDK & API License Agreement](https://developers.jup.ag/docs/legal/sdk-api-license-agreement).294  The V1 parameters `userPublicKey`, `asLegacyTransaction`, `useSharedAccounts`, `dynamicComputeUnitLimit`,295  `prioritizationFeeLamports` belong to `POST /swap/v1/swap[-instructions]` ("no longer actively maintained") and are not296  parameters of `/build`; passing `useSharedAccounts=true` to `/build` had no visible effect (measured).297- The aggregator program `JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4` **(on-chain)**: owned by298  `BPFLoaderUpgradeab1e11111111111111111111111`, ProgramData `4Ec7ZxZS6Sbdg5UGSLHbAnM7GQHp2eFd4KYWRexAipQT`, last deployed299  at **slot 462802085**, executable 2 892 224 bytes, SHA-256300  `53d53f447f594158a259cbdca1f5e613caf40bc14ce759ffd5953580dc2af8dd`, **upgrade authority301  `CvQZZ23qYDWF2RUpxYJ8y9K4skmuvYEEjH7fK58jtipQ`** (a system-owned account with no data; whether a multisig stands behind302  it was not established). Identical on both endpoints.303- Its **on-chain Anchor IDL** (account `C88XWfp26heEmDkmfSzeXP7Fd7GQJ2j9dDTUsyiZbUTa`, zlib JSON, 36 kB, IDL authority304  `9u9iZBWqGsp5hXBxkVZtBTuLSGNAG9gEQLgpuVw39ASg`). No source code of the program was found or read: everything about its305  behaviour below comes from the IDL and from simulation.306307### 10.2 The instructions (on-chain IDL)308309| Instruction | Discriminator | Arguments (little-endian) | Fixed accounts |310|---|---|---|---|311| `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 |312| `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 |313314Also present and **never signed**: `route`, `route_with_token_ledger`, `shared_accounts_route`, `exact_out_route`,315`shared_accounts_exact_out_route`, `exact_out_route_v2`, `shared_accounts_exact_out_route_v2`, `route_with_attestation`,316`shared_accounts_route_with_attestation`, and the housekeeping instructions. `RoutePlanStepV2 = { swap: Swap (enum of317venues), bps u16, input_index u8, output_index u8 }`; the plan stays opaque to the signer. Derived and pinned by test on318every recorded answer: program authority = PDA `["authority", id]`; the program's own token accounts `[3]`, `[4]` = its319associated token accounts for the two mints.320321**Minimum output (simulated).** With the real output `A` of a still pool: `quoted = A, 0 bps` succeeds; `quoted = A + 1,3220 bps` fails with 6001 `SlippageToleranceExceeded`; at 1 bp a quoted amount whose exact minimum is `A + 0.48` fails. The323program therefore enforces **ceil(quoted × (10 000 − bps) / 10 000)** at the destination account (the same figure as the324API's `otherAmountThreshold`). The signer uses **floor(…)**, a lower bound of it; the approved minimum shown to the owner325is that floor (one base unit under Jupiter's figure when the product is not whole).326327### 10.3 Why only `shared_accounts_route_v2` is signed328329In `route_v2` the venue programs of each leg are invoked **with the wallet's signature** and take their input straight from330whatever accounts the route lists. The program does **not** tie those legs to the declared source account: a genuine331`USDT → SOL` `route_v2`, re-declared as "USDC → SOL, source = the wallet's USDC account" by editing accounts `[1]` and332`[3]` only, **executes and debits 20 USDT** **(simulated)**. A signer that checks the declared accounts of a `route_v2`333would be approving something else. Beyond that, `route_v2` lends the wallet's signature to the venue programs of the route334(about 110 are listed by Jupiter, several without public source), with accounts chosen by the route builder. No offline335check makes that comparable to the EVM path, so **`route_v2` is refused whatever it declares**.336337In `shared_accounts_route_v2` the declared source account is bound: the same re-declaration fails inside the aggregator338with the token program's "Account not associated with this Mint" **(simulated)**, i.e. the aggregator itself moves the339input out of the declared account, and the simulated debit is exactly `in_amount`. In the routes Jupiter builds this way340the legs name the **program authority** and the program's own token accounts, not the wallet. The signer additionally341requires that **neither the wallet nor its source/destination account appears among the route's accounts**, so that no342venue can be handed the wallet's signature (Jupiter does build some shared-accounts routes with the wallet inside a leg —343seen in routes that included Kipseli, Perena or Bonkswap legs — those are refused too).344345**Consequence and deviation.** `/build` answers direct routes (the usual case for SOL ↔ USDC and other liquid pairs) with346`route_v2`, and has no documented parameter to ask for the other form. The API therefore **rewrites** a `route_v2`347instruction into `shared_accounts_route_v2` (`jupiterRouteToShared`: same route plan; discriminator and `id` byte; the348wallet and its token accounts inside the legs replaced by the program authority and the program's own token accounts,349including for intermediate mints) and keeps it only if the exact bytes simulate successfully. This is not a documented350Jupiter feature; it relies on the two instructions sharing their route format, which held for the routes met in the proof351runs (most recorded pairs were rewritten) and fails cleanly — simulation error, then another route is requested without the352venues already tried, at most three `/build` calls — for venues that need the wallet inside a leg. The signer does not care353who built the instruction: it checks the result.354355### 10.4 Lookup tables: none356357A v0 transaction refers to most accounts through address lookup tables whose content the signer cannot read offline; the358API would have to supply it, and a table can be created by anyone. Rather than reason about what may hide behind a table,359the wallet uses **Solana v1 transactions** ([SIMD-0385](https://solana.com/upgrades/larger-transaction-sizes), live on360mainnet: confirmed v1 Jupiter transactions were read back from both RPC endpoints): 4 096 bytes, at most 64 accounts,361**all inline**, compute budget in the message header. `/build?transactionVersion=1` returns instructions for it. The signer362**refuses any message whose first byte is not `0x81`** — legacy and v0 alike — so there is no table to trust. With every363account visible, the signer also refuses a transaction that references any *other* registry token account of the wallet.364Recorded messages: 20–61 accounts, 845–2 290 bytes. Routes that need more than 64 accounts cannot be expressed and are not365offered.366367Wire format (hand-written in `swap-sol.ts`, cross-checked against `@solana/kit` 8.6 in tests): `0x81`, header (3 bytes),368config mask `u32` (must be `0x0f`: priority fee, compute-unit limit, loaded-accounts data limit; no heap request), blockhash,369instruction count, account count, accounts, config values (`u64` lamports, `u32`, `u32`), instruction headers, payloads;370the transaction is the message followed by the 64-byte signature. The node accepts the signer's signature and rejects it371with one bit flipped **(simulated with `sigVerify: true`)**.372373### 10.5 What the signer guarantees before signing (`verifyJupiterSwap`)374375From the message bytes alone, with the intent the owner approved by passkey (which commits to the SHA-256 of those bytes):3763771. v1 message, exact length, no trailing bytes, unique accounts; **one required signature, the fee payer, = the wallet**;378   the blockhash is the approved one and not zero.3792. Compute budget: limit and priority fee equal the approved ones (`limit ≤ 400 000`, fee =380   `ceil(limit × price / 10⁶)`); **5 000 + priority fee + rent bound ≤ the approved fee ceiling**, itself ≤ the policy381   ceiling for Solana (0.003 SOL).3823. Instruction list, in this order and nothing else: `ATA.CreateIdempotent` for the wallet's own associated token account383   of the input or output mint (payer and owner = wallet) · when SOL is sold, `System.Transfer` wallet → its own wSOL384   account for **exactly the approved amount** then `Token.SyncNative` · **one** Jupiter instruction ·385   when SOL is sold or bought, `Token.CloseAccount` of the wallet's wSOL account **to the wallet**. No other program, no386   ComputeBudget instruction, no transfer, approve, setAuthority or close for anyone else.3874. Jupiter instruction: pinned program id; `shared_accounts_route_v2` only; the twelve fixed accounts are exactly the388   derived ones (program authority for `id`, wallet, wallet's source ATA, program's source and destination accounts,389   wallet's destination ATA, the two registry mints — wSOL for SOL —, their token programs, event authority, program);390   `in_amount` = approved amount; `floor(quoted_out × (10 000 − slippage_bps) / 10 000)` ≥ approved minimum;391   `platform_fee_bps = 0`; `positive_slippage_bps = 0`; route accounts contain neither the wallet nor its two accounts.3925. Both assets are registry assets on Solana and differ; `to = from`; policy limits as for a send (per operation, per day,393   fee ceiling, approval window, halt); one signature per proposal.394395Anything else is refused: see the mutations in the proof script and in the unit tests. Several of them **would have396executed on-chain** (another destination account, a halved minimum, a doubled input, an extra transfer, approve or397third-party close) — the checks are what stops them.398399### 10.6 Residual trust (what the signer cannot prove)400401- **The aggregator program's own behaviour.** Its source was not read. That it takes exactly `in_amount` from the declared402  source and reverts unless the declared destination receives the minimum is established by its IDL, by mainnet403  simulation of the recorded pairs, and by the failing mutations above — not by reading code, unlike the KyberSwap router.404  In particular, that the aggregator never passes the wallet's signature to a venue when the wallet is absent from the405  route accounts follows from how its routes are built and from Solana's rule that a program can only pass on accounts406  it lists; it was not read in code.407- **Upgradeability.** The program can be replaced by its upgrade authority at any time; a replaced program could do408  anything the wallet's signature allows on the accounts named in the transaction. Mitigation: `JUPITER_PIN` (ProgramData409  address, last-deploy slot, upgrade authority, executable hash). The API refuses to prepare a swap unless the ProgramData410  header shows the pinned slot and authority **on both RPC endpoints**, and re-hashes the 2.9 MB executable on both at411  first use and every six hours. This is an API-side guard: the signer cannot see the chain. An upgrade landing between412  preparation and inclusion (seconds) is not covered. After any upgrade swaps stop until the new deployment is reviewed413  and the pin changed in code.414- **The route between source and destination**: venues, pools, intermediate mints are not checked (as with KyberSwap's415  executor data). The worst a hostile route can do, if the program behaves as observed, is deliver only the approved minimum.416- **Rent.** A new token account costs a deposit set by the protocol (measured 0.00148844 SOL for a 165-byte account on417  2026-11-10). The signer bounds it with constants (`RENT_TOKEN_ACCOUNT` 2 039 280 lamports; 2 498 640 for a Token-2022418  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 above419  these bounds, the API's simulation would refuse the swap; the signer's figure would no longer be a bound.420- **Token-2022 (PYUSD, USDG): handled, with an API-side guard.** Accounts are derived with the Token-2022 program and the421  instruction must name it. The mint's extensions are read on both endpoints before preparing: a non-zero **transfer fee**422  or a **transfer hook program** → refused. The signer cannot see the mint. PYUSD simulated in both directions; USDG not.423- **Other wallet accounts the signer cannot name**: token accounts of the wallet that are not the associated account of a424  registry mint are not recognised by address.425- The API and the RPC endpoints for everything that is not in the message: balances, simulation, confirmation.426427### 10.7 API life cycle428429`POST /api/v1/swap/proposals` with `network: "solana"` → `kind: 'swap'` proposal in the existing table and worker.430Prepare: genesis hash on both endpoints; program pin; wallet is a plain system account; Token-2022 guard; balance;431`/build` (v1, no fee, no tip, no referral; an answer carrying a platform fee, a tip, lookup tables, another amount or mint432is refused); arrangement/rewrite; the signer's own verification; **simulation of the exact bytes** (`sigVerify: false`,433own blockhash) which must succeed, spend exactly the amount, deliver ≥ the minimum, cost exactly the computed fee and at434most the rent bound, and show no other token account of the wallet (nor its lamports) losing value. Then passkey approval435over the intent hash, signature, and a **second simulation of the signed bytes with the signature checked** before436`sendTransaction` (existing relay to both endpoints, existing rebroadcast: always the same bytes, hence the same signature437— a retry can never become a second swap). A blockhash that is no longer valid before sending → failed without sending;438not included before `lastValidBlockHeight` → failed, nothing moved. Outcome = the wallet's real balance change from the439confirmed transaction's `pre/postBalances` and `pre/postTokenBalances` (`maxSupportedTransactionVersion: 1`). Audit:440`swap.prepared`, `swap.not_sent`, `swap.settled`, plus the usual `transfer.*`. One operation in flight per network, as for441sends.442443Costs shown to the owner before approval: network fee (5 000 lamports + priority fee; Jupiter's suggested price, capped at4442 lamports per compute unit and lowered if the fee ceiling requires), the deposit of a new token account when one is445created, the approved ceiling.446447### 10.8 Licence and rate limits in execution448449- **§2.3 label:** every place a Jupiter route or swap is shown says **"Jupiter (Metis)"** (route cards, execute button,450  review sheet, API `swap.detail.label`).451- **Rate limit:** execution calls `/build` at most once every 2.1 s and at most three times per swap, in the same keyless452  bucket as quotes (0.5 request/s); a 429 is reported to the owner, not retried in a loop. `JUPITER_API_KEY` is used if set.453- **No integrator fee, no referral, no tip** are requested, and an answer or instruction carrying one is refused.454- The rewrite of §10.3 changes the instruction Jupiter returned. The licence text read does not address it, and this use455  was **not confirmed with Jupiter**.456- §3.2 f/g (no benchmarks, no mixing with other sources) still applies to anything derived from the API; the simulation457  figures in this file and the script output stay internal.458459### 10.9 Verified / not verified460461Verified (2026-11-10): on-chain IDL and program data on two endpoints; v1 transactions accepted by both endpoints462(simulation, and confirmed ones read back); swaps prepared by the real API code for public funded addresses, signed by the463real signer code with that address standing in for the wallet, the signed bytes simulated successfully with outputs ≥ the464verified minimum (USDC→SOL with and without an existing wSOL account, SOL→USDC twice, USDC→JUP creating the token account,465JUP→USDC, USDC→USDT, USDC→PYUSD, PYUSD→USDC, USDC→BONK); signature format with a fresh unfunded key (`sigVerify: true`);466the minimum-output rule; the refusals; the `route_v2` re-declaration.467468**Not verified:** any transaction actually sent, landed or confirmed on Solana (so: real inclusion, priority-fee adequacy,469the worker's confirmation path against a live cluster, the outcome read from a real confirmed swap of this wallet);470`solana-test-validator` was not available on this machine and was not installed; the program's source; who controls the471upgrade authority; behaviour of the rewrite for venues not met in the proof runs; Token-2022 mints with an active fee or472hook (refused, not exercised on-chain); USDG; routes above 400 000 compute units (refused); the web screens in a browser;473Jupiter's position on the rewrite.474
Files main
Menu

Theme