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.

Version
1.4
Updated
2026-09-26
Source
Public Data API routes · Public Data API library · Metric definitions v1.2

The API#

At a glance
Base path/api/v1, served from this origin
Endpoints19. Every one answers GET and OPTIONS, and nothing else.
AuthenticationNone. An X-API-Key header raises the rate limit and gates nothing.
ChainOne per deployment. There is no chainId parameter; the chain that answered is freshness.chainId.
FormatsJSON. ?format=csv on the twelve tabular endpoints.
CORSAccess-Control-Allow-Origin: * on every response. Credentials are never allowed.
Where the figures come fromThe indexer’s stable views, over a read-only connection. Nothing is computed in the application.
Stored data only
The APIRule
ServesOnly data already stored in The Hood’s own database — the indexer’s views, read over a read-only connection
Never callsA 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 missData 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 exposesOur provider keys, endpoints or their error text — in a body, a header or an error
X-API-KeyA key for this API’s rate limit. It unlocks no provider and no extra data

Versioning#

What is versioned, and how a change reaches you
SubjectRule
The versionIn the path — /api/v1. There is no version header, no query parameter and no content negotiation.
Adding a fieldOrdinary, and not announced. Parse defensively: treat an unknown key as data you do not use yet, never as an error.
Adding an error codeOrdinary. Branch on the codes you handle and fall through on the rest rather than matching exhaustively.
Changing or removing an error codeBreaking, because a consumer is already branching on it.
Renaming or removing a fieldBreaking.
The underlying view schemaReported 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, quieter

Convert 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.

thresholdsBlocks
FieldValueWhat it marks
incident3,00046% of the window. Where an operator is paged
degraded4,370Two thirds of the window
critical, stateWindow6,555The measured state-retention window. Past it, nothing is re-derivable
stateWindowMeasuredOnChainId46630The chain the 6,555 figure was measured on
stateWindowMeasuredOn2026-08-06, by binary search against the testnet RPCWhen, and how
stateWindowMeasuredOnThisChainbooleanWhether the measurement was taken on the chain that just answered you

CORS, caching and conditional requests#

CORS
HeaderValue
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-methodsGET, HEAD, OPTIONS
access-control-allow-headerscontent-type, x-api-key
access-control-expose-headersetag, the four rate-limit headers, x-thehood-as-of-block, x-thehood-coverage
CredentialsNever allowed. access-control-allow-credentials is not sent at all, so no future edit can make the wildcard unsafe
Caching and conditional requests
HeaderValue
cache-controlpublic, max-age=0, s-maxage=<n>, stale-while-revalidate=30. The browser always revalidates; the CDN serves one answer to everybody for <n> seconds
etagA 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-typeapplication/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 }] }'