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
Needs Bun. Clone
9summits/swap-cli,
bun install, then ./build →
dist/swap.
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.
Quotes work out of the box. Building a transaction, simulating, or
swapping max needs an RPC. The first time one 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, … 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, monad, plasma.
|
--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 / -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. |
-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.
|
--debug |
With --simulate: also list ERC20 transfers landing
at REFERRAL_ADDRESS.
|
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 amountOut 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 silently skipped
under -v all. Selecting one explicitly
(-v matcha) errors with the signup URL. See
09.
-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 (09).
--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 full interactive
dApp locally instead — the same UI as
swap.9summits.io, served from your machine 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) 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.
$ 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
| 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 | — |
uniswap |
sync | native | UNISWAP_API_KEY |
openocean |
sync | refine | OPENOCEAN_API_KEY |
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.
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 needs an RPC.
| Venue | eth | base | arb | op | unichain | bsc | avax | hype | robinhood | monad | plasma |
|---|---|---|---|---|---|---|---|---|---|---|---|
kyber sync |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
velora sync |
✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ | · | · | · | · |
matcha sync |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ | ✓ |
1inch sync |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ | · |
curve sync |
✓ | ✓ | ✓ | ✓ | · | · | ✓ | · | · | · | · |
uniswap sync |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ | · |
openocean sync |
✓ | ✓ | ✓ | · | · | ✓ | ✓ | · | ✓ | · | · |
cow intent |
✓ | ✓ | ✓ | · | · | · | · | · | · | · | · |
ophis intent |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | · | ✓ |
delta intent |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | · |
uniswapx intent |
✓ | ✓ | ✓ | · | ✓ | · | · | · | · | · | · |
fusion intent |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ | · |
| venues per chain | 12 | 12 | 12 | 9 | 8 | 9 | 10 | 1 | 7 | 5 | 3 |
✓ 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 |
monad | Monad | MON |
plasma | Plasma | XPL |
Needed for -d, --simulate,
--browser and max. 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 |
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 |
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.
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.
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'
# 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 or -s stays clean.
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 / -s
also suppress it.
0 success, 1
any error, message on stderr and {"error": "…"} on
stdout under --json.
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.
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.