How Ilyris works, and the addresses to verify it against. Everything below is readable on-chain — nothing here asks you to take our word for it.
Liquidity sits in discrete price steps rather than along a continuous curve. Each bin holds reserves at one fixed price, so a trade that stays inside a bin has zero price impact — the price does not move because there is no curve to move along. Price only changes when a trade exhausts a bin and crosses into the next.
Bin n sits at price = (1 + binStep/10000)^n. Bin step is
chosen per pool, not by the protocol: WETH/USDG uses 10 bps (each step
0.10%), WETH/YRIS uses 250 bps (each step 2.50%). Discover shows each pool's.
The base token sits at and above the active bin, the quote token at and below —
and which token is which is decided by address order, not by what you would
call the pair.
The swap fee is charged once on the way in, before any bin is touched:
netIn = amountIn × (1e9 − rate) / 1e9. Charging per bin would compound it
across a multi-bin trade. The fee is per pool: WETH/USDG charges
30 bps, WETH/YRIS 100 bps. Discover lists each pool's base fee. The
rate a swap actually pays can be higher than the base — a volatility surcharge is
added on top after recent price movement.
A deposit that arrives imbalanced relative to the bin it lands in also pays a composition fee, on the imbalance only. Without it, depositing one-sided into a bin and immediately withdrawing would be a free swap at the bin price, paid for by the LPs already there.
Fees accrue per position through a share accumulator and are claimed separately, so claiming never touches principal. The protocol's share is skimmed before anything reaches LPs and is held outside bin reserves — the two can never draw on each other.
Equity pools must respect a closed market. The guard can pause swaps and enforce a corporate-action freeze — and it makes no external calls, so a quote can never be invalidated by an oracle nobody can see.
It cannot block a withdrawal. That is structural, not policy: the guard is not consulted on the removal path at all. Whatever else happens, liquidity can leave.
⚠ Integrators: quoteExactIn and quoteExactOut do
not consult the guard — only the swap functions do. A simulator built from
quotes alone will happily price a swap that reverts. Read
swapsPaused() and freezeEnd() alongside pool state.
Switching pools. The pair name at the top left is a menu listing every pool the factory has recorded. Deprecated pools are tagged; permanently mispriced ones are not offered at all.
Which way the price runs. A pool stores one price — quote token per base token — and the factory picks which is which by address order, so on some pairs the native number reads backwards. The ⇄ control beside the range, the position book and the liquidity chart flips the display; it is a labelling change only, remembered per pool. Ranges are measured in bins, so flipping cannot alter which bins a deposit touches. Bar colour always names a token and never moves; bid/ask follows the direction you are reading.
Creating a pool. The starting price is the one number that cannot be fixed
later: activeId is immutable outside of swaps, and the factory key is
(tokenX, tokenY, bin step, policy, guard) — so a pool seeded at the wrong price
permanently burns that combination and it can never be created again. The create form
therefore states the consequence as a sentence with no units to misread —
"1 WETH = 24,038,448 YRIS" — and will not submit until you confirm it.
Verify on Blockscout. Discover lists every pool the factory has recorded, read live from the chain.
Superseded, still on chain. These are listed so an LP with funds in one can find it, not because anything should be deposited into them. The factory was replaced; its pools remain withdrawable and are tagged deprecated in Discover.
BinPoolLens.getPoolState(pool, radius) is one-call discovery:
tokens, decimals, bin step, active bin and its price, the resolved fee rate
(totalFeeRate, 1e9 precision, volatility surcharge already applied) and the
guard address. MAX_SCAN is 512 and the check is
radius * 2 + 1 > MAX_SCAN, so the usable radius is 0–255 and 256 reverts
ScanTooWide().
It does not price a swap, and its reserves are not the pool's reserves.
reserveX/reserveY are summed over
activeId ± radius only; scannedFrom, scannedTo
and populatedBins say what was counted, and liquidity outside that window
is invisible with no error and no flag. Measured on WETH/USDG: at
radius = 10 the lens reports 0.00717 WETH, while a single 39 USDG
swap pays out 0.01582 WETH and ends 14 bins beyond scannedTo. The
swap's own output is more than twice what the lens called the reserves. If
populatedBins equals the window width, assume you are clipping and widen.
Pricing needs the bins. A swap walks bin by bin and spends each bin's own reserve
at that bin's own price, so no formula over an aggregate reproduces it. Two honest
routes: getBins(pool, from, to) returns every populated bin, or
quoteExactInWithGas / quoteExactOutWithGas return
amountOut, feeAmount, finalId,
binsCrossed and a gas estimate in one call. Both cap their scan at
MAX_SCAN and revert rather than truncate silently.
A pool may have no guard. marketGuard is the zero address on
WETH/YRIS, and an eth_call to an address with no code succeeds with
empty return data rather than reverting. Decode that as "cannot be paused", not as a
failed read — a bare 0x will otherwise reach your decoder and throw, which
is exactly how it broke ours.
Swap the pool directly. No router is deployed on chain 4663 and the manifest has
no router entry — a deployment fact, not an architectural one.
BinRouter.sol exists in the repo and is deployed on testnet, so do
not carry that address, or the TypeScript SDK's
buildRouterExactInputCall, into a mainnet integration. There is no Permit2,
no permit and no swap callback, so there is no shared allowance target:
approve each pool you trade and call swapExactIn /
swapExactOut on it.
The pool pulls, and only pulls. It takes the input from msg.sender
via transferFrom and never reads its own balance for credit. A V2-style
transfer-then-swap therefore fails — but it fails because you no longer hold the tokens
you must approve, not because the pre-transfer was detected. Tokens sent to a
pool are never credited and never recoverable: there is no skim,
sync or rescue path. Settlement is exact-delta on both legs, so
fee-on-transfer and rebasing tokens revert TransferFailed(). For
exact-output, approve maxAmountIn — only the actual amountIn
is pulled.
Gas hints are a routing input, not a limit. BASE_SWAP_GAS (130,000)
and PER_EXTRA_BIN_GAS (41,500) are readable on the lens and applied by
estimateSwapGas(binsCrossed) — but getPoolState does not
return binsCrossed, so that number comes from
quoteExactInWithGas or your own walk. The figure is execution gas only: it
excludes the 21,000 intrinsic, calldata, cold-account access and the guard's staticcall,
and on a guarded pool it currently understates — a real five-bin WETH/USDG swap used
372,159 against a 296,000 hint. Rank routes with it; never send it as a gas limit. Call
eth_estimateGas.
Most aggregators do not need this — 0x, KyberSwap and ParaSwap all read the chain directly, and the section above is what they build against. It exists for integrators whose pipeline requires an HTTP quote source rather than a contract call.
Base URL https://ilyris-quote-api.ilyris.workers.dev —
no API key, no auth, CORS open. Chain 4663 only.
GET /healthliveness plus the number of routable poolsGET /poolsevery routable pool with its tokens, bin step, fee and guardGET /quotetokenIn, tokenOut, amountIn (wei, decimal string)
A quote returns amountOut, feeAmount,
binsCrossed, gasEstimate, the pool to approve and call, and
the guard state. It is quoteExactInWithGas on the lens — the same call our
own front end prices with — so it agrees with the chain to the wei.
Two pools are deliberately excluded from /pools and from routing.
They were created at an inverted starting price, hold nothing, and anything deposited
into them is lost on the first trade. Do not route to them if you find them by
enumerating the factory yourself.
Errors say what was wrong with the request. A size the book cannot fill returns
422 insufficient_liquidity rather than a raw revert string — on this lens a
quote makes no external calls and does not consult the guard, so depth is essentially
the only way it fails. An unknown pair returns 404 no_pool; a malformed
address returns 400 naming the field.
Rate: provisioned for 100 req/s per client, and measured well above it — 7,000
consecutive requests at 428 req/s drew zero throttling. A rate limiter exists as a
backstop against abuse, but it is Cloudflare's per-colo limiter rather than a hard
ceiling; when it does trigger you get 429 with a Retry-After
header rather than a silent drop. Ask if you need more and it will be raised.
Latency: about 0.2 s warm, up to ~1 s on a cold edge isolate.
If you get a 403 from a script, check your user-agent. The endpoint sits behind
Cloudflare, whose bot rules reject Python-urllib with error 1010. Go,
node-fetch, axios, python-requests, okhttp and curl all pass — this only
bites Python's standard-library client, and it is not us refusing you.
⚠ A quote is not a guarantee, and the gas figure is not a limit. Quotes do not
consult the market guard but swaps do, so check tradable in the response.
gasEstimate is execution gas only and currently understates a guarded
pool — rank routes with it and call eth_estimateGas for a limit.
Unaudited. Extensively tested — a full Solidity suite, invariant tests that unwind every position to prove solvency, and wei-exact parity between the contract and the off-chain simulators — but tests are not an audit. Treat every deposit accordingly.
The source is the documentation for anything not covered here: the contracts and this interface are commented to explain why they are built the way they are, not just what they do.