Kova DOCS
/
Documentation

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.
Read only

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.

PieceWhereWhat it does
Discoveryjs/discovery.jsAsks DexScreener which pools list each token
Book enginejs/book.jsClassifies pools, reads their state, prices them and builds the book
KovaLenscontracts/KovaLens.solBulk reads and exact quotes, run as a state override inside eth_call
Quotesjs/quote.jsBuilds a swap path for every pool that can fill an order
Tapejs/tape.jsDecodes Swap events from the busiest pools
APIapi/v1/Read-only JSON over the same engine, cached at the edge
RPC proxyapi/rpc.jsRead-only relay from this site's pages to the node
Terminaldashboard.html, js/terminal.jsThe 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

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.

ConstantValue
Equity beacon0xe10b6f6b275de231345c20d14ab812db62151b00
Beacon slot0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
USDG, 6 decimals0x5fc5360d0400a0fd4f2af552add042d716f1d168
WETH, 18 decimals0x0bd7d308f8e1639fab988df18a8011f41eacad73

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.

EvidenceKindState read with
globalState() answersalgebraglobalState, liquidity
slot0() answersv3, Uniswap v3 and its forksslot0, liquidity
getReserves() returns three wordsv2getReserves
The id is 32 bytes, not an addressv4extsload 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.

Fallback

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:

  1. USDG is one dollar. A pool against USDG is priced directly.
  2. 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.
  3. Pools against WETH or native ether convert through that price.
  4. 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.

StatusMeaning
activeLively, priced, and within 25% of the asset's lively mid
thinPriced, but under the liquidity or trading bar. Shown, not counted
outlierLively and priced, but more than 25% away from the mid
unpricedRead, but quoted in a token with no dollar path, often a memecoin
unreadA known kind whose state did not come back this round
unknownWorth reading, but not a pool Kova recognises
dustUnder $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.

LegHow it is quoted
v3-style: Uniswap v3, its forks, AlgebraCalls 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.
v4Calls unlock on the PoolManager. Inside unlockCallback the lens swaps exact-in and reverts with the balance delta.
v2Reads 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

FieldMeaning
amountOutShares received on a buy, USDG received on a sell
amountInUsedWhat the first leg actually took, below the order size when depth ran out
filled, partialWhether the pool took the whole order or only part of it
execPriceUsdDollars per share from what the pool took and gave, never from the full order when it ran dry
costPctThe price paid against the pool's own mid, fee included
vsBestPctOutput compared with the best pool that filled in full
viaWETH when the path crosses the WETH/USDG pool
Limit

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.

PoolsEventAmounts
v3-style and AlgebraSwap(address,address,int256,int256,uint160,uint128,int24), from the poolThe pool's side: positive is what it took in
v4Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24), from the PoolManager, keyed by pool idThe swapper's side, so the signs are flipped
v2Swap(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.

View only

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.

EndpointReturnsCache, fresh / stale
GET /api/v1The endpoint list300 s / 900 s
GET /api/v1/healthChain head, market phase, registry size, lens fingerprint5 s / 15 s
GET /api/v1/tokensCanonical tokens and the check that admitted them1 h / 24 h
GET /api/v1/venuesVenues with pool counts and measured kinds30 s / 90 s
GET /api/v1/bookThe whole book, or one asset of it15 s / 45 s
GET /api/v1/quoteA buy or a sell quoted in every pool that can fill it5 s / 10 s
GET /api/v1/openapi.jsonAn OpenAPI 3.1 description1 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

ParameterEffect
symbol=NVDAOne asset with every pool row. 404 when the symbol is not in the book
rows=0Drops pool rows and keeps the headline figures of every asset
include=NVDA,SPYWith 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 fieldMeaning
idPool address, or the 32-byte v4 pool id
kindv2, v3, algebra, v4 or unknown, as measured
quote, quoteAddressThe token on the other side
feePpm, tickSpacing, keyFee in parts per million, spacing, and the v4 key when recovered
priceQuote, priceUsdPrice in the quote token and in dollars
priceUsdReported, driftPctThe indexer's price for the pool, and how far the chain's differs
liquidityUsd, volume24h, txns24hSize and activity, as the indexer reports them
status, active, outlier, quotableSee row statuses
deltaPctDistance from the asset's price

GET /api/v1/quote

ParameterEffect
symbolThe stock, required
sidebuy (default) or sell
amountUSDG to spend on a buy, shares to sell on a sell, as a decimal string
tokenIn, tokenOut, amountInThe 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.

Load

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.

RuleLimit
MethodPOST only
CallersPages on the same host, localhost, or origins listed in KOVA_ALLOWED_ORIGINS. Anyone else gets 403
Callseth_call, eth_blockNumber, eth_chainId, eth_getBalance, eth_getCode, eth_getStorageAt, eth_getLogs
Batches1 to 100 calls
LogsNumeric fromBlock and toBlock, at most 10,000 blocks apart
BodyUp to 1 MB, room for 250 peek reads plus the lens code
UpstreamKOVA_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

VariableUsed byNotes
KOVA_RPC_URLAPI engine, RPC proxyNode endpoint. Defaults to the public Robinhood Chain RPC
KOVA_ALLOWED_ORIGINSRPC proxyExtra origins allowed to use the proxy, comma separated
PORTdev-server.mjsLocal port, 4720 by default
CHROME_PATHscripts/verify-site.mjsThe Chrome used for the site check

Development

CommandWhat it does
npm run devServes the pages and /api on port 4720, the way Vercel does
npm testUnit tests
npm run build:lensCompiles KovaLens into js/lens-code.js
npm run build:registryRebuilds the token list and pool facts from the chain
npm run build:brandRenders the logos, icons and the social image
node scripts/time-book.mjsBuilds the book twice against the node and prints where the time went
node scripts/verify-site.mjsOpens 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

SettingValueSet in
Lively pool$10,000 of liquidity and 5 trades in 24 hoursjs/chain.js
OutlierMore than 25% from the lively midjs/chain.js
Drift flag2%js/chain.js
Read floor$5,000 of liquidityjs/book.js
Discovery cache5 minutesjs/chain.js
Bulk reads250 per call, 300,000 gas eachjs/lens.js, KovaLens.sol
Quotes6 paths per call, 2,500,000 gas per leg, 30,000,000 per calljs/lens.js, KovaLens.sol
Tape window9,000 blocks, about 15 minutesjs/tape.js
Server bookFresh for 15 s, served stale for up to 180 slib/engine.js
Terminal refresh20 s during the US session, 120 s outside itjs/chain.js
Landing refresh60 sjs/landing.js
RPC proxy100 calls per batch, 10,000 blocks per log query, 1 MB bodyapi/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.