Skip to content

API ReferenceRouting engine

Reference

Routing engine

How the off-chain half finds routes. Code: indexer.ts, router.ts, plan.ts.

Indexer

Discovery

VenueEventSource of addresses
Uniswap V3PoolCreated(token0, token1, fee, tickSpacing, pool)Configured factories + any emitter of that event that passes an interface check
Uniswap V2PairCreated(token0, token1, pair, index)Configured factories + any emitter whose pair bytecode reveals a known fee (×1000…×3 → 30 bps, ×10000…×25 → 25 bps). Unknown formulas are rejected, never guessed
Uniswap v4Initialize(id, currency0, currency1, fee, tickSpacing, hooks, …)Configured PoolManagers. Pools that traded within V4_LOOKBACK_BLOCKS (default 2M) plus every new one
Solidly / Slipstream / AlgebraPoolCreated(…, bool stable, …), PairCreated(…, bool stable, …), PoolCreated(…, int24 tickSpacing, pool), Pool(token0, token1, pool)Configured + auto-discovered factories (sample pool must point back to its factory and expose the expected entry points)

Logs are fetched with getLogsSplit: when the RPC refuses a range (too many results), it bisects; LOGS_MAX_RANGE also caps the block span for RPCs that limit it (Elysium community RPCs: 10,000).

State

VenueRead at discoveryKept fresh by
V3slot0, liquidity, fee, tick bitmap ±16 words around the price + ticks(i).liquidityNet of every initialized tickSwap log (price, tick, liquidity, no call); Mint/Burn (tick liquidity updated exactly; re-read in range)
V2getReservesSync log (no call)
V4extsload of the pool's slot0 and liquidity in the PoolManagerv4 Swap, ModifyLiquidity logs

Reads are batched in Multicall3 chunks of 1,000 calls. A read that fails on a transport error is retried on the next sync, not dropped.

Sync loop

  1. On each new block (WebSocket WS_URL) or every POLL_MS (1.5 s), read the logs from lastBlock + 1 to the tip.
  2. Apply them; collect the pools they touched.
  3. Notify subscribers (/ws/prices, keeper).

Reorgs

The indexer keeps the hash of the last 128 blocks it processed. If the parent hash of a new block doesn't match, it walks back to the common ancestor, re-reads every pool touched since, and replays from there. Tested with anvil_reorg.

Background enrichment

Once V2/V3 pools are loaded the indexer reports ready, then runs these steps in the background, each on its own (a failure is logged and retried on the next run, every 60 s, without skipping the others): token metadata → factory auto-discovery → token metadata → v4 discovery → token metadata → V3 ticks → snapshot. Metadata goes first because /tokens and /price are empty without it.

Snapshots

api/data/snapshot.json (or the Redis key router:snapshot with REDIS_URL) holds every pool, token metadata, probe results and lastBlock. It's written atomically every 60 s and on SIGINT/SIGTERM. At start-up:

  • Replay the missed blocks if the snapshot is recent (≤ 500,000 blocks behind), its version matches, and the factory list is the same.
  • Full backfill otherwise (~35 s on the testnet RPC; minutes on a large chain).

Graph

Nodes are tokens, edges are live pools (liquidity > 0, or both reserves > 0). Native HYPE isn't a node: it's represented by each allow-listed WHYPE. A special wrap edge links every pair of allow-listed WHYPEs at 1:1.

Depth-first search from tokenIn to tokenOut:

  • at most 3 swaps (MAX_SWAPS), plus at most one wrap edge, which doesn't count as a swap,
  • no pool used twice in one path,
  • the last hop only considers pools that contain tokenOut (prunes most of the tree),
  • at most 5,000 paths enumerated (MAX_PATHS), to bound the work around hub tokens.

For native input or output, the search runs once per allow-listed WHYPE; a wrap edge right after wrapping (or right before unwrapping) is skipped because it's just the other WHYPE's route plus gas.

Estimators

Each path is simulated hop by hop in memory, to rank paths before the exact quote.

VenueModelExactness
V2out = in·(1−fee)·R_out / (R_in + in·(1−fee))Exact
V3Swap within the current tick at constant L; cross into the next initialized tick (applying liquidityNet) when the price reaches it; repeatExact inside the loaded tick window (±16 bitmap words); constant L beyond it
V4Constant L at the current priceApproximate (the exact quote fixes it)
Solidly volatilex·y=k with the pool feeExact
Solidly stablex³y + xy³ = k, solved with Newton's method on 18-decimal normalized reservesExact up to rounding
Wrap1:1Exact

Guards: a fee read from a pool or factory outside [0, 100%) prices that pool at zero (never routed), and the stable-curve Newton solve stops after 64 steps.

The spot output (no size impact) of each route is computed the same way with an infinitesimal amount; it's used for priceImpactBps and /price.

Ranking (gas-aware)

  1. The top 20 paths by estimate (net of gas) become candidates.

  2. All candidates are quoted exactly with Executor.quote in one Multicall3 eth_call, together with feeBps and the gas price.

  3. Each candidate's exact output is reduced by its gas cost in tokenOut:

    gasUnits = 60,000 (base) + Σ legs (20,000 + Σ hops: v3 90k · v2 70k · v4 130k · solidly 80k · wrap 45k) gasCostInTokenOut = gasUnits × gasPrice (in HYPE) converted at the HYPE→tokenOut spot price.

  4. The best net output wins as the single-route plan.

Splits

  1. Take the winning route plus up to 2 more candidates that share no pool with it or each other (pickDisjoint), and use the same WHYPE.
  2. Quote each of them at 5%, 10%, … 95% of amountIn (one more multicall; 19 points × up to 3 legs).
  3. bestAllocation tries every way to split 100% into 5% steps across the legs (231 allocations for 3 legs), using the quoted table, each used leg paying its own gas.
  4. If the best allocation's net output beats the single route, the split is re-quoted exactly as a whole and kept if it still wins.

Because legs are pool-disjoint, each leg's quote is independent of the others, so the table is accurate.

Prices (/price)

priceUsdc(token) runs the in-memory search from the token to USDC with a one-unit amount and returns the spot output, net of LP fees. HYPE is the best of the WHYPEs. No RPC call is made, so /price and the WebSocket are cheap.

Shared by the keeper

The keeper calls the same bestPlan for every fill and fee conversion, so orders get the same routing quality as /quote.

lane.markets · Make the most of every move.