Skip to content
Sounder
Back to the board

Robinhood Chain · 4663 · read from pool state

How it works.

Sounder reads every pool on chain 4663 directly from its own storage, replays each pool’s swap arithmetic to find where it stops absorbing, and publishes two numbers per ticker: how far apart the venues are, and how much each one can actually take. This is the whole method, including the parts that make the numbers smaller.

01

What Sounder is

An instrument, not a venue. It measures and publishes; it never takes custody of anything.

A price tells you what one share costs. It tells you nothing about what a thousand cost, or whether the venue quoting it can sell you a thousand at all. Sounder exists to publish the second number.

It does three things and refuses a fourth:

DoesHow
Reads every pool from chain stateFour geometries, each with its own read path. No price feeds, no indexer on the request path.
Walks depth tick by tickReplays the pool’s own swap loop to the point where it can absorb nothing more.
Tests whether a spread is takeableBuys on one venue and sells on another in simulation, through both pools’ real arithmetic.
Does not executeNo contracts deployed, no funds held, no signature requested. See the roadmap for when that changes.

Everything published here is derived from chain state at a stated block. If what you compute disagrees with what is published, what is published is wrong.

02

The read path

“Pool” is not one thing. Four different arithmetics live on this chain, and each keeps its price somewhere else.

Reading one geometry as another is not a rounding error. An Algebra pool read as constant product puts a quote tens of percent away from the market. So each kind gets its own path, and a venue whose kind cannot be established is excluded rather than guessed at.

Geometry, and how the price comes out
KindPrice fromDepth fromVenues
v3slot0()tickBitmap then ticks()Uniswap v3, Ramses, Giga, Up, PancakeSwap v3, RobinSwap, SushiSwap v3
v4extsload on the singletonthe same, derived from raw storage slotsUniswap v4, Orvex
algebraglobalState()as v3; the live fee is the third wordAlandale
v2getReserves()closed form — exact, not sampledUniswap v2, PancakeSwap v2, Pons v2

Decoding is positional, not ABI-shaped

Several forks on this chain return one word fewer from slot0() than Uniswap does. A strict ABI decoder throws on that and the venue disappears from the book entirely. Sounder reads the returned words by position instead, so a fork with an extra field, or one missing, still prices.

export function word(data: `0x${string}`, i: number): bigint {
  const start = 2 + i * 64;
  const hex = data.slice(start, start + 64);
  if (hex.length < 64) throw new Error(`word ${i} missing`);
  return BigInt('0x' + hex);
}
engine/core/raw.ts — reading word n without caring how many follow

Everything goes through Multicall3

A single sounding is hundreds of storage reads. They are batched into one eth_call against the canonical Multicall3 deployment, which is present on this chain at the usual address.

slot0()         0x3850c7bd      ticks(int24)       0xf30dba93
globalState()   0xe76c01e4      tickBitmap(int16)  0x5339c296
liquidity()     0x1a686502      getReserves()      0x0902f1ac
token0()        0x0dfe1681      extsload(bytes32)  0x1e2eaeaf
token1()        0xd21220a7      balanceOf(address) 0x70a08231
fee()           0xddca3f43      tickSpacing()      0xd0c93a7c
selectors used on the read path

Venues that hide their pair

Some forks expose reserves but revert on token0() and token1(), and do not order their reserves by token address either. Guessing the orientation inverts the price. Sounder establishes it by asking each token how much the pool holds of it and matching that against the reserves — entirely on chain, no directory involved.

03

Pools with no contract

Half the fillable depth on this chain sits inside singletons. There is no pool address to call, and most tools simply do not see it.

In a v4-style AMM every pool lives inside one shared contract. State is reached through extsload, keyed by a 32-byte pool id, and the slot is computed rather than queried.

slot = keccak256(abi.encode(poolId, POOLS_SLOT))

  +0   slot0 = lpFee(24) | protocolFee(24) | tick(24) | sqrtPriceX96(160)
  +1   feeGrowthGlobal0X128
  +2   feeGrowthGlobal1X128
  +3   liquidity
  +4   ticks       mapping(int24  => TickInfo)
  +5   tickBitmap  mapping(int16  => uint256)
the address of a pool's state

The two singletons do not agree

They keep the pools mapping at different slot numbers. Nothing published states either one; both were established by hashing a known pool id against each candidate until state came back.

