swap — CLI manual

Everything in swap --help, with examples · Operator: 9summits

swap is a terminal CLI that asks every major DEX aggregator (KyberSwap, Velora, 0x/Matcha, 1inch, Curve, Uniswap, OpenOcean, plus intent venues like CoW, UniswapX, Velora Delta and 1inch Fusion) for a swap quote on EVM chains, ranks the answers by token-out received, and — optionally — builds the executable calldata, checks your allowance, simulates the trade against your pranked balance, and hands the transaction to your wallet in the browser. Single Bun-compiled binary, no daemon, no telemetry.

$ swap 1 WBTC ETH                       # best quote across every venue
$ swap 1000 USDC USDT -v all           # side-by-side comparison block
$ swap 100 USDC USDT --from 0xYou --simulate   # dry-run, real tokenOut
$ swap 0.1 WETH USDC --from 0xYou --browser    # sign & send in your wallet
$ swap                                  # no args: open the local dApp

01Install & setup

One command downloads a prebuilt binary for macOS (arm64, x64) or Linux (arm64, x64) into ~/.local/bin. Re-run it to upgrade, or use swap update from then on.

$ curl -fsSL https://swap.9summits.io/install.sh | bash
Self-update

swap update checks the published checksum and replaces the binary in place only when a newer build exists.

Installer env: SWAP_INSTALL_DIR (default ~/.local/bin), SWAP_INSTALL_BASE (asset base), SWAP_NO_PROGRESS=1.

Hosted by default

The public prebuilt binary quotes and builds through https://swap.9summits.io/api/* out of the box (--hosted), so every venue works with zero key configuration and no RPC, except --simulate. Pass --local (or set SWAP_API_DISABLED=true) to switch to the local engine and your own venue keys and RPC. See 10.

First run

Quotes work out of the box. Building a transaction, simulating, or swapping max needs an RPC in local mode (--local or SWAP_API_URL unset); hosted mode needs one only for --simulate. The first time an RPC is required, the CLI prompts for an Alchemy key or an RPC URL and saves it under ~/.swap/config. You can trigger that prompt any time:

$ swap --init      # prompt for ALCHEMY_API_KEY or RPC URL, write ~/.swap/config
$ swap update      # pull the latest public prebuilt (no-op when current)
$ swap --help

02Usage & flags

$ swap [options] <amount> <tokenIn> [tokenOut]
Flag Effect
-v, --venue <list> Venue(s) to query. One name, a comma-separated list (kyber,matcha,1inch), or all (default). --all is an alias for -v all.
-c, --chain <alias> eth (default), base, arb, op, avax, bsc, hype, unichain, robinhood, arc, monad, plasma, polygon, gnosis, ink.
--slippage <pct> Tolerance in percent — 0.5 = 50 bps. Default 0.1. The same value drives the quote and the build.
--from <addr|alias> Sender and recipient of the swap. Required for -d, --simulate, --browser and max. Also read from $SENDER_ADDRESS. Accepts a wallet alias (see actions).
-d, --data Also build the tx: target, calldata, value, gas params, allowance check and an approve tx when needed.
--simulate / --simu Run approve + swap through eth_simulateV1 with a pranked tokenIn balance; reports the tokenOut actually received and any revert reason. Sync venues only.
--browser Open a local RainbowKit page to connect a wallet and sign / send. Covers sync txs, intent orders and Uniswap Permit2 flows. Exclusive with --json / --json-stream / -s.
--allow-async Include intent venues (cow, delta, uniswapx, fusion, ophis). Output is an EIP-712 order to sign and POST, not a tx.
--exact-out amount is the tokenOut to receive; the CLI minimises tokenIn paid. Not allowed with send / wrap / special actions.
-a, --action <name> swap (default), send, addwallet, plus protocol helpers listed in 07.
--to <addr|alias> Recipient for -a send.
--rpc <url> Override the RPC for this run — beats every env var. Handy for a local fork.
--json Structured machine-readable output, one object.
--json-stream NDJSON, one line per venue as it settles instead of one object after the slowest. Under -d each line carries that route's calldata. Ends with a done line. See 11.
-s, --simple Print only the number: amountOut, or amountIn under --exact-out.
--nofee Zero the partner fee (REFERRAL_FEE_BPS) for this run on every venue. Refused in hosted mode (fee policy is server-side); use --local.
--leg-venue <venue> 9summits fxSAVE exit only: send its fxUSD / USDC swap legs through this aggregator (kyber, velora, matcha, 1inch, openocean, fynd) when it quotes them all; otherwise each leg takes its best quote. The 9summits row lists the alternatives.
--debug With --simulate: also list ERC20 transfers landing at REFERRAL_ADDRESS.
--hosted Quote and build through the hosted API (https://swap.9summits.io) for this run instead of the local engine. No venue key, no RPC except --simulate. Nothing is signed remotely. Exclusive with --local.
--local Force the local engine for this run even when SWAP_API_URL is set, using your own venue keys and RPC.
--show-mode Print the effective mode (hosted <base> or self-hosted) on stdout and exit, without quoting. Takes no positional argument; --hosted / --local alongside it are honoured.
-V, --version Print the version and the commit the binary was built from, e.g. swap 0.1.0 (2c02339, 2026-09-15), then exit. Takes no positional argument and makes no network call. A build with no git metadata prints swap 0.1.0 (unknown build).

03Quoting

The default action races every venue in parallel and prints the winner: route tree, rate, gas, chain, venue, router and token rows, then a bold summary line. With -v all a venues comparison block streams below, sorted by guaranteed amountOut (Fusion ranks on auction-end minAmountOut, shown next to the cote) with ★ on the current leader. When CoinGecko prices both legs, ranking is net of gas.

# best quote, all venues (default)
$ swap 1 WBTC ETH

# pin one venue, or race a subset
$ swap 1 WBTC ETH -v kyber
$ swap 1 WBTC ETH -v kyber,matcha,1inch

# another chain
$ swap 1000 USDT USDC --chain bsc
$ swap 0.5 ETH USDC -c base

# include intent venues (CoW, UniswapX, Velora Delta, 1inch Fusion)
$ swap 2 WETH stETH --all --allow-async

# wider tolerance
$ swap 5000 USDC PEPE --slippage 1

# address instead of symbol
$ swap 1 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 USDC
Keys

Venues that need an API key you haven't set are skipped under -v all (stderr names the missing env vars). Selecting one explicitly (-v matcha) errors with the signup URL. See 10.

04Build & simulate

-d re-quotes with your real sender and prints an approve tx block (only when allowance is insufficient) and a swap tx block with the target, calldata, value and gas parameters, ready to broadcast. The approval target is the venue's spender, which can differ from the tx target.

# calldata + allowance check
$ swap 100 USDC USDT --from 0xYou -d

# swap the whole balance
$ swap max USDC WETH --from 0xYou -d

# dry-run: pranked balance, approve + swap, actual tokenOut received
$ swap 100 USDC USDT --from 0xYou --simulate
$ swap 2000 WETH stETH -v all --from 0xYou --simu -d

# against a local fork
$ swap 100 USDC USDT --from 0xYou --simulate --rpc http://127.0.0.1:8545

--simulate appends a simulation block: gas used, revert reason if any, and the tokenOut that actually landed. Calldata stays hidden unless you also pass -d. Both need an RPC (10).

05Browser signing

--browser builds the swap as for -d, starts a tiny local server on 127.0.0.1:5151 and opens a RainbowKit page in your default browser. The page shows the quote, lets you connect MetaMask, Rabby, WalletConnect, …, offers a simulate button, and hard-blocks signing if the connected wallet is not --from. The tx hash or order id is reported back to the terminal.

$ swap 0.1 WETH USDC --from 0xYou --browser
$ swap 1 ETH WETH --from 0xYou --browser                # wrap
$ swap 100 USDC -a send --to 0xRecip --from 0xYou --browser
$ swap 2 WETH stETH --allow-async --from 0xYou --browser   # sign an intent order

Run swap with no arguments to open the interactive dApp. In hosted mode (the default) this opens swap.9summits.io directly in your browser; pass --local to start the same dApp server on your machine instead, with your own keys.

06Exact-out (buy)

With --exact-out the amount is the tokenOut you want and venues are ranked by the least tokenIn paid. Venues with a native buy path (velora, matcha, uniswap, cow, ophis) are quoted directly; sell-only venues are refined as exact-in at the best native pay and must still deliver at least your target. A sell-only venue selected alone (-v kyber --exact-out) is rejected.

# receive exactly 1 WBTC, pay as little USDC as possible
$ swap 1 USDC WBTC --exact-out

# build it
$ swap 1 USDC WBTC --exact-out --from 0xYou -d

# just the USDC to pay
$ swap 1 USDC WBTC --exact-out -s

07Wrap, send & other actions

Wrap / unwrap

When the pair is the chain's native ↔ wrapped token (ETH ↔ WETH, BNB ↔ WBNB, AVAX ↔ WAVAX, HYPE ↔ WHYPE, POL ↔ WPOL, XDAI ↔ WXDAI) the venue loop is skipped: the rate is 1:1 by construction, so the CLI emits a direct deposit() / withdraw() on WETH9. No approval needed either way. The CLI prints 9summits on its venue / via lines and the dApp shows the 9Summits label and mark; --json and the API keep "venue": "wrap".

$ swap 1 ETH WETH -d --from 0xYou              # wrap
$ swap 1 WETH ETH -d --from 0xYou              # unwrap
$ swap 1 BNB WBNB --chain bsc -d --from 0xYou

Send (-a send)

A plain token transfer with the same calldata builder and --browser flow, no DEX involved. ERC20 emits transfer(to, amount); native emits a value-only tx. tokenOut is ignored.

$ swap 100 USDC -a send --to 0xRecip --from 0xYou -d
$ swap 0.5 ETH  -a send --to 0xRecip --from 0xYou --browser
$ swap max USDC -a send --to treasury --from main   # aliases, see below

Wallet aliases (-a addwallet)

Register a readable name for an address once; then pass the alias to --from or --to. Stored in ~/.swap/wallets.json.

$ swap -a addwallet main 0xYourAddress
$ swap -a addwallet treasury 0xTreasuryAddress
$ swap 1 WBTC ETH --from main -d

Protocol helpers

Action Chain What it builds
-a unwrapwrseth Base Kelp wrsETH → rsETH
-a withdrawsparkweth Ethereum Spark spWETH → WETH
-a unstakesavax Avalanche BENQI sAVAX → AVAX requestUnlock
-a claimsavax Avalanche Redeem matured sAVAX unlock requests
$ swap 10 sAVAX -a unstakesavax --chain avax --from 0xYou --browser
$ swap 0 sAVAX  -a claimsavax   --chain avax --from 0xYou --browser

08Loop (Morpho leverage)

swap loop opens a leveraged fxSAVE position on Morpho Blue, or adds to an open one, in a single transaction: flash-borrow USDC, turn it into fxSAVE, supply the fxSAVE as collateral, borrow USDC against it, and repay the flash loan with that borrow plus the USDC you bring. It runs on Ethereum (the default), Base, Robinhood, Monad and Arc. This section details fxsave-usdc (fxSAVE collateral, USDC loan, LLTV 91.5 %); --market also takes 19 other stable markets (swap loop --help lists them), which buy the collateral with one aggregator swap from the loan token. Their entry cost cap is also 25 bps of the oracle value, except pt-fxsave-29oct2026-usdc at 2 %: its oracle values the PT about 1.6 % under its market price, which lowers its max leverage (about 7.6x instead of 8.6x at the default max LTV). The dApp runs the same engine in its Loop tab (Beta).

Chains and markets. The 20 aliases are Ethereum markets, each fork-tested. --market also takes any other Morpho Blue market id (0x + 64 hex) on the chain --chain names: Ethereum, base, robinhood, monad or arc. Such a market is read from that chain (tokens, decimals, oracle) and from the Morpho API (listing, warnings), its entry is one loan token → collateral swap delivered to the chain's General Adapter, and its build is simulated with eth_simulateV1 first: a route that reverts or delivers under the plan's floor is rebuilt through the next venue, and the build fails when none passes. Base's Morpho has Ethereum's address, so the same id can name a market on both: an id is only ever read on the chain you pass. Aggregator coverage differs per chain; a pair no venue can deliver to the adapter is refused.

$ swap loop <equity> usdc --market fxsave-usdc --leverage <x> [options]
# plan only: flash, borrow, LTV, health, liq. price, max leverage, APRs
$ swap loop 1000 usdc --market fxsave-usdc --leverage 3

# preflight txs + the Bundler3 multicall
$ swap loop 1000 usdc --market fxsave-usdc --leverage 3 --from 0xYou -d

# dry-run: approve + Morpho authorization + multicall, position before/after
$ swap loop 1000 usdc --market fxsave-usdc --leverage 3 --from 0xYou --simulate

# sign & send in your wallet
$ swap loop 1000 usdc --market fxsave-usdc --leverage 3 --from 0xYou --browser

# whole USDC balance, JSON
$ swap loop max usdc --market fxsave-usdc --leverage 2.5 --from 0xYou --json

# open position: take the whole position to 5x with a flash only
$ swap loop 0 usdc --market fxsave-usdc --leverage 5 --from 0xYou -d

# past the default max LTV, up to LLTV (health factor 1)
$ swap loop 1000 usdc --market fxsave-usdc --leverage 9 --max-ltv 0.915 --from 0xYou
Argument / flag Effect
<equity> USDC you bring, in human units. max reads the sender's USDC balance. 0 levers up an open position with a flash only.
<token> Loan token: usdc (or its address).
--market <id> Required. A market alias (swap loop --help lists the 20, Ethereum only), or any Morpho market id on --chain.
--leverage <x> Required. 3, 1.5, 3x; at least 1, up to 4 decimals. On an open position it is the whole position's target after the loop.
--max-ltv <fraction> Max loan-to-value as a fraction (0.885). Default LLTV − 3 % (88.5 %). Capped at LLTV (0.915): health factor 1, liquidatable as soon as interest accrues.
--slippage-bps <bps> Default 10. Bounds the entry swap's minimum out, and on fxsave-usdc the fxSP deposits and the fxSAVE share price. Basis points, not the --slippage percent of a swap.
--chain <chain> eth (default; mainnet and ethereum too), base, robinhood, monad or arc. Any other chain is refused.
--from, -w, --wallet Sender, address or wallet alias; the position is opened on it. Falls back to $SENDER_ADDRESS. Required for -d, --json, --simulate, --browser and max. Without a sender the plan ignores any open position.
-d, --data Print the preflight txs and the Bundler3 multicall.
--json Plan, preflight and tx as one object (it builds, so it needs a sender). Exclusive with --browser.
--simulate / --simu eth_simulateV1 of approve, authorization and multicall with the sender's USDC balance set to the equity. Prints each step's gas and the collateral and borrow after. Exit 1 on a revert.
--browser Local page that sends the preflight txs, then the multicall.
--rpc <url> RPC of the loop's chain for this run.
--nofee Zero the partner fee on the embedded USDC → fxUSD swap.
--hosted Refused. Loop always runs on the local engine.
Needs your RPC

Loop reads Morpho, fxSP and Curve directly and builds the bundle locally, even in the hosted-by-default binary: with SWAP_API_URL set it prints a note on stderr and runs locally. Every run, a plain plan included, needs an RPC for its chain (10); a market resolved from its id also needs one that serves eth_simulateV1 to build.

What the transaction does

On a fresh position the flash is equity × leverage USDC and the borrow is flash − equity. The multicall is one Bundler3 call to the General Adapter's morphoFlashLoan; its callback runs, in order:

  1. The flash USDC goes into fxSP, f(x)'s stability pool, through up to two legs: a direct USDC deposit, and a USDC → fxUSD swap whose fxUSD is deposited instead, used only when it pays (see Entry below).
  2. All fxSP is deposited into fxSAVE (ERC-4626) under a max share price.
  3. The fxSAVE is supplied as Morpho collateral for the sender.
  4. USDC is borrowed on Morpho (skipped at 1x, where there is no debt).
  5. The equity is pulled from the sender's wallet (skipped for 0 USDC). Borrow plus equity repays the flash exactly.

The swap pays its fxUSD to the General Adapter. The adapter hands over exactly the swap's minimum out for a deposit that must succeed, then the rest of the quoted amount for a deposit that is skipped when the swap filled short. Any fxUSD left on the adapter goes back to the sender as fxUSD, so nothing stays on Bundler3 or the adapter. The plan reports LTV, health factor and liquidation price on the minimum fxSAVE the must-succeed deposits guarantee under the slippage bounds.

Entry: direct deposit or swap

fxSP credits deposited USDC at its stable-token price. The engine checks whether the Curve USDC/fxUSD pool's marginal rate beats that credit. If it does, it bisects for the largest amount still worth swapping, and keeps the swap only if the gain beats the extra leg's gas (about 170k gas) by a 50 % margin. That amount is then quoted on the aggregators whose transaction can pay the General Adapter (KyberSwap, Velora, 0x/Matcha, 1inch, OpenOcean) and on the Curve pool itself. The best output wins, the pool on a tie. The winner is re-quoted and built with Bundler3 as the sender and the General Adapter as the recipient; if no aggregator builds, the swap goes straight through the Curve pool. When the swap does not pay, the whole flash is deposited as USDC. The plan's entry row names the choice and the venue.

Leverage, LTV and health

Open position

When the sender already has collateral or debt on the market, --leverage is the target for the whole position after the loop, not the leverage of the new USDC. With C the collateral value, E = C − debt and N the new USDC, the flash is (E + N) × L − C and the borrow is flash − N. The target cannot be below (C + N) / (E + N), the position with N added and nothing borrowed. With 0 usdc it must be above the current leverage, so a top-up can only lever up. LTV, health, liquidation price and Loop APR are reported for the combined position, and the CLI adds position and after loop rows.

Preflight and fees

The multicall needs two one-time grants, and -d prints only the missing ones: a USDC approve to Morpho's General Adapter for exactly the equity (none for 0 USDC), and Morpho setAuthorization(GeneralAdapter1, true) so the adapter can borrow for the sender. The multicall's tx.to is Bundler3 and its tx.spender is the General Adapter, the USDC approval target. On the authorization tx, to is Morpho and spender names the address being authorized; it is not an ERC-20 spender.

The flash carries a fee only when LOOP_FLASH_FEE_BPS is set (integer 0–1000, default 0): that share of the flash, in the loan token, goes to REFERRAL_ADDRESS inside the callback, and the plan reports it as fee / feeBps (a Fees row in the dApp). The deposits and the borrow carry none. The embedded swap, when used, pays the normal partner fee plus a loop surcharge: REFERRAL_FEE_BPS + LOOP_SWAP_FEE_SURCHARGE_BPS (default 1 bp, integer 0–1000), capped at 1000 bps, on venues that take a partner fee (KyberSwap, Velora, 0x, 1inch, OpenOcean). It is 0 on Curve and Fynd, without REFERRAL_ADDRESS, and under --nofee. The fee is already inside the quoted fxUSD out.

Unloop

swap unloop unwinds the sender's Morpho borrow on a Loop chain (--from required). Without --market it lists that wallet's borrows. With --market, pass exactly one of --leverage <x> (partial, down to 1x) or --close (--receive collateral or loan). It is local only (--hosted is refused) and needs your RPC. --max-exit-cost raises the exit-cost cap in the CLI only. On the fxSAVE market the sale also races fxsave-redeem (fxSAVE → fxSP → fxUSD + USDC through f(x)'s 1% instant redeem, then aggregator legs); the better net of gas wins, -v fxsave-redeem forces it, and the exit-cost cap leaves that disclosed fee out. --leg-venue <venue> sends that exit's fxUSD → loan leg through one aggregator when it quotes it (the plan's legs line lists the alternatives; dApp: the 9Summits row's "Swap leg via" picker).

In the dApp

The Loop tab sits next to Swap and Send (/?action=loop) and follows the header's chain. On Ethereum the Market pill picks one of the 20 markets, grouped by loan token with their LLTV, and links to the market on Morpho; on every Loop chain its search lists Morpho's listed markets of that chain, and a pasted market id or Morpho URL opens that market. The pick rides in the URL with its chain (/?action=loop&market=pst-pyusd, /?chain=base&action=loop&market=0x…). You pay is the market's loan token and starts at the connected wallet's balance of it. The leverage slider moves in 0.1x steps up to the max leverage at LLTV, with the last 5 % of its track in red; past the default max LTV the request sends maxLtvBps = LLTV. With an open position the slider reads Target leverage and marks Now; 0 only levers up. The plan shows the collateral's APR (Fixed APY and the maturity for a PT). The left pane draws the path (on fxsave-usdc: flash → swap and/or direct USDC → fxSP → fxSAVE → Morpho borrow; elsewhere: flash → swap to the collateral → supply → borrow), the right pane shows the position (collateral, borrow, LTV, leverage, health) as Now → After. Loop sends the missing approval and authorization, then the multicall, as one wallet_sendCalls batch when the wallet supports it, one at a time otherwise. The terminal button copies the matching swap loop command.

HTTP API

POST /api/loop/quote and POST /api/loop/build, on the public host and the local server, take the same body. chain is eth (or mainnet), base, robinhood, monad or arc; market an alias (Ethereum) or a Morpho market id on that chain. equity is in the market's loan-token base units ("0" for a top-up), leverage and LTV are in bps, sender, maxLtvBps and slippageBps (default 10) are optional for a quote. GET /api/loop/markets?chain=<chain> takes market=<alias or id> (the market resolved on the chain, with its APRs) or q=<text> (Morpho's listed markets of the chain matching the tokens); without either it lists the 20 Ethereum markets' APRs, and nothing on another chain.

# 1,000 USDC at 3x, no sender: plan without any open position
$ curl -s https://swap.9summits.io/api/loop/quote -H 'content-type: application/json' \
  -d '{"chain":"eth","market":"fxsave-usdc","equity":"1000000000","leverageBps":30000,"slippageBps":10}'

Quote returns the plan, the same object as swap loop --json with preflight and tx null. Build requires sender and returns the swap Payload with venue: "loop", approval: null, an ordered preflight list and the plan. A refused plan is HTTP 400 with error and maxLeverageBps. Schemas in openapi.json.

POST /api/loop/unwind/quote, /api/loop/unwind/quote/stream and /api/loop/unwind/build take {chain, market, sender, mode, targetLeverageBps?, receive?, slippageBps?, venues?, venue?}. sender is required. mode is partial (integer targetLeverageBps ≥ 10000, no receive) or close (receive collateral or loan, no target). curve is not a sale venue. The API does not accept a max exit cost. Quote returns the unwind plan; the stream is NDJSON (leg, route / verror, then plan or error); build returns the same Payload as the Loop build with tokenIn the collateral and tokenOut the loan token, plus plan. GET /api/loop/positions?chain=<alias>&sender=<address> returns that wallet's borrows ({positions}), cached 15s in-process, cache-control: private, no-store. A refused plan or a bad body is HTTP 400; a Morpho or RPC failure is 502.

Chain Morpho Blue Bundler3 (tx.to) GeneralAdapter1 (approve, authorize)
Ethereum 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb 0x6566194141eefa99Af43Bb5Aa71460Ca2Dc90245 0x4A6c312ec70E8747a587EE860a0353cd42Be0aE0
Base 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb 0x6BFd8137e702540E7A42B74178A4a49Ba43920C4 0xb98c948CFA24072e58935BC004a8A7b376AE746A
Robinhood 0x9D53d5E3bd5E8d4Cbfa6DB1ca238AEA02E651010 0x6478e9393d4C5bB4d53ee881d1DE78786A0344a6 0xc5E188541D107e8B79e43478bDE365F1406665D6
Monad 0xD5D960E8C380B724a48AC59E2DfF1b2CB4a1eAee 0x82b684483e844422FD339df0b67b3B111F02c66E 0x725AB8CAd931BCb80Fdbf10955a806765cCe00e5
Arc 0x34CD04070dD72b14E241112F6d83812Df5Af7fCD 0x855dbe13C409Df75CAF6a985Cf6993a4D0319Feb 0x0AF5BE7BD7B615e94e1d9275414482D028952466

fxsave-usdc (Ethereum):

Contract Address
fxSAVE (collateral) 0x7743e50F534a7f9F1791DdE7dCD89F7783Eefc39
fxSP 0x65C9A641afCEB9C0E6034e558A319488FA0FA3be
USDC (loan) 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
fxUSD 0x085780639CC2cACd35E474e71f4d000e2405d8f6
Curve USDC/fxUSD 0x5018BE882DccE5E3F2f3B0913AE2096B9b3fB61f
Market id 0x17f7ae1b52670010976b3fe41324cbb2b1eb7dd8f492e51764b2828371b86b84

09Venues & chains

Venue Kind Exact-out Key
kyber sync refine optional
velora sync native —
matcha sync native ZEROEX_API_KEY
1inch sync refine ONEINCH_API_KEY
curve sync refine — (needs RPC)
uniswap sync native UNISWAP_API_KEY
openocean sync refine optional OPENOCEAN_API_KEY
fynd sync refine FYND_API_KEY
9summits sync refine — (needs RPC)
cow intent native —
ophis intent native OPHIS_REFERRAL_CODE
delta intent refine —
uniswapx intent refine UNISWAP_API_KEY
fusion intent refine ONEINCH_API_KEY
odos, odosv2 disabled — API discontinued 2026-07-30

Sync venues return a broadcastable transaction. Intent venues return an EIP-712 order that you sign and POST off-chain; a solver settles it on-chain within the order's validity window. They require --allow-async because the output shape differs.

9summits is the in-house route. It only quotes USDC → fxSAVE, fxUSD → fxSAVE and fxSAVE → any token on Ethereum; on any other pair it is absent from the race with no error row, and -v 9summits exits with "9summits only quotes USDC or fxUSD → fxSAVE, or fxSAVE → any token, on Ethereum". It mints fxSAVE in one Bundler3 multicall. fxUSD goes straight into the f(x) stability pool (fxSP). USDC is split the way swap loop splits its deposit. The engine sizes a USDC → fxUSD swap where the Curve USDC/fxUSD pool's marginal rate meets fxSP's USDC credit, keeps it only when the gain beats its gas, and runs it on the best of KyberSwap, Velora, 0x/Matcha, 1inch, OpenOcean, Fynd and the Curve pool itself. The rest of the USDC goes into fxSP directly. All the fxSP becomes fxSAVE for the sender, and any fxUSD the swap delivers above what was deposited goes back to the sender as fxUSD. tx.to is Bundler3, tx.spender (the approval target) is Morpho's GeneralAdapter1, and value is 0. Only the swap leg pays a partner fee, at the leg venue's normal REFERRAL_FEE_BPS (0 on the Curve pool); the direct deposit and the mint carry none, and the loop surcharge does not apply. The reverse, fxSAVE → any token (native ETH included) except fxSP, redeems through fxSP's instant redeem and sells the fxUSD and USDC it returns on aggregators only (never the Curve pool), in one multicall. f(x) charges a 1% instant-redemption fee, shown on the row as Incl. 1% f(x) redemption fee — 1h cooldown avoids it; ranking is net of gas. Each leg takes its best aggregator by default; the row lists the aggregators that could take every leg with the route's output through each, and --leg-venue <venue> (dApp: the row's "Swap leg via" picker) sends the legs through one of them. The route tree shows each leg's own pools, labelled venue · pool.

Availability by chain

Which venues answer on which chain. Under -v all the CLI only races the venues that serve the selected chain; the others are skipped silently. Async venues still need --allow-async; curve and 9summits need an RPC, and 9summits also needs one of its two pairs.

Venue ethbasearbopunichainbscavaxhyperobinhoodarcmonadplasmapolygongnosisink
kyber sync ✓✓✓✓✓✓✓✓✓✓✓✓✓··
velora sync ✓✓✓✓·✓✓·····✓✓·
matcha sync ✓✓✓✓✓✓✓·✓✓✓✓✓·✓
1inch sync ✓✓✓✓✓✓✓✓✓✓✓·✓✓·
curve sync ✓✓✓✓··✓······✓·
uniswap sync ✓✓✓✓✓✓✓·✓✓✓·✓·✓
openocean sync ✓✓✓··✓✓·✓✓··✓✓·
fynd sync ✓✓✓·✓✓··✓···✓··
9summits sync ✓··············
cow intent ✓✓✓·········✓✓✓
ophis intent ✓✓✓✓✓✓✓·✓··✓✓✓✓
delta intent ✓✓✓✓✓✓✓·····✓··
uniswapx intent ✓✓✓·✓··········
fusion intent ✓✓✓✓✓✓✓·✓·✓·✓✓·
venues per chain 141313991010285531174

✓ served   · not served. Snapshot of the adapters' chain tables; the live set for a deployment is GET /api/mode.

Chains

Alias Chain Native
ethEthereum (default)ETH
baseBaseETH
arbArbitrum OneETH
opOptimismETH
unichainUnichainETH
bscBNB ChainBNB
avaxAvalancheAVAX
hypeHyperEVMHYPE
robinhoodRobinhood ChainETH
arcArcUSDC
monadMonadMON
plasmaPlasmaXPL
polygonPolygonPOL
gnosisGnosisXDAI
inkInkETH

10RPC, keys & config

RPC

Needed for -d, --simulate, --browser, max and every swap loop run. Resolution order, first match wins:

  1. --rpc <url> on the command line
  2. RPC_URL_<chainId> — e.g. RPC_URL_1
  3. <ALIAS>_RPC_URL — e.g. ETH_RPC_URL, BASE_RPC_URL
  4. ALCHEMY_API_KEY — one key covers every supported chain (free tier is enough)
No public RPC fallback (CLI)

Missing RPC config is a hard failure that names the exact variable to set. Public RPCs rate-limit and return inconsistent allowance reads, which would silently break the CLI build path. The interactive dApp server last-resorts to the shared PublicNode table when Alchemy and per-chain overrides are unset.

Where config lives

Shell environment first, then a .env in the current directory, then ~/.swap/config (written by --init / the first-run prompt), then values embedded in the binary at build time. Prebuilt binaries ship venue keys and referral settings; your RPC key is always yours to configure.

Venue API keys

Variable Unlocks Free signup
ZEROEX_API_KEY matcha dashboard.0x.org
ONEINCH_API_KEY 1inch, fusion portal.1inch.dev
UNISWAP_API_KEY uniswap, uniswapx hub.uniswap.org
FYND_API_KEY fynd @fynd_portal_bot (Telegram)
KYBER_API_KEY kyber API gateway, higher rate limits (optional). KYBER_SOURCE / KYBER_REFERRAL add on-chain attribution business@kyber.network
OPENOCEAN_API_KEY openocean pro host, higher limits (optional; public host is keyless, 2 rps) docs.openocean.finance
OPHIS_REFERRAL_CODE ophis (skipped without it) CIP-75 rebate code
SENDER_ADDRESS default for --from —
WALLETCONNECT_PROJECT_ID WalletConnect in --browser (injected wallets work without it) —

Partner fees

Set REFERRAL_ADDRESS and optionally REFERRAL_FEE_BPS (1 bp = 0.01 %) to attribute a fee on every venue that supports it; the fee is already reflected in the quoted amounts so venues stay comparable. --nofee zeroes it for one run.

Hosted mode

By default the public binary quotes and builds through https://swap.9summits.io/api/* instead of the local venue engine: no venue key and no RPC needed for a plain quote or a -d build, and every key-gated venue (1inch, Fusion, Matcha/0x, Uniswap, UniswapX) works out of the box. --simulate and the local-only actions (send, unwrapwrseth, withdrawsparkweth, unstakesavax, claimsavax), and swap loop, still run against your own RPC.

Resolution order: --local > --hosted > SWAP_API_DISABLED=true > SWAP_API_URL env var > local engine. The public prebuilt binary embeds SWAP_API_URL=https://swap.9summits.io. To self-host, pass --local for one run, set SWAP_API_DISABLED=true, or clear SWAP_API_URL (unset it in the shell, .env, or ~/.swap/config), and configure your own venue keys and RPC.

swap --show-mode prints which backend the current configuration resolves to, one line on stdout: hosted https://swap.9summits.io or self-hosted. It never quotes, and swap --show-mode --local prints self-hosted.

Variable Default Effect
SWAP_API_URL https://swap.9summits.io (embedded in the public binary) Base URL of the /api/* deployment the CLI quotes and builds through. Empty/unset disables hosted mode
SWAP_API_DISABLED false true forces the local engine with your own keys and RPC even when SWAP_API_URL is set, so the embedded URL can be opted out of without clearing it. Accepts 1/true/yes/on and 0/false/no/off; any other value is an error

Hosted specifics: --nofee is refused (fee policy is server-side); the offered venue list comes from GET /api/mode and omits curve; a rate-limited request (429) reports the retry delay; an unreachable API fails loud and suggests --local rather than falling back silently; a stale binary whose apiVersion no longer matches the deployment fails with hosted API contract mismatch … run swap update. No telemetry is added by hosted mode (the CLI still never calls /done), but the server does see the caller's IP, the pair, the amounts, and the --from address on a build, the same as the web dApp.

11Output & scripting

default

Route tree, rate / gas / venue / router rows, bold summary, and the live venue comparison under -v all.

--json

One object: best (venue name), quotes (keyed by venue), approval, tx, order, simulation. Errors are {"error": "…"} with exit 1.

--json-stream

NDJSON as venues settle: a meta line, one route (or verror) line per venue, then done with the winner. Under -d every route is built on arrival and carries its tx / approval. Errors end with {"type":"fatal","error":"…"}, exit 1.

-s / --simple

Just the human-units number. amountOut by default, amountIn under --exact-out.

# capture the rate in a script
$ RATE=$(swap 1 WBTC ETH -s)

# the winning venue, then its full quote
$ swap 1 WBTC ETH --all --json | jq -r '.best'
$ swap 1 WBTC ETH --all --json | jq '.quotes[.best]'

# calldata only, straight to another tool
$ swap 100 USDC USDT --from 0xYou -d --json | jq -r '.tx.data'

# routes as they land, then the winner (one JSON object per line)
$ swap 1000 USDC USDT --json-stream | jq -c 'select(.type=="route") | [.venue, .elapsedMs, .quote.amountOut.human]'
$ swap 1000 USDC USDT --from 0xYou -d --json-stream | jq -r 'select(.type=="done") | .tx.data'

# every venue's amountOut, best first
$ swap 1000 USDC USDT --json | jq -r '.quotes | to_entries[] | select(.value.amountOut) | "\(.key)\t\(.value.amountOut.human)"' | sort -k2 -gr

Human-readable output goes to stdout; warnings and progress go to stderr, so piping --json, --json-stream or -s stays clean.

--json-stream line shapes (same event names as /api/quote/stream):

{"type":"meta", side, chain, tokenIn, tokenOut, amountIn|amountOut, slippageBps}
{"type":"route", venue, elapsedMs, quote}                                    # without -d
{"type":"route", venue, elapsedMs, buildMs, quote, approval, kind, tx, order, permitTx}  # -d
{"type":"route", venue, elapsedMs, buildMs, quote, kind: null, buildError}  # -d, build failed
{"type":"verror", venue, elapsedMs, error}
{"type":"done", best, quote, approval, kind, tx, order, permitTx, simulation}

route.quote is provisional (no intermediary symbols, USD may be missing); done.quote is the enriched winner, and done.tx is the route build already streamed, not a second one. --simulate runs on the winner only. Wrap, send and the other short-circuit actions emit only done.

12Unattended use & agents

The CLI is safe to drive from scripts, CI and AI agents: it never waits on stdin when it is not a terminal, everything is configurable through the environment, and --json gives a stable shape.

Machine-readable copies

This manual as Markdown: /docs.md (also served at /docs with Accept: text/markdown). Installable agent skill: /skills/swap-cli/SKILL.md. Site index for LLMs: /llms.txt and /llms-full.txt.

> back to 9Swap · Markdown · OpenAPI · Disclaimer · contact@9summits.io