Kova
One book for tokenized equities on Robinhood Chain. Every pool that trades a canonical stock token, read from the chain, priced in dollars, quoted at size and set side by side.
Overview
A tokenized share on Robinhood Chain rarely trades in one place. The same stock sits in Uniswap v2, v3 and v4 pools, in Algebra pools and in several v3 forks, against USDG, WETH, native ether and other stocks, and every pool keeps its own price. Kova reads all of them and sets them side by side: what a share costs, how far apart the pools are and what a real order would get in each one.
One book
Every pool behind every canonical stock token, with its kind, fee, liquidity and price, ranked by activity.
Read from the chain
Pool state comes from the contracts at the current block. The indexer only names pools and serves as a second opinion.
Quotes at size
Each pool runs the real swap inside an eth_call, so a quote already includes fees, tick crossings and depth.
What Kova is not
- Not a venue. It routes no orders, matches nothing and takes no fee. Trades happen in the pools, wherever you choose.
- Not a wallet. It never asks for a signature, a network switch or a transaction.
- Not an oracle. The book price is a reading aid, a liquidity-weighted median of live pools, not a reference rate to settle anything against.
Nothing in Kova holds funds or keys. The server reads the chain, the pages read the chain or the API, and the one contract involved is never deployed.
How it works
The same modules run on the server and in the browser. The API builds the book once for everyone looking; the terminal can skip the API and rebuild the whole book in the page, straight from the chain.
| Piece | Where | What it does |
|---|---|---|
| Discovery | js/discovery.js | Asks DexScreener which pools list each token |
| Book engine | js/book.js | Classifies pools, reads their state, prices them and builds the book |
| KovaLens | contracts/KovaLens.sol | Bulk reads and exact quotes, run as a state override inside eth_call |
| Quotes | js/quote.js | Builds a swap path for every pool that can fill an order |
| Tape | js/tape.js | Decodes Swap events from the busiest pools |
| API | api/v1/ | Read-only JSON over the same engine, cached at the edge |
| RPC proxy | api/rpc.js | Read-only relay from this site's pages to the node |
| Terminal | dashboard.html, js/terminal.js | The book, asset pages, quotes, the tape and holdings |
Flow
DexScreener: which pools list NVDA, SPY, AAPL ...?
|
v
classify each pool on chain ---> v2 | v3 | algebra | v4 | unknown
|
v
read state: slot0, globalState, reserves, v4 slots KovaLens.peek, 250 reads per eth_call
|
v
price: USDG pools set the dollar; ETH and equity legs convert through their own pools
|
v
book: price, low, high, spread and every pool row, per asset
|
+--> /api/v1/book, the terminal, the landing page
+--> quotes: every pool swaps inside eth_call KovaLens.quote
+--> tape: Swap events from the busiest pools
A cold build, discovery included, takes a few seconds against the public node; a warm one, with discovery cached, takes one or two. The server keeps a finished book for 15 seconds and serves it for up to three minutes while a fresh one is built behind it.
Quickstart
Run the site
# from a checkout of this repository
npm install
npm run dev # http://localhost:4720, pages and /api like on Vercel
Check the engine
npm test # unit tests
node scripts/time-book.mjs # two builds against the node, with timings
Ask the API
curl -s "localhost:4720/api/v1/book?rows=0" | jq .stats
curl -s "localhost:4720/api/v1/quote?symbol=NVDA&side=buy&amount=1000" | jq .best
Node 20 or newer. Everything runs on the standard library plus two build tools: solc for the lens and playwright-core for the brand images and the site check.
Discovery
The book starts from a list of canonical tokens and asks DexScreener which pools list each
one, through /token-pairs/v1/robinhood/{token}, six tokens at a time. Lists are
cached for five minutes. WETH is looked up as well, because its USDG pools set the dollar
price of ether.
A listing only names a pool. Its labels and prices are not trusted: every pool worth reading
is measured on chain in the next steps. Pools with less than $5,000 of reported liquidity stay
in the book as dust and are never read.
Canonical tokens
Every tokenized equity Robinhood issues is a beacon proxy pointing at the same beacon. A token enters the registry only when its EIP-1967 beacon slot holds that beacon. A matching name or ticker is not enough, so a look-alike token never reaches the book.
| Constant | Value |
|---|---|
| Equity beacon | 0xe10b6f6b275de231345c20d14ab812db62151b00 |
| Beacon slot | 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50 |
| USDG, 6 decimals | 0x5fc5360d0400a0fd4f2af552add042d716f1d168 |
| WETH, 18 decimals | 0x0bd7d308f8e1639fab988df18a8011f41eacad73 |
npm run build:registry writes the list to js/registry.js, together with
the settled facts of every pool it could classify, so a cold start does not rediscover what
never changes. A token issued after the last build is missing until the registry is rebuilt.
Pool kinds
A pool's kind decides how its price is read, so it is measured rather than taken from a label.
Every pool not yet classified gets seven calls: globalState, slot0,
getReserves, token0, token1, fee and
tickSpacing. Whatever answers decides.
| Evidence | Kind | State read with |
|---|---|---|
globalState() answers | algebra | globalState, liquidity |
slot0() answers | v3, Uniswap v3 and its forks | slot0, liquidity |
getReserves() returns three words | v2 | getReserves |
| The id is 32 bytes, not an address | v4 | extsload on the PoolManager |
A pool that stays silent three rounds in a row is marked unknown. Token order, fee
and tick spacing are kept with the kind, and the registry build stores them for next time.
Reading state
Every read for the book goes through KovaLens.peek, a multicall that exists only
inside the call: its bytecode travels with each eth_call as a state override at
0x4b4f564100000000000000000000000000000000, an address whose bytes spell KOVA.
One call carries 250 reads, each with its own 300,000 gas, and the calls share one HTTP batch.
Around 840 pools cost a handful of requests instead of hundreds.
A node that refuses state overrides gets the same reads as plain batched eth_calls. Slower, same answers.
Uniswap v4
v4 pools have no contract of their own. Their state lives in the PoolManager at
0x8366a39cc670b4001a1121b8f6a443a643e40951, in mapping slot 6: the pool's state slot
is keccak256(poolId, 6), slot0 is packed into it and liquidity sits three
slots further on. Both are read with extsload.
A quote needs the pool's full key, not just its id. Kova recovers the key by hashing candidates: the two currencies, the LP fee or the dynamic-fee flag, and each common tick spacing, with no hooks, until one matches the id. A pool whose key does not turn up, a pool with hooks for example, is priced but never quoted.
Pricing
A pool price starts as a ratio of its two tokens, from sqrtPriceX96 for v3, Algebra
and v4 pools or from reserves for v2, corrected for decimals. Turning it into dollars takes one
more step, and the book never borrows that step from an indexer:
- USDG is one dollar. A pool against USDG is priced directly.
- Ether is priced from WETH's own USDG pools, as a liquidity-weighted median of the lively ones. If none answered, the indexer's figure stands in and every ETH-priced row says so with
ethPriceReported. - Pools against WETH or native ether convert through that price.
- Each stock gets a dollar mid from those pools, and pools quoted in another stock convert through that stock's mid.
An asset's price is the liquidity-weighted median of its active pools: the price with half the active liquidity on either side. A thin pool can print any number; it cannot drag this one far.
Row statuses
A pool is lively when it holds at least $10,000 and traded at least five times in the last 24 hours. Only active pools feed an asset's price, low, high and spread.
| Status | Meaning |
|---|---|
active | Lively, priced, and within 25% of the asset's lively mid |
thin | Priced, but under the liquidity or trading bar. Shown, not counted |
outlier | Lively and priced, but more than 25% away from the mid |
unpriced | Read, but quoted in a token with no dollar path, often a memecoin |
unread | A known kind whose state did not come back this round |
unknown | Worth reading, but not a pool Kova recognises |
dust | Under $5,000 of liquidity. Listed, never read |
Spreads and drift
spreadPct is how far the richest active pool sits above the cheapest,
(high / low - 1) × 100, given once an asset has two active pools. The book's
medianSpreadPct is the median of those spreads across assets, and every row carries
deltaPct, its distance from the asset's price.
driftPct sets a pool's on-chain dollar price against the price the indexer reports for
the same pool, on rows where that price refers to the stock. The terminal and the landing page
flag anything past 2%. A large drift usually means one of the two numbers is stale, and the row
shows both so you can tell which.
Quotes
A quote asks every pool the same question at once: what would this order get here, right now? A buy spends USDG on a stock; a sell turns shares into USDG.
The candidates are an asset's active and thin pools that are quotable. A
pool against USDG is quoted directly. A pool against WETH is quoted through the deepest
quotable WETH/USDG pool, so a buy there crosses two pools: USDG to WETH, then WETH to the stock.
Outliers and unpriced pools are never quoted.
KovaLens
Quotes run in KovaLens, a contract that is never deployed. Its runtime code, about
5 KB, travels with each eth_call as a state override, so a quote needs no deployment,
approval or signature and describes the pool at the current block.
| Leg | How it is quoted |
|---|---|
| v3-style: Uniswap v3, its forks, Algebra | Calls the pool's swap with a price limit at the edge of the range. The pool calls back for payment; whatever name a fork gives that callback, it lands in the lens's fallback, which reverts with the two amounts the pool asked for. |
| v4 | Calls unlock on the PoolManager. Inside unlockCallback the lens swaps exact-in and reverts with the balance delta. |
| v2 | Reads the reserves and applies the constant-product formula with the pool's fee. |
Each revert is caught and decoded, so the pool's own arithmetic, fees, tick crossings and running out of depth included, becomes the answer while the swap itself unwinds. A leg that fails zeroes only its own path. Each leg gets 2,500,000 gas, six paths travel per call, and the calls share one HTTP batch.
function quote(Leg[] calldata legs, uint256[] calldata lengths, uint256 amountIn)
external returns (uint256[] memory amountsOut, uint256[] memory amountsInUsed);
function peek(Read[] calldata reads) external view returns (bytes[] memory results);
The source is contracts/KovaLens.sol. npm run build:lens compiles it with
solc 0.8.26 into js/lens-code.js, and /api/v1/health reports the fingerprint
of the code in use.
Reading a quote
| Field | Meaning |
|---|---|
amountOut | Shares received on a buy, USDG received on a sell |
amountInUsed | What the first leg actually took, below the order size when depth ran out |
filled, partial | Whether the pool took the whole order or only part of it |
execPriceUsd | Dollars per share from what the pool took and gave, never from the full order when it ran dry |
costPct | The price paid against the pool's own mid, fee included |
vsBestPct | Output compared with the best pool that filled in full |
via | WETH when the path crosses the WETH/USDG pool |
A quote is exact for the block it was read at. Prices move between a quote and a trade, and nothing here reserves liquidity.
Tape
The tape lists trades as they printed. It reads Swap events from the busiest active pools, up to 40 ranked by 24-hour volume, over the last 9,000 blocks: about fifteen minutes at a tenth of a second per block, and inside the ten thousand blocks the node allows per log query.
| Pools | Event | Amounts |
|---|---|---|
| v3-style and Algebra | Swap(address,address,int256,int256,uint160,uint128,int24), from the pool | The pool's side: positive is what it took in |
| v4 | Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24), from the PoolManager, keyed by pool id | The swapper's side, so the signs are flipped |
| v2 | Swap(address,uint256,uint256,uint256,uint256,address) | In and out for each token |
A trade is a buy when the pool paid the stock out. Its value comes from the pool's own quote amount at the row's dollar rate, and each trade links to its transaction on Blockscout. The tape has no API endpoint; pages read it through the RPC proxy.
Holdings
The terminal can show what an address holds: every canonical stock token, USDG, WETH and ether, valued at book prices. Paste an address or connect a browser wallet. Connecting only asks the wallet which account is selected.
Balances are read in one pass through KovaLens.peek, via this site's RPC proxy, which
stores nothing. The address is remembered in your browser under kova.watch until you
clear it.
There is no send button, no approval and no network switch anywhere in Kova.
API
Everything the site shows is available as read-only JSON under /api/v1. There is no
key and no sign-up. Responses allow any origin and carry cache headers, so a CDN in front of the
functions answers most requests without touching the engine.
| Endpoint | Returns | Cache, fresh / stale |
|---|---|---|
GET /api/v1 | The endpoint list | 300 s / 900 s |
GET /api/v1/health | Chain head, market phase, registry size, lens fingerprint | 5 s / 15 s |
GET /api/v1/tokens | Canonical tokens and the check that admitted them | 1 h / 24 h |
GET /api/v1/venues | Venues with pool counts and measured kinds | 30 s / 90 s |
GET /api/v1/book | The whole book, or one asset of it | 15 s / 45 s |
GET /api/v1/quote | A buy or a sell quoted in every pool that can fill it | 5 s / 10 s |
GET /api/v1/openapi.json | An OpenAPI 3.1 description | 1 h / 24 h |
Errors come back as { "error": "..." } with a matching status: 400 for a bad
parameter, 404 for an asset that is not in the book, 405 for anything but GET, and 502 when the
chain or the indexer did not answer in time.
GET /api/v1/book
| Parameter | Effect |
|---|---|
symbol=NVDA | One asset with every pool row. 404 when the symbol is not in the book |
rows=0 | Drops pool rows and keeps the headline figures of every asset |
include=NVDA,SPY | With rows=0, keeps the rows of the listed symbols |
curl -s "https://your-host/api/v1/book?rows=0&include=NVDA"
{
"at": 1789300000000, "block": 61502221, "phase": "closed", "ethUsd": 2523.08,
"stats": { "assets": 44, "tokens": 54, "pools": 1196, "activePools": 304, "venues": 8,
"volume24h": ..., "liquidityUsd": ..., "medianSpreadPct": 0.96, "widestSpreadPct": ... },
"venues": [ { "name": "Uniswap", "pools": ..., "activePools": ..., "kinds": ["v3", "v4", "v2"] } ],
"assets": [
{ "symbol": "NVDA", "name": "NVIDIA", "price": ..., "low": ..., "high": ..., "spreadPct": ...,
"activePools": 27, "quotablePools": ..., "cheapest": { ... }, "richest": { ... },
"rows": [ { "id": "0x...", "venue": "Uniswap", "kind": "v3", "quote": "USDG", "status": "active", ... } ] }
]
}
| Row field | Meaning |
|---|---|
id | Pool address, or the 32-byte v4 pool id |
kind | v2, v3, algebra, v4 or unknown, as measured |
quote, quoteAddress | The token on the other side |
feePpm, tickSpacing, key | Fee in parts per million, spacing, and the v4 key when recovered |
priceQuote, priceUsd | Price in the quote token and in dollars |
priceUsdReported, driftPct | The indexer's price for the pool, and how far the chain's differs |
liquidityUsd, volume24h, txns24h | Size and activity, as the indexer reports them |
status, active, outlier, quotable | See row statuses |
deltaPct | Distance from the asset's price |
GET /api/v1/quote
| Parameter | Effect |
|---|---|
symbol | The stock, required |
side | buy (default) or sell |
amount | USDG to spend on a buy, shares to sell on a sell, as a decimal string |
tokenIn, tokenOut, amountIn | The same request as a pair; one side must be USDG |
curl -s "https://your-host/api/v1/quote?symbol=NVDA&side=buy&amount=10000"
curl -s "https://your-host/api/v1/quote?tokenIn=NVDA&tokenOut=USDG&amountIn=5"
The answer holds best, the pool that fills the whole order for the most, and
results for every pool quoted, with the fields described in
Reading a quote. block is the block of the book the pools
came from; the swaps themselves run against the latest block.
Tokens, venues, health
/api/v1/tokens lists every canonical token with its address and decimals, the beacon
check that admitted it and the currencies prices are built from. /api/v1/venues lists
venues with pool counts and the kinds measured behind each label. /api/v1/health says
whether the node answers as chain 4663, with the head block and latency, the market phase, the
registry's size and date, and the lens fingerprint with deployed: false.
No keys and no per-caller limits. Every request shares the same book: at most one build per instance every 15 seconds, however many requests arrive.
RPC proxy
/api/rpc relays JSON-RPC from this site's own pages to the node. It exists because the
public node, under load, sends Access-Control-Allow-Origin twice, and browsers discard
such answers. It is not a public relay.
| Rule | Limit |
|---|---|
| Method | POST only |
| Callers | Pages on the same host, localhost, or origins listed in KOVA_ALLOWED_ORIGINS. Anyone else gets 403 |
| Calls | eth_call, eth_blockNumber, eth_chainId, eth_getBalance, eth_getCode, eth_getStorageAt, eth_getLogs |
| Batches | 1 to 100 calls |
| Logs | Numeric fromBlock and toBlock, at most 10,000 blocks apart |
| Body | Up to 1 MB, room for 250 peek reads plus the lens code |
| Upstream | KOVA_RPC_URL or the public node, 25 s timeout, never cached |
Pages try the proxy first and the node directly second. A throttled batch is retried with backoff, and a batch that fails is split in half and tried again, so a node with an unknown limit costs more requests rather than an error.
Self-hosting
Kova is a static site plus a few Node functions. On Vercel it deploys as it is: pages and assets
are served statically, and every file under api/ becomes a function that imports the
shared modules from js/ and lib/.
vercel deploy # a preview
vercel deploy --prod # production
Elsewhere, any Node server that hands the handlers a plain request and response will do.
dev-server.mjs is a complete example in about a hundred lines. Functions keep the book in
memory per instance, so a warm instance answers from cache; allow them up to 60 seconds, although
a cold build usually takes a few.
Environment
| Variable | Used by | Notes |
|---|---|---|
KOVA_RPC_URL | API engine, RPC proxy | Node endpoint. Defaults to the public Robinhood Chain RPC |
KOVA_ALLOWED_ORIGINS | RPC proxy | Extra origins allowed to use the proxy, comma separated |
PORT | dev-server.mjs | Local port, 4720 by default |
CHROME_PATH | scripts/verify-site.mjs | The Chrome used for the site check |
Development
| Command | What it does |
|---|---|
npm run dev | Serves the pages and /api on port 4720, the way Vercel does |
npm test | Unit tests |
npm run build:lens | Compiles KovaLens into js/lens-code.js |
npm run build:registry | Rebuilds the token list and pool facts from the chain |
npm run build:brand | Renders the logos, icons and the social image |
node scripts/time-book.mjs | Builds the book twice against the node and prints where the time went |
node scripts/verify-site.mjs | Opens the site in headless Chrome, desktop and phone, and reports errors |
The chain code has no dependencies. Keccak-256, ABI encoding, sqrtPriceX96 prices and
the v4 storage layout live in js/keccak.js and js/evm.js, and the same files
run in Node and in the browser.
Numbers
| Setting | Value | Set in |
|---|---|---|
| Lively pool | $10,000 of liquidity and 5 trades in 24 hours | js/chain.js |
| Outlier | More than 25% from the lively mid | js/chain.js |
| Drift flag | 2% | js/chain.js |
| Read floor | $5,000 of liquidity | js/book.js |
| Discovery cache | 5 minutes | js/chain.js |
| Bulk reads | 250 per call, 300,000 gas each | js/lens.js, KovaLens.sol |
| Quotes | 6 paths per call, 2,500,000 gas per leg, 30,000,000 per call | js/lens.js, KovaLens.sol |
| Tape window | 9,000 blocks, about 15 minutes | js/tape.js |
| Server book | Fresh for 15 s, served stale for up to 180 s | lib/engine.js |
| Terminal refresh | 20 s during the US session, 120 s outside it | js/chain.js |
| Landing refresh | 60 s | js/landing.js |
| RPC proxy | 100 calls per batch, 10,000 blocks per log query, 1 MB body | api/rpc.js |
Limits
Knowing where the edges are is cheaper than finding them.
- KovaLens is unaudited. It is never deployed and can move nothing, but a wrong quote would still be wrong.
- Discovery depends on DexScreener. A pool it does not list is not in the book.
- Pools with hooks, or with a tick spacing outside the common set, can be priced but not quoted.
- Prices are read at one block from a public node. They are not a signed feed and can trail a busy block.
- The market phase follows the regular US session in New York time and knows nothing about holidays.
- The tape covers fifteen minutes and has no reorg handling: a range is read once, as it stood.
- A newly issued token appears only after the registry is rebuilt.