tickSpacing is not in storage

It belongs to the pool key, not to the pool’s state, so it cannot be read back from the singleton at all. Defaulting it would silently produce a pool with no initialized ticks — indistinguishable from a pool that has genuinely run dry. Sounder probes instead: the correct spacing is the one whose bitmap actually lights up.

This matters beyond bookkeeping. A router that cannot read singletons is routing against half a chain and does not know it.

04

Walking the depth

Liquidity is deposited in bands. An order walks through them, paying more in each, until there is nothing left to sell it at any price.

Sounder finds that point by replaying the pool’s own swap loop against its own tick data, in the same 256-bit integer arithmetic the contract runs. Not a curve fitted to trades. Not an estimate.

while (remaining > 0n && sqrtP !== limit) {
  const next = ordered[cursor];              // next initialized tick ahead
  if (next === undefined) { exhausted = true; break; }

  const step = computeSwapStep(sqrtP, target, liquidity, remaining, feePips);
  remaining -= step.amountIn + step.feeAmount;
  amountOut += step.amountOut;
  sqrtP      = step.sqrtRatioNextX96;

  if (sqrtP === target) {                    // crossed a tick
    liquidity += zeroForOne ? -next.liquidityNet : next.liquidityNet;
    cursor++;
  } else break;                              // filled inside this band
}
engine/math/simulate.ts — the loop, in outline

When the input exceeds what the pool holds, the fill comes back marked exhausted together with the amount actually absorbed. Nothing is extrapolated past that point, which is why a venue’s line on the charts simply ends: a short line is a fact about that venue, not a gap in the data.

The ladder

Every ticker is sounded at a fixed set of order sizes, in dollars:

$100   $1,000   $5,000   $25,000   $100,000   $500,000   $2,000,000

Constant product has no wall

It never runs out — it just gets arbitrarily expensive. Reporting that as bottomless would publish an eight-thousand-dollar pool as absorbing past two million. So a constant-product venue reports the last size that still fills for less than the order is worth, and the site marks it with an asterisk to say the limit is of a different kind.

05

Dispersion, and what counts

The same share quotes differently on every venue at the same instant. Which of those quotes belong in the number is a decision, and here it is.

Dispersion is the gap in basis points between the cheapest and the dearest venue at one block. Three rules decide which venues are in it. Each of them makes the headline smaller.

A drained pool is not a quote

A pool that has been emptied keeps whatever price it was abandoned at, forever, and that stale number will sit in aggregate feeds looking like an opportunity. Only venues holding enough depth to fill the smallest rung on the ladder count towards the spread. The rest stay in the table, labelled.

A ticker is not an identity

Several tokens answer to the same symbol on this chain. Resolving by name alone is how a book ends up quoting a nine-cent impostor beside a two-hundred-dollar share. Every ticker resolves to the address whose pools carry the turnover, and the ones dropped are recorded rather than hidden.

An unclassified venue is dropped

11 venues have an explicit read path. Anything else is excluded from the book. That is why the venue list is shorter than the number of names on the chain, and why nothing on it needs a caveat.

06

The crossing

A spread is only interesting if you can take it. Sounder buys on one venue and sells on another, in simulation, and publishes what survives.

For every size on the ladder, the engine buys on each venue and tries selling the proceeds on every other one, using each pool’s real state in both directions. Both fees and both price impacts are already inside the result. No extra chain reads are needed — the state has already been fetched.

What is not inside it: gas, the risk that someone else takes it first, and the fact that both pools move the moment anybody acts. It is a measurement of a disagreement, not a promise of profit — and most of the time it comes back at nothing at all.

{
  sizeUsd, tokens, returnedUsd, profitUsd, profitBps,
  buy:  { ref, label, pair },
  sell: { ref, label, pair }
}
the shape returned

07

The registry

An indexer is a directory, not a price source. It is consulted once, offline, and never on the request path.

Something has to say which pools exist. That is the only job an indexer does here: npm run canon walks it, resolves which address carries the real market for each ticker, records which pools quote it, and writes the result to disk. From then on a page load is chain reads and nothing else, so a rate limit somewhere else cannot change a number published here.

FieldMeaning
tickerssymbol → canonical address, its pools, and the same-symbol tokens that were dropped
poolsSeenhow many pools the walk classified
ethRefPoolthe deepest WETH/USDG pool — the only place the ether leg is priced from
generatedAtwhen the walk ran; prices are never taken from this file

Current registry: 462 pools and 49 tickers, last recomputed 2026-09-04 10:35 UTC. A ticker that appears after that falls through to live discovery instead.

08

The API

The endpoint the site itself calls, unchanged. It answers with the block it read at and how long the read took.

GET /api/sound?ticker=NVDA
GET /api/sound?ticker=SPY&sizes=100,1000,25000

  ticker   symbol, or a 0x address
  sizes    optional, comma separated, in dollars, up to nine
GET /api/sound
FieldWhat it is
blockNumberthe block every number on the response was read at
readMshow long the read took, end to end
dispersionBpscheapest to dearest, counting only fillable venues
fillableCounthow many venues the spread was taken across
venues[]per pool: spot, fee, ticks read, the ladder, capacity, and any error
crossingthe best profitable crossing found, or null
impostors[]same-symbol tokens that were dropped, and their volume

Responses carry s-maxage=20 with stale-while-revalidate, so bursts are served from cache rather than from the chain. A reading is only true for the block it names.

https://www.sounderliq.app/?ticker=SPY&size=100000
every reading is linkable

09

Reproduce every number

Each figure on the site has a command behind it. These are the same entry points the site calls.

npm run book NVDA     sound one ticker: every venue, the full ladder
npm run pulse         the whole watchlist, the way the board computes it
npm run canon         rebuild the registry — the only step that uses an indexer

The engine is plain TypeScript with no service behind it. Point SOUNDER_RPC at any node for chain 4663 and the numbers come out the same, because they come out of the chain.

10

Performance notes

Two findings that were measured rather than assumed, and that anyone reading this chain will hit.

JSON-RPC request batching is a trap here

Coalescing reads into JSON-RPC batches made six parallel soundings take 13.4 seconds. Sending them as concurrent single requests takes 0.9 seconds. Everything that can be bundled is already one eth_call through Multicall3, so a second layer of batching buys nothing and costs an order of magnitude.

Order endpoints by measured latency

robinhood-rpc.publicnode.com answers an eth_call in about 60 ms; the official endpoint spikes past two seconds. Archive log queries go the other way — publicnode refuses them, the official endpoint serves them.

A full sounding of a pool with four hundred initialized ticks lands in roughly 480 ms; the whole watchlist, with depth, in about 1.1 s.

11

Addresses

Everything the read path touches.

WhatAddress
USDG · 6 decimals0x5fc5360d0400a0fd4f2af552add042d716f1d168
WETH · 18 decimals0x0bd7d308f8e1639fab988df18a8011f41eacad73
Native ether sentinel0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
Multicall30xcA11bde05977b3631167028862bE2a173976CA11
SOUNDER · 18 decimals0x4214e44c97865858d78e37d5d4ee3262acf30406

The indexer’s pool names are not trustworthy for ordering: it prints “USDG / WETH” for a pool whose token0 is WETH. Every pair is established by reading symbol() and decimals() off the tokens themselves.

12

Glossary

The words this site uses, and exactly what it means by them.

TermMeaning here
SoundingOne complete reading of a ticker: every venue, priced from its own state, with the depth ladder walked.
TickA discrete price step in a concentrated pool. Liquidity is deposited between ticks, and crossing one changes how much is available.
sqrtPriceX96The pool’s price, stored as a square root in 96-bit fixed point. Squaring it and adjusting for decimals gives the price.
FillableA venue holding enough depth to fill the smallest order on the ladder. Only fillable venues count towards a spread.
Capacity · stops atThe order size past which a venue can absorb no more. For constant product, the size past which the fill costs more than the order is worth.
Cost to fillWhat you actually pay per share, fee and price impact included, versus the quoted price. Quoted in basis points.
Dispersion · spreadThe gap between the cheapest and dearest fillable venue at one block, in basis points.
CrossingBuying on one venue and selling on another in the same block. Published only when it survives both fees and both impacts.
Basis pointOne hundredth of a percent. A hundred dollars at 30 bps costs thirty cents more than the quote.

Anything unclear, wrong, or missing here is worth saying out loud — the method is the product, and a method nobody can check is just a claim.