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:
| Does | How |
|---|---|
| Reads every pool from chain state | Four geometries, each with its own read path. No price feeds, no indexer on the request path. |
| Walks depth tick by tick | Replays the pool’s own swap loop to the point where it can absorb nothing more. |
| Tests whether a spread is takeable | Buys on one venue and sells on another in simulation, through both pools’ real arithmetic. |
| Does not execute | No 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.
| Kind | Price from | Depth from | Venues |
|---|---|---|---|
| v3 | slot0() | tickBitmap then ticks() | Uniswap v3, Ramses, Giga, Up, PancakeSwap v3, RobinSwap, SushiSwap v3 |
| v4 | extsload on the singleton | the same, derived from raw storage slots | Uniswap v4, Orvex |
| algebra | globalState() | as v3; the live fee is the third word | Alandale |
| v2 | getReserves() | closed form — exact, not sampled | Uniswap 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);
}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
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 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.
| Venue | Singleton | Pools mapping |
|---|---|---|
| Uniswap v4 | 0x8366a39cc670b4001a1121b8f6a443a643e40951 | slot 6 |
| Orvex | 0xd01c774d4a66408326bc65728ac5ae5aaf004032 | slot 4 |
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
}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 }
}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.
| Field | Meaning |
|---|---|
tickers | symbol → canonical address, its pools, and the same-symbol tokens that were dropped |
poolsSeen | how many pools the walk classified |
ethRefPool | the deepest WETH/USDG pool — the only place the ether leg is priced from |
generatedAt | when 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
| Field | What it is |
|---|---|
blockNumber | the block every number on the response was read at |
readMs | how long the read took, end to end |
dispersionBps | cheapest to dearest, counting only fillable venues |
fillableCount | how many venues the spread was taken across |
venues[] | per pool: spot, fee, ticks read, the ladder, capacity, and any error |
crossing | the 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
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.
| What | Address |
|---|---|
| USDG · 6 decimals | 0x5fc5360d0400a0fd4f2af552add042d716f1d168 |
| WETH · 18 decimals | 0x0bd7d308f8e1639fab988df18a8011f41eacad73 |
| Native ether sentinel | 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE |
| Multicall3 | 0xcA11bde05977b3631167028862bE2a173976CA11 |
| SOUNDER · 18 decimals | 0x4214e44c97865858d78e37d5d4ee3262acf30406 |
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.
| Term | Meaning here |
|---|---|
| Sounding | One complete reading of a ticker: every venue, priced from its own state, with the depth ladder walked. |
| Tick | A discrete price step in a concentrated pool. Liquidity is deposited between ticks, and crossing one changes how much is available. |
sqrtPriceX96 | The pool’s price, stored as a square root in 96-bit fixed point. Squaring it and adjusting for decimals gives the price. |
| Fillable | A venue holding enough depth to fill the smallest order on the ladder. Only fillable venues count towards a spread. |
| Capacity · stops at | The 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 fill | What you actually pay per share, fee and price impact included, versus the quoted price. Quoted in basis points. |
| Dispersion · spread | The gap between the cheapest and dearest fillable venue at one block, in basis points. |
| Crossing | Buying on one venue and selling on another in the same block. Published only when it survives both fees and both impacts. |
| Basis point | One 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.