{"openapi":"3.1.0","info":{"title":"fletch chain read API","version":"1.0.0","summary":"Measured, unauthenticated reads over the Robinhood Chain index behind fletch.bond.","description":"The public read API behind fletch — a trading terminal for Robinhood Chain (chainId 4663).\n\nEvery figure served here is a MEASUREMENT with a stated window, never a verdict. There is no safety score, no rating, no recommendation and no paid placement anywhere in this API, and no field that could become one.\n\n## the contract\n**a failure is a status, never an empty success** — A read that answered NOTHING returns 503. It never returns 200 with an empty list: `rows: []` under a 200 means the index was read and held nothing, which is a claim about the chain; a 503 is a fact about us and makes no claim at all. Branch on the STATUS.\n\n**…but a 200 can still carry a failed SUB-read, and it is flagged in-band** — The status covers the whole read. A subordinate read that failed while the measurement succeeded does NOT take the response down — decoration must never 503 evidence — so it is disclosed as a field instead, and a consumer that ignores these fields will render our outage as a fact about a token. The complete list: `identity:\"unavailable\"` (board, search, activity — names could not be read, rows degrade to addresses); `prices:\"unavailable\"` (search — lastE36 is null because OUR candle read failed, not because there is no candle); `exact:false` (pools — a numeric did not parse, treat every figure in the response as unreadable); `depth.reason:\"read-failed\"` (pools — that one pool's depth read failed); `coverage.indexedSwapsSince:null` (traders — the denominator read failed); `unvaluedReason` of `unavailable`/`pools-unread` and `ethBalanceWei:null` and `totals.unreadTokens > 0` (wallet — our read, not the wallet's holdings); `watch.rows[].eth1hWei:null` (activity — unreadable, where \"0\" would be a measurement); and a `notes[]` entry on candles when the identity index was down. `index:null` is a third thing again: a successful read of a database the worker has never run against.\n\n**do not branch on the 503 text** — Two failure bodies exist. Routes that author their own reason send {\"error\":\"unavailable\",\"reason\":\"<sentence>\"}; routes served through the shared responder send {\"error\":\"service unavailable\"} with the real reason kept in our server log, because these bodies are public and a raw driver error can carry a database host. Both are 503. The reason is for a human to read in your logs, never a value to switch on.\n\n**every payload carries its measured-at — with one named exception** — Each body states when it was measured (serverSec/measuredSec, readAt, snapshot.atSec or measuredAt) and, where an index stands behind it, when that index last advanced (index.updatedSec, updatedAgoSec) plus the threshold behind the stale verdict (staleSec). The stale verdict is decided server-side so a wrong client clock cannot vote; a client may only ADD its own elapsed time on top. THE EXCEPTION IS /quotes: it carries no body-level clock at all — only index.updatedAt, which dates the INDEX and not the response. Date a /quotes frame from the Age response header plus index.updatedAt, and do not assume it was built when it arrived.\n\n**unmeasurable is not zero — but read the flag, because the wire still says 0** — A figure we could not READ is null or is flagged unmeasured, never 0: a V4 pool's depth is `measured:false, reason:\"v4-singleton\"` because the singleton PoolManager makes per-pool reserves unreadable, and a wei value that did not parse exactly fails the response rather than becoming a 0. A figure that is structurally UNMEASURABLE is the case to watch: a token with no ETH-side pool carries `ethQuoted:false`, and its ETH volume crosses the wire as the string \"0\" beside that flag, because the aggregate genuinely sums to zero. So the flag is the measurement and the 0 is an artefact — read ethQuoted (board, search, pulse, pools) or the depth `reason` BEFORE you render any zero. /quotes is the trap: it carries no such flag, so resolve its zeroes elsewhere.\n\n**wei are decimal strings — and /candles is the one place they are not** — Every wei quantity crosses the wire as a decimal string, because numeric(78,0) does not fit an IEEE double and a whale total that lost its low-order digits is a wrong whale total. Parse with BigInt, never Number. Prices are exact integers too where they are served as such — /board and /search carry lastE36, wei of ETH per RAW token unit x 1e36, decimals-free on purpose. THE EXCEPTION IS /candles: its o/h/l/c are doubles converted at that boundary, and its `eth` is ETH (not wei) as a double floored to 6 decimals. Chart with them, never settle with them.\n\n**coverage is disclosed, and absence is not evidence** — Every list here is bounded, and the bound rides in the payload (limit, rowBound, totalPools, total, coverage.*, queried). A token missing from a response was either measured and absent, or never asked about — and those are different facts, which is why `queried` exists on the batch endpoints. Absence from our index is a fact about our coverage window, never proof about the chain.\n\n**identity is decoration** — symbol, name and image are strings the token deployer wrote. They are sanitised server-side, they rank nothing, and they are not evidence of anything. The address is the only unforgeable field on a row. `image` is null whenever we hold a URL we will not load (imageState:\"offsite\") — loading an arbitrary host would beacon a viewer's IP to whoever deployed the token.\n\n**no scores, no badges, no placement** — Nothing in this API ranks by anything a token deployer or a payer can influence. There is no safety score, no rating, no sponsored slot, and no field that could become one. Ordering is measured volume, measured recency, or a stated tier — and every response echoes the order it applied.\n\n## authentication\nThese endpoints are open, unauthenticated GETs. We issue no API keys, accept no tokens, and have no account tier — so there is nothing to sign up for and nothing that would raise a limit. The application stores nothing about a caller: no key, no account, no identifier, and no record of who asked what. What we cannot claim away is the hosting platform's own request log, which like any host's records the requested URL — so a query string is not a private channel, and an address you would not want in a server log should not be sent in one. Requests to /api/* are excluded from the site's jurisdiction middleware, so a data feed is never handed an HTML notice page.\n\n## fair use\n- No rate limit is implemented today. There is no limiter and no quota in front of the endpoints documented here — so treat the numbers below as courtesy, not as a ceiling you have been granted. We reserve the right to add one, and if we do it will return 429 with a Retry-After header rather than a degraded body.\n- ONE 429 exists already, and it is a capacity refusal rather than a quota: /api/v1/chain/holders/{address} (not documented here) runs a 120,000-block Transfer-log walk measured at 233 upstream JSON-RPC calls when it runs undisturbed and 288–321 when it does not — the count rises with every log range the node makes us bisect, so read it as a floor. Only ONE of those may run at a time on one instance, and a request that finds the slot busy gets 429 + Retry-After with a body naming `walk-capacity` — never a degraded or zeroed measurement. It is a fact about our queue at that instant, not about the token, and it clears in seconds. The cap is one rather than a friendlier number because we measured what two concurrent walks buy: 1,148 calls in one session and 1,185 in another, and in both, zero measurements.\n- Poll no faster than the data moves. The chainwide indexer writes about every 5 minutes; the board and activity feeds are polled by our own client at 12s and 15s, the chart at 15s, and the stats frame at 60s. Polling the board every second buys you the same frame twelve times.\n- Use the conditional GET on /board and /candles. Both carry a weak ETag over the measurement; echo it as If-None-Match and an unchanged frame answers 304 with no body. A 304 is a poll that SUCCEEDED — the server re-confirmed the frame is current.\n- Batch instead of fanning out. /quotes takes up to 100 addresses in one call and /activity?watch= takes up to 60; one batched request is cheaper for both of us than 100 single-token calls.\n- Cache what is already cacheable. /stats, /pulse, /quotes, /pools, /traders and /wallet ship shared-cache directives; honour the Age header rather than re-requesting inside the window you were given. Measured in production: the edge CONSUMES those directives, so what reaches you is a bare `Cache-Control: public` plus an `Age` — the s-maxage and stale-while-revalidate figures disclosed per endpoint are what the route sets, and Age is how you date the frame you got.\n- Identify yourself in User-Agent if you are running something sustained, with a way to reach you. We have no way to contact an anonymous heavy caller before we have to make a decision about them.\n- Attribute the measurement, and carry its disclosures. If you re-publish a figure from here, carry its window and its caveats with it — a floor called a total, or a capped list called a ranking, becomes our credibility problem the moment it has our name on it.\n\n## not offered\n- No CORS headers are sent. Verified in production: responses carry no Access-Control-Allow-Origin, so a browser will block a cross-origin fetch from your page. Call these from your server, or proxy them through your own origin.\n- No webhooks, no websockets, no server-sent events. Every endpoint is a poll.\n- No writes and no trading. This is a read API over an index; it holds no keys, signs nothing and cannot move funds. Execution happens in the app, from the user's own wallet.\n- No cost basis, and therefore no PnL — but read the reason, because half of it IS measured. The attribution table stores, per (token, transaction signer), the ETH that went in across witnessed buys and the ETH that came out across witnessed sells, and /traders serves exactly that as boughtEthWei/soldEthWei/netEthWei. What it does NOT store is token amounts per trade, and without those there is no average cost, no FIFO and no per-unit entry price to report — so /wallet returns basis.known:false rather than a guess. Adding a held bag's mark to a witnessed cash flow is not a fix for that: the index window is hours old, most bags were acquired outside it, and the sum prices the unseen entry at zero. We refuse to publish that number and so should you.\n- No price on /quotes. That endpoint measures volume only; a plausible-looking derivation (ETH per swap, say) rendered where a price belongs would be a number nothing traded at. Prices come from /candles and /board, per pool.\n- No 1-minute candles. 5 minutes is the finest grain this index stores; every coarser bar is rolled up from it server-side and says so in aggregatedFromSecs.\n- No promise that this list is every route under /api. It is every route we DOCUMENT and hold ourselves to. Anything else you find by reading our HTML is an internal surface with no stability contract, no versioning and no notice before it changes shape or disappears — treat an undocumented endpoint as a private one you happened to see.\n\n`info.x-verified-at` (2026-08-06T17:36:00Z) is when the ten DATA endpoints below were last hit against production and compared field-by-field with the real response. It dates the DOCUMENT, not the data. The eleventh path — this document — is generated from the same catalog it describes, so its example is marked `x-fletch-example-kind: \"sketch\"` rather than claimed as a capture.\n\nDocs for humans: https://www.fletch.bond/chain/api — rendered from the same catalog as this document.","x-verified-at":"2026-08-06T17:36:00Z"},"servers":[{"url":"https://www.fletch.bond","description":"production (the canonical origin — the apex redirects here)"}],"paths":{"/api/v1/chain/board":{"get":{"operationId":"board","summary":"the board frame","description":"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.\n\n### bounds — what this response is NOT\n- At most 250 rows in any response, whatever ?limit= asks for. The response echoes the limit actually applied.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n\n### how to date this payload\n- serverSec — the server clock when the body was built.\n- 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.\n- index.updatedSec / index.updatedAgoSec — when the chainwide indexer last advanced, and how long ago as of THIS response. Computed server-side.\n- 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.\n- index.startSec / index.startBlock — when the index opened. Everything is measured since then, so no figure here is an all-time figure.\n- 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.\n- 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.\n- 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.\n\n### caching\nThe route sets `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.","parameters":[{"name":"limit","in":"query","required":false,"description":"how many rows to return, from the top of the ranking Absent: 100. Not usable: clamped to 250; a non-positive or unparseable value falls back to 100. Verified: ?limit=9999 returns 250 rows and echoes limit:250..","schema":{"type":"integer"},"example":"50"},{"name":"venue","in":"query","required":false,"description":"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. Absent: \"all\". Not usable: degrades to \"all\" and echoes venue:\"all\" — a typo yields a wider measurement, correctly labelled, rather than an error. Verified with ?venue=v9..","schema":{"type":"string"},"example":"v4"},{"name":"shape","in":"query","required":false,"description":"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. Absent: the long shape (no v field). Not usable: 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..","schema":{"type":"string"},"example":"2"}],"responses":{"200":{"description":"the read succeeded. index may still be null — that is the worker never having run here, not a failure.\n\n```json\nBoardPayload (above)\n```","content":{"application/json":{"example":"{\n  \"serverSec\": 1786033540,\n  \"measuredSec\": 1786033539,\n  \"index\": {\n    \"startSec\": 1785950941, \"startBlock\": 28665903, \"safeBlock\": 29488406,\n    \"updatedSec\": 1786033318, \"updatedAgoSec\": 221,\n    \"stale\": false, \"staleSec\": 600, \"unattributedSwaps\": 895849\n  },\n  \"rows\": [\n    {\n      \"token\": \"0x1df1f09c4dd65c76746d53467361d536cdfdc719\",\n      \"tokenChecksum\": \"0x1dF1F09c4Dd65C76746d53467361D536cDfDC719\",\n      \"symbol\": \"FROGE\", \"symbolState\": \"read\", \"name\": \"FROGE\",\n      \"image\": \"https://indigo-nearby-elephant-244.mypinata.cloud/ipfs/bafkrei...\",\n      \"imageState\": \"image\",\n      \"decimals\": 18, \"totalSupply\": \"1000000000000000000000000000\",\n      \"hooked\": false, \"ethQuoted\": true, \"pools\": 15,\n      \"eth24\": \"6626354151128638040632\", \"eth1\": \"30234745808866256341\",\n      \"swaps24\": 56067, \"swaps1\": 288,\n      \"lastE36\": \"489292246661846327077954297342\", \"chgPct\": 64.1631,\n      \"poolId\": \"0xf5eecbfa6aaad42fc1db776221e88a7da12a5895d9404424f8e419eda9aba56d\",\n      \"bars\": 173, \"firstBarSec\": 1785981600, \"lastBarSec\": 1786033200,\n      \"createdSec\": 1785981612\n    }\n    ... 2 more rows\n  ],\n  \"totals\": {\n    \"eth24\": \"103048121422589693420973\", \"swaps24\": 3065749,\n    \"pools\": 25268, \"tokens\": 24331\n  },\n  \"windowHours\": 24, \"flowGrainSecs\": 300,\n  \"limit\": 3, \"venue\": \"all\", \"order\": \"eth24-desc\", \"identity\": \"ok\"\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:25:40Z","x-fletch-example-trimmed":"two rows elided at the `...` line, and the image URL truncated — so this block is not valid JSON as printed."}}},"304":{"description":"If-None-Match matched the current measurement. This is a SUCCESSFUL poll: the server re-confirmed the frame.\n\n```json\n(no body; ETag only)\n```"},"503":{"description":"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.\n\n```json\n{\"error\":\"unavailable\",\"reason\":\"the volume index could not be read\"}\n```"}},"x-fletch-bounds":["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."],"x-fletch-freshness":["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."],"x-fletch-cache":{"routeHeader":"no-store, must-revalidate","delivered":"no-store, must-revalidate","deliveredNote":"Delivered as sent: no-store survives the edge intact.","validator":"weak ETag over the measurement; If-None-Match ⇒ 304 with no body (verified in production)"},"x-fletch-fields":[{"path":"serverSec","type":"integer (unix s)","what":"server clock when this body was built"},{"path":"measuredSec","type":"integer (unix s)","what":"when the rows were measured — may lag serverSec by the memo window"},{"path":"index","type":"object | null","what":"cursor facts; null = the worker has never run against this database"},{"path":"rows[].token","type":"string (0x, lowercase)","what":"the row key and the only unforgeable field on the row"},{"path":"rows[].tokenChecksum","type":"string (EIP-55)","what":"checksummed form, so links do not eat a redirect"},{"path":"rows[].symbol","type":"string | null","what":"deployer-written decoration, sanitised. null = no usable ticker (never \"\", never the \"·\" marker)"},{"path":"rows[].symbolState","type":"\"read\" | \"unreadable\" | \"unread\"","what":"WHY symbol is null: it answered / symbol() refused (about the token) / we have not asked yet (about our index)"},{"path":"rows[].name","type":"string | null","what":"the deployer's name(), sanitised and capped at 40 characters — decoration, like symbol. null = no usable name."},{"path":"rows[].image","type":"string | null","what":"a URL renderable under our CSP, or null"},{"path":"rows[].imageState","type":"\"image\" | \"none\" | \"offsite\" | \"unread\"","what":"\"offsite\" = we hold a URL and will not load it. Only \"none\" says anything about the token."},{"path":"rows[].decimals","type":"integer | null","what":"null = decimals() never answered usably. NEVER assume 18 from this."},{"path":"rows[].totalSupply","type":"string (raw units) | null","what":"exact supply in raw units; null = unread. Market cap = lastE36 x totalSupply (both raw, so decimals cancel); a null on either side means uncomputable."},{"path":"rows[].hooked","type":"boolean","what":"at least one of this token's pools runs a V4 hook"},{"path":"rows[].ethQuoted","type":"boolean","what":"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."},{"path":"rows[].pools","type":"integer","what":"pools with at least one indexed swap in the window"},{"path":"rows[].eth24 / eth1","type":"string (decimal wei)","what":"the 24h window / a rolling 60min at the candle grain. Parse with BigInt. A \"0\" is only a volume measurement when ethQuoted is true."},{"path":"rows[].swaps24 / swaps1","type":"integer","what":"indexed swap counts over the same two windows"},{"path":"rows[].lastE36","type":"string | null","what":"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."},{"path":"rows[].chgPct","type":"number | null","what":"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."},{"path":"rows[].poolId","type":"string | null","what":"WHICH pool the price, change and bars came from"},{"path":"rows[].bars","type":"integer (never null)","what":"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."},{"path":"rows[].firstBarSec / lastBarSec","type":"integer | null","what":"the first and last bar behind chgPct. null when there is no candle in the window."},{"path":"rows[].createdSec","type":"integer (unix s)","what":"oldest indexed pool for this token — a FLOOR on age, and only within the indexed window"},{"path":"totals","type":"object","what":"{eth24, swaps24, pools, tokens} across the whole window, before the limit"},{"path":"windowHours / flowGrainSecs","type":"integer","what":"24 / 300 — the windows the columns above were summed over"},{"path":"limit / venue / order","type":"integer / string / string","what":"what was actually applied, echoed. Render under these, not under what you asked for."},{"path":"identity","type":"\"ok\" | \"unavailable\"","what":"\"unavailable\" ONLY when the identity read FAILED — rows then degrade to addresses. Never set when nothing was asked."}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/board?limit=3'"}},"/api/v1/chain/search":{"get":{"operationId":"search","summary":"token search","description":"Find a token in the index by address, ticker or name. Ranking is a measurement, never a judgement: three tiers — the query IS the address (0), the query equals a ticker case-insensitively (1), the query prefixes or appears inside a ticker or name (2) — and INSIDE each tier the order is measured 24h ETH volume, descending. Never alphabetical, and never any signal a deployer or a payer can influence. Tickers are not unique on this chain, which is exactly why volume ranks within a tier.\n\n### bounds — what this response is NOT\n- At most 20 hits, whatever ?limit= asks for. `total` is the match count BEFORE the cap — the honest denominator for \"showing 10 of 37\".\n- SUBSTRING matching only engages at a query length of 2 or more. A single character matches PREFIXES only, so a 1-char query will not find mid-string matches — that is a deliberate cost bound, not a gap in the index.\n- Absence from the hits is a fact about OUR INDEX, not about the chain. A token the walker has not passed yet is \"not indexed\", not \"not found\", and must be worded that way.\n- An address query takes the exact-address path only, and resolves through chain_pools as well as chain_tokens — so a token whose identity row was never written is still findable by the one string about it that is certainly true.\n- hits[].pools counts every indexed pool for the token, not just ones that traded. hits[].eth24 is over the 24h window.\n\n### how to date this payload\n- serverSec — the server clock when the body was built. There is no separate measured-at: this query is not memoised, so the two are the same instant.\n- windowHours (24) — the window eth24/swaps24 were summed over, so the ranking basis is stated rather than implied.\n\n### caching\nThe route sets `no-store, must-revalidate`. Delivered as sent: no-store survives the edge intact. no-store: a type-ahead served from a shared cache is stale keystrokes.","parameters":[{"name":"q","in":"query","required":true,"description":"the query: a 0x address, a ticker, or a fragment of a ticker or name Absent: an empty query returns 200 with hits:[] and total:0 — no database round trip, and identity/prices both \"ok\" because nothing was asked and nothing failed. Not usable: trimmed, then capped at 80 CODE POINTS (not UTF-16 units, so an emoji is never sliced in half). LIKE wildcards in the query are escaped, so a query of \"100%\" matches the literal string..","schema":{"type":"string"},"example":"froge"},{"name":"limit","in":"query","required":false,"description":"rows per response Absent: 10. Not usable: clamped to 20; non-positive or unparseable falls back to 10. Verified: ?limit=999 echoes limit:20..","schema":{"type":"integer"},"example":"20"}],"responses":{"200":{"description":"the search ran. hits:[] with a non-empty query means the index holds no match — a claim about our coverage, not about the chain.\n\n```json\nSearchPayload (above)\n```\n\nOR — ?q= was empty or absent. Served without a database round trip, and nothing is claimed to have failed.\n\n```json\n{\"query\":\"\",\"hits\":[],\"total\":0,...,\"identity\":\"ok\",\"prices\":\"ok\"}\n```","content":{"application/json":{"example":"{\n  \"query\": \"frog\",\n  \"hits\": [\n    {\n      \"token\": \"0x0d773d917df773dfbe509dafd8ec1715eeeec3fb\",\n      \"tokenChecksum\": \"0x0D773d917Df773dFBe509dAfd8EC1715EEeec3FB\",\n      \"symbol\": \"FROG\", \"symbolState\": \"read\", \"name\": \"The life of a frog\",\n      \"image\": null, \"imageState\": \"none\", \"decimals\": 18,\n      \"lastE36\": \"52432197657775410572920692202\",\n      \"eth24\": \"416491804724256071256\", \"swaps24\": 3131,\n      \"ethQuoted\": true, \"pools\": 2,\n      \"venues\": { \"v2\": 0, \"v3\": 0, \"v4\": 2 },\n      \"match\": \"ticker\"\n    }\n    ... 4 more hits\n  ],\n  \"total\": 1253, \"limit\": 5, \"windowHours\": 24, \"serverSec\": 1786033657,\n  \"identity\": \"ok\", \"prices\": \"ok\"\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:27:37Z","x-fletch-example-trimmed":"four hits elided at the `...` line, so this block is not valid JSON as printed. Note total:1253 against limit:5 — \"frog\" appears in over a thousand indexed tickers and names on this chain, which is exactly why `total` exists."}}},"503":{"description":"our read failed. Never served as an empty hit list.\n\n```json\n{\"error\":\"unavailable\",\"reason\":\"the token index could not be searched\"}\n```"}},"x-fletch-bounds":["At most 20 hits, whatever ?limit= asks for. `total` is the match count BEFORE the cap — the honest denominator for \"showing 10 of 37\".","SUBSTRING matching only engages at a query length of 2 or more. A single character matches PREFIXES only, so a 1-char query will not find mid-string matches — that is a deliberate cost bound, not a gap in the index.","Absence from the hits is a fact about OUR INDEX, not about the chain. A token the walker has not passed yet is \"not indexed\", not \"not found\", and must be worded that way.","An address query takes the exact-address path only, and resolves through chain_pools as well as chain_tokens — so a token whose identity row was never written is still findable by the one string about it that is certainly true.","hits[].pools counts every indexed pool for the token, not just ones that traded. hits[].eth24 is over the 24h window."],"x-fletch-freshness":["serverSec — the server clock when the body was built. There is no separate measured-at: this query is not memoised, so the two are the same instant.","windowHours (24) — the window eth24/swaps24 were summed over, so the ranking basis is stated rather than implied."],"x-fletch-cache":{"routeHeader":"no-store, must-revalidate","delivered":"no-store, must-revalidate","deliveredNote":"Delivered as sent: no-store survives the edge intact.","validator":null},"x-fletch-fields":[{"path":"query","type":"string","what":"the query as the route actually evaluated it (trimmed, capped) — echoed so a late response for an old query is detectable"},{"path":"hits[].token / tokenChecksum","type":"string","what":"lowercase and EIP-55 forms of the address — the identity"},{"path":"hits[].symbol / symbolState / name / image / imageState / decimals","type":"various","what":"the same identity ladder and the same three/four-state disclosures as the board"},{"path":"hits[].lastE36","type":"string | null","what":"last 5-minute close of the token's busiest ETH-quoted pool in the window, as the stored exact integer. null = no candle in the window (see `prices` for the other reason it can be null)."},{"path":"hits[].eth24 / swaps24","type":"string (wei) / integer","what":"the ranking basis, over windowHours"},{"path":"hits[].ethQuoted","type":"boolean","what":"false = no pool has an ETH side, so eth24 is UNMEASURABLE — and it still arrives as the string \"0\", because with no ETH leg the sum really is zero. Verified live against a token with one non-ETH V4 pool. Read this flag before rendering eth24."},{"path":"hits[].pools / venues","type":"integer / {v2,v3,v4}","what":"every indexed pool for this token, and where they are — \"where can I trade this\""},{"path":"hits[].match","type":"\"address\" | \"ticker\" | \"text\"","what":"which tier matched. Ranking is tier, then volume."},{"path":"total","type":"integer","what":"matches in the index BEFORE the row cap"},{"path":"limit / windowHours / serverSec","type":"integer","what":"what was applied, and the window and clock behind it"},{"path":"identity","type":"\"ok\" | \"unavailable\"","what":"\"unavailable\" ONLY when the identity read failed; hits then degrade to addresses"},{"path":"prices","type":"\"ok\" | \"unavailable\"","what":"separate from identity ON PURPOSE: without it, lastE36:null would mean both \"no candle in the window\" (a measurement) and \"we could not read the candle table\" (our outage) at once"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/search?q=frog&limit=5'"}},"/api/v1/chain/stats":{"get":{"operationId":"stats","summary":"the machine, counted","description":"The whole index in one cheap aggregate: pools by venue and ETH-side split, tokens seen and identified, ETH-leg volume and swaps over 24h AND since the index opened (both labelled), the cursor facts, and the trader-attribution row counts. This is the frame /chain/stats renders — the \"why trust this terminal\" measurement, with its own exclusions attached.\n\n### bounds — what this response is NOT\n- pools.* count EXISTENCE, not activity: every pool the walker has ever decoded, including ones that have never traded. The board's totals.pools counts only pools with indexed volume in its 24h window, which is why the two differ by thousands.\n- flow reports TWO windows and labels both. ethAllWei/swapsAll are since the index OPENED (index.startSec), never all-time — pools and swaps that predate the window are invisible to us.\n- tokens.symbolRead counts real symbols only: the worker's \"·\" marker (asked, and the contract refused) is counted separately as symbolRefused rather than dressed as a name.\n- traders.rows is a count of (token, signer) attribution ROWS, not distinct traders — a whale trading 80 tokens is 80 rows.\n- Every figure is a floor over what the indexer could walk in its per-tick budget.\n\n### how to date this payload\n- measuredSec — when the counters were actually read (there is a short per-instance memo, about 10s, and this is what discloses it).\n- cacheSecs (60) + swrSecs (240) — how long a shared cache may re-serve this exact body. The true ceiling is the SUM; the Age response header tells you where in that window you landed.\n- index.updatedSec / index.updatedAgoSec — when the chainwide indexer last advanced, and how long ago as of THIS response. Computed server-side.\n- 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.\n- index.startSec / index.startBlock — when the index opened. Everything is measured since then, so no figure here is an all-time figure.\n- 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.\n- A cached frame can hold the index.stale verdict back by at most that ceiling — which is why the verdict's INPUTS (updatedAgoSec, staleSec) ride in the payload for you to re-derive on top of by only ADDING elapsed time.\n\n### caching\nThe route sets `public, s-maxage=60, stale-while-revalidate=240` — a shared cache may re-serve this body for up to 300s (s-maxage + stale-while-revalidate). What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for. Cacheable precisely because nothing here is a liveness claim: every figure is machine-scale and moves slowly. The body discloses its own ceiling as cacheSecs + swrSecs (300s total) so a reader can see it rather than trust it. Failures are always 503 + no-store: an outage must never be cached into a minute of pretending. Note the edge consumes s-maxage/stale-while-revalidate itself — a browser sees `Cache-Control: public` plus an `Age` header, and Age is how you date a cached frame.","parameters":[],"responses":{"200":{"description":"the counters were read. index may be null — the worker has never run here.\n\n```json\nStatsPayload (above)\n```","content":{"application/json":{"example":"{\n  \"serverSec\": 1786033482, \"measuredSec\": 1786033482,\n  \"cacheSecs\": 60, \"swrSecs\": 240,\n  \"index\": {\n    \"startSec\": 1785950941, \"startBlock\": 28665903, \"safeBlock\": 29488406,\n    \"updatedSec\": 1786033318, \"updatedAgoSec\": 164,\n    \"stale\": false, \"staleSec\": 600, \"unattributedSwaps\": 895849\n  },\n  \"pools\": {\n    \"total\": 39324, \"v2\": 10024, \"v3\": 2463, \"v4\": 26837,\n    \"ethQuoted\": 36598, \"nonEth\": 2726\n  },\n  \"tokens\": {\n    \"seen\": 36180, \"symbolRead\": 36171, \"symbolRefused\": 9, \"decimalsRead\": 36179\n  },\n  \"flow\": {\n    \"windowSecs\": 86400,\n    \"eth24Wei\": \"103048121422589693420973\", \"swaps24\": 3065749,\n    \"ethAllWei\": \"103048121422589693420973\", \"swapsAll\": 3065749\n  },\n  \"traders\": { \"rows\": 196505, \"routerRows\": 0 }\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:24:42Z","x-fletch-example-trimmed":null}}},"503":{"description":"our read failed, or a count/wei value would not parse exactly. Never served as a frame of zeros: \"0 pools indexed\" over a database we could not read is the forbidden error class wearing a number.\n\n```json\n{\"error\":\"unavailable\",\"reason\":\"the machine counters could not be read\"}\n```"}},"x-fletch-bounds":["pools.* count EXISTENCE, not activity: every pool the walker has ever decoded, including ones that have never traded. The board's totals.pools counts only pools with indexed volume in its 24h window, which is why the two differ by thousands.","flow reports TWO windows and labels both. ethAllWei/swapsAll are since the index OPENED (index.startSec), never all-time — pools and swaps that predate the window are invisible to us.","tokens.symbolRead counts real symbols only: the worker's \"·\" marker (asked, and the contract refused) is counted separately as symbolRefused rather than dressed as a name.","traders.rows is a count of (token, signer) attribution ROWS, not distinct traders — a whale trading 80 tokens is 80 rows.","Every figure is a floor over what the indexer could walk in its per-tick budget."],"x-fletch-freshness":["measuredSec — when the counters were actually read (there is a short per-instance memo, about 10s, and this is what discloses it).","cacheSecs (60) + swrSecs (240) — how long a shared cache may re-serve this exact body. The true ceiling is the SUM; the Age response header tells you where in that window you landed.","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.","A cached frame can hold the index.stale verdict back by at most that ceiling — which is why the verdict's INPUTS (updatedAgoSec, staleSec) ride in the payload for you to re-derive on top of by only ADDING elapsed time."],"x-fletch-cache":{"routeHeader":"public, s-maxage=60, stale-while-revalidate=240","delivered":"public (+ Age)","deliveredNote":"What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for.","sharedMaxAgeSecs":60,"staleWhileRevalidateSecs":240},"x-fletch-fields":[{"path":"serverSec / measuredSec","type":"integer (unix s)","what":"body built / counters read"},{"path":"cacheSecs / swrSecs","type":"integer","what":"the shared-cache window and its stale-while-revalidate tail — disclosed, not implied"},{"path":"index","type":"object | null","what":"cursor facts; null = the worker has never run here"},{"path":"pools","type":"{total,v2,v3,v4,ethQuoted,nonEth}","what":"every pool decoded, split by venue and by whether it has an ETH side"},{"path":"tokens","type":"{seen,symbolRead,symbolRefused,decimalsRead}","what":"distinct ERC-20s in at least one indexed pool, and how far identity got on them"},{"path":"flow","type":"{windowSecs,eth24Wei,swaps24,ethAllWei,swapsAll}","what":"ETH-leg volume and swap counts over the 24h window and since the index opened"},{"path":"traders","type":"{rows,routerRows}","what":"per-(token, transaction-signer) attribution rows, and how many are marked infrastructure"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/stats'"}},"/api/v1/chain/activity":{"get":{"operationId":"activity","summary":"notable events","description":"Events across the whole chain, small enough to poll. This chain does millions of swaps a day, so a tape of every one would be noise wearing a feed's clothes: everything here is THRESHOLDED and CAPPED, and every threshold rides in the payload rather than being buried in a constant. Four kinds — new_pool (initialized inside the window and already carrying real volume), volume (a token whose rolling-hour ETH crossed a ladder line the prior hour was below, with BOTH measurements served so the claim is checkable), burst (one 5-minute bucket carrying a large ETH leg), move (first-open to last-close on ONE pool, over a dust floor).\n\n### bounds — what this response is NOT\n- At most 20 events PER KIND from the database, merged and capped at 60 in the response. The merge keeps the LARGEST of each kind, then serves newest first — so this is a tape, not a leaderboard, and it is not every event that crossed a threshold.\n- Every threshold is disclosed in `thresholds`, and events below them are not in the feed at all: a new pool needs 0.01 ETH (or 10 swaps when it has no ETH side to measure), a burst needs 0.5 ETH in one 5-minute bucket, a move needs 20% AND 0.1 ETH of measured flow behind it, and the volume ladder is fixed at 1/5/10/25/50/100 ETH per rolling hour.\n- Candle-grain sums are a FLOOR: a swap whose price could not be derived has no candle and is not in them. A floor crossing a ladder line is still a crossing, but it is a floor.\n- The `volume` kind fires on a CROSSING, not on a level: a token sitting above 100 ETH/h with no line crossed produces no event. prevWei is served so the crossing is checkable from the row itself.\n- ?watch= reads at most 60 addresses. `queried` is the list this response actually speaks about — evaluate nothing for an address outside it.\n- order is always \"at-desc\". There is no paging and no cursor: this endpoint only ever serves the current window.\n\n### how to date this payload\n- serverSec / measuredSec — body built / events snapshot measured. Add the difference to the frame's age.\n- index.updatedSec / index.updatedAgoSec — when the chainwide indexer last advanced, and how long ago as of THIS response. Computed server-side.\n- 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.\n- index.startSec / index.startBlock — when the index opened. Everything is measured since then, so no figure here is an all-time figure.\n- 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.\n- window.eventSecs (3600) and window.grainSecs (300) — the event window and the candle grain behind every candle-derived figure. A burst's atSec is the BUCKET END: the swaps landed somewhere inside it, and the end is the only instant we can state without inventing a finer resolution.\n\n### caching\nThe route sets `no-store, must-revalidate`. Delivered as sent: no-store survives the edge intact. no-store: every consumer draws a live/stale verdict from this body. The database is protected by a per-instance events memo (about 3s) whose age is disclosed as measuredSec. Watch readings are NEVER memoised — they feed edge detection, and serving one caller's frame to another's rules would compare readings across two different question sets.","parameters":[{"name":"watch","in":"query","required":false,"description":"comma-separated 0x addresses to get a per-token reading for (price, rolling-hour ETH, pool count) — most of which will never clear an event threshold. Intended for driving client-side alert rules. Absent: watch is null in the response — no readings were asked for and none are claimed. Not usable: malformed entries are DROPPED, and so is everything past 60 addresses — dropped from `queried` as well, so a truncated request reads as \"not asked\" and never as \"no pool for this token\".","schema":{"type":"string"},"example":"0x1df1f09c4dd65c76746d53467361d536cdfdc719,0x020bfc650a365f8bb26819deaabf3e21291018b4"}],"responses":{"200":{"description":"the read succeeded. events:[] means nothing crossed a threshold in the window — the thresholds are in the same body so that is checkable.\n\n```json\nActivityPayload (above)\n```","content":{"application/json":{"example":"{\n  \"serverSec\": 1786033679, \"measuredSec\": 1786033679,\n  \"index\": { \"startSec\": 1785950941, \"updatedAgoSec\": 361, \"stale\": false, \"staleSec\": 600, ... },\n  \"events\": [\n    {\n      \"id\": \"vol:0x1daa0137477277d5ddecbb2b7d2d8407173a8fb2:100:496120\",\n      \"kind\": \"volume\", \"atSec\": 1786033500,\n      \"token\": \"0x1daa0137477277d5ddecbb2b7d2d8407173a8fb2\",\n      \"tokenChecksum\": \"0x1daa0137477277d5ddeCBB2b7d2d8407173a8FB2\",\n      \"symbol\": \"TINYTANK\",\n      \"venue\": null, \"poolId\": null, \"ethQuoted\": true,\n      \"ethWei\": \"113385831451629843283\", \"swaps\": 1471, \"windowSecs\": 3600,\n      \"crossedEth\": 100, \"prevWei\": \"5598510424211546923\",\n      \"chgPct\": null, \"bars\": null, \"firstBarSec\": null\n    }\n    ... 59 more events (the cap)\n  ],\n  \"watch\": {\n    \"queried\": [\"0x1df1f09c4dd65c76746d53467361d536cdfdc719\"],\n    \"rows\": [\n      {\n        \"token\": \"0x1df1f09c4dd65c76746d53467361d536cdfdc719\", \"symbol\": \"FROGE\",\n        \"pools\": 20, \"latestPoolSec\": 1786030155,\n        \"latestPoolId\": \"0xe6b06bb4...816d53\", \"latestVenue\": \"v4\",\n        \"ethQuoted\": true, \"eth1hWei\": \"30234745808866256341\",\n        \"lastE36\": \"489292246661846327077954297342\",\n        \"pricePoolId\": \"0xf5eecbfa...aba56d\", \"lastE36Sec\": 1786033200,\n        \"decimals\": 18\n      }\n    ]\n  },\n  \"window\": { \"eventSecs\": 3600, \"grainSecs\": 300 },\n  \"thresholds\": {\n    \"newPoolMinWei\": \"10000000000000000\", \"newPoolMinSwaps\": 10,\n    \"burstMinWei\": \"500000000000000000\",\n    \"volumeLadderEth\": [1, 5, 10, 25, 50, 100],\n    \"moveMinPct\": 20, \"moveMinWei\": \"100000000000000000\"\n  },\n  \"identity\": \"ok\", \"order\": \"at-desc\"\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:27:59Z","x-fletch-example-trimmed":"59 events elided, the index object and two pool ids abbreviated — so this block is not valid JSON as printed."}}},"503":{"description":"any read failed, including a ?watch= read: a 200 with watch:null would render our outage as \"your rules have nothing to measure\". An empty feed would read as \"the chain is quiet\", which is a claim about the chain.\n\n```json\n{\"error\":\"unavailable\",\"reason\":\"the burst window could not be read\"}\n```"}},"x-fletch-bounds":["At most 20 events PER KIND from the database, merged and capped at 60 in the response. The merge keeps the LARGEST of each kind, then serves newest first — so this is a tape, not a leaderboard, and it is not every event that crossed a threshold.","Every threshold is disclosed in `thresholds`, and events below them are not in the feed at all: a new pool needs 0.01 ETH (or 10 swaps when it has no ETH side to measure), a burst needs 0.5 ETH in one 5-minute bucket, a move needs 20% AND 0.1 ETH of measured flow behind it, and the volume ladder is fixed at 1/5/10/25/50/100 ETH per rolling hour.","Candle-grain sums are a FLOOR: a swap whose price could not be derived has no candle and is not in them. A floor crossing a ladder line is still a crossing, but it is a floor.","The `volume` kind fires on a CROSSING, not on a level: a token sitting above 100 ETH/h with no line crossed produces no event. prevWei is served so the crossing is checkable from the row itself.","?watch= reads at most 60 addresses. `queried` is the list this response actually speaks about — evaluate nothing for an address outside it.","order is always \"at-desc\". There is no paging and no cursor: this endpoint only ever serves the current window."],"x-fletch-freshness":["serverSec / measuredSec — body built / events snapshot measured. Add the difference to the frame's age.","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.","window.eventSecs (3600) and window.grainSecs (300) — the event window and the candle grain behind every candle-derived figure. A burst's atSec is the BUCKET END: the swaps landed somewhere inside it, and the end is the only instant we can state without inventing a finer resolution."],"x-fletch-cache":{"routeHeader":"no-store, must-revalidate","delivered":"no-store, must-revalidate","deliveredNote":"Delivered as sent: no-store survives the edge intact.","validator":null},"x-fletch-fields":[{"path":"serverSec / measuredSec","type":"integer (unix s)","what":"body built / events snapshot measured. The difference is the memo window and it is disclosed, not hidden."},{"path":"index","type":"object | null","what":"cursor facts, same shape as the board's; null = the worker has never run against this database"},{"path":"window","type":"{eventSecs, grainSecs}","what":"3600 / 300 — the event window and the candle grain behind every candle-derived figure"},{"path":"order","type":"\"at-desc\"","what":"always newest-first. There is no paging and no cursor."},{"path":"events[].id","type":"string","what":"a dedupe/render key, stable across polls for the same underlying occurrence. Never parse it for meaning."},{"path":"events[].kind","type":"\"new_pool\" | \"volume\" | \"burst\" | \"move\"","what":"which measurement produced the row"},{"path":"events[].atSec","type":"integer (unix s)","what":"when it was measured — bucket END for candle-derived events, pool creation for new_pool"},{"path":"events[].token / tokenChecksum","type":"string","what":"lowercase and EIP-55 forms of the address — the identity, and the only unforgeable field on the row"},{"path":"events[].symbol","type":"string | null","what":"the deployer's decoration beside the address, sanitised. null = no usable ticker — render the address, never a placeholder."},{"path":"events[].venue","type":"\"v2\" | \"v3\" | \"v4\" | null","what":"null on TOKEN-level events (volume, move), whose figures may span venues"},{"path":"events[].poolId","type":"string | null","what":"null ONLY on the volume kind. move carries the pool id it measured (one pool — the token's most-swapped in the window) even though its venue is null; new_pool and burst carry both."},{"path":"events[].ethQuoted / ethWei","type":"boolean / string | null","what":"the headline measurement in decimal wei. null = unmeasurable (no ETH side) — render n/a, never 0."},{"path":"events[].swaps / windowSecs","type":"integer","what":"the swap count and the window the figures were measured over"},{"path":"events[].crossedEth / prevWei","type":"number | null / string | null","what":"volume kind only: the ladder line crossed, and the prior window's measured wei — both sides of the claim"},{"path":"events[].chgPct / bars / firstBarSec","type":"number | null / integer | null","what":"move kind only: the percent change and the candles behind it. 1 bar is an intra-bucket move and says so."},{"path":"watch","type":"{queried,rows} | null","what":"null when no ?watch= was sent. `queried` is what this response speaks about."},{"path":"watch.rows[].token / symbol / decimals","type":"string / string | null / integer | null","what":"the identity, the sanitised decoration, and decimals through the same guard as everywhere else — null, never a guessed 18"},{"path":"watch.rows[].pools / latestPoolSec / latestPoolId / latestVenue","type":"integer / integer | null / string | null / string | null","what":"every indexed pool for the token, and the newest one — what a new-pool rule names. All null when we hold no pool row for it."},{"path":"watch.rows[].ethQuoted","type":"boolean","what":"false ⇒ eth1hWei is null because there is nothing to measure, not because the read failed"},{"path":"watch.rows[].eth1hWei","type":"string | null","what":"a TRUE rolling hour at the 5-minute candle grain — unlike /quotes' ethWei1h. \"0\" is a real measurement (no indexed swap this hour); null means either ethQuoted is false or we could not read the figure. Not the same facts."},{"path":"watch.rows[].lastE36 / pricePoolId / lastE36Sec","type":"string | null / string | null / integer | null","what":"last close, WHICH pool it closed on, and when. The reach is 24 HOURS, so a price here can legitimately be hours old — printing it as \"now\" would assert a freshness nobody measured."},{"path":"thresholds","type":"object","what":"every line an event had to cross, in wei and percent"},{"path":"identity","type":"\"ok\" | \"unavailable\"","what":"covers BOTH identity reads (events and watch), folded"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/activity?watch=0x1df1f09c4dd65c76746d53467361d536cdfdc719'"}},"/api/v1/chain/pulse":{"get":{"operationId":"pulse","summary":"discovery lanes","description":"Three lanes over the newest end of the chain, each a measurement with a stated basis. fresh — most recently initialized pools, ordered by creation and NOT by volume (a pool with no swaps yet is precisely what the lane is for, so it renders with zero volume rather than being hidden). first — created inside the last 6 hours and already carrying volume: the \"it is actually trading\" moment, ranked by measured ETH. heating — one hour's ETH leg against the mean of that pool's OWN prior indexed hours, filtered by the multiple and floored on dust, so a 40x print on nothing never outranks real flow.\n\n### bounds — what this response is NOT\n- 12 rows per lane, per response.\n- index.firstComplete is the honesty flag on the `first` lane: the ranking is computed over the newest 500 pools (index.candidateDepth), and when more pools were created inside firstMovesHours than that reaches back over, firstComplete is FALSE and the lane is a ranking of a SUBSET. It was false in production at capture time — a chain minting pools this fast makes it the normal case, not the edge case.\n- heating requires at least 2 complete prior hours of baseline (a baseline of one hour is not a baseline), a positive baseline, at least 0.005 ETH in the basis hour (index.heatMinWei), and a multiple of at least 2x (index.heatMinX). Rows below any of those are absent, not zeroed.\n- A pool with no ETH side sums to exactly 0 wei BY CONSTRUCTION — the worker counts its swaps and refuses to invent an ETH figure. Read ethQuoted before reading ethWei, and render n/a rather than 0.\n- firstFillSec is only the pool's TRUE first bucket when the pool is younger than the 24h volume window; older pools get null rather than a \"first fill\" that is really just the oldest bucket still in view.\n- fee is in hundredths of a bip (3000 = 0.30%). A value at or above 0x800000 is Uniswap V4's dynamic-fee sentinel, not a fee — rendering it as a percentage prints a number that was never on chain.\n- Rows are per POOL here, not per token: a token with several pools appears once per pool.\n\n### how to date this payload\n- serverSec — the server clock when the body was built.\n- index.updatedSec / updatedAgoSec / stale / staleMinutes — the cursor facts. Note this endpoint discloses the threshold in MINUTES (staleMinutes) where newer endpoints use seconds (staleSec).\n- index.basisSec + index.basisPartial — WHICH hour heating compares, and whether that hour is still filling. When it is partial, every multiple in the lane is a FLOOR that can only rise as the hour fills. Nothing is prorated to a full-hour rate.\n- index: null = no cursor row, i.e. the worker has never run here. 200, not an error.\n\n### caching\nThe route sets `public, s-maxage=5, stale-while-revalidate=30` — a shared cache may re-serve this body for up to 35s (s-maxage + stale-while-revalidate). What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for. 5s at the edge under a 10s client poll — enough to absorb a crowd without serving a frame older than the interval it is rendered in. A shared cache can still hand back a body older than that under stale-while-revalidate, which is why you must add the Age header to the frame's age rather than assume a hit was generated just now.","parameters":[],"responses":{"200":{"description":"the read succeeded, or there is no cursor (index:null with empty lanes — a deployment fact, explained rather than implied).\n\n```json\nPulsePayload (above)\n```","content":{"application/json":{"example":"{\n  \"serverSec\": 1786033540,\n  \"index\": {\n    \"startSec\": 1785950941, \"startBlock\": 28665903, \"safeBlock\": 29488406,\n    \"updatedSec\": 1786033318, \"updatedAgoSec\": 222,\n    \"stale\": false, \"staleMinutes\": 10, \"unattributedSwaps\": 895849,\n    \"windowHours\": 24, \"firstMovesHours\": 6,\n    \"firstComplete\": false, \"candidateDepth\": 500,\n    \"basisSec\": 1786032000, \"basisPartial\": true,\n    \"heatMinX\": 2, \"heatMinWei\": \"5000000000000000\"\n  },\n  \"lanes\": {\n    \"fresh\": [\n      {\n        \"poolId\": \"0x4887ca34...442cca\",\n        \"token\": \"0xf8560c4467Cd2BFc5ae85971292d23611dd5673A\",\n        \"symbol\": null, \"hooked\": false, \"fee\": 2500, \"ethQuoted\": true,\n        \"ethWei\": \"10000000000000\", \"swaps\": 1,\n        \"createdSec\": 1786033311, \"firstFillSec\": 1786032000,\n        \"basisWei\": null, \"basisSwaps\": null, \"baselineWei\": null,\n        \"heatX\": null, \"priorHours\": null\n      }\n      ... 11 more\n    ],\n    \"first\": [ ... 12 rows ],\n    \"heating\": [\n      {\n        \"poolId\": \"0x4db307fb...17e983\",\n        \"token\": \"0xd0c50e8Add18320f5004Fba25CF6CE1F98dc8d61\",\n        \"symbol\": \"BIRD\", \"hooked\": false, \"fee\": 2500, \"ethQuoted\": true,\n        \"ethWei\": \"20728653800048297316\", \"swaps\": 512,\n        \"createdSec\": 1785952352, \"firstFillSec\": null,\n        \"basisWei\": \"9603959719681402170\", \"basisSwaps\": 229,\n        \"baselineWei\": \"2781173520091723786\", \"heatX\": 3.453, \"priorHours\": 4\n      }\n      ... 2 more\n    ]\n  }\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:25:40Z","x-fletch-example-trimmed":"lane rows elided at the `...` lines and pool ids abbreviated — so this block is not valid JSON as printed."}}},"503":{"description":"our read failed, OR the cursor's heartbeat could not be parsed (figures with no freshness attached are the one thing this surface must never serve). The body is generic by design — see the contract above; branch on the status.\n\n```json\n{\"error\":\"service unavailable\"}\n```"}},"x-fletch-bounds":["12 rows per lane, per response.","index.firstComplete is the honesty flag on the `first` lane: the ranking is computed over the newest 500 pools (index.candidateDepth), and when more pools were created inside firstMovesHours than that reaches back over, firstComplete is FALSE and the lane is a ranking of a SUBSET. It was false in production at capture time — a chain minting pools this fast makes it the normal case, not the edge case.","heating requires at least 2 complete prior hours of baseline (a baseline of one hour is not a baseline), a positive baseline, at least 0.005 ETH in the basis hour (index.heatMinWei), and a multiple of at least 2x (index.heatMinX). Rows below any of those are absent, not zeroed.","A pool with no ETH side sums to exactly 0 wei BY CONSTRUCTION — the worker counts its swaps and refuses to invent an ETH figure. Read ethQuoted before reading ethWei, and render n/a rather than 0.","firstFillSec is only the pool's TRUE first bucket when the pool is younger than the 24h volume window; older pools get null rather than a \"first fill\" that is really just the oldest bucket still in view.","fee is in hundredths of a bip (3000 = 0.30%). A value at or above 0x800000 is Uniswap V4's dynamic-fee sentinel, not a fee — rendering it as a percentage prints a number that was never on chain.","Rows are per POOL here, not per token: a token with several pools appears once per pool."],"x-fletch-freshness":["serverSec — the server clock when the body was built.","index.updatedSec / updatedAgoSec / stale / staleMinutes — the cursor facts. Note this endpoint discloses the threshold in MINUTES (staleMinutes) where newer endpoints use seconds (staleSec).","index.basisSec + index.basisPartial — WHICH hour heating compares, and whether that hour is still filling. When it is partial, every multiple in the lane is a FLOOR that can only rise as the hour fills. Nothing is prorated to a full-hour rate.","index: null = no cursor row, i.e. the worker has never run here. 200, not an error."],"x-fletch-cache":{"routeHeader":"public, s-maxage=5, stale-while-revalidate=30","delivered":"public (+ Age)","deliveredNote":"What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for.","sharedMaxAgeSecs":5,"staleWhileRevalidateSecs":30},"x-fletch-fields":[{"path":"serverSec","type":"integer (unix s)","what":"server clock when this body was built"},{"path":"index","type":"object | null","what":"cursor facts plus every lane parameter: windowHours, firstMovesHours, firstComplete, candidateDepth, basisSec, basisPartial, heatMinX, heatMinWei"},{"path":"lanes.fresh[] / first[] / heating[]","type":"PulseRow[]","what":"the three lanes; a pool can appear in more than one"},{"path":"lanes.*[].poolId / token / symbol","type":"string","what":"the pool id is the row key; token is EIP-55 checksummed"},{"path":"lanes.*[].hooked / fee / ethQuoted","type":"boolean / integer / boolean","what":"V4 hook present, fee in hundredths of a bip, and whether an ETH figure is measurable at all"},{"path":"lanes.*[].ethWei / swaps","type":"string (wei) / integer","what":"over the disclosed window (index.windowHours)"},{"path":"lanes.*[].createdSec / firstFillSec","type":"integer / integer | null","what":"pool Initialize, and the first indexed volume bucket at HOUR resolution — a floor"},{"path":"lanes.heating[].basisWei / basisSwaps","type":"string (wei) / integer","what":"the BASIS HOUR's ETH leg and that same hour's swap count — pairing a 1h ETH figure with a 24h swap count would read as one measurement and be two"},{"path":"lanes.heating[].baselineWei / heatX / priorHours","type":"string (wei) / number / integer","what":"mean ETH leg of the prior complete hours, the multiple, and how many hours the baseline averages"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/pulse'"}},"/api/v1/chain/quotes":{"get":{"operationId":"quotes","summary":"batch volume readings","description":"Volume readings for up to 100 tokens in one call — built for a client-held watchlist, so it takes a list of addresses and returns a list of measurements. Nothing about the caller is stored, read or logged; the request is the whole session. It deliberately returns NO PRICE: this index measures the ETH leg of indexed swaps in hourly buckets and nothing else, and a plausible-looking derivation rendered where a price belongs would be a number nothing traded at.\n\n### bounds — what this response is NOT\n- ethWei1h / swaps1h ARE NOT A ROLLING HOUR, and must not be labelled as one. They sum the hourly buckets whose START lies inside the last 3600 seconds — which at 17:36 is the 17:00 bucket alone, i.e. 36 minutes of trading, and one minute after the hour it is one minute. The figure therefore collapses at every hour boundary and climbs back across the hour with no trade behind the motion. Measured live on 2026-08-06 at 17:36Z, same token, same minute: /quotes ethWei1h read 27.45 ETH (305 swaps) while the true rolling hour — /board rows[].eth1 and /activity watch.rows[].eth1hWei, both at the 5-minute candle grain — read 61.53 ETH (577 swaps). Read this as \"volume so far in the current clock hour\" or take the rolling hour from /board or /activity; do not build an alert on it.\n- ethWei24h / swaps24h ARE comparable across the API: the same hourly-bucket selection /board's eth24 uses, and they matched it exactly in that same live check (6,689.87 ETH on both).\n- This endpoint carries NO ethQuoted flag, so a \"0\" here is ambiguous in a way it is not elsewhere: it means EITHER the token's pools traded no ETH in the window, OR the token has no ETH-side pool at all and its ETH volume is structurally unmeasurable. Verified live on a token with one non-ETH V4 pool — /quotes returned ethWei24h \"0\" while /search returned ethQuoted:false for the same address. If you render a 0 from here, resolve which one it is against /search, /board or /pools first, or render it as \"0 measured\" rather than as the token's volume.\n- At most 100 addresses per call.\n- FOUR OUTCOMES, kept deliberately distinct — collapsing any two is the forbidden error class: (1) our read failed = 503; (2) no index here = 200 with index:null; (3) the token WAS looked at and is not in our index = it appears in `queried` and NOT in `tokens`; (4) the token was NOT looked at (malformed, or past the cap) = absent from BOTH lists. `queried` exists precisely so you can tell 3 from 4.\n- A token WITH an indexed pool and no swaps in the window DOES come back, with zeroes — that zero is a real measurement.\n- ethWei1h / ethWei24h are string | null and never fall back to \"0\". A null means we could not express the figure; it is a statement about us, not about the token.\n- `pools` counts every indexed pool for the token, not just ones that traded in the window — which is why it can exceed the board's per-row pool count for the same token.\n- firstPoolTs is the earliest pool creation we INDEXED — an upper bound on the token's age, never a claim about when the token was deployed.\n\n### how to date this payload\n- THIS PAYLOAD HAS NO BODY-LEVEL CLOCK — it is the one endpoint here that does not stamp itself. There is no serverSec, no measuredSec and no readAt, so you cannot tell from the body when it was built. Date it from the Age response header (it is shared-cached) plus index.updatedAt, and never assume a frame was measured when it arrived.\n- index.updatedAt (ISO) and index.stale — the cursor heartbeat and the verdict, decided server-side against the same threshold every other surface uses. This dates the INDEX, not this response.\n- index.startTs / startBlock / safeBlock — when the index opened and how far it has walked. Every figure is measured since startTs.\n- This endpoint is the oldest shape here and states its timestamps as ISO strings rather than unix seconds. That is a shape difference, not a different measurement.\n\n### caching\nThe route sets `public, s-maxage=10, stale-while-revalidate=60` — a shared cache may re-serve this body for up to 70s (s-maxage + stale-while-revalidate). What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for. 10s + 60s swr; the no-cursor path is 30s + 180s. The response is keyed by the address list, so a shared watchlist shares a cache entry while a personal one simply misses. Honour the Age header — with no body clock here it is the only thing that dates the frame.","parameters":[{"name":"tokens","in":"query","required":false,"description":"comma-separated 0x addresses Absent: 200 with queried:[] and tokens:[] — nothing was asked, so nothing is claimed. Not usable: malformed and duplicate entries are dropped, and so is everything past 100 addresses. Dropped addresses are missing from `queried` too, so a truncated request reads as \"not asked\" rather than as \"not indexed\"..","schema":{"type":"string"},"example":"0x1df1f09c4dd65c76746d53467361d536cdfdc719"}],"responses":{"200":{"description":"the read succeeded. An address in `queried` but not in `tokens` was looked at and is not in our index.\n\n```json\n{index, queried, tokens}\n```\n\nOR — the database answered and holds no chainwide index — a deployment fact, returned as a success.\n\n```json\n{\"index\":null,\"queried\":[...],\"tokens\":[]}\n```","content":{"application/json":{"example":"{\n  \"index\": {\n    \"startTs\": \"2026-08-05T17:29:01.000Z\",\n    \"startBlock\": 28665903, \"safeBlock\": 29488406,\n    \"updatedAt\": \"2026-08-06T16:21:58.425Z\",\n    \"stale\": false, \"unattributedSwaps\": 895849\n  },\n  \"queried\": [\"0x1df1f09c4dd65c76746d53467361d536cdfdc719\"],\n  \"tokens\": [\n    {\n      \"token\": \"0x1df1f09c4dd65c76746d53467361d536cdfdc719\",\n      \"symbol\": \"FROGE\", \"pools\": 20,\n      \"ethWei1h\": \"18355679014953291857\",\n      \"ethWei24h\": \"6626354151128638040632\",\n      \"swaps1h\": 131, \"swaps24h\": 56067,\n      \"firstPoolTs\": \"2026-08-06T02:00:12.000Z\"\n    }\n  ]\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:28:11Z","x-fletch-example-trimmed":null}}},"503":{"description":"our read failed. Generic body by design; branch on the status.\n\n```json\n{\"error\":\"service unavailable\"}\n```"}},"x-fletch-bounds":["ethWei1h / swaps1h ARE NOT A ROLLING HOUR, and must not be labelled as one. They sum the hourly buckets whose START lies inside the last 3600 seconds — which at 17:36 is the 17:00 bucket alone, i.e. 36 minutes of trading, and one minute after the hour it is one minute. The figure therefore collapses at every hour boundary and climbs back across the hour with no trade behind the motion. Measured live on 2026-08-06 at 17:36Z, same token, same minute: /quotes ethWei1h read 27.45 ETH (305 swaps) while the true rolling hour — /board rows[].eth1 and /activity watch.rows[].eth1hWei, both at the 5-minute candle grain — read 61.53 ETH (577 swaps). Read this as \"volume so far in the current clock hour\" or take the rolling hour from /board or /activity; do not build an alert on it.","ethWei24h / swaps24h ARE comparable across the API: the same hourly-bucket selection /board's eth24 uses, and they matched it exactly in that same live check (6,689.87 ETH on both).","This endpoint carries NO ethQuoted flag, so a \"0\" here is ambiguous in a way it is not elsewhere: it means EITHER the token's pools traded no ETH in the window, OR the token has no ETH-side pool at all and its ETH volume is structurally unmeasurable. Verified live on a token with one non-ETH V4 pool — /quotes returned ethWei24h \"0\" while /search returned ethQuoted:false for the same address. If you render a 0 from here, resolve which one it is against /search, /board or /pools first, or render it as \"0 measured\" rather than as the token's volume.","At most 100 addresses per call.","FOUR OUTCOMES, kept deliberately distinct — collapsing any two is the forbidden error class: (1) our read failed = 503; (2) no index here = 200 with index:null; (3) the token WAS looked at and is not in our index = it appears in `queried` and NOT in `tokens`; (4) the token was NOT looked at (malformed, or past the cap) = absent from BOTH lists. `queried` exists precisely so you can tell 3 from 4.","A token WITH an indexed pool and no swaps in the window DOES come back, with zeroes — that zero is a real measurement.","ethWei1h / ethWei24h are string | null and never fall back to \"0\". A null means we could not express the figure; it is a statement about us, not about the token.","`pools` counts every indexed pool for the token, not just ones that traded in the window — which is why it can exceed the board's per-row pool count for the same token.","firstPoolTs is the earliest pool creation we INDEXED — an upper bound on the token's age, never a claim about when the token was deployed."],"x-fletch-freshness":["THIS PAYLOAD HAS NO BODY-LEVEL CLOCK — it is the one endpoint here that does not stamp itself. There is no serverSec, no measuredSec and no readAt, so you cannot tell from the body when it was built. Date it from the Age response header (it is shared-cached) plus index.updatedAt, and never assume a frame was measured when it arrived.","index.updatedAt (ISO) and index.stale — the cursor heartbeat and the verdict, decided server-side against the same threshold every other surface uses. This dates the INDEX, not this response.","index.startTs / startBlock / safeBlock — when the index opened and how far it has walked. Every figure is measured since startTs.","This endpoint is the oldest shape here and states its timestamps as ISO strings rather than unix seconds. That is a shape difference, not a different measurement."],"x-fletch-cache":{"routeHeader":"public, s-maxage=10, stale-while-revalidate=60","delivered":"public (+ Age)","deliveredNote":"What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for.","sharedMaxAgeSecs":10,"staleWhileRevalidateSecs":60},"x-fletch-fields":[{"path":"index","type":"object | null","what":"{startTs, startBlock, safeBlock, updatedAt, stale, unattributedSwaps} as ISO strings; null = no index here"},{"path":"queried","type":"string[]","what":"the lowercased addresses this response actually speaks about"},{"path":"tokens[].token / symbol","type":"string / string | null","what":"the address, and the sanitised deployer symbol (null rather than the \"·\" marker)"},{"path":"tokens[].pools","type":"integer","what":"every indexed pool for this token"},{"path":"tokens[].ethWei1h","type":"string | null","what":"decimal wei so far in the CURRENT CLOCK HOUR — not a rolling hour, and not comparable with /board's eth1. See bounds. null = unreadable, NOT zero."},{"path":"tokens[].ethWei24h","type":"string | null","what":"decimal wei over the 24h window — the same selection /board's eth24 uses. null = unreadable, NOT zero."},{"path":"tokens[].swaps1h / swaps24h","type":"integer","what":"indexed swap counts over the same two windows, with the same clock-hour caveat on the 1h"},{"path":"tokens[].firstPoolTs","type":"string (ISO) | null","what":"earliest indexed pool creation — an upper bound on age"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/quotes?tokens=0x1df1f09c4dd65c76746d53467361d536cdfdc719'"}},"/api/v1/chain/candles/{address}":{"get":{"operationId":"candles","summary":"one token's candle series","description":"OHLCV bars for one token, from ONE pool. The pool pick is the house rule — the token's most-indexed-swaps ETH-quoted pool in the window, ties on pool id — unless ?pool= pins one, and either way the payload NAMES the pool it used. This is exactly what the chart on /t/chain/{address} draws, through the same reader, so the live chart and a machine consumer can never disagree about a bar.\n\n### bounds — what this response is NOT\n- At most 2,000 bars per response. When the cap bites, the OLDEST end is cut (the newest bars are kept, because a truncated series rendering out-of-date bars as current would be worse) and a note in `notes` says so. A response exactly at 2,000 has almost certainly lost history.\n- There is no grain finer than 300 seconds in this index. aggregatedFromSecs is 300 on every coarser grain and a note states the roll-up rule; any surface implying 1-minute data would be inventing it.\n- A missing bucket between two bars is a GAP — no swap with a derivable price was indexed there. It must render as a gap; bridging or interpolating it draws a price that did not exist.\n- The series is from ONE pool. poolCount tells you how many ETH-quoted pools the token has, so a reader can see that a choice was made; when poolPinned is true, poolCount is 1 and means \"the one you asked for\".\n- `unit` is eth-per-token only when decimals() answered. When it did not, the series is eth-per-raw-unit and the unit says so — the numbers are NOT rescaled by a guessed 18.\n- `notes` is part of the payload, not decoration. It carries the pool-count disclosure, the aggregation rule, the truncation warning, and the sentence distinguishing \"decimals unread\" from \"our identity index was down\".\n- THIS IS THE ONE ENDPOINT WHOSE NUMBERS ARE NOT EXACT, and it is the exception to \"wei are decimal strings\". o/h/l/c are IEEE doubles converted at this boundary from the exact e36 integers the index stores; `eth` is ETH (not wei) as a double, floored to 6 decimal places — literally Number(wei * 1e6 / 1e18) / 1e6 — so each bar can under-report by up to 0.000001 ETH and summing bars compounds that. Draw with these; do not settle with them. For an exact figure take /board rows[].lastE36 (the stored integer) and /board or /pools eth24 wei strings.\n\n### how to date this payload\n- serverSec — the server clock when the body was built.\n- updatedSec / updatedAgoSec — when the index last advanced, computed server-side. null when the index cannot be dated, which `stale` already reports as stale rather than as fresh.\n- stale + staleSec — the verdict and the threshold behind it.\n- windowStartTs / indexedToBlock — the index's backfill origin and how far it has walked: the reason every series here is partial history and not the token's whole life.\n- queriedFromTs — the lower bound of the query. \"No bars\" is a statement about THIS interval, nothing wider.\n- x-candles-server-sec / x-candles-updated-ago-sec / x-candles-stale ride as headers beside the ETag, on the 304 as well as the 200 — but /board's measured caution applies here too: this deployment's edge can strip x- headers from a 304, so take a full 200 when you need to re-age a frame rather than rely on them.\n\n### caching\nThe route sets `no-store, must-revalidate` — weak ETag over the measurement (same discipline as /board); If-None-Match ⇒ 304 with no body. Delivered as sent: no-store survives the edge intact. no-store, like the board feed and for the same reason: a liveness surface served from a shared cache is a frame whose age the consumer cannot see. Cost is bounded without a memo — each poll is one primary-key range scan. The ETag covers the bars, unit, pool identity, cursor facts and the applied grain/window/pin — never the per-request clock fields (serverSec, queriedFromTs, updatedAgoSec), because a validator that never matches validates nothing.","parameters":[{"name":"address","in":"path","required":true,"description":"the token address, checksummed or lowercase Absent: n/a — it is the path. Not usable: 404 {\"error\":\"rejected\",\"reason\":\"not a token address\"}. Verified in production..","schema":{"type":"string"},"example":"0x1df1f09c4dd65c76746d53467361d536cdfdc719"},{"name":"bucket","in":"query","required":false,"description":"bar grain in seconds: 300 | 900 | 3600 | 14400. 300 is the STORED grain; the rest are rolled up server-side (open = first, high = max, low = min, close = last, volume summed). Absent: 300. Not usable: degrades to 300 and echoes bucketSecs:300 — a typo yields a correctly-labelled 5m series rather than an error or, worse, a mislabelled one. Verified: ?bucket=60 returns bucketSecs:300. Render under the ECHOED value..","schema":{"type":"integer"},"example":"3600"},{"name":"window","in":"query","required":false,"description":"seconds of history to query, back from now Absent: 288 bars of the chosen grain — a screenful. Not usable: clamped to [bucket x 2, 30 days]; unparseable falls back to the grain default. Verified: ?bucket=300&window=1 returns windowSecs:600..","schema":{"type":"integer"},"example":"86400"},{"name":"pool","in":"query","required":false,"description":"pin one pool: a V2/V3 pair address (20 bytes) or a V4 pool id (32 bytes) Absent: the busiest-pool rule; poolPinned:false in the response. Not usable: three different refusals for three different facts — 400 for a malformed id, 404 \"that pool id is not in the index\", 400 \"that pool is not a pool of this token\", 400 \"that pool has no ETH side\". A pool that is not this token's is refused rather than charted under the wrong name. If OUR registry read fails it is 503, never a silent fallback to a different market..","schema":{"type":"string"},"example":"0xf5eecbfa6aaad42fc1db776221e88a7da12a5895d9404424f8e419eda9aba56d"}],"responses":{"200":{"description":"the series was read. candles:[] means no indexed swap with a derivable price landed in the queried interval — bounded by queriedFromTs, and a statement about that interval only.\n\n```json\nCandlesPayload (above)\n```","content":{"application/json":{"example":"{\n  \"serverSec\": 1786033658,\n  \"token\": \"0x1df1f09c4dd65c76746d53467361d536cdfdc719\",\n  \"bucketSecs\": 3600, \"aggregatedFromSecs\": 300,\n  \"windowSecs\": 86400, \"queriedFromTs\": 1785947258,\n  \"candles\": [\n    {\n      \"ts\": 1785981600,\n      \"o\": 2.980524787834726e-7, \"h\": 0.0000032432038263717456,\n      \"l\": 1.8266793248209968e-7, \"c\": 0.000001644419571896578,\n      \"swaps\": 26349, \"eth\": 3230.058971\n    }\n    ... 14 more bars\n  ],\n  \"unit\": \"eth-per-token\",\n  \"poolId\": \"0xf5eecbfa6aaad42fc1db776221e88a7da12a5895d9404424f8e419eda9aba56d\",\n  \"poolCount\": 20, \"poolPinned\": false,\n  \"windowStartTs\": 1785950941, \"indexedToBlock\": 29488406,\n  \"updatedSec\": 1786033318, \"updatedAgoSec\": 340,\n  \"stale\": false, \"staleSec\": 600,\n  \"notes\": [\n    \"20 ETH-quoted pools exist for this token; this series is one of them — prices are per pool and are not averaged\",\n    \"each 1h bar is aggregated server-side from stored 5-minute bars — open is the first, high the max, low the min, close the last, volume summed; 5 minutes is the finest grain this index stores\"\n  ]\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:27:38Z","x-fletch-example-trimmed":"14 bars elided at the `...` line, so this block is not valid JSON as printed."}}},"304":{"description":"If-None-Match matched the current measurement. This is a SUCCESSFUL poll: the server re-confirmed the series.\n\n```json\n(no body; ETag only)\n```"},"400":{"description":"a ?pool= we will not use, and the reason names WHICH of three facts it is — all verified in production: \"pool must be a 0x pool id — a V2/V3 pair address or a V4 32-byte pool id\" (malformed); \"that pool is not a pool of this token — refusing to chart one market under another token's name\" (someone else's market); \"that pool has no ETH side, so there is no ETH price series to serve for it\". The reason is for a human in your logs; branch on the status.\n\n```json\n{\"error\":\"rejected\",\"reason\":\"<one of three>\"}\n```"},"404":{"description":"the path segment is not an address (first body), or the pinned pool id is not in our registry (second). Both verified in production.\n\n```json\n{\"error\":\"rejected\",\"reason\":\"not a token address\"}  /  {\"error\":\"rejected\",\"reason\":\"that pool id is not in the index\"}\n```"},"503":{"description":"our read failed, and — like the 400 above — the reason names WHICH of three facts it is: \"could not read the price index\" (the query threw); \"the chainwide index is not running here\" (there is no cursor row at all, a fact about this DEPLOYMENT, not about the token or the read); \"the pool registry could not be read\" (only with ?pool= — we could not confirm the pin, and we will not silently fall back to a different market). All three verified by driving the handler against a seeded database. Never served as an empty series: `candles: []` under a 200 renders as \"this token did not trade\", which is a claim about the token. The reason is for a human in your logs; branch on the status.\n\n```json\n{\"error\":\"unavailable\",\"reason\":\"<one of three>\"}\n```"}},"x-fletch-bounds":["At most 2,000 bars per response. When the cap bites, the OLDEST end is cut (the newest bars are kept, because a truncated series rendering out-of-date bars as current would be worse) and a note in `notes` says so. A response exactly at 2,000 has almost certainly lost history.","There is no grain finer than 300 seconds in this index. aggregatedFromSecs is 300 on every coarser grain and a note states the roll-up rule; any surface implying 1-minute data would be inventing it.","A missing bucket between two bars is a GAP — no swap with a derivable price was indexed there. It must render as a gap; bridging or interpolating it draws a price that did not exist.","The series is from ONE pool. poolCount tells you how many ETH-quoted pools the token has, so a reader can see that a choice was made; when poolPinned is true, poolCount is 1 and means \"the one you asked for\".","`unit` is eth-per-token only when decimals() answered. When it did not, the series is eth-per-raw-unit and the unit says so — the numbers are NOT rescaled by a guessed 18.","`notes` is part of the payload, not decoration. It carries the pool-count disclosure, the aggregation rule, the truncation warning, and the sentence distinguishing \"decimals unread\" from \"our identity index was down\".","THIS IS THE ONE ENDPOINT WHOSE NUMBERS ARE NOT EXACT, and it is the exception to \"wei are decimal strings\". o/h/l/c are IEEE doubles converted at this boundary from the exact e36 integers the index stores; `eth` is ETH (not wei) as a double, floored to 6 decimal places — literally Number(wei * 1e6 / 1e18) / 1e6 — so each bar can under-report by up to 0.000001 ETH and summing bars compounds that. Draw with these; do not settle with them. For an exact figure take /board rows[].lastE36 (the stored integer) and /board or /pools eth24 wei strings."],"x-fletch-freshness":["serverSec — the server clock when the body was built.","updatedSec / updatedAgoSec — when the index last advanced, computed server-side. null when the index cannot be dated, which `stale` already reports as stale rather than as fresh.","stale + staleSec — the verdict and the threshold behind it.","windowStartTs / indexedToBlock — the index's backfill origin and how far it has walked: the reason every series here is partial history and not the token's whole life.","queriedFromTs — the lower bound of the query. \"No bars\" is a statement about THIS interval, nothing wider.","x-candles-server-sec / x-candles-updated-ago-sec / x-candles-stale ride as headers beside the ETag, on the 304 as well as the 200 — but /board's measured caution applies here too: this deployment's edge can strip x- headers from a 304, so take a full 200 when you need to re-age a frame rather than rely on them."],"x-fletch-cache":{"routeHeader":"no-store, must-revalidate","delivered":"no-store, must-revalidate","deliveredNote":"Delivered as sent: no-store survives the edge intact.","validator":"weak ETag over the measurement (same discipline as /board); If-None-Match ⇒ 304 with no body"},"x-fletch-fields":[{"path":"token / bucketSecs / windowSecs","type":"string / integer / integer","what":"what was actually applied — render under these, not under what you asked for"},{"path":"aggregatedFromSecs","type":"integer | null","what":"300 when the bars were rolled up server-side; null when they ARE the stored grain. Non-null means your UI must say \"aggregated from 5m\"."},{"path":"queriedFromTs","type":"integer (unix s)","what":"the lower bound of the query"},{"path":"serverSec","type":"integer (unix s)","what":"server clock when this body was built"},{"path":"candles[]","type":"{ts,o,h,l,c,swaps,eth}[]","what":"ascending by ts. o/h/l/c are DOUBLES in `unit`, converted here from the exact stored e36 integers; eth is the bucket's ETH leg in ETH (not wei) as a double floored to 6 dp; swaps is the exact indexed count. See bounds — these are the only lossy numerics in this API."},{"path":"unit","type":"\"eth-per-token\" | \"eth-per-raw-unit\"","what":"which one, stated rather than assumed"},{"path":"poolId / poolCount / poolPinned","type":"string | null / integer / boolean","what":"the ONE pool this series is from, how many were available, and whether you pinned it"},{"path":"windowStartTs / indexedToBlock","type":"integer | null","what":"the index's origin and head — why the history is partial"},{"path":"updatedSec / updatedAgoSec / stale / staleSec","type":"integer | null / boolean / integer","what":"index freshness and the threshold behind the verdict"},{"path":"notes","type":"string[]","what":"everything the read chose or could not determine, in sentences. Surface them."}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/candles/0x1df1f09c4dd65c76746d53467361d536cdfdc719?bucket=3600&window=86400'"}},"/api/v1/chain/pools/{address}":{"get":{"operationId":"pools","summary":"where a token trades, and how deep","description":"Every indexed pool for one token, its measured 24h flow, and a depth figure whose MEANING is stated per venue rather than presented as one comparable number. Served through the same reader the app's pool panel renders, so the API and the panel cannot disagree about a depth figure.\n\n### bounds — what this response is NOT\n- Rows are capped at `rowBound` (60); `totalPools` is the TRUE count. A V4 pool is keyed on its whole PoolKey, so anyone can mint unlimited pools for a token — an unbounded read is a cost anyone can impose on us.\n- The cap is not a blind head: the CHARTED pool and every V2/V3 ETH-quoted pool — the only rows whose depth is measurable at all — are always retained in the capped set, so pool spam can hide neither the chart's pool nor the measurable rows.\n- DEPTH MEANS A DIFFERENT THING PER VENUE and must not be compared across them: v2 = the pair's ETH-side reserve (exact, the constant-product basis); v3 = the WETH the pool contract HOLDS, a hard ceiling on what any sell can be paid, which includes out-of-range positions and uncollected fees, so a trade near spot may reach less; v4 = measured:false with reason v4-singleton, because the singleton PoolManager holds every pool's tokens in one balance and per-pool reserves are not readable. Structurally unmeasured — not zero, not a guess.\n- reason \"read-failed\" is OUR read failing and makes no claim about the pool. reason \"no-eth-side\" means there is no ETH depth to state at all. These are different facts from v4-singleton.\n- `exact` is the parse flag for the whole response: when false, treat every numeric as unreadable rather than trusting the value a failed parse produced.\n- sharePct divides by the token's 24h ETH across ALL its pools (the true total), not across the returned rows — so the returned shares can sum to less than 100. null when the denominator is 0.\n- v2 createdSec is a FLOOR on age (ageIsFloor:true): the registry seed records that a pair exists, not its birthday.\n- `fee` is in hundredths of a bip (3000 = 0.30%) AND IT IS NOT ALWAYS A FEE. A value at or above 8388608 (0x800000) is Uniswap V4's dynamic-fee sentinel: the pool's hook sets the fee per swap and no static rate exists to print. Dividing it by 10,000 renders \"838.86%\", a number that was never on chain. Measured live on 2026-08-06: 7 of the 186 pools across the top twelve tokens carried the sentinel. Check for it before formatting, and render dynamic-fee pools as such.\n- Volume figures are floors over what the indexer could walk in its budget.\n- snapshot.attempted:false means NO depth read was made — and then snapshot.blockNumber and snapshot.atSec are BOTH null, not 0. Verified in production on a token with no measurable pool. Guard before you date a depth figure with them.\n\n### how to date this payload\n- readAt — the server clock at read time, so a cached frame is dateable.\n- snapshot.blockNumber + snapshot.atSec — EVERY depth figure in one response is pinned to ONE block. Liquidity can leave in a single block, so treat these as that instant, not as now. snapshot.attempted:false means no depth read was made at all.\n- index.updatedSec / index.stale — the chainwide index's own freshness behind the volume figures.\n\n### caching\nThe route sets `public, s-maxage=15, stale-while-revalidate=90` — a shared cache may re-serve this body for up to 105s (s-maxage + stale-while-revalidate). What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for. 15s at the edge. Depth carries its own finer stamp (snapshot.atSec / snapshot.blockNumber), which is the timestamp that matters for a liquidity figure.","parameters":[{"name":"address","in":"path","required":true,"description":"the token address, checksummed or lowercase Absent: n/a — it is the path. Not usable: 404 {\"error\":\"not found\"}. Verified in production..","schema":{"type":"string"},"example":"0x1df1f09c4dd65c76746d53467361d536cdfdc719"}],"responses":{"200":{"description":"the read succeeded and we hold pools for this token.\n\n```json\n{token, known:true, ..., pools:[...], notes:[...]}\n```\n\nOR — the read worked and our index holds no pool. A fact about our coverage window — NOT proof the token has no pool: a pool created before the window opened, on a venue we discover by creation event, stays invisible to us.\n\n```json\n{\"token\":\"0x...\",\"known\":false,\"readAt\":...,\"index\":{...},\"note\":\"no pool for this token has been seen since the index window opened...\"}\n```","content":{"application/json":{"example":"{\n  \"token\": \"0x1df1f09c4dd65c76746d53467361d536cdfdc719\",\n  \"known\": true, \"readAt\": 1786033597,\n  \"index\": {\n    \"startSec\": 1785950941, \"safeBlock\": 29488406,\n    \"updatedSec\": 1786033318, \"stale\": false\n  },\n  \"totalPools\": 20, \"rowBound\": 60, \"exact\": true,\n  \"totals\": { \"eth24Wei\": \"6626354151128638040632\", \"swaps24\": 56067 },\n  \"chartPoolId\": \"0xf5eecbfa...aba56d\",\n  \"snapshot\": { \"attempted\": true, \"blockNumber\": 29491218, \"atSec\": 1786033597 },\n  \"pools\": [\n    {\n      \"id\": \"0xf5eecbfa...aba56d\",\n      \"venue\": \"v4\", \"fee\": 2500, \"tickSpacing\": 50,\n      \"hooks\": \"0x0000000000000000000000000000000000000000\",\n      \"currency0\": \"0x0000000000000000000000000000000000000000\",\n      \"currency1\": \"0x1df1f09c4dd65c76746d53467361d536cdfdc719\",\n      \"ethQuoted\": true, \"createdSec\": 1785981612, \"ageIsFloor\": false,\n      \"eth24Wei\": \"6549464610356605787008\", \"swaps24\": 52429,\n      \"sharePct\": 98.83, \"charted\": true,\n      \"depth\": { \"measured\": false, \"reason\": \"v4-singleton\" }\n    }\n    ... 19 more pools\n  ],\n  \"notes\": [ \"depth semantics per venue: v2 = ...\", ... ]\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:26:37Z","x-fletch-example-trimmed":"19 pools and the notes array elided, pool ids abbreviated — so this block is not valid JSON as printed."}}},"404":{"description":"the path segment is not an address. Verified in production.\n\n```json\n{\"error\":\"not found\"}\n```"},"503":{"description":"our read failed. Generic body by design; branch on the status.\n\n```json\n{\"error\":\"service unavailable\"}\n```"}},"x-fletch-bounds":["Rows are capped at `rowBound` (60); `totalPools` is the TRUE count. A V4 pool is keyed on its whole PoolKey, so anyone can mint unlimited pools for a token — an unbounded read is a cost anyone can impose on us.","The cap is not a blind head: the CHARTED pool and every V2/V3 ETH-quoted pool — the only rows whose depth is measurable at all — are always retained in the capped set, so pool spam can hide neither the chart's pool nor the measurable rows.","DEPTH MEANS A DIFFERENT THING PER VENUE and must not be compared across them: v2 = the pair's ETH-side reserve (exact, the constant-product basis); v3 = the WETH the pool contract HOLDS, a hard ceiling on what any sell can be paid, which includes out-of-range positions and uncollected fees, so a trade near spot may reach less; v4 = measured:false with reason v4-singleton, because the singleton PoolManager holds every pool's tokens in one balance and per-pool reserves are not readable. Structurally unmeasured — not zero, not a guess.","reason \"read-failed\" is OUR read failing and makes no claim about the pool. reason \"no-eth-side\" means there is no ETH depth to state at all. These are different facts from v4-singleton.","`exact` is the parse flag for the whole response: when false, treat every numeric as unreadable rather than trusting the value a failed parse produced.","sharePct divides by the token's 24h ETH across ALL its pools (the true total), not across the returned rows — so the returned shares can sum to less than 100. null when the denominator is 0.","v2 createdSec is a FLOOR on age (ageIsFloor:true): the registry seed records that a pair exists, not its birthday.","`fee` is in hundredths of a bip (3000 = 0.30%) AND IT IS NOT ALWAYS A FEE. A value at or above 8388608 (0x800000) is Uniswap V4's dynamic-fee sentinel: the pool's hook sets the fee per swap and no static rate exists to print. Dividing it by 10,000 renders \"838.86%\", a number that was never on chain. Measured live on 2026-08-06: 7 of the 186 pools across the top twelve tokens carried the sentinel. Check for it before formatting, and render dynamic-fee pools as such.","Volume figures are floors over what the indexer could walk in its budget.","snapshot.attempted:false means NO depth read was made — and then snapshot.blockNumber and snapshot.atSec are BOTH null, not 0. Verified in production on a token with no measurable pool. Guard before you date a depth figure with them."],"x-fletch-freshness":["readAt — the server clock at read time, so a cached frame is dateable.","snapshot.blockNumber + snapshot.atSec — EVERY depth figure in one response is pinned to ONE block. Liquidity can leave in a single block, so treat these as that instant, not as now. snapshot.attempted:false means no depth read was made at all.","index.updatedSec / index.stale — the chainwide index's own freshness behind the volume figures."],"x-fletch-cache":{"routeHeader":"public, s-maxage=15, stale-while-revalidate=90","delivered":"public (+ Age)","deliveredNote":"What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for.","sharedMaxAgeSecs":15,"staleWhileRevalidateSecs":90},"x-fletch-fields":[{"path":"token / known / readAt","type":"string / boolean / integer","what":"known:false = the read worked and our index holds no pool for this token"},{"path":"index","type":"object | null","what":"{startSec, safeBlock, updatedSec, stale}"},{"path":"totalPools / rowBound","type":"integer","what":"the true pool count, and the row cap that was applied to it"},{"path":"exact","type":"boolean","what":"false = a numeric did not parse exactly; do not trust the figures"},{"path":"totals","type":"{eth24Wei, swaps24}","what":"across ALL the token's pools — the denominator behind sharePct"},{"path":"chartPoolId","type":"string | null","what":"the pool the token's chart and tape follow (most indexed swaps in 24h). The app's trade ticket quotes its own route and may fill elsewhere."},{"path":"snapshot","type":"{attempted: boolean, blockNumber: integer | null, atSec: integer | null}","what":"the ONE block every depth figure was read at. Both numbers are null when attempted is false."},{"path":"pools[].id / venue / tickSpacing / hooks","type":"string / \"v2\"|\"v3\"|\"v4\" / integer / string (0x)","what":"the pool's identity and parameters; hooks is the zero address when the pool runs no V4 hook"},{"path":"pools[].fee","type":"integer","what":"hundredths of a bip (3000 = 0.30%) — EXCEPT at or above 8388608, which is V4's dynamic-fee sentinel and not a rate. See bounds."},{"path":"pools[].currency0 / currency1 / ethQuoted","type":"string / boolean","what":"the pair, and whether an ETH figure is measurable"},{"path":"pools[].createdSec / ageIsFloor","type":"integer / boolean","what":"creation as indexed; ageIsFloor:true (v2) means it is a floor, not a birthday"},{"path":"pools[].eth24Wei / swaps24 / sharePct","type":"string (wei) / integer / number | null","what":"measured 24h flow, and this pool's share of the token's total"},{"path":"pools[].charted","type":"boolean","what":"true on the pool the chart follows"},{"path":"pools[].depth","type":"object","what":"{measured:true, venue, kind, ethWei, ...} or {measured:false, reason}. Read `kind` before comparing anything."},{"path":"notes","type":"string[]","what":"the venue semantics, the one-block pin, the floor disclosure, the cap disclosure and the shared-cache window (s-maxage=15 + swr=90 — date the frame by Age + readAt), in sentences"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/pools/0x1df1f09c4dd65c76746d53467361d536cdfdc719'"}},"/api/v1/chain/traders/{address}":{"get":{"operationId":"traders","summary":"top traders for one token","description":"The top accounts by measured ETH bought and by ETH sold for one token. `trader` is the TRANSACTION SIGNER, never the swap event's sender: on this chain the sender is a router or launchpad contract essentially always (measured live — 0 of 1,974 sampled swaps had sender equal to signer), so ranking senders would be a list of routers wearing a whale costume.\n\n### bounds — what this response is NOT\n- AT MOST 8 buyers and 8 sellers. The route carries no field naming this cap, so it is stated here: `buyers` and `sellers` are the top 8 by bought/sold ETH respectively, ties broken on last activity then address. `traders` is the TRUE count of distinct non-router accounts (2,627 for the example token against 8 rows returned) — do not read the lists as the whole set.\n- Rows marked infrastructure are excluded from the lists and counted in routersExcluded.\n- coverage.attributedSwaps / coverage.indexedSwapsSince are a numerator and denominator you may NOT blindly divide. indexedSwapsSince is null when that read failed (never a fabricated 0), and it can legitimately come back BELOW attributedSwaps — the trader walk can see a pool the volume walk skipped on a given tick. When it does, the denominator is provably incomplete and no coverage ratio may be derived. Do not clamp the quotient to 1.\n- All totals are FLOORS over [earliestTs, latestTs].\n- tokensTraded is that account's distinct-token count across the WHOLE attribution table — the measurement that lets a reader see an infrastructure-shaped account rather than us silently deciding for them.\n\n### how to date this payload\n- readAt — the server clock at read time.\n- coverage.earliestTs / coverage.latestTs — the interval every total is a floor over. There is no claim outside it.\n- Trader attribution started at its own wiring date, later than the volume index — so this endpoint's window is NARROWER than the board's, and known:false can mean \"we started attributing after this token's activity\".\n\n### caching\nThe route sets `public, s-maxage=10, stale-while-revalidate=60` — a shared cache may re-serve this body for up to 70s (s-maxage + stale-while-revalidate). What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for. 10s at the edge; readAt stamps the body so a cached frame is dateable.","parameters":[{"name":"address","in":"path","required":true,"description":"the token address, checksummed or lowercase Absent: n/a — it is the path. Not usable: 404 {\"error\":\"not found\"}.","schema":{"type":"string"},"example":"0x1df1f09c4dd65c76746d53467361d536cdfdc719"}],"responses":{"200":{"description":"the read succeeded and there are rankable rows.\n\n```json\n{token, known:true, attribution, buyers, sellers, traders, routersExcluded, coverage}\n```\n\nOR — the read worked and nothing is rankable. A 404 here would read as \"no such token\", which we have not measured. When routersExcluded > 0 the note says the different thing: everything we attributed is marked infrastructure. Verified in production against an address with no attribution.\n\n```json\n{\"token\":\"0x...\",\"known\":false,\"readAt\":...,\"routersExcluded\":0,\"note\":\"no swaps attributed for this token yet...\"}\n```","content":{"application/json":{"example":"{\n  \"token\": \"0x1df1f09c4dd65c76746d53467361d536cdfdc719\",\n  \"known\": true, \"readAt\": 1786033658,\n  \"attribution\": \"tx-signer\",\n  \"buyers\": [\n    {\n      \"trader\": \"0xf70da97812cb96acdf810712aa562db8dfa3dbef\",\n      \"boughtEthWei\": \"66957809549812611082\",\n      \"soldEthWei\": \"6792602694989304123\",\n      \"netEthWei\": \"60165206854823306959\",\n      \"buys\": 388, \"sells\": 9,\n      \"firstTs\": 1785981953, \"lastTs\": 1786028441,\n      \"tokensTraded\": 82\n    }\n    ... 7 more (the cap is 8)\n  ],\n  \"sellers\": [ ... 8 rows ],\n  \"traders\": 2627, \"routersExcluded\": 0,\n  \"coverage\": {\n    \"attributedSwaps\": 15931, \"indexedSwapsSince\": 56067,\n    \"earliestTs\": 1785981613, \"latestTs\": 1786033315,\n    \"note\": \"totals are floors over [earliestTs, latestTs]; ...\"\n  }\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:27:38Z","x-fletch-example-trimmed":"buyer/seller rows and the coverage note elided — so this block is not valid JSON as printed."}}},"404":{"description":"the path segment is not an address.\n\n```json\n{\"error\":\"not found\"}\n```"},"503":{"description":"our read failed. Generic body by design; branch on the status.\n\n```json\n{\"error\":\"service unavailable\"}\n```"}},"x-fletch-bounds":["AT MOST 8 buyers and 8 sellers. The route carries no field naming this cap, so it is stated here: `buyers` and `sellers` are the top 8 by bought/sold ETH respectively, ties broken on last activity then address. `traders` is the TRUE count of distinct non-router accounts (2,627 for the example token against 8 rows returned) — do not read the lists as the whole set.","Rows marked infrastructure are excluded from the lists and counted in routersExcluded.","coverage.attributedSwaps / coverage.indexedSwapsSince are a numerator and denominator you may NOT blindly divide. indexedSwapsSince is null when that read failed (never a fabricated 0), and it can legitimately come back BELOW attributedSwaps — the trader walk can see a pool the volume walk skipped on a given tick. When it does, the denominator is provably incomplete and no coverage ratio may be derived. Do not clamp the quotient to 1.","All totals are FLOORS over [earliestTs, latestTs].","tokensTraded is that account's distinct-token count across the WHOLE attribution table — the measurement that lets a reader see an infrastructure-shaped account rather than us silently deciding for them."],"x-fletch-freshness":["readAt — the server clock at read time.","coverage.earliestTs / coverage.latestTs — the interval every total is a floor over. There is no claim outside it.","Trader attribution started at its own wiring date, later than the volume index — so this endpoint's window is NARROWER than the board's, and known:false can mean \"we started attributing after this token's activity\"."],"x-fletch-cache":{"routeHeader":"public, s-maxage=10, stale-while-revalidate=60","delivered":"public (+ Age)","deliveredNote":"What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for.","sharedMaxAgeSecs":10,"staleWhileRevalidateSecs":60},"x-fletch-fields":[{"path":"token / known / readAt","type":"string / boolean / integer","what":"known:false = the read worked and nothing is rankable"},{"path":"attribution","type":"\"tx-signer\"","what":"stated on every response so the basis is never assumed"},{"path":"buyers[] / sellers[]","type":"TraderRow[]","what":"top 8 each; a row can appear in both"},{"path":"*[].trader","type":"string (0x)","what":"the transaction signer"},{"path":"*[].boughtEthWei / soldEthWei / netEthWei","type":"string (decimal wei)","what":"net is derived here so consumers do not each re-derive it differently. Parse with BigInt."},{"path":"*[].buys / sells","type":"integer","what":"attributed swap counts"},{"path":"*[].firstTs / lastTs","type":"integer (unix s)","what":"first and last attributed activity for this account on this token"},{"path":"*[].tokensTraded","type":"integer","what":"distinct tokens this account has rows for, across the whole table"},{"path":"traders / routersExcluded","type":"integer","what":"distinct non-router accounts seen, and rows excluded as infrastructure"},{"path":"coverage","type":"{attributedSwaps, indexedSwapsSince, earliestTs, latestTs, note}","what":"the coverage numerator, denominator, interval, and the sentence saying what may not be derived from them"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/traders/0x1df1f09c4dd65c76746d53467361d536cdfdc719'"}},"/api/v1/chain/wallet/{address}":{"get":{"operationId":"wallet","summary":"one wallet's positions","description":"Token balances for one address, valued by a live sell quote. Unlike everything else here this read hits the CHAIN, not just the index: multicall balances plus per-position quotes, which is why it is allowed up to 60 seconds and why it is never prerendered. Cost basis and PnL are NOT derivable from this index, and it says so rather than guessing.\n\n### bounds — what this response is NOT\n- THE CANDIDATE SET IS CAPPED AND IT IS NOT RANKED BY THIS WALLET. We ask balanceOf for the top `coverage.tokenCap` tokens (150) by CHAIN-WIDE 24h volume, then all-time volume, then recency — out of coverage.tokensKnownToIndex (36,180 in production). coverage.capped is true whenever the cap bit. A position in a token outside that slice IS NOT RETURNED, and its absence is NOT a claim that the wallet does not hold it. Verified in production: tokensAsked 150 of 36,180.\n- Only the first `coverage.valuationCap` (25) positions are quoted. Beyond it a position comes back with unvaluedReason \"not-attempted\" — explicitly not priced, never valued at 0.\n- totals.ethWei is a FLOOR whenever totals.complete is false. complete is true only when every held position carried a quote AND every balance read answered; totals.unvalued and totals.unreadTokens tell you how much is missing on each side.\n- unvaluedReason distinguishes facts about the token from facts about us: no-pool / refused / out-of-range are about the token or the pool; unavailable / pools-unread / not-attempted are about our read or our cap. Collapsing them is the forbidden error class.\n- valuation.method is \"sell-quote-full-balance-single-pool\": each value is the ETH a sell of the ENTIRE balance quoted at just now, through ONE pool — the highest-volume ETH-quoted V4 pool we know for that token. Price impact is included; splitting across pools could do better; our fee and gas are NOT deducted. It is a quote in one block, not a settled price.\n- basis.known is always false. The index stores hourly ETH volume per pool, not this wallet's trades, so no entry price exists to report.\n\n### how to date this payload\n- measuredAt (ISO) — when the reads were taken.\n- coverage.indexUpdatedAt / coverage.indexStale / coverage.indexedToBlock — the index's own freshness behind the CANDIDATE LIST (the balances themselves are live chain reads).\n- coverage.measuringSince — when the index opened, which bounds which tokens could be candidates at all.\n\n### caching\nThe route sets `public, s-maxage=5, stale-while-revalidate=30` — a shared cache may re-serve this body for up to 35s (s-maxage + stale-while-revalidate). What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for. 5s: balances move every block, and a cached wallet page that is wrong about money is worse than a slow one.","parameters":[{"name":"address","in":"path","required":true,"description":"the wallet address, checksummed or lowercase Absent: n/a — it is the path. Not usable: 404 {\"error\":\"not found\"}.","schema":{"type":"string"},"example":"0xf70da97812cb96acdf810712aa562db8dfa3dbef"}],"responses":{"200":{"description":"a read actually happened. Even then, check totals.complete and coverage.capped before treating it as a full answer.\n\n```json\nthe positions body (above)\n```","content":{"application/json":{"example":"{\n  \"address\": \"0xf70da97812CB96acDF810712Aa562db8dfA3dbEF\",\n  \"measuredAt\": \"2026-08-06T16:27:59.096Z\",\n  \"read\": \"ok\",\n  \"ethBalanceWei\": \"306904334439542490385\",\n  \"positions\": [\n    {\n      \"token\": \"0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168\",\n      \"symbol\": \"USDG\", \"decimals\": 6,\n      \"balanceRaw\": \"4746733284247\",\n      \"valueEthWei\": \"40690935590461110003\",\n      \"valuedVia\": \"0x20d39bb3...b12d71\",\n      \"unvaluedReason\": null, \"unvaluedNote\": null\n    }\n  ],\n  \"totals\": {\n    \"ethWei\": \"40690935590461110003\", \"complete\": true,\n    \"valued\": 1, \"unvalued\": 0, \"unreadTokens\": 0\n  },\n  \"coverage\": {\n    \"tokensAsked\": 150, \"tokensKnownToIndex\": 36180,\n    \"tokenCap\": 150, \"valuationCap\": 25, \"capped\": true,\n    \"indexedToBlock\": 29488406,\n    \"measuringSince\": \"2026-08-05T17:29:01.000Z\",\n    \"indexUpdatedAt\": \"2026-08-06T16:21:58.425Z\", \"indexStale\": false\n  },\n  \"basis\": { \"known\": false, \"note\": \"Cost basis and PnL are not derivable...\" },\n  \"valuation\": { \"method\": \"sell-quote-full-balance-single-pool\", \"note\": \"...\" }\n}","x-fletch-example-kind":"captured","x-fletch-example-captured-at":"2026-08-06T16:27:59Z","x-fletch-example-trimmed":"the basis and valuation notes truncated and one pool id abbreviated — so this block is not valid JSON as printed."}}},"404":{"description":"the path segment is not an address.\n\n```json\n{\"error\":\"not found\"}\n```"},"503":{"description":"we measured nothing: no index (so no token list exists at all), the node was unreachable, or the node answered as the WRONG CHAIN. A 200 with positions:[] here would render as \"this wallet holds nothing\" in someone else's UI — the forbidden error class, one hop removed. Generic body by design; branch on the status.\n\n```json\n{\"error\":\"service unavailable\"}\n```"}},"x-fletch-bounds":["THE CANDIDATE SET IS CAPPED AND IT IS NOT RANKED BY THIS WALLET. We ask balanceOf for the top `coverage.tokenCap` tokens (150) by CHAIN-WIDE 24h volume, then all-time volume, then recency — out of coverage.tokensKnownToIndex (36,180 in production). coverage.capped is true whenever the cap bit. A position in a token outside that slice IS NOT RETURNED, and its absence is NOT a claim that the wallet does not hold it. Verified in production: tokensAsked 150 of 36,180.","Only the first `coverage.valuationCap` (25) positions are quoted. Beyond it a position comes back with unvaluedReason \"not-attempted\" — explicitly not priced, never valued at 0.","totals.ethWei is a FLOOR whenever totals.complete is false. complete is true only when every held position carried a quote AND every balance read answered; totals.unvalued and totals.unreadTokens tell you how much is missing on each side.","unvaluedReason distinguishes facts about the token from facts about us: no-pool / refused / out-of-range are about the token or the pool; unavailable / pools-unread / not-attempted are about our read or our cap. Collapsing them is the forbidden error class.","valuation.method is \"sell-quote-full-balance-single-pool\": each value is the ETH a sell of the ENTIRE balance quoted at just now, through ONE pool — the highest-volume ETH-quoted V4 pool we know for that token. Price impact is included; splitting across pools could do better; our fee and gas are NOT deducted. It is a quote in one block, not a settled price.","basis.known is always false. The index stores hourly ETH volume per pool, not this wallet's trades, so no entry price exists to report."],"x-fletch-freshness":["measuredAt (ISO) — when the reads were taken.","coverage.indexUpdatedAt / coverage.indexStale / coverage.indexedToBlock — the index's own freshness behind the CANDIDATE LIST (the balances themselves are live chain reads).","coverage.measuringSince — when the index opened, which bounds which tokens could be candidates at all."],"x-fletch-cache":{"routeHeader":"public, s-maxage=5, stale-while-revalidate=30","delivered":"public (+ Age)","deliveredNote":"What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for.","sharedMaxAgeSecs":5,"staleWhileRevalidateSecs":30},"x-fletch-fields":[{"path":"address / measuredAt","type":"string / string (ISO)","what":"the wallet, checksummed, and when it was measured"},{"path":"read","type":"\"ok\"","what":"always \"ok\" in a 200 — every other value (no-index, rpc-unavailable, wrong-chain) is returned as a 503 instead, because a wallet we measured NOTHING about must make no claim"},{"path":"ethBalanceWei","type":"string | null","what":"native ETH. null = the read did not answer — unknown, not zero."},{"path":"positions[].token / symbol / decimals","type":"string / string | null / integer | null","what":"the identity; decimals null means the balance is only expressible in RAW base units"},{"path":"positions[].balanceRaw","type":"string (raw units)","what":"exact balance in base units. Divide by 10^decimals only when decimals is non-null."},{"path":"positions[].valueEthWei / valuedVia","type":"string | null / string | null","what":"the quoted ETH and WHICH pool quoted it. null = unvalued, and the next two fields say why."},{"path":"positions[].unvaluedReason / unvaluedNote","type":"string | null","what":"the machine-readable reason and the authored sentence behind it"},{"path":"totals","type":"{ethWei, complete, valued, unvalued, unreadTokens}","what":"complete:false ⇒ ethWei is a FLOOR"},{"path":"coverage","type":"{tokensAsked, tokensKnownToIndex, tokenCap, valuationCap, capped, indexedToBlock, measuringSince, indexUpdatedAt, indexStale}","what":"the candidate-set disclosure — read `capped` before treating the list as the wallet's holdings"},{"path":"basis","type":"{known:false, note}","what":"cost basis is not derivable from this index, said out loud"},{"path":"valuation","type":"{method, note}","what":"what a value here IS, in one sentence, so it is never mistaken for a settled price"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/wallet/0xf70da97812cb96acdf810712aa562db8dfa3dbef'"}},"/api/v1/chain/openapi.json":{"get":{"operationId":"openapi","summary":"this document, for machines","description":"An OpenAPI 3.1 description of every endpoint above, generated from the same catalog this page renders — so a client generated from it cannot be generated against a shape the docs do not describe. It carries the caps, the freshness fields and the failure shapes as prose in each operation's description, plus x-fletch-* extensions for tooling that wants them structured.\n\n### bounds — what this response is NOT\n- It describes shapes and semantics, not a schema you can validate a response against field-by-field: response bodies are documented with an example and prose rather than a full JSON Schema, because a half-right schema that a generator trusts is worse than a described shape a human reads.\n- It declares NO security schemes, because we implement none.\n- The response `example` inside each media type is a STRING holding the captured body, not a parsed object — several are trimmed and would not parse. `x-fletch-example-trimmed` is non-null exactly when something was cut, and `x-fletch-example-kind` says whether the block is a real capture or a hand-written sketch. Do not feed these to a validator.\n- This entry is the one thing in the catalog that describes itself rather than a route someone else wrote, so there is no independent capture to check it against. Its own Cache-Control is subject to the same edge behaviour as every other shared-cached route here.\n\n### how to date this payload\n- info.x-verified-at — when the catalog behind this document was last checked field-by-field against production responses. It dates the DOCS, not the data, and it covers the ten DATA endpoints; this entry describes the generator itself.\n\n### caching\nThe route sets `public, s-maxage=3600, stale-while-revalidate=86400` — a shared cache may re-serve this body for up to 90000s (s-maxage + stale-while-revalidate). What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for. This document is static text that changes only on deploy, so it is safe to cache hard. It contains no measurement, so caching it cannot stale a number.","parameters":[],"responses":{"200":{"description":"always — it is generated from a static catalog and cannot fail a read.\n\n```json\nan OpenAPI 3.1 document\n```","content":{"application/json":{"example":"{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"fletch chain read API\",\n    \"version\": \"1.0.0\",\n    \"x-verified-at\": \"2026-08-06T17:36:00Z\",\n    \"description\": \"...\"\n  },\n  \"servers\": [{ \"url\": \"https://www.fletch.bond\" }],\n  \"paths\": { \"/api/v1/chain/board\": { \"get\": { ... } }, ... }\n}","x-fletch-example-kind":"sketch","x-fletch-example-captured-at":null,"x-fletch-example-trimmed":"the document body elided — this is a shape sketch, not valid JSON."}}}},"x-fletch-bounds":["It describes shapes and semantics, not a schema you can validate a response against field-by-field: response bodies are documented with an example and prose rather than a full JSON Schema, because a half-right schema that a generator trusts is worse than a described shape a human reads.","It declares NO security schemes, because we implement none.","The response `example` inside each media type is a STRING holding the captured body, not a parsed object — several are trimmed and would not parse. `x-fletch-example-trimmed` is non-null exactly when something was cut, and `x-fletch-example-kind` says whether the block is a real capture or a hand-written sketch. Do not feed these to a validator.","This entry is the one thing in the catalog that describes itself rather than a route someone else wrote, so there is no independent capture to check it against. Its own Cache-Control is subject to the same edge behaviour as every other shared-cached route here."],"x-fletch-freshness":["info.x-verified-at — when the catalog behind this document was last checked field-by-field against production responses. It dates the DOCS, not the data, and it covers the ten DATA endpoints; this entry describes the generator itself."],"x-fletch-cache":{"routeHeader":"public, s-maxage=3600, stale-while-revalidate=86400","delivered":"public (+ Age)","deliveredNote":"What you actually receive is `Cache-Control: public` plus an `Age` header — measured in production, the edge consumes s-maxage and stale-while-revalidate itself. Age is how you date the frame; the two figures above are what the route asked for.","sharedMaxAgeSecs":3600,"staleWhileRevalidateSecs":86400},"x-fletch-fields":[{"path":"openapi","type":"\"3.1.0\"","what":"the spec version"},{"path":"info","type":"object","what":"title, version, the honesty contract as description, and x-verified-at"},{"path":"servers","type":"array","what":"the served origin (www; the apex 308s to it)"},{"path":"paths","type":"object","what":"one GET operation per endpoint, with parameters, responses and x-fletch-* extensions"}],"x-fletch-example-curl":"curl -s 'https://www.fletch.bond/api/v1/chain/openapi.json'"}}},"x-fletch-authentication":"These endpoints are open, unauthenticated GETs. We issue no API keys, accept no tokens, and have no account tier — so there is nothing to sign up for and nothing that would raise a limit. The application stores nothing about a caller: no key, no account, no identifier, and no record of who asked what. What we cannot claim away is the hosting platform's own request log, which like any host's records the requested URL — so a query string is not a private channel, and an address you would not want in a server log should not be sent in one. Requests to /api/* are excluded from the site's jurisdiction middleware, so a data feed is never handed an HTML notice page.","x-fletch-fair-use":["No rate limit is implemented today. There is no limiter and no quota in front of the endpoints documented here — so treat the numbers below as courtesy, not as a ceiling you have been granted. We reserve the right to add one, and if we do it will return 429 with a Retry-After header rather than a degraded body.","ONE 429 exists already, and it is a capacity refusal rather than a quota: /api/v1/chain/holders/{address} (not documented here) runs a 120,000-block Transfer-log walk measured at 233 upstream JSON-RPC calls when it runs undisturbed and 288–321 when it does not — the count rises with every log range the node makes us bisect, so read it as a floor. Only ONE of those may run at a time on one instance, and a request that finds the slot busy gets 429 + Retry-After with a body naming `walk-capacity` — never a degraded or zeroed measurement. It is a fact about our queue at that instant, not about the token, and it clears in seconds. The cap is one rather than a friendlier number because we measured what two concurrent walks buy: 1,148 calls in one session and 1,185 in another, and in both, zero measurements.","Poll no faster than the data moves. The chainwide indexer writes about every 5 minutes; the board and activity feeds are polled by our own client at 12s and 15s, the chart at 15s, and the stats frame at 60s. Polling the board every second buys you the same frame twelve times.","Use the conditional GET on /board and /candles. Both carry a weak ETag over the measurement; echo it as If-None-Match and an unchanged frame answers 304 with no body. A 304 is a poll that SUCCEEDED — the server re-confirmed the frame is current.","Batch instead of fanning out. /quotes takes up to 100 addresses in one call and /activity?watch= takes up to 60; one batched request is cheaper for both of us than 100 single-token calls.","Cache what is already cacheable. /stats, /pulse, /quotes, /pools, /traders and /wallet ship shared-cache directives; honour the Age header rather than re-requesting inside the window you were given. Measured in production: the edge CONSUMES those directives, so what reaches you is a bare `Cache-Control: public` plus an `Age` — the s-maxage and stale-while-revalidate figures disclosed per endpoint are what the route sets, and Age is how you date the frame you got.","Identify yourself in User-Agent if you are running something sustained, with a way to reach you. We have no way to contact an anonymous heavy caller before we have to make a decision about them.","Attribute the measurement, and carry its disclosures. If you re-publish a figure from here, carry its window and its caveats with it — a floor called a total, or a capped list called a ranking, becomes our credibility problem the moment it has our name on it."],"x-fletch-not-offered":["No CORS headers are sent. Verified in production: responses carry no Access-Control-Allow-Origin, so a browser will block a cross-origin fetch from your page. Call these from your server, or proxy them through your own origin.","No webhooks, no websockets, no server-sent events. Every endpoint is a poll.","No writes and no trading. This is a read API over an index; it holds no keys, signs nothing and cannot move funds. Execution happens in the app, from the user's own wallet.","No cost basis, and therefore no PnL — but read the reason, because half of it IS measured. The attribution table stores, per (token, transaction signer), the ETH that went in across witnessed buys and the ETH that came out across witnessed sells, and /traders serves exactly that as boughtEthWei/soldEthWei/netEthWei. What it does NOT store is token amounts per trade, and without those there is no average cost, no FIFO and no per-unit entry price to report — so /wallet returns basis.known:false rather than a guess. Adding a held bag's mark to a witnessed cash flow is not a fix for that: the index window is hours old, most bags were acquired outside it, and the sum prices the unseen entry at zero. We refuse to publish that number and so should you.","No price on /quotes. That endpoint measures volume only; a plausible-looking derivation (ETH per swap, say) rendered where a price belongs would be a number nothing traded at. Prices come from /candles and /board, per pool.","No 1-minute candles. 5 minutes is the finest grain this index stores; every coarser bar is rolled up from it server-side and says so in aggregatedFromSecs.","No promise that this list is every route under /api. It is every route we DOCUMENT and hold ourselves to. Anything else you find by reading our HTML is an internal surface with no stability contract, no versioning and no notice before it changes shape or disappears — treat an undocumented endpoint as a private one you happened to see."],"x-fletch-contract":[{"title":"a failure is a status, never an empty success","body":"A read that answered NOTHING returns 503. It never returns 200 with an empty list: `rows: []` under a 200 means the index was read and held nothing, which is a claim about the chain; a 503 is a fact about us and makes no claim at all. Branch on the STATUS."},{"title":"…but a 200 can still carry a failed SUB-read, and it is flagged in-band","body":"The status covers the whole read. A subordinate read that failed while the measurement succeeded does NOT take the response down — decoration must never 503 evidence — so it is disclosed as a field instead, and a consumer that ignores these fields will render our outage as a fact about a token. The complete list: `identity:\"unavailable\"` (board, search, activity — names could not be read, rows degrade to addresses); `prices:\"unavailable\"` (search — lastE36 is null because OUR candle read failed, not because there is no candle); `exact:false` (pools — a numeric did not parse, treat every figure in the response as unreadable); `depth.reason:\"read-failed\"` (pools — that one pool's depth read failed); `coverage.indexedSwapsSince:null` (traders — the denominator read failed); `unvaluedReason` of `unavailable`/`pools-unread` and `ethBalanceWei:null` and `totals.unreadTokens > 0` (wallet — our read, not the wallet's holdings); `watch.rows[].eth1hWei:null` (activity — unreadable, where \"0\" would be a measurement); and a `notes[]` entry on candles when the identity index was down. `index:null` is a third thing again: a successful read of a database the worker has never run against."},{"title":"do not branch on the 503 text","body":"Two failure bodies exist. Routes that author their own reason send {\"error\":\"unavailable\",\"reason\":\"<sentence>\"}; routes served through the shared responder send {\"error\":\"service unavailable\"} with the real reason kept in our server log, because these bodies are public and a raw driver error can carry a database host. Both are 503. The reason is for a human to read in your logs, never a value to switch on."},{"title":"every payload carries its measured-at — with one named exception","body":"Each body states when it was measured (serverSec/measuredSec, readAt, snapshot.atSec or measuredAt) and, where an index stands behind it, when that index last advanced (index.updatedSec, updatedAgoSec) plus the threshold behind the stale verdict (staleSec). The stale verdict is decided server-side so a wrong client clock cannot vote; a client may only ADD its own elapsed time on top. THE EXCEPTION IS /quotes: it carries no body-level clock at all — only index.updatedAt, which dates the INDEX and not the response. Date a /quotes frame from the Age response header plus index.updatedAt, and do not assume it was built when it arrived."},{"title":"unmeasurable is not zero — but read the flag, because the wire still says 0","body":"A figure we could not READ is null or is flagged unmeasured, never 0: a V4 pool's depth is `measured:false, reason:\"v4-singleton\"` because the singleton PoolManager makes per-pool reserves unreadable, and a wei value that did not parse exactly fails the response rather than becoming a 0. A figure that is structurally UNMEASURABLE is the case to watch: a token with no ETH-side pool carries `ethQuoted:false`, and its ETH volume crosses the wire as the string \"0\" beside that flag, because the aggregate genuinely sums to zero. So the flag is the measurement and the 0 is an artefact — read ethQuoted (board, search, pulse, pools) or the depth `reason` BEFORE you render any zero. /quotes is the trap: it carries no such flag, so resolve its zeroes elsewhere."},{"title":"wei are decimal strings — and /candles is the one place they are not","body":"Every wei quantity crosses the wire as a decimal string, because numeric(78,0) does not fit an IEEE double and a whale total that lost its low-order digits is a wrong whale total. Parse with BigInt, never Number. Prices are exact integers too where they are served as such — /board and /search carry lastE36, wei of ETH per RAW token unit x 1e36, decimals-free on purpose. THE EXCEPTION IS /candles: its o/h/l/c are doubles converted at that boundary, and its `eth` is ETH (not wei) as a double floored to 6 decimals. Chart with them, never settle with them."},{"title":"coverage is disclosed, and absence is not evidence","body":"Every list here is bounded, and the bound rides in the payload (limit, rowBound, totalPools, total, coverage.*, queried). A token missing from a response was either measured and absent, or never asked about — and those are different facts, which is why `queried` exists on the batch endpoints. Absence from our index is a fact about our coverage window, never proof about the chain."},{"title":"identity is decoration","body":"symbol, name and image are strings the token deployer wrote. They are sanitised server-side, they rank nothing, and they are not evidence of anything. The address is the only unforgeable field on a row. `image` is null whenever we hold a URL we will not load (imageState:\"offsite\") — loading an arbitrary host would beacon a viewer's IP to whoever deployed the token."},{"title":"no scores, no badges, no placement","body":"Nothing in this API ranks by anything a token deployer or a payer can influence. There is no safety score, no rating, no sponsored slot, and no field that could become one. Ordering is measured volume, measured recency, or a stated tier — and every response echoes the order it applied."}]}