One row per TOKEN over a 24-hour window, ranked by measured ETH-leg volume — the same measurement /chain renders, computed from the same aggregates so the two can never disagree by a rounding. A token with pools on V2, V3 and V4 appears once, summed. Price, change and bar counts come from ONE pool (the token's most-indexed-swaps ETH-quoted pool in the window), because a price is a property of a pool and an averaged cross-pool price is a number that never traded.
parameters
| name | in | what it selects | absent | not usable |
|---|---|---|---|---|
| limit | query | how many rows to return, from the top of the ranking | 100 | clamped to 250; a non-positive or unparseable value falls back to 100. Verified: ?limit=9999 returns 250 rows and echoes limit:250. |
| venue | query | a LENS, not a row filter: all | v2 | v3 | v4. With a venue set, every figure on every row is re-measured from that venue's pools alone — including which pool the price comes from. | "all" | degrades to "all" and echoes venue:"all" — a typo yields a wider measurement, correctly labelled, rather than an error. Verified with ?venue=v9. |
| shape | query | exactly "2" opts into the compact row encoding: short keys, booleans as 0|1, enums as indexes, null encoded as absent, and an added v:2 on the envelope. It is an ENCODING — every field is the same measurement under a shorter key. | the long shape (no v field) | anything but the exact string 2 gets the long shape, so a consumer that never asked cannot be handed keys it does not know. Verified with ?shape=7. |
bounds — what this response is not
- At most 250 rows in any response, whatever ?limit= asks for. The response echoes the limit actually applied.
- totals is WINDOW-WIDE — every token in the 24h window, before the limit — so totals.tokens is the honest denominator for "showing 100 of N". Summing the returned rows gives a different, smaller number, and is not what the totals mean.
- Exactly ONE order exists: order:"eth24-desc". The board UI's ?sort= lenses and its sparkline are not served here. If you render a different sort, merge these rows by token and keep your own ordering.
- rows[].pools counts pools with at least one indexed swap IN THE WINDOW — not every pool the token has. /quotes and /pools count all indexed pools, which is why the same token can read 15 here and 20 there.
- eth1/swaps1 are a ROLLING sixty minutes at the 5-minute candle grain (flowGrainSecs), not the last clock hour, and they are a FLOOR: a swap whose price could not be derived lands in eth24 and not here.
- Every figure is a floor over what the indexer could walk in its per-tick budget, measured since index.startSec. Nothing here is all-time.
fields
| path | type | what it is |
|---|---|---|
| serverSec | integer (unix s) | server clock when this body was built |
| measuredSec | integer (unix s) | when the rows were measured — may lag serverSec by the memo window |
| index | object | null | cursor facts; null = the worker has never run against this database |
| rows[].token | string (0x, lowercase) | the row key and the only unforgeable field on the row |
| rows[].tokenChecksum | string (EIP-55) | checksummed form, so links do not eat a redirect |
| rows[].symbol | string | null | deployer-written decoration, sanitised. null = no usable ticker (never "", never the "·" marker) |
| rows[].symbolState | "read" | "unreadable" | "unread" | WHY symbol is null: it answered / symbol() refused (about the token) / we have not asked yet (about our index) |
| rows[].name | string | null | the deployer's name(), sanitised and capped at 40 characters — decoration, like symbol. null = no usable name. |
| rows[].image | string | null | a URL renderable under our CSP, or null |
| rows[].imageState | "image" | "none" | "offsite" | "unread" | "offsite" = we hold a URL and will not load it. Only "none" says anything about the token. |
| rows[].decimals | integer | null | null = decimals() never answered usably. NEVER assume 18 from this. |
| rows[].totalSupply | string (raw units) | null | exact supply in raw units; null = unread. Market cap = lastE36 x totalSupply (both raw, so decimals cancel); a null on either side means uncomputable. |
| rows[].hooked | boolean | at least one of this token's pools runs a V4 hook |
| rows[].ethQuoted | boolean | false = no pool has an ETH side, so ETH volume is UNMEASURABLE. eth24/eth1 still arrive as the string "0" on such a row — the sum is genuinely zero because there is no ETH leg to count. Read this flag before rendering either figure, and print n/a rather than 0. |
| rows[].pools | integer | pools with at least one indexed swap in the window |
| rows[].eth24 / eth1 | string (decimal wei) | the 24h window / a rolling 60min at the candle grain. Parse with BigInt. A "0" is only a volume measurement when ethQuoted is true. |
| rows[].swaps24 / swaps1 | integer | indexed swap counts over the same two windows |
| rows[].lastE36 | string | null | last close of the chosen pool as the stored exact integer: wei of ETH per RAW token unit x 1e36. null = no candle in the window — no price, not a price of zero. |
| rows[].chgPct | number | null | first-bar open to last-bar close on the same pool. null = no opening bar, which is not a change of 0%. Read it beside bars and firstBarSec. |
| rows[].poolId | string | null | WHICH pool the price, change and bars came from |
| rows[].bars | integer (never null) | how many 5-minute bars the price/change came from — the basis behind chgPct, and on a token minutes old this discloses a partial window. 0 is a real value and means no candle was found for this token in the window; it arrives together with poolId/lastE36/chgPct null, and those nulls are the honest signal, not this 0. |
| rows[].firstBarSec / lastBarSec | integer | null | the first and last bar behind chgPct. null when there is no candle in the window. |
| rows[].createdSec | integer (unix s) | oldest indexed pool for this token — a FLOOR on age, and only within the indexed window |
| totals | object | {eth24, swaps24, pools, tokens} across the whole window, before the limit |
| windowHours / flowGrainSecs | integer | 24 / 300 — the windows the columns above were summed over |
| limit / venue / order | integer / string / string | what was actually applied, echoed. Render under these, not under what you asked for. |
| identity | "ok" | "unavailable" | "unavailable" ONLY when the identity read FAILED — rows then degrade to addresses. Never set when nothing was asked. |
status codes
| code | body | when |
|---|---|---|
| 200 | BoardPayload (above) | the read succeeded. index may still be null — that is the worker never having run here, not a failure. |
| 304 | (no body; ETag only) | If-None-Match matched the current measurement. This is a SUCCESSFUL poll: the server re-confirmed the frame. |
| 503 | {"error":"unavailable","reason":"the volume index could not be read"} | our read failed — database unreachable, cursor unreadable, an unparseable heartbeat (we then cannot date any figure), or a wei value that would not parse exactly. The whole frame fails as one rather than shipping a partial measurement. |
freshness & caching
- serverSec — the server clock when the body was built.
- measuredSec — when the ROWS were measured. The route memoises one aggregate per lens for a few seconds, so the true frame age is (serverSec - measuredSec) plus your own elapsed time. It is disclosed, not hidden.
- index.updatedSec / index.updatedAgoSec — when the chainwide indexer last advanced, and how long ago as of THIS response. Computed server-side.
- index.stale — the verdict, decided here; index.staleSec is the threshold behind it (600s in production). A client may only add its own elapsed time on top.
- index.startSec / index.startBlock — when the index opened. Everything is measured since then, so no figure here is an all-time figure.
- index: null is not an error — it means no cursor row exists, i.e. the worker has never run against this database. A deployment fact, served as 200.
- The ETag covers the MEASUREMENT only — rows, totals, identity, cursor facts and the shape/lens/limit that framed them. It deliberately excludes the request-time clock fields, because a validator that never matches is decoration.
- CAUTION, measured in production: the x-board-server-sec / x-board-measured-sec / x-board-updated-ago-sec / x-board-stale headers are present on the 200 but are NOT delivered on the 304 through this deployment's edge — only the ETag survives. Do not rely on them to re-age a frame over a 304; take a full 200 when you need the frame's age.
The route sets Cache-Control: no-store, must-revalidate — weak ETag over the measurement; If-None-Match ⇒ 304 with no body (verified in production). Delivered as sent: no-store survives the edge intact. no-store because every consumer draws a live/stale verdict from this body, and a shared cache would make the frame's age invisible. The database is protected by a per-instance snapshot memo (about 3s) instead, whose age is DISCLOSED as measuredSec rather than hidden.
example
curl -s 'https://www.fletch.bond/api/v1/chain/board?limit=3'
A real production response, captured 2026-08-06T16:25:40Z. Trimmed for this page: two rows elided at the `...` line, and the image URL truncated — so this block is not valid JSON as printed.
{
"serverSec": 1786033540,
"measuredSec": 1786033539,
"index": {
"startSec": 1785950941, "startBlock": 28665903, "safeBlock": 29488406,
"updatedSec": 1786033318, "updatedAgoSec": 221,
"stale": false, "staleSec": 600, "unattributedSwaps": 895849
},
"rows": [
{
"token": "0x1df1f09c4dd65c76746d53467361d536cdfdc719",
"tokenChecksum": "0x1dF1F09c4Dd65C76746d53467361D536cDfDC719",
"symbol": "FROGE", "symbolState": "read", "name": "FROGE",
"image": "https://indigo-nearby-elephant-244.mypinata.cloud/ipfs/bafkrei...",
"imageState": "image",
"decimals": 18, "totalSupply": "1000000000000000000000000000",
"hooked": false, "ethQuoted": true, "pools": 15,
"eth24": "6626354151128638040632", "eth1": "30234745808866256341",
"swaps24": 56067, "swaps1": 288,
"lastE36": "489292246661846327077954297342", "chgPct": 64.1631,
"poolId": "0xf5eecbfa6aaad42fc1db776221e88a7da12a5895d9404424f8e419eda9aba56d",
"bars": 173, "firstBarSec": 1785981600, "lastBarSec": 1786033200,
"createdSec": 1785981612
}
... 2 more rows
],
"totals": {
"eth24": "103048121422589693420973", "swaps24": 3065749,
"pools": 25268, "tokens": 24331
},
"windowHours": 24, "flowGrainSecs": 300,
"limit": 3, "venue": "all", "order": "eth24-desc", "identity": "ok"
}