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
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
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.
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.
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
$ swap [options] <amount> <tokenIn> [tokenOut]
amount is in human units
(1.5, 0.001). max swaps the
sender's entire balance (needs --from + RPC). By default it
is the amount of tokenIn; with
--exact-out it becomes the amount of
tokenOut you want to receive.
tokenIn / tokenOut accept a
symbol (WBTC, case-insensitive) or a 0x
address. Unknown symbols are resolved through KyberSwap, then
CoinGecko, then on-chain decimals(). Native gas token is
ETH, BNB, AVAX,
HYPE, MON, XPL,
POL, XDAI depending on the chain.
tokenOut is optional only with
-a send.
| 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).
|
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
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.
-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).
--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.
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
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
-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
-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
| 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
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. |
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.
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:
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.
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.
nav() growth, annualized; Loop APR =
(collateral × fxSAVE APR − debt × borrow APY) / equity, negative when
the borrow costs more than the yield.
--max-ltv is above LLTV, the equity is 0
without an open position, or the target leverage would need an
unloop.
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.
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.
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).
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.
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 |
| 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.
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 | eth | base | arb | op | unichain | bsc | avax | hype | robinhood | arc | monad | plasma | polygon | gnosis | ink |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
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 | 14 | 13 | 13 | 9 | 9 | 10 | 10 | 2 | 8 | 5 | 5 | 3 | 11 | 7 | 4 |
✓ served · not served.
Snapshot of the adapters' chain tables; the live set for a deployment
is GET /api/mode.
| Alias | Chain | Native |
|---|---|---|
eth | Ethereum (default) | ETH |
base | Base | ETH |
arb | Arbitrum One | ETH |
op | Optimism | ETH |
unichain | Unichain | ETH |
bsc | BNB Chain | BNB |
avax | Avalanche | AVAX |
hype | HyperEVM | HYPE |
robinhood | Robinhood Chain | ETH |
arc | Arc | USDC |
monad | Monad | MON |
plasma | Plasma | XPL |
polygon | Polygon | POL |
gnosis | Gnosis | XDAI |
ink | Ink | ETH |
Needed for -d, --simulate,
--browser, max and every
swap loop run. Resolution order, first match wins:
--rpc <url> on the command lineRPC_URL_<chainId> — e.g. RPC_URL_1<ALIAS>_RPC_URL — e.g. ETH_RPC_URL, BASE_RPC_URLALCHEMY_API_KEY — one key covers every supported chain (free tier is enough)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.
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.
| 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) |
— |
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.
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.
Route tree, rate / gas / venue / router rows, bold summary, and the
live venue comparison under -v all.
One object: best (venue name),
quotes (keyed by venue), approval,
tx, order, simulation.
Errors are {"error": "…"} with exit 1.
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.
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.
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.
--init
exits 1 instead of hanging. --json /
--json-stream / -s also suppress it.
0 success, 1
any error, message on stderr and {"error": "…"} on
stdout under --json (a final
{"type":"fatal",…} line under --json-stream).
ALCHEMY_API_KEY or
RPC_URL_<chainId>, SENDER_ADDRESS,
venue keys as needed. A .env in the working directory is
read too.
NO_BROWSER_OPEN=1 starts
swap / --browser without launching a
browser; the URL is printed to stderr. SWAP_NO_AUTH=1
drops the local server's ?id= gate.
/api/* contract, described in
openapi.json. The public host is rate
limited per IP and spends the operator's venue keys: cache, and
prefer the NDJSON stream.
tx.spender, not tx.to. Quote and build at
the same slippage. Never put the quote-only dEaD placeholder in a
signed order: CoW stamps the real sender on the reused envelope;
other async venues re-quote.
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.