Reference
Routing engine
How the off-chain half finds routes. Code: indexer.ts, router.ts, plan.ts.
Indexer
Discovery
| Venue | Event | Source of addresses |
|---|---|---|
| Uniswap V3 | PoolCreated(token0, token1, fee, tickSpacing, pool) | Configured factories + any emitter of that event that passes an interface check |
| Uniswap V2 | PairCreated(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 v4 | Initialize(id, currency0, currency1, fee, tickSpacing, hooks, …) | Configured PoolManagers. Pools that traded within V4_LOOKBACK_BLOCKS (default 2M) plus every new one |
| Solidly / Slipstream / Algebra | PoolCreated(…, 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
| Venue | Read at discovery | Kept fresh by |
|---|---|---|
| V3 | slot0, liquidity, fee, tick bitmap ±16 words around the price + ticks(i).liquidityNet of every initialized tick | Swap log (price, tick, liquidity, no call); Mint/Burn (tick liquidity updated exactly; re-read in range) |
| V2 | getReserves | Sync log (no call) |
| V4 | extsload of the pool's slot0 and liquidity in the PoolManager | v4 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
- On each new block (WebSocket
WS_URL) or everyPOLL_MS(1.5 s), read the logs fromlastBlock + 1to the tip. - Apply them; collect the pools they touched.
- 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.
Path search
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.
| Venue | Model | Exactness |
|---|---|---|
| V2 | out = in·(1−fee)·R_out / (R_in + in·(1−fee)) | Exact |
| V3 | Swap within the current tick at constant L; cross into the next initialized tick (applying liquidityNet) when the price reaches it; repeat | Exact inside the loaded tick window (±16 bitmap words); constant L beyond it |
| V4 | Constant L at the current price | Approximate (the exact quote fixes it) |
| Solidly volatile | x·y=k with the pool fee | Exact |
| Solidly stable | x³y + xy³ = k, solved with Newton's method on 18-decimal normalized reserves | Exact up to rounding |
| Wrap | 1:1 | Exact |
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)
-
The top 20 paths by estimate (net of gas) become candidates.
-
All candidates are quoted exactly with
Executor.quotein one Multicall3eth_call, together withfeeBpsand the gas price. -
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→tokenOutspot price. -
The best net output wins as the single-route plan.
Splits
- 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. - Quote each of them at 5%, 10%, … 95% of
amountIn(one more multicall; 19 points × up to 3 legs). bestAllocationtries 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.- 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.