How it reads.
LiqNet reads every pool it knows about straight from storage, replays each pool's own swap arithmetic until it stops absorbing, and publishes two numbers per ticker: how far apart the venues are, and how much each one will take. This page is the whole method, including the rules that make the numbers smaller.
In plain terms
When you buy a tokenized share on this chain, nobody sells it to you. You draw it out of a pool.
A pool is one contract holding two things, say NVIDIA shares and dollars. A buyer puts dollars in and takes shares out, and the contract's arithmetic sets the rate. Every share that leaves makes the next one dearer, so a large order pays more per share than a small one. That difference is the cost to fill.
Most pools here do not spread their liquidity evenly. It sits in bands around the current price, deposited by whoever chose to. An order walks through the bands, paying a little more in each, until it is filled or the bands run out. Past the last band the pool has nothing left to sell at any price. LiqNet calls that point the wall, and publishes where it is.
The same share trades in many pools at once, on several exchanges, and none of them talk to each other. At any instant they quote different prices. The cheapest quote is often the shallowest pool, which is exactly why a price on its own is not enough.
What LiqNet is
An instrument, not an exchange. It reads and publishes. It holds no funds, deploys no contracts and never asks a wallet to sign anything.
The name is the number it reads. Every concentrated pool stores a liquidityNet at each initialized tick: how much liquidity begins or ends there. Add them up as you walk and you have the depth; reach the last one and you know where it runs dry.
| It does | How |
|---|---|
| Read every pool from chain state | Four geometries, four read paths. No price feed and no indexer on the request path. |
| Walk depth tick by tick | The pool's own swap loop, replayed in the same 256-bit integer arithmetic, until it fills or stops. |
| Test whether a spread can be taken | Buy on one venue and sell on another in simulation, through both pools' real arithmetic. |
| Not execute | No router, no custody, no signature. A reading is an eth_call: nobody sees it, nothing moves. |
The read path
"Pool" is four different things on this chain. Each keeps its price in a different place and walks its liquidity a different way, and reading one as another puts a quote tens of percent from the market. So each kind gets its own path, and a pool whose kind cannot be established is left out.
| Kind | Price from | Depth from | Seen on |
|---|---|---|---|
| v3 | slot0() | tickBitmap(int16) → ticks(int24) | Uniswap v3, Ramses, Aero, Up, Giga, PancakeSwap v3, SushiSwap v3 |
| v4 | extsload | the same structures, from computed storage slots | Uniswap v4, Orvex |
| algebra | globalState() | tickTable(int16) | Alandale |
| v2 | getReserves() | closed form, exact | Uniswap v2 and its forks |
Read by position, not by shape
Forks disagree about how many words slot0() returns: Uniswap v3 sends seven, Aero and Up send six. A strict ABI decoder rejects the short ones and the venue vanishes from the book. LiqNet reads the returned words by index, so an extra or missing trailing field changes nothing.
// engine/abi.mjs
export function word(data, i) {
const hex = data.slice(2 + i * 64, 2 + (i + 1) * 64);
if (hex.length < 64) throw new Error(`word ${i} missing`);
return BigInt('0x' + hex);
}
Three round trips, one block
A reading is three batches through Multicall3 at 0xcA11…CA11, all pinned to the same block: the heads of every pool (price, tick, liquidity, fee), then the bitmap words around each price, then every initialized tick those words point to. Multicall3's own getBlockNumber() rides in the first batch, so the block a reading names is the block it was read at.
Requests go out as concurrent single calls, never as JSON-RPC batches: the bundling already happened inside the eth_call, and a second layer of batching only queues them behind each other.
Pools with no address
In a v4-style exchange every pool lives inside one shared contract, the singleton. There is no pool address to call. Its state is reached through extsload, with the slot computed from the pool's 32-byte id. A great deal of this chain's depth sits there, and a tool that only knows pool contracts cannot see it.
// where a v4 pool's state lives
base = keccak256(poolId ‖ POOLS_SLOT)
base + 0 slot0 sqrtPriceX96 (160) · tick (24) · protocolFee (24) · lpFee (24)
base + 3 liquidity
base + 4 ticks keccak256(int24 tick ‖ base + 4) → liquidityNet in the high 128 bits
base + 5 tickBitmap keccak256(int16 word ‖ base + 5)
The two singletons on this chain keep the pools mapping at different slots: 6 for Uniswap v4 at 0x8366…0951, 4 for Orvex at 0xd01c…4032.
Tick spacing is not in storage
It belongs to the pool's key, not its state, so it cannot be read back. Guess it wrong and the bitmap decodes into ticks that do not exist, or into none, which looks exactly like a pool that has run dry. The registry probes instead: for each candidate spacing it reads the bitmap around the price and checks that every tick it points to actually holds liquidity. The spacing whose ticks all check out is the one recorded.
The protocol fee is part of the fee
Some v4 pools charge a protocol fee on top of the LP fee. The swap uses the combination, protocol + lp − protocol·lp / 10⁶, separately for each direction. LiqNet reads both halves out of slot0 and uses the same formula the pool does.
Walking the seam
Liquidity sits between ticks. An order walks from the current price toward the next initialized tick, takes whatever that band holds, crosses the tick (adding or removing the liquidity that starts or ends there), and carries on. LiqNet runs that loop against the pool's own tick data, in BigInt, with the contract's own rounding. It is not a curve fitted to past trades.
// engine/simulate.mjs, the loop in outline while (remaining > 0n) { const next = ticks[idx]; // next initialized tick ahead const target = next ? sqrtAtTick(next.t) : limit; const step = swapStep(sqrtP, clamp(target, limit), L, remaining, fee); remaining -= step.amountIn + step.feeAmount; out += step.amountOut; sqrtP = step.next; if (sqrtP !== target) break; // filled inside this band L = zeroForOne ? L - next.net : L + next.net; // crossed: the band changes idx += zeroForOne ? -1 : 1; }
The math (TickMath, SqrtPriceMath, SwapMath) is ported line for line, and the tests check every TickMath constant against two thousand random ticks and the known values at both ends of the range.
The ladder
Every ticker is drilled at the same six sizes, in dollars, plus any size you type:
$100 $1,000 $10,000 $50,000 $250,000 $1,000,000
Pools quoted in ether are walked in ether, at the price of the deepest USDG/WETH pool, read in the same batch. Nothing else prices the ether leg.
The wall
A walk stops in one of three places: the order is filled, the liquidity runs out, or the marginal price has moved 10% from where it started. The third is a rule, not a fact about the pool, and it is the same rule for every geometry.
Without it, a pool with one wide position would report that it "absorbs" any size at all, just at an absurd price, and constant-product pools never run out by construction. Ten percent is far past anything anyone fills on purpose. When you see runs dry, the pool genuinely had nothing left; when you see wall, it still had liquidity, but only at prices nobody should accept.
Depth to the wall is the dollars an order can put in before either happens. It is what the "takes" column means, and what the locked-versus-liquid table compares against reserves.
What counts in a spread
The spread is the gap, in basis points, between the cheapest and dearest venue at one block. Which venues belong in it is a decision. Here are the three rules. Each one makes the headline smaller.
An emptied pool is not a quote
A drained pool keeps the price it was abandoned at forever, and that number will happily sit in an aggregate looking like an opportunity. Only venues that can fill the smallest rung ($100) whole count toward the spread. The rest stay in the table, marked.
A ticker is not an identity
Several tokens on this chain answer to the same symbol. LiqNet resolves each ticker to the address whose money-paired pools hold the most, and lists the others it set aside, with what they hold.
Unknown geometry is dropped
A pool that answers none of the four read paths, or whose price disagrees with the indexer's by more than threefold both ways round, is excluded and recorded as unclassified. That is why the venue list is shorter than the list of exchange names on the chain.
The consensus spot, the number costs are measured against, is the median mid-price of the fillable venues.
The crossing
A spread is only interesting if it can be taken. At every rung, LiqNet buys on each fillable venue and tries selling exactly what came out on every other one, using the state it has already read. Both fees and both price impacts are inside the result.
Not inside it: gas, the chance that someone else takes it first, and the fact that both pools move the moment anyone acts. It measures a disagreement. It promises nothing, and most of the time it comes back empty.
The registry
Something has to say which pools exist. That is the only job an indexer does here. npm run canon walks GeckoTerminal and DexScreener once, offline, then asks the chain what each pool actually is: which read path answers, which way round its tokens are, what spacing its ticks use. The result is written to data/registry.json. From then on a page load is chain reads and nothing else, so another service's rate limit or outage cannot change a published number.
Reading the registry…
The API
The same endpoints the pages call. CORS is on, there is no key, and every response names the block it was read at.
GET /api/assay?ticker=NVDA GET /api/assay?ticker=SPY&size=25000 GET /api/assay?ticker=0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec&sizes=100,5000,1m blockNumber the block every number was read at readMs how long the read took spot median mid of the fillable venues dispersionBps cheapest to dearest, fillable venues only venues[] per pool: kind, fee, spot, capacity, stop, ticks, the ladder, the curve best[] the venue that gives the most shares at each rung crossing the best buy-here, sell-there that survives, or null impostors[] same-symbol tokens that were set aside
GET /api/board the watchlist at one block: spread, depth, best per rung GET /api/compare?tickers=NVDA,AAPL,TSLA&size=25000 ranked by all-in cost GET /api/venues every venue: wins, fills and median cost at each size GET /api/tickers what the registry knows GET /api/registry?ticker=NVDA the pools behind one ticker
Readings are cached for fifteen seconds so bursts are served from memory rather than the chain. A reading is only true for the block it names.
Reproduce it
Every figure has a command behind it. They are the same functions the server calls.
npm run assay NVDA # one ticker, every venue, the whole ladder npm run assay SPY 25k 1m # at the sizes you name npm run canon # rebuild the registry: the only step that uses an indexer npm test # the math, against the contract's own constants
Point LIQNET_RPC at any node for chain 4663 and the numbers come out the same, because they come out of the chain.
What it cannot see
Being plain about the edges is part of the method.
- Hooks. A v4 pool can carry a hook that takes a toll inside the swap. Storage does not show it, so a hooked pool may pay less than its reading. LiqNet reads the pool's own arithmetic, hook excluded.
- Dynamic fees. Several venues change their fee with volatility. LiqNet uses the fee the pool reports at the block it reads, which is the fee a swap at that block would pay, and no more than that.
- Venues with no read path. Bin-based pools, stable-swap curves and off-chain market makers are listed as unclassified rather than priced.
- Routing. The best venue at a size is a single pool taking the whole order. Splitting across pools can do better; that is a router's job, and LiqNet is not a router.
Glossary
| Term | What it means here |
|---|---|
| Assay | One complete reading of a ticker: every venue priced from its own state, the ladder walked, at one block. |
| Seam | One pool's liquidity, as an order would meet it. |
| Band | The stretch of price between two initialized ticks, with its own liquidity. |
| Wall | Where a walk stops without filling: the marginal price moved 10%, or nothing was left. |
| Runs dry | The pool had no liquidity left at any price within reach. |
| Takes · depth | The dollars an order can put in before the wall. |
| Fillable | Can fill the smallest rung, $100, whole. Only fillable venues count toward a spread. |
| Cost over spot | What you pay per share, fee and impact included, against the consensus spot, in basis points. |
| Spread | The gap between the cheapest and dearest fillable venue at one block. |
| Crossing | Buying on one venue and selling on another at the same block, published only if it survives both fees and both impacts. |
| Basis point | A hundredth of a percent. A thousand dollars at 30 bps costs three dollars more than spot. |