Docs·Public Data API
Public Data API
Public Data API
Twenty read-only endpoints under /api/v1: the response envelope, the versioning contract, and the two conventions — base-unit strings and block-denominated freshness — a consumer gets wrong first.
The API#
| Base path | /api/v1, served from this origin |
|---|---|
| Endpoints | 19. Every one answers GET and OPTIONS, and nothing else. |
| Authentication | None. An X-API-Key header raises the rate limit and gates nothing. |
| Chain | One per deployment. There is no chainId parameter; the chain that answered is freshness.chainId. |
| Formats | JSON. ?format=csv on the twelve tabular endpoints. |
| CORS | Access-Control-Allow-Origin: * on every response. Credentials are never allowed. |
| Where the figures come from | The indexer’s stable views, over a read-only connection. Nothing is computed in the application. |
| The API | Rule |
|---|---|
| Serves | Only data already stored in The Hood’s own database — the indexer’s views, read over a read-only connection |
| Never calls | A provider on your behalf — no RPC node, price feed, explorer or social API is reached by any /api/v1 request. Chain head is never read |
| Never fetches on a miss | Data we have not stored is refused with API_V1_DATA_NOT_STORED, naming what is stored. It is not fetched, back-filled or proxied |
| Never exposes | Our provider keys, endpoints or their error text — in a body, a header or an error |
X-API-Key | A key for this API’s rate limit. It unlocks no provider and no extra data |
Versioning#
| Subject | Rule |
|---|---|
| The version | In the path — /api/v1. There is no version header, no query parameter and no content negotiation. |
| Adding a field | Ordinary, and not announced. Parse defensively: treat an unknown key as data you do not use yet, never as an error. |
Adding an error code | Ordinary. Branch on the codes you handle and fall through on the rest rather than matching exhaustively. |
Changing or removing an error code | Breaking, because a consumer is already branching on it. |
| Renaming or removing a field | Breaking. |
| The underlying view schema | Reported per response as freshness.viewsSchema. It names the database schema the figures were read from, so a response can be traced to the shape that produced it. |
The response envelope#
{
"data": { ... }, // the endpoint's own body
"freshness": { ... }, // on every response, always
"pagination": { // only on a paged endpoint
"cursor": null, // the cursor this page was read at
"next": "eyJxIjoi..." // the cursor for the next page, or null on the last
}
}data is an object rather than a bare array on every endpoint, including the list ones — a list endpoint puts its rows under a named key (launches, markets, trades, days, and so on) alongside the filters that produced them. On failure the body is the error shape instead, and no envelope is sent:
{
"error": "The Hood's public data API cannot use `limit` = \"9000\". It must be a whole number between 1 and 500; the default is 50. …",
"code": "API_V1_BAD_PARAMETER",
"details": { "parameter": "limit", "received": "9000", "min": 1, "max": 500, "fallback": 50 }
}error is the message to show a human, code is the stable string to branch on, and details carries the same subject and values as machine-readable fields. All three are sent verbatim — nothing between the failing layer and the response substitutes a generic string for a specific one, so a client that renders its own fallback over error is discarding the only useful part.
Amounts are decimal strings#
Every wei and token-base-unit quantity is a decimal string, never a JSON number. That is the entire reason the API returns strings: JSON.stringify throws on a bigint, and the obvious repair — Number(wei) — is silently lossy above 253, which is roughly 0.009 ETH. Every figure published here is larger than that.
// Right — exact, at any magnitude.
const raised = BigInt(launch.stats.netRaisedWei) // 1234567890123456789012n
// Wrong — silently rounded, and unrecoverable afterwards.
const raised = Number(launch.stats.netRaisedWei) // 1.2345678901234568e+21
const raised = parseFloat(launch.stats.netRaisedWei) // the same loss, quieterConvert to a display string at the edge, from the bigint, and never in between. A value that arrives from the database as a float is refused rather than published: precision was lost before this API saw it, and publishing it would state a quantity that is not any amount of anything. A bigint that reaches the serialiser throws for the mirror-image reason.
Counts (trades, holders, stakes) and basis-point fields are ordinary JSON numbers — they are bounded and safe. Timestamps are decimal strings of seconds, with an ISO rendering beside them.
Blocks, not minutes#
Every threshold in this API is defined as a block count. Because chain head is never read, status compares the stored checkpoint’s age with these counts at the chain’s measured block time — see freshness. The public RPC prunes state past a fixed number of blocks, and past that line an eth_call at the block in question fails outright — it is a cliff, not a slope, and a threshold in minutes hides that because block time differs between the two chains by a factor of 2.7 and moves under load.
| Field | Value | What it marks |
|---|---|---|
incident | 3,000 | 46% of the window. Where an operator is paged |
degraded | 4,370 | Two thirds of the window |
critical, stateWindow | 6,555 | The measured state-retention window. Past it, nothing is re-derivable |
stateWindowMeasuredOnChainId | 46630 | The chain the 6,555 figure was measured on |
stateWindowMeasuredOn | 2026-08-06, by binary search against the testnet RPC | When, and how |
stateWindowMeasuredOnThisChain | boolean | Whether the measurement was taken on the chain that just answered you |
CORS, caching and conditional requests#
| Header | Value |
|---|---|
access-control-allow-origin | *. Every route is a read of public chain data, no route reads a cookie, and no route has a side effect |
access-control-allow-methods | GET, HEAD, OPTIONS |
access-control-allow-headers | content-type, x-api-key |
access-control-expose-headers | etag, the four rate-limit headers, x-thehood-as-of-block, x-thehood-coverage |
| Credentials | Never allowed. access-control-allow-credentials is not sent at all, so no future edit can make the wildcard unsafe |
| Header | Value |
|---|---|
cache-control | public, max-age=0, s-maxage=<n>, stale-while-revalidate=30. The browser always revalidates; the CDN serves one answer to everybody for <n> seconds |
etag | A strong hash of the exact bytes served. Send it back as If-None-Match and an unchanged response costs a 304 rather than a repeated database read |
content-type | application/json; charset=utf-8, or text/csv; charset=utf-8 |
# Everything an integration needs to start, in three requests.
curl -s <origin>/api/v1/health | jq '{ chainId, environment, factories, indexed, readiness }'
curl -s <origin>/api/v1/metrics | jq '.data.metrics[] | { id, unit, derivation }'
curl -s '<origin>/api/v1/launches?lifecycle=CURVE_TRADING&sort=VOLUME&limit=10' \
| jq '{ asOf: .freshness.asOfBlock, coverage: .freshness.coverage, scope: .freshness.scope,
launches: [.data.launches[] | { token, forceName, raised: .stats.netRaisedWei }] }'