API docs
pegproof reconstructs, from the chain logs, the issuance ledger of the tokenized Robinhood tokens and publishes what it found — including its limits. This page is the contract: what each field means, what it does not promise, and what each route costs.
Versioning
Paid routes are prefixed /api/v1; the free citable-series routes are under /v1 (see the dedicated section) and carry their own schema_version. A breaking change gets a new prefix (/api/v2); /api/v1 is never changed retroactively under an existing consumer.
Errors
While the public surface is stopped, every route answers with a 503 JSON { "error": "..." } (with a Retry-After header), whether that route normally answers with JSON or HTML — it is a stop of the whole public surface, not an error specific to one route. Outside that case, the shape of an error depends on the kind of route:
- JSON data routes (
/health,/v1/series*,/v1/address/{address}and the paid/api/v1/*routes) answer any error with JSON of the form{ "error": "..." }and the correct status code. - HTML pages (
/,/t/{ticker|address}) answer an error with a readable HTML page, never with JSON — for example a404with suggestions for an unknown ticker: someone who mistyped a ticker deserves a readable page, not a JSON blob.
In both cases the status code is meaningful and can be checked by a script regardless of the body shape: 200 success, 404 does not exist, 503 public surface temporarily stopped, 402 payment required (paid routes).
Token resolution
A token is identified by ticker or by its exact address — never by an internal numeric id, which is not a stable public identifier. An ambiguous ticker (used by several tokens) returns 200 with candidates; an unknown ticker returns 404 with suggestions. On the paid routes, a parameter that is not exactly a ticker or a 0x address of 40 hex digits (surrounding whitespace or control characters included) returns 400, unbilled, and is never trimmed into a match. A known look-alike is never resolved by ticker; its classification is visible for free only at its exact address. impostor in the API is a look-alike on the pages; unknown means the contract's identity is not resolved.
Common envelope
Every data route (free or paid) wraps its result in the same envelope:
{
"data": ...,
"state_as_of_block": number | null,
"state_as_of_time": string | null,
"ledger_as_of_block": number,
"ledger_coverage": { "h24": "complete" | "partial" | "gap", "d7": "complete" | "partial" | "gap" },
"market_state": "open" | "closed" | "holiday",
"flags": string[]
}
| Field | What it means |
|---|---|
data | the route's own payload — the exact shape differs per route (see the tables below). |
state_as_of_blockstate_as_of_time | the last confirmed on-chain balance (the most recent successful snapshot) for this token — null when the token is not measured yet, never an invented value in place of null. |
ledger_as_of_block | the block up to which this token's events are included in the aggregates (events_24h/events_7d/ledger_coverage). It can trail the newest event already recorded for the token, so it never promises more coverage than the aggregates have at this moment. |
ledger_coverage | see the dedicated section below — it is not a freshness guarantee. |
market_state | open / closed / holiday — an independent market-state axis; the market can be closed even when collection is perfectly up to date, and it never masks a real interruption in ledger_coverage or flags. |
flags | the active flags on this token — an empty list when there are none; factual vocabulary, never an accusation. |
Why there are two separate "as of" values, not one: state_as_of_* describes the last successful balance snapshot; ledger_as_of_block describes how far the token's events are included in the aggregates. The two advance independently, at different rates — a consumer who conflates them may believe the event ledger covers more than the published aggregates actually cover at that moment.
The kind enum
Every event in the ledger has a kind from the fixed vocabulary — the same word appears on the public pages:
kind | Word used on the pages |
|---|---|
mint | minted |
issue | issued |
burn | burned |
redeem | redeemed |
transfer | not shown: an ordinary transfer between wallets, not issuance |
| any other value | other: a kind added later that the pages do not know yet |
Units
All amounts are in raw units, with decimals=18 (like any standard ERC20 contract) — never pre-divided by the decimals before being put in data. ui_multiplier is scaled by 1e18 (fixed point): a ui_multiplier of 1000000000000000000 means a multiplier of 1.0×. The exact formula for the shares outstanding shown on the public pages:
shares outstanding = total_supply × ui_multiplier / 1e18
What ledger_coverage promises and what it does not promise
Coverage compares the token's ledger_as_of_block with the chain head as the collector last observed it, not with how far collection has read: collection that has stalled cannot make its own backlog look complete. complete tolerates about one hour of lag (36,000 blocks, at this chain's nominal ~0.1s/block) between that head and ledger_as_of_block; beyond it the value is partial. If the collector could not read the chain head at all, coverage is partial too: a ledger that could not be compared with the chain is never called complete. gap means an unresolved interruption that overlaps the window in question (24h or 7 days) and takes precedence over both.
ledger_coverage: complete is not a freshness guarantee. It only means "within the tolerance of the chain head the collector last saw", not "updated right now". Real freshness is read from the dated figures in the envelope — state_as_of_time, ledger_as_of_block — plus the block lag computed from them; never from the word complete alone.
CORS, cache, ETag
Every free GET route answers with Access-Control-Allow-Origin: * — it can be consumed directly from a browser, from any origin, with no intermediate proxy. Every free response also carries a route-specific Cache-Control (the number of seconds reflects how often that route's data actually changes) plus a stable ETag over the serialized body: the same request repeated with an If-None-Match header gets a bodiless 304 instead of the same bytes being resent. A 304 response does not resend Cache-Control (standard HTTP behavior) — a consumer reading directly from fetch(), without an intermediate HTTP cache that keeps the original value, must retain the Cache-Control from the last 200 response rather than expect it on every 304. Paid routes are never answered 304: a paid request always gets the body and its payment receipt.
Free routes
| Route | Cache-Control |
|---|---|
GET / | 60s |
GET /tokens.csv | 300s |
GET /reports | 300s |
GET /reports/{week} | 3600s (300s while not published) |
GET /reports/{week}.csv | 300s |
GET /t/{ticker|address} | 60s |
GET /t/{ticker|address}/series.csv | 300s |
GET /t/{ticker|address}/verify.sh | 300s |
GET /v1/address/{address} | 3600s |
GET /health | 60s |
GET /status | 300s |
GET /docs | 3600s |
GET /methodology | 3600s |
GET /methodology.md | 3600s (the same document as Markdown) |
GET /llms.txt | 3600s (a summary for language models, llmstxt.org format) |
GET /openapi.json | 3600s (the paid routes and the address lookup, OpenAPI 3.1 with x402 pricing) |
GET /.well-known/x402 | 3600s (the paid resources as JSON, x402scan's discovery format) |
GET /search?q= | — (a redirect to /t/{q}, never cached) |
GET /registry.json, GET /registry.csv, GET /robots.txt | — (static files with their own cache headers) |
/reports/{week}serves the frozen report as stored, never recomputed. A week with no report answers200"Not published" with the reason; a malformed or impossible name is a404./t/{ticker|address}/verify.shis a POSIX sh script (curl and bc only) that re-reads the token's supply, multiplier, price and paused flag witheth_callat the block the page read them and prints MATCH or DIFFERS for each, or NOT CHECKED when the node gives no answer. Then it prints the shares and the value estimate they give. Plain text; run it withcurl -fsSL …/verify.sh | sh, or read it first.
Citable series (free)
Two free routes — no payment, no account — that publish the daily history of the token supply, made to be cited. They live under /v1/series, not under /api/v1/: the /api/v1/* routes are the paid ones (x402), and these are never billed.
| Route | Content | Cache-Control |
|---|---|---|
GET /v1/series | manifest: every tracked token — ticker, address, state_as_of_block, state_as_of_time, ledger_coverage, events_24h, events_7d, flags, history (the token's full-history check: status match / mismatch / ledger-behind, diff, checked_at, or null before the first check). Built from the per-token summaries; the response grows with the number of tokens. In the envelope, ledger_as_of_block and ledger_coverage describe the weakest token; state_as_of_* are null. | 3600s |
GET /v1/series/{ticker|address}?limit=N | a token's snapshot history, newest first: ts, block, total_supply, treasury_balance, ui_multiplier, finalized, plus the token's history. limit: default 400, maximum 400 — above the maximum it is cut to 400, not rejected; a non-numeric value or one <1 becomes 1, as on the paid routes. | 3600s |
finalized appears on every point and is never hidden: a snapshot with finalized: false can still change — whoever cites a figure must see that. The token is resolved as on the paid routes: ticker or exact address, a 404 JSON { "error": "..." } for an unknown token or a look-alike, 200 with candidates for an ambiguous ticker, 200 with an empty series for a token that is not processed yet.
The citation block — present only on these two routes, added to the common envelope, so that a figure copied elsewhere keeps its provenance:
"citation": {
"schema_version": "1",
"license": "CC-BY-4.0",
"publisher": "pegproof",
"canonical_url": "...", // the request URL, without the query string
"cite_as": "...", // ready-to-paste citation: publisher, series, date (UTC), URL
"methodology_url": "...", // the methodology, /methodology
"source": "..." // figures are read directly from Robinhood Chain logs
}
schema_version is a string and changes only on a breaking change; an added field does not change it. The date in cite_as is the day the body was generated — a cached response can be up to an hour old.
Address lookup (free)
GET /v1/address/{address} says whether one Robinhood Chain address is a token in the pegproof registry (Robinhood's own Stock Token list, checked on-chain), a known look-alike, or neither, and whether another issuer of stock tokens lists it in its own published contract list (today Ondo Global Markets). Free, no account; the status is answered from the registry alone. Every well-formed address gets a 200; anything other than 0x followed by 40 hex digits (either case) is a 400 { "error": "..." }, never trimmed into a match. The status changes only when the registry is rebuilt; Cache-Control is 3600s.
| Field | What it means |
|---|---|
address | the address asked about, in checksummed mixed case (/registry.json lists addresses in lowercase). |
chain_id | always 4663, Robinhood Chain. |
status | official: the address is a token in the pegproof registry. look-alike: another contract whose symbol is a registry ticker and that pegproof's catalogue classifies as an imitation of a registry token, or a contract that copies the name and symbol of a token another issuer lists (see imitates). unknown: anything else — not in the registry, and either not in the catalogue or in it without that classification. Another issuer's token is unknown too, since it is not a Robinhood Stock Token; issuer_list says who lists it. |
catalogued | unknown only: true when the address is in pegproof's look-alike catalogue but not classified (its identity is not resolved; its page says what is known), false when it is in neither list. |
ticker, name | for a registry token, the registry's ticker and name; for a catalogued address (look-alike, or unknown with catalogued: true), the ticker and name the contract itself reports on-chain, as catalogued; null when catalogued is false. |
imitates | look-alikes only: the token it copies, { "ticker", "address" } for a registry token; for another issuer's token it also has issuer (such as "ondo"), and ticker and address are as that issuer lists them. No issuer means a registry token. |
issuer_list | only on an unknown address that another issuer lists in its own published contract list for Robinhood Chain: issuer, the ticker and name as listed, source (the list) and as_of (the date pegproof copied it and checked each address on-chain). pegproof does not check that token's supply; absent on every other answer. |
registry_as_of | date the registry data was last rebuilt: the as_of of the registry build that answered, the same value as in /registry.json. It is not when the registry was last compared with Robinhood's list; see the next field. |
robinhood_list_checked | the daily comparison of the registry with Robinhood's published Stock Token list: at, the UTC timestamp of the last successful comparison, or null if none has succeeded yet, and consecutive_failures, the daily checks that have failed since. null when no check has been recorded. The examples below show a sample value. |
source | the registry file this answer comes from. |
page | registry tokens, catalogued addresses and addresses with issuer_list only: the address's page on https://pegproof.com. |
note | unknown only: what that status does and does not mean. |
Fields are only added, never renamed or removed, within /v1. A registry token:
{
"address": "0x232B8ed6377BE97813853B0Ac104c4Cda8378d1B",
"chain_id": 4663,
"status": "official",
"ticker": "ADBE",
"name": "Adobe • Robinhood Token",
"registry_as_of": "2026-10-29",
"robinhood_list_checked": {
"at": "2026-10-29T00:05:12.000Z",
"consecutive_failures": 0
},
"source": "https://pegproof.com/registry.json",
"page": "https://pegproof.com/t/0x232b8ed6377be97813853b0ac104c4cda8378d1b"
}
A look-alike:
{
"address": "0xDeCF74e4AA6fF30b1612e65665AAF650BEdecbA3",
"chain_id": 4663,
"status": "look-alike",
"ticker": "NVDA",
"name": "NVDA",
"imitates": {
"ticker": "NVDA",
"address": "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
},
"registry_as_of": "2026-10-29",
"robinhood_list_checked": {
"at": "2026-10-29T00:05:12.000Z",
"consecutive_failures": 0
},
"source": "https://pegproof.com/registry.json",
"page": "https://pegproof.com/t/0xdecf74e4aa6ff30b1612e65665aaf650bedecba3"
}
A catalogued address that is not classified:
{
"address": "0x72EAd0C700F07B645a8f91badd82be5C545b9D37",
"chain_id": 4663,
"status": "unknown",
"catalogued": true,
"ticker": "NVDA",
"name": "NVIDIA Robinhood Coin",
"registry_as_of": "2026-10-29",
"robinhood_list_checked": {
"at": "2026-10-29T00:05:12.000Z",
"consecutive_failures": 0
},
"source": "https://pegproof.com/registry.json",
"page": "https://pegproof.com/t/0x72ead0c700f07b645a8f91badd82be5c545b9d37",
"note": "this address uses a tracked ticker or name and is in pegproof's look-alike catalogue, but it has not been classified; it is not in pegproof's registry."
}
An address in neither list:
{
"address": "0x1111111111111111111111111111111111111111",
"chain_id": 4663,
"status": "unknown",
"catalogued": false,
"ticker": null,
"name": null,
"registry_as_of": "2026-10-29",
"robinhood_list_checked": {
"at": "2026-10-29T00:05:12.000Z",
"consecutive_failures": 0
},
"source": "https://pegproof.com/registry.json",
"note": "unknown means this address is not in pegproof's registry or its list of known look-alikes; it says nothing else about the contract."
}
Another issuer's token (Ondo's NVDAon):
{
"address": "0xf1b95AE4E7700b962714702EA53784Dc8492a856",
"chain_id": 4663,
"status": "unknown",
"catalogued": false,
"ticker": null,
"name": null,
"issuer_list": {
"issuer": "ondo",
"ticker": "NVDAon",
"name": "NVIDIA (Ondo Tokenized)",
"source": "https://docs.ondo.finance/addresses",
"as_of": "2026-10-28"
},
"registry_as_of": "2026-10-29",
"robinhood_list_checked": {
"at": "2026-10-29T00:05:12.000Z",
"consecutive_failures": 0
},
"source": "https://pegproof.com/registry.json",
"page": "https://pegproof.com/t/0xf1b95ae4e7700b962714702ea53784dc8492a856",
"note": "this address is not in pegproof's registry of Robinhood Stock Tokens; issuer_list names the other issuer whose own published contract list includes it, as of the date given. pegproof does not check that token's supply."
}
A copy of another issuer's token (Ondo's BMNRon):
{
"address": "0x05d8e73787AcD02C70A9C43dD6f0443E3b8D3D77",
"chain_id": 4663,
"status": "look-alike",
"ticker": "BMNRon",
"name": "BitMine Immersion Technologies (Ondo Tokenized)",
"imitates": {
"ticker": "BMNRon",
"address": "0x900F8970d2fAe8060D06bB29234F629C837c3383",
"issuer": "ondo"
},
"registry_as_of": "2026-10-29",
"robinhood_list_checked": {
"at": "2026-10-29T00:05:12.000Z",
"consecutive_failures": 0
},
"source": "https://pegproof.com/registry.json",
"page": "https://pegproof.com/t/0x05d8e73787acd02c70a9c43dd6f0443e3b8d3d77"
}
Labelling many tokens: cache /registry.json and check membership locally; use /v1/address for single lookups. /registry.json lists the registry tokens only (lowercase address, ticker, name, and status, which there means the token's listing state, such as active); look-alikes are not in it, so an address that is not in /registry.json may still be a catalogued look-alike, and /v1/address tells the two apart.
Paid routes (x402): the price contract
Payable per request through the x402 protocol (version 2) — no account, no API key: a request without the PAYMENT-SIGNATURE header gets a 402 with the payment terms in its PAYMENT-REQUIRED header, never partial data. Payment is accepted in USDC on Base or on Arbitrum One, at the same price on both; the receipt comes back in the PAYMENT-RESPONSE header. x402 version 1 clients (an X-PAYMENT header) are not accepted. Paid routes answer GET only: any other method gets a 405 with Allow: GET, OPTIONS, and a browser's CORS preflight is answered without a charge. Every response on a paid route, errors included, carries Access-Control-Allow-Origin: * and exposes the PAYMENT-REQUIRED, PAYMENT-RESPONSE and Retry-After headers to browser scripts. Requests that carry a PAYMENT-SIGNATURE are limited to 100 per minute per client IP, counted separately from the free routes' limit: over it they get a 429 with Retry-After before any verification, so nothing is charged. A request without payment (the 402) is not limited. The same facts as the free pages and /registry.json, sold per call in structured form.
| Route | Content | Price |
|---|---|---|
GET /api/v1/token/{address|ticker} | the summary row, and the look-alike classification if the address is one | $0.002 |
GET /api/v1/token/{...}/events?limit≤100&cursor= | events keyset-paginated on (block, log_index), opaque cursor | $0.005 |
GET /api/v1/incidents?limit≤100&type= | factual feed of integrity incidents; type=integrity (default: R0 divergences and rebaselines), operational (collector errors) or all; an unknown value gets 400, not the default | $0.005 |
GET /api/v1/snapshot/{...}?block=N | snapshot ≤ block N | $0.01 |
A response without the full data can still be billed in full — only 402/404/400 are never billed. A request for an ambiguous ticker, on any of the three per-token routes above, returns 200 with the list of candidates (data.candidates[]) — a real response, not an error — and is billed in full. On GET /api/v1/token/{...}, a known look-alike address still returns 200, with its classification, and is billed the same; on /events and /snapshot, the same address returns 404 instead, unbilled — this project tracks no event ledger or balance history for a look-alike. A real, known token that is not processed yet — or a snapshot request for which no confirmed row exists at or before the requested block — also returns 200, with no financial data (events: [], snapshot: null, etc.), and is billed in full.
What /health reports
/health reports live / degraded / paused from the age of the data, not from how often collection succeeds. It is also degraded while a declared ledger gap overlaps the last seven days of blocks; open_gaps counts those gaps.