A free, public DEX aggregator that lives entirely on-chain. No API keys, no servers, no tracking. Call the contracts from any Ethereum RPC, or use the REST API for a single HTTP call.
One HTTP call returns the best quote + ready-to-send calldata. No SDK, no ABI encoding, no Multicall3 batching. The worker queries zQuoter on-chain and compares all route strategies for you.
Returns the optimal swap route and transaction calldata.
Parameters
tokenIn — ERC20 address or 0x0000...0000 for ETH (required)tokenOut — ERC20 address or 0x0000...0000 for ETH (required)amount — raw integer in tokenIn's smallest unit (or tokenOut's if exactOut=true) (required)to — recipient address. Required for executable calldata; without it the quote is still valid but tx.data will target address(0) (optional)slippage — basis points, default 50 (0.5%) (optional)exactOut — true for exact output mode (optional)Example — swap 1 ETH for USDC:
curl "https://api.zfi.wei.is/quote?tokenIn=0x0000000000000000000000000000000000000000&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amount=1000000000000000000"
Response:
{
"bestRoute": {
"expectedOutput": "1850000000",
"source": "Uniswap V3",
"isTwoHop": false,
"isSplit": false
},
"tx": {
"to": "0x000000000000FB114709235f1ccBFfb925F600e4",
"data": "0x...",
"value": "1000000000000000000"
},
"allQuotes": [
{ "source": "Uniswap V3", "amountOut": "1850000000" },
{ "source": "Uniswap V2", "amountOut": "1845000000" }
]
}
Execute the swap:
const quote = await fetch("https://api.zfi.wei.is/quote?...").then(r => r.json());
await signer.sendTransaction({
to: quote.tx.to,
data: quote.tx.data,
value: quote.tx.value
});
Other endpoints:
GET /health — returns {"status":"ok"}Open to all origins, no API keys required. Rate limited by Cloudflare Workers (100k req/day free tier).
zQuoter — read-only route discovery and calldata builder.
0x000000a7DfdD39f4D74c7b201501eaD119F8b86C
zRouter — swap executor. Send the calldata built by zQuoter here.
0x000000000000FB114709235f1ccBFfb925F600e4
Aggregates Uniswap V2/V3/V4, SushiSwap, Curve, zAMM, and Lido. Zero protocol fees.
All routing is deterministic and happens on-chain via view functions. The REST API above handles this for you, but you can also call the contracts directly. The pattern is two steps:
No solvers, no relay infrastructure. Just an RPC call to get calldata, then a transaction to execute it.
Swap 1 ETH for USDC using ethers.js and buildSwapAuto — the one-call entry point that cascades single-hop → 2-hop hub → 3-hop:
import { ethers } from "ethers";
const ZQUOTER = "0x000000a7DfdD39f4D74c7b201501eaD119F8b86C";
const ZROUTER = "0x000000000000FB114709235f1ccBFfb925F600e4";
const quoterAbi = [
"function buildSwapAuto(address to, bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, uint256 slippageBps, uint256 deadline) view returns (tuple(uint8 source, uint256 feeBps, uint256 amountIn, uint256 amountOut) best, bytes callData, uint256 amountLimit, uint256 msgValue)"
];
const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const signer = /* your wallet signer */;
const me = await signer.getAddress();
const quoter = new ethers.Contract(ZQUOTER, quoterAbi, provider);
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const ETH = "0x0000000000000000000000000000000000000000";
const oneETH = ethers.parseEther("1");
const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
// 1. Quote (view call — free, no gas)
const r = await quoter.buildSwapAuto(
me, // to: recipient
false, // exactOut: false = exact input
ETH, // tokenIn
USDC, // tokenOut
oneETH, // swapAmount: 1 ETH
50n, // slippageBps: 0.5%
deadline
);
console.log("Expected USDC out:", ethers.formatUnits(r.best.amountOut, 6));
// 2. Execute (send transaction)
const tx = await signer.sendTransaction({
to: ZROUTER,
data: r.callData,
value: r.msgValue
});
console.log("Tx:", tx.hash);
For ERC20 → ERC20 or ERC20 → ETH swaps, the user must first approve zRouter to spend their tokens.
// Approve zRouter to spend your USDC
const token = new ethers.Contract(USDC, [
"function approve(address,uint256) returns (bool)"
], signer);
await token.approve(ZROUTER, ethers.MaxUint256);
// Then quote and execute as before, with tokenIn = USDC
const r = await quoter.buildSwapAuto(
me, false,
USDC, // tokenIn: USDC
ETH, // tokenOut: ETH
ethers.parseUnits("1000", 6), // 1000 USDC
50n, deadline
);
const tx = await signer.sendTransaction({
to: ZROUTER,
data: r.callData,
value: r.msgValue // 0 for ERC20 input
});
zRouter also supports permit and permit2TransferFrom for gasless approvals. These can be batched into a multicall alongside the swap calls.
One-call entry point that cascades through route depths and returns a single flat result (Quote, calldata, amountLimit, msgValue). Use this when you just want the best available route without composing multiple entry points yourself.
to — recipient of output tokensexactOut — false for exact input, true for exact outputtokenIn / tokenOut — ERC20 address, or address(0) for ETHswapAmount — amount of tokenIn (exact-in) or desired tokenOut (exact-out)slippageBps — slippage tolerance in basis points (50 = 0.5%)deadline — unix timestamp after which the swap revertsReturns:
best — aggregated Quote summarizing end-to-end input/output (source = final leg's AMM)callData — ready to send to zRouter as the transaction's data field (multicall envelope when multi-hop, raw swap when single-hop)amountLimit — min output (exact-in) or max input (exact-out) after slippagemsgValue — ETH value to attach to the transactionCascade (NOT a head-to-head comparison across depths): single/2-hop is tried first, 3-hop is a fallback only for pairs that can't build at shallower depth. For the best possible output across depths on a single pair, compare buildBestSwapViaETHMulticall + buildSplitSwap + buildHybridSplit yourself (see Best Quote Selection).
Direct single-hop best-of quote and calldata. Same return shape as buildSwapAuto, but no multicall envelope when the result is a raw single-hop swap routed directly to to. Use when you know a single-hop route exists and want minimum overhead.
Lower-level builder that explicitly returns both legs of a potential 2-hop hub route plus the individual call bytes. Use when you need to inspect or modify the multicall (e.g. prepending permit calls, custom sweeps).
refundTo — address for excess ETH / leftover MID refunds (usually msg.sender)a, b — Quote structs for leg A and leg B (b.amountOut == 0 for single-hop)calls — individual call bytes for each leg (for prepending permit / custom actions)multicall — encoded multicall(bytes[]) calldataSplits liquidity across two AMMs in parallel for better execution on large trades.
Combines a single-hop and a 2-hop route in parallel. Captures cases where splitting across route depths beats any single strategy.
Three-hop routing through two hub tokens for exotic pairs.
Returns the best single-hop quote and all individual AMM quotes (V2, Sushi, zAMM, V3, V4). Useful for displaying price comparison across venues.
Auto-discovers Curve pools via MetaRegistry and returns the best Curve-only quote.
Direct Lido staking quote for ETH → stETH or wstETH.
Low-level quoting for specific venues. Available on the base quoter at 0x658bF1A6608210FDE7310760f391AD4eC8006A5F.
struct Quote {
AMM source; // 0=V2, 1=Sushi, 2=zAMM, 3=V3, 4=V4,
// 5=Curve, 6=Lido, 7=WETH_WRAP, 8=V4_HOOKED
uint256 feeBps; // pool fee in basis points
uint256 amountIn;
uint256 amountOut;
}
For maximum output, compare multiple route strategies and take the best. This is what the swap page does. (If you don't need the cross-depth comparison, just use buildSwapAuto.)
const [best, split, hybrid] = await Promise.allSettled([
quoter.buildBestSwapViaETHMulticall(
me, me, false, tokenIn, tokenOut,
amountIn, 50n, deadline),
quoter.buildSplitSwap(
me, tokenIn, tokenOut,
amountIn, 50n, deadline),
quoter.buildHybridSplit(
me, tokenIn, tokenOut,
amountIn, 50n, deadline)
]);
// Pick the best output across all strategies
function outputOf(r) {
// buildBest/3hop: b is final leg (0 for single-hop)
if (r.a) return r.b?.amountOut > 0n ? r.b.amountOut : r.a.amountOut;
// split/hybrid: sum both legs (same output token)
return r.legs[0].amountOut + r.legs[1].amountOut;
}
let winner = best.value;
let winnerOut = outputOf(winner);
for (const r of [split, hybrid]) {
if (r.status === "fulfilled" && outputOf(r.value) > winnerOut) {
winner = r.value;
winnerOut = outputOf(winner);
}
}
// Send the winning route
await signer.sendTransaction({
to: ZROUTER,
data: winner.multicall,
value: winner.msgValue
});
For gasless approvals, build a multicall that includes a permit call before the swap legs:
const routerAbi = [
"function multicall(bytes[]) payable returns (bytes[])",
"function permit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
"function permit2TransferFrom(address token, uint256 amount, uint256 nonce, uint256 deadline, bytes signature)"
];
const routerIface = new ethers.Interface(routerAbi);
// Sign EIP-2612 permit off-chain, then:
const permitCall = routerIface.encodeFunctionData("permit", [
tokenAddress, ethers.MaxUint256, permitDeadline, v, r, s
]);
const tx = await signer.sendTransaction({
to: ZROUTER,
data: routerIface.encodeFunctionData("multicall", [
[permitCall, ...result.calls]
]),
value: result.msgValue
});
You generally don't call these directly — zQuoter builds the calldata for you. Listed here for reference.
Batches multiple calls atomically. Each call is delegatecall'd in sequence. If any sub-call reverts, the entire batch reverts.
Transfers tokens into the router and marks them in transient storage for multi-leg chaining.
Sends tokens from the router to a recipient. Pass amount=0 to sweep full balance.
ETH → WETH. Pass amount=0 to wrap full ETH balance.
WETH → ETH.
Direct Lido staking via msg.value.
For 2-hop and 3-hop routes, zQuoter automatically routes through the most liquid hub:
WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7 DAI 0x6B175474E89094C44Da98b954EedeAC495271d0F WBTC 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 wstETH 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0
ETH (native) address(0) WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7 DAI 0x6B175474E89094C44Da98b954EedeAC495271d0F WBTC 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 stETH 0xae7ab96520DE3A18E5e111B5eaAb095312D7fE84 wstETH 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0
view — they cost no gas and can be called from any RPCaddress(0) for native ETH in both tokenIn and tokenOut50 bps, each leg tolerates 0.5% independentlydeadline parameter is a unix timestamp. Set it to now + 300 (5 minutes) for typical swapsdeadline = type(uint256).max to swapV2swapCurve calldata may not target optimally. For these, you can get a better rate by using quoteCurve for the quote, then building your own calldata via zRouter's execute or snwap functions to call the pool directly. This pattern works as a general escape hatch for any external protocol or pool with a custom interfacezRouter's snwap and execute functions let you route through any external contract — useful for legacy pools, staking protocols, or any on-chain action that isn't natively supported by the swap functions.
// Example: direct ETH→stETH via legacy Curve pool
const CURVE_POOL = "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022";
const exchangeData = curveIface.encodeFunctionData(
"exchange", [0, 1, amountIn, minOut] // int128 indices
);
const routerIface = new ethers.Interface([
"function multicall(bytes[]) payable returns (bytes[])",
"function snwap(address tokenIn, uint256 amountIn, address recipient, address tokenOut, uint256 amountOutMin, address executor, bytes executorData) payable returns (uint256 amountOut)"
]);
// snwap: executes arbitrary calldata with slippage protection
const tx = await signer.sendTransaction({
to: ZROUTER,
data: routerIface.encodeFunctionData("snwap", [
ETH, // tokenIn
amountIn, // amountIn
me, // recipient
STETH, // tokenOut
minOut, // amountOutMin (enforced by router)
CURVE_POOL, // executor (target contract)
exchangeData // calldata forwarded to executor
]),
value: amountIn
});
snwap measures the recipient's balance change and reverts if amountOutMin isn't met — so you get slippage protection regardless of the external contract's interface.