On-Chain API

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.

REST API

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.

GET /quote

Returns the optimal swap route and transaction calldata.

Parameters

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:

Open to all origins, no API keys required. Rate limited by Cloudflare Workers (100k req/day free tier).


Contracts

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.


How It Works

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.


Quick Start

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);

ERC20 Swaps

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.


zQuoter Functions

buildSwapAutoprimary

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.

buildSwapAuto(address to, bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, uint256 slippageBps, uint256 deadline) → (Quote best, bytes callData, uint256 amountLimit, uint256 msgValue)

Returns:

Cascade (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).

buildBestSwap

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.

buildBestSwap(address to, bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, uint256 slippageBps, uint256 deadline) → (Quote best, bytes callData, uint256 amountLimit, uint256 msgValue)

buildBestSwapViaETHMulticall

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).

buildBestSwapViaETHMulticall(address to, address refundTo, bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, uint256 slippageBps, uint256 deadline) → (Quote a, Quote b, bytes[] calls, bytes multicall, uint256 msgValue)

buildSplitSwap

Splits liquidity across two AMMs in parallel for better execution on large trades.

buildSplitSwap(address to, address tokenIn, address tokenOut, uint256 swapAmount, uint256 slippageBps, uint256 deadline) → (Quote[2] legs, bytes multicall, uint256 msgValue)

buildHybridSplit

Combines a single-hop and a 2-hop route in parallel. Captures cases where splitting across route depths beats any single strategy.

buildHybridSplit(address to, address tokenIn, address tokenOut, uint256 swapAmount, uint256 slippageBps, uint256 deadline) → (Quote[2] legs, bytes multicall, uint256 msgValue)

build3HopMulticall

Three-hop routing through two hub tokens for exotic pairs.

build3HopMulticall(address to, bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, uint256 slippageBps, uint256 deadline) → (Quote a, Quote b, Quote c, bytes[] calls, bytes multicall, uint256 msgValue)

getQuotesview

Returns the best single-hop quote and all individual AMM quotes (V2, Sushi, zAMM, V3, V4). Useful for displaying price comparison across venues.

getQuotes(bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount) → (Quote best, Quote[] quotes)

quoteCurveview

Auto-discovers Curve pools via MetaRegistry and returns the best Curve-only quote.

quoteCurve(bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, uint256 maxCandidates) → (uint256 amountIn, uint256 amountOut, address bestPool, bool usedUnderlying, bool usedStable, uint8 iIndex, uint8 jIndex)

quoteLidoview

Direct Lido staking quote for ETH → stETH or wstETH.

quoteLido(bool exactOut, address tokenOut, uint256 swapAmount) → (uint256 amountIn, uint256 amountOut)

Individual AMM Quotesview

Low-level quoting for specific venues. Available on the base quoter at 0x658bF1A6608210FDE7310760f391AD4eC8006A5F.

quoteV2(bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, bool sushi) → (uint256 amountIn, uint256 amountOut)
quoteV3(bool exactOut, address tokenIn, address tokenOut, uint24 fee, uint256 swapAmount) → (uint256 amountIn, uint256 amountOut)
quoteV4(bool exactOut, address tokenIn, address tokenOut, uint24 fee, int24 tickSpacing, address hooks, uint256 swapAmount) → (uint256 amountIn, uint256 amountOut)
quoteZAMM(bool exactOut, uint256 feeOrHook, address tokenIn, address tokenOut, uint256 idIn, uint256 idOut, uint256 swapAmount) → (uint256 amountIn, uint256 amountOut)

Quote Struct

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;
}

Best Quote Selection

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
});

Prepending Permit Calls

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
});

zRouter Functions

You generally don't call these directly — zQuoter builds the calldata for you. Listed here for reference.

multicall

Batches multiple calls atomically. Each call is delegatecall'd in sequence. If any sub-call reverts, the entire batch reverts.

multicall(bytes[] data) payable → bytes[] results

Swap Functions

swapV2(address to, bool exactOut, address tokenIn, address tokenOut, uint256 swapAmount, uint256 amountLimit, uint256 deadline) payable
swapV3(address to, bool exactOut, uint24 swapFee, address tokenIn, address tokenOut, uint256 swapAmount, uint256 amountLimit, uint256 deadline) payable
swapV4(address to, bool exactOut, uint24 swapFee, int24 tickSpace, address tokenIn, address tokenOut, uint256 swapAmount, uint256 amountLimit, uint256 deadline) payable
swapVZ(address to, bool exactOut, uint256 feeOrHook, address tokenIn, address tokenOut, uint256 idIn, uint256 idOut, uint256 swapAmount, uint256 amountLimit, uint256 deadline) payable
swapCurve(address to, bool exactOut, address[11] route, uint256[4][5] swapParams, address tokenIn, address tokenOut, uint256 swapAmount, uint256 amountLimit, uint256 deadline) payable

Helpers

deposit(address token, uint256 id, uint256 amount) payable

Transfers tokens into the router and marks them in transient storage for multi-leg chaining.

sweep(address token, uint256 id, uint256 amount, address to) payable

Sends tokens from the router to a recipient. Pass amount=0 to sweep full balance.

wrap(uint256 amount) payable

ETH → WETH. Pass amount=0 to wrap full ETH balance.

unwrap(uint256 amount) payable

WETH → ETH.

exactETHToSTETH(address to) payable → uint256 shares
exactETHToWSTETH(address to) payable → uint256 wstOut

Direct Lido staking via msg.value.


Hub Tokens

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

Common Addresses

ETH (native)   address(0)
WETH           0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
USDC           0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
USDT           0xdAC17F958D2ee523a2206206994597C13D831ec7
DAI            0x6B175474E89094C44Da98b954EedeAC495271d0F
WBTC           0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599
stETH          0xae7ab96520DE3A18E5e111B5eaAb095312D7fE84
wstETH         0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0

Notes


Advanced: Custom Zaps via execute / snwap

zRouter'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.


Source