HartiiLabs docs

HartiiLabs

Base URL: https://hartiilabs.com. All routes below are functions/api/** (Cloudflare Pages Functions) backed by D1. There are no API keys — every route documented as "public" is unauthenticated and free to call.

Conventions

  • All monetary/token amounts are wei-scaled decimal strings, e.g. "raisedWei": "165825527731240739544166". 1 QUAI = 10^18 wei, same scale as ETH. These are too large for a JS number past ~15 digits — parse with BigInt(value), not Number(value). A handful of fields break this convention on purpose and are called out explicitly below (wallet/:wallet/stats's volumeQuai, and chart sparkline arrays, which are QUAI-scaled floats for plotting only — never treat them as exact).
  • CORS is wide open: every JSON route responds Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type, and answers OPTIONS with 204. (GET /api/quai-price is the one exception — see its entry.) Verified live:
    text
    curl -sI -H "Origin: https://example.com" https://hartiilabs.com/api/burns
    # Access-Control-Allow-Origin: *   (same for every Origin — there is no allowlist)
  • Caching: every route sets an explicit Cache-Control (see each entry) and most are also CDN-cached (cf-cache-status: HIT/MISS/DYNAMIC in response headers) — polling faster than a route's max-age just re-serves the same cached body.
  • Errors are honest, not fabricated. A route that cannot reach D1 answers 503 { "error": "temporarily unavailable", "retryable": true } with Cache-Control: no-store — never a fake empty/zero result. A route that reads D1 successfully and finds nothing answers 200 with a genuinely empty shape (items: [], "totalWei": null, etc.) — that is a final answer, not a loading state. A hidden/moderated token is indistinguishable from one that was never indexed: every read path for it 404s or empty-shapes exactly like "doesn't exist."
  • No rate limiting on read routes. functions/_lib/rateLimit.js exists and is wired into the signed write endpoints (comments, reactions, flags, metadata edits — see below), but none of the GET/read routes in this reference are rate-limited. Be a good citizen anyway — see Integration recipes.
  • Network is always "mainnet" — there is no other network to select.

Tokens

GET /api/tokens

Launchpad directory/feed — same endpoint the site's board uses.

Query paramTypeDefaultNotes
sortstringtrendingOne of trending, new, volume, price, change, marketcap, graduation, holders, age, latest; unknown values silently fall back to trending.
limitint50Clamped 1–100.
cursorstringnoneOffset-based; the opaque string from a previous response's nextCursor.
viewstringgridgrid or table — echoed back, a client display hint only.
spark1 to enableoffAttaches sparkline (7-day window of 4h candle closes, QUAI-scaled floats, oldest first) to each item. Empty array if the token has no recent candles — never a fabricated flat line.

trending and volume (and the other ranked sorts) are computed by re-ranking an in-process directory snapshot rather than a plain ORDER BY — trending sorts on the indexer-maintained trendingScore (written by the indexer's summary pass, not computed per-request), volume on volume24hWei. These ranked results are memoized per-isolate for 60 seconds.

Caching: ranked sorts (trending, volume, price, change, marketcap, graduation, holders, age, latest) get public, max-age=60, s-maxage=60; the base path gets the default public, max-age=15, s-maxage=15. No DB binding → 200 with items: [], partial: true. D1 refusal → 503 { "error": "temporarily unavailable", "retryable": true }, no-store.

Example (GET /api/tokens?limit=2, trimmed, one item):

json
{
  "network": "mainnet",
  "view": "grid",
  "items": [{
    "network": "mainnet",
    "address": "0x0035187a7660f595d93cd53a4d16c635d6cffc8f",
    "creator": "0x0011111111111111111111111111111111111111",
    "name": "QuaiAxe",
    "symbol": "QAXE",
    "curveAddress": "0x004bc407903a51506bcf0b1ab423958c5991c237",
    "launchTx": "0x004a0045...",
    "launchBlock": 9955113,
    "launchedAt": "2026-09-06T13:09:12.447Z",
    "supply": null,
    "totalSupplyWei": "877074085029199910587818112",
    "burnedWei": "122925914970800089412181888",
    "meta": { "description": "...", "socials": { "website": "...", "twitter": "...", "telegram": "...", "discord": "" }, "logoKv": true, "logoCid": "Qme..." },
    "logoUrl": "/api/token-logo/0x0035187a...?v=2026-09-21T20%3A54%3A33.773Z",
    "description": "...",
    "socials": { "website": "...", "twitter": "...", "telegram": "...", "discord": "" },
    "style": null,
    "status": "graduated",
    "updatedAt": "2026-09-21T20:54:33.773Z",
    "lastPriceWei": "3042142570835435",
    "raisedWei": "0",
    "tokensSold": "784000000000000000000000000",
    "curveSupply": "784000000000000000000000000",
    "poolQuaiWei": "165825527731240739544166",
    "volume24hWei": "59620483275532255828565",
    "tradeCount24h": 25,
    "change24h": 44.51,
    "priceChange24h": 44.51,
    "trendingScore": 178861.44982659677,
    "marketCapWei": "2668184411843867129265764",
    "holderCount": 134,
    "ageSeconds": 1474080,
    "source": "hartii-launchpad",
    "lastTradeAt": "2026-09-23T07:48:32.368Z"
  }],
  "nextCursor": "2",
  "partial": false,
  "at": "2026-09-23T14:37:32.135Z"
}

Field notes (per item):

FieldTypeNotes
address, creator, curveAddressstringLowercase 0x… addresses.
statusstringactive or graduated.
supplystring|nullMinted supply as recorded at launch; null for every current token. Use totalSupplyWei.
totalSupplyWeistring|nullLive total supply = 1,000,000,000 minted − burnedWei. Matches token.totalSupply() on chain to the wei. Use this for supply and market cap — never curveSupply.
burnedWeistring|nullAll-time burns; null means never computed, not zero.
lastPriceWei, raisedWei, tokensSold, curveSupply, poolQuaiWei, volume24hWeistring|null (wei)null until the indexer's summary pass has run for this token.
marketCapWeistring|null (wei)lastPriceWei × totalSupplyWei / 1e18. null only while lastPriceWei is unknown. See Pricing a token.
tradeCount24hint|null
change24h / priceChange24hnumber|nullPercent, same value under both keys.
trendingScorenumber|nullWash-resistant score, see Indexer.
holderCountint|nullDistinct addresses with non-zero balance.
ageSecondsint|nullDerived from launchedAt at response time.
logoUrlstring|nullRelative path to GET /api/token-logo/:addr; null if no logo set.
metaobject|nullRaw stored metadata blob; description/socials/style are also hoisted to top level for convenience.

GET /api/token/:addressOrTicker

Single token detail. :addressOrTicker is either a 0x + 40-hex address or a ticker symbol (/api/token/HRT works) — first-launched token owns a symbol, first-come-first-served; later tokens sharing a symbol are only reachable by address.

No query params. Caching: public, max-age=15, s-maxage=15. Errors: unknown/hidden token → 404 { "error": "not indexed", "address": "<param>" }; D1 refusal → 503 { "error": "temporarily unavailable", "retryable": true, "address": "<param>" }, no-store.

json
{
  "token": { "...": "same shape as one /api/tokens item" },
  "graduation_progress": { "graduated": true, "progressPct": 100 },
  "at": "2026-09-23T14:37:40.152Z"
}

graduation_progress.progressPct is tokensSold / curveSupply × 100 (sellout basis, matching what the contract actually graduates on) — not a raised-QUAI percentage. A graduated token is always 100.

Trades & candles

GET /api/token/:addr/trades

Query paramTypeDefaultNotes
limitint501–100.

Returns this token's trades merged with its burns into one time-ordered feed (a burn is a Transfer to the zero address — announced beside buys/sells rather than requiring a second request). Hidden/unknown token → 200 { items: [] }, not a 404. Caching: public, max-age=15, s-maxage=15.

json
{
  "address": "0x0035187a7660f595d93cd53a4d16c635d6cffc8f",
  "items": [
    { "network": "mainnet", "tokenAddress": "0x0035...", "txHash": "0x007a...", "logIndex": 1,
      "blockNumber": 10245041, "blockTime": "2026-09-23T07:48:32.368Z",
      "trader": "0x0044444444444444444444444444444444444444", "side": "sell",
      "quaiAmount": "3435148981866778576855", "tokenAmount": "1117215912812398093612081",
      "priceWei": "3074740470908066" }
  ],
  "at": "2026-09-23T14:37:40.785Z"
}

A burn row has side: "burn", quaiAmount: null, and tokenAmount set to the wei destroyed.

GET /api/token/:addr/candles

Query paramTypeDefaultNotes
tfstring5mOne of 1m, 5m, 15m, 1h, 4h, 1d.
limitint720Newest-N buckets, capped at 2000.
json
{
  "address": "0x0035...",
  "items": [
    { "tf": "1h", "bucketStart": 1790121600, "open": "2770797937922332", "high": "2770797937922332",
      "low": "2770797937922332", "close": "2770797937922332", "volumeQuai": "343194826528785000000",
      "tradeCount": 1 }
  ],
  "at": "2026-09-23T14:37:43.946Z",
  "bounded": true
}

bucketStart is unix seconds, ascending order. open/high/low/close/volumeQuai are wei strings. Hidden/unknown token → 200 { items: [] }. Caching: public, max-age=15, s-maxage=15.

GET /api/token/:addr/holders

Query paramTypeDefaultNotes
limitint100Clamped 1–100 — this is also the hard per-call row cap; limit above 100 is silently clamped down, it does not widen a page.
offsetnon-negative int0Added 2026-10-04. Row offset into the sorted (largest balance first) holder list. Anything that doesn't parse as a non-negative integer (missing, negative, non-numeric) is treated as 0.
json
{
  "address": "0x0035...",
  "items": [
    { "network": "mainnet", "tokenAddress": "0x0035...",
      "holder": "0x0033333333333333333333333333333333333333",
      "balance": "107241610597565238455244285", "updatedAt": "2026-09-17T18:43:40.590Z" }
  ],
  "offset": 0,
  "nextOffset": 100,
  "at": "2026-09-23T14:37:41.493Z"
}

Sorted largest balance first (see Indexer for why this sort is exact without an integer cast). balance is a wei string; see Indexer for how balances are derived.

Paging (added 2026-10-04): nextOffset is offset + <rows returned> when the page came back full (items.length equals the effective, 100-capped limit), and null on a short/last page — walk the whole holder list by passing each response's nextOffset back in as the next request's offset until you get null. Because the row cap is enforced before the paging decision, asking for limit=5000 still only ever returns up to 100 rows per call and advances nextOffset by 100 at a time, not by 5000. No DB binding or an unindexed/hidden token still return 200 { items: [], offset, nextOffset: null } — paging never turns an honest-empty result into an error. Caching: public, max-age=15, s-maxage=15.

GET /api/token/:addr/dev-activity

The token creator's own buy/sell footprint — a rug-pull tell. No query params.

json
{
  "token": "0x0035187a7660f595d93cd53a4d16c635d6cffc8f",
  "creator": "0x0011111111111111111111111111111111111111",
  "devActivity": {
    "devBuyQuai": 23268, "devSellQuai": 0, "devBuyCount": 29, "devSellCount": 0,
    "lastSellTime": null, "hasSold": false, "dumpedPct": 0
  },
  "at": "2026-09-23T14:37:44.545Z"
}

Caching: public, max-age=15, s-maxage=15.

GET /api/trades/recent

Newest trades (merged with burns) across all visible tokens — the site-wide ticker.

Query paramTypeDefault
limitint30 (cap 50)
json
{
  "items": [
    { "tokenAddress": "0x0076bc...", "txHash": "0x00460053...", "logIndex": 5,
      "blockNumber": 10249831, "blockTime": "2026-09-23T14:28:33.457Z",
      "trader": "0x0044bed4...", "side": "burn", "quaiAmount": null,
      "tokenAmount": "1963537265200000000000000", "symbol": "POEM",
      "curveAddress": "0x0036790c..." }
  ],
  "at": "2026-09-23T14:37:53.462Z"
}

Caching: public, max-age=15, s-maxage=15.

GET /api/trades/:wallet

Full trade history for one wallet, newest first, offset-free cursor pagination.

Query paramTypeDefaultNotes
limitint50Clamped 1–200.
cursorstring (ISO timestamp)noneFrom a previous response's nextCursor; strictly-less-than on block_time.
sidebuy|sellnoneOptional filter; any other value is ignored.
json
{
  "wallet": "0x0033333333333333333333333333333333333333",
  "items": [
    { "tokenAddress": "0x0035...", "txHash": "0x0060...", "logIndex": 9, "blockNumber": 10116349,
      "blockTime": "2026-09-15T21:10:43.664Z", "trader": "0x004f7f...", "side": "buy",
      "quaiAmount": "10000000000000000000000", "tokenAmount": "10820365474840429875562399",
      "priceWei": "924183200951211", "symbol": "QAXE", "name": "QuaiAxe" }
  ],
  "total": 5,
  "nextCursor": "2026-09-15T21:10:43.664Z",
  "at": "2026-09-23T14:38:06.516Z"
}

total is the full matching count (ignoring pagination, honoring side). Hidden tokens are excluded. Caching: public, max-age=15, s-maxage=15.

GET /api/traders/top

Leaderboard by all-time QUAI volume, across all tokens or scoped to one.

Query paramTypeDefaultNotes
limitint501–100.
tokenaddressnoneOptional; must match /^0x[0-9a-fA-F]{40}$/ or it's ignored.
json
{
  "items": [
    { "trader": "0x0022222222222222222222222222222222222222", "trades": 73,
      "volumeQuai": "71251793299521694334976", "lastActive": "2026-09-23T14:28:32.246Z" }
  ],
  "at": "2026-09-23T14:37:57.601Z"
}

volumeQuai here is a wei string (despite the name reading like a display value) — ranked via CAST(...AS REAL) internally, so it is precision-lossy past ~15 digits; fine for a leaderboard, don't use it for accounting. Caching: public, max-age=15, s-maxage=15.

Wallet, portfolio, creator

GET /api/wallet/:wallet/stats

Activity aggregates that drive achievement badges on the site.

json
{
  "wallet": "0x0033333333333333333333333333333333333333",
  "stats": {
    "tradeCount": 5, "buyCount": 5, "sellCount": 0,
    "volumeQuai": 70000, "largestTradeQuai": 30000, "distinctTokens": 2,
    "firstTradeTime": "2026-09-14T14:50:43.005Z", "lastTradeTime": "2026-09-15T21:10:43.664Z",
    "tokensCreated": 0, "graduatedCount": 0
  },
  "at": "2026-09-23T14:38:05.069Z"
}

No query params; wallet is honest-zeroed (never an error) if it has no activity. Caching: public, max-age=15, s-maxage=15.

GET /api/portfolio/:wallet

Holdings, cost basis (average-cost method, not FIFO), and P&L for a wallet.

json
{
  "wallet": "0x0033333333333333333333333333333333333333",
  "holdings": [
    { "tokenAddress": "0x000010c7602a0b91e81d7d18c12a47792ffb09f8", "name": "Ask Quai", "symbol": "ASK",
      "curveAddress": "0x002690512c43a48db94cc479a36797f7f49d4bc6", "launchBlock": 10079201,
      "curveSupply": "784000000000000000000000000",
      "logoUrl": "/api/token-logo/0x000010c7...?v=2026-09-18T07%3A55%3A46.534Z", "status": "active",
      "balance": "350040929693482655359602290", "priceQuai": "40862631322710",
      "valueQuai": "14303593457923433272243", "costBasisQuai": "10000000000000000000000",
      "realizedPnlQuai": "0", "unrealizedPnlQuai": "4303593457923433272243" }
  ],
  "totals": { "valueQuai": "344043713727620615784671", "realizedPnlQuai": "0",
              "unrealizedPnlQuai": "274043713727620615784671" },
  "at": "2026-09-23T14:38:05.697Z"
}

All of balance, priceQuai, valueQuai, costBasisQuai, realizedPnlQuai, unrealizedPnlQuai are wei strings despite the Quai-suffixed names. priceQuai is the token's current price (its single most recent trade's priceWei). costBasisQuai/unrealizedPnlQuai are null when there's no buy history to attribute a basis to (e.g. balance arrived via transfer, not a tracked trade) — a null here means "unknowable," never a fabricated 0. No query params. Caching: public, max-age=15, s-maxage=15.

GET /api/creator/:wallet

Everything the Creator dashboard shows: one row per token this wallet launched.

json
{
  "wallet": "0x0011111111111111111111111111111111111111",
  "curves": [
    { "tokenAddress": "0x0035187a...", "symbol": "QAXE", "name": "QuaiAxe",
      "curveAddress": "0x004bc407...", "status": "graduated", "launchedAt": "2026-09-06T13:09:12.447Z",
      "volumeWei": "628349616832067248456440", "tradeCount": 356, "holderCount": 134,
      "feeBps": 100, "earnedToDateWei": "3141748084160336242282",
      "claimableWei": "2037293380981762530647",
      "autoBuyback": false,
      "lastClaim": { "txHash": "0x002a0019...", "at": "2026-09-19T11:02:29.988Z",
                     "amountWei": "142350007421997897392" },
      "claimCount": 1, "claimedTotalWei": "142350007421997897392" }
  ],
  "totals": {
    "tokensLaunched": 2, "volumeWei": "628654616832067248456440",
    "earnedToDateWei": "3143273084160336242282", "claimableWei": "2038818380981762530647",
    "queuedBurnWei": "0", "autoBuybackCurves": 0, "holders": 138, "liveReadCap": 30
  },
  "at": "2026-09-23T14:38:08.414Z"
}
  • volumeWei, tradeCount, holderCount, lastClaim/claimCount/claimedTotalWei come from D1 (indexed Buy/Sell/CreatorFeesWithdrawn).
  • feeBps and claimableWei are live eth_calls to the curve contract (feeBps(), creatorFees()), bounded to the first liveReadCap curves (30) per request — beyond the cap the D1-derived fields still return, but feeBps/claimableWei/autoBuyback are null.
  • earnedToDateWei = volumeWei × feeBps × 50% (creator's half), computed in exact BigInt math from the two numbers above (only when both are known).
  • autoBuyback: true marks a V2 curve whose "claim" call burns the pot instead of paying the creator — its balance is reported separately as totals.queuedBurnWei, never folded into totals.claimableWei, so the two are never presented as one interchangeable figure.
  • Any field that could not be read live is null, not 0 — "unknown" and "zero" are always kept distinct in this route.

No query params. Errors: bad wallet param → 400; no DB/D1 refusal → 503 { curves: [], totals: null, error: "temporarily unavailable", retryable: true }, no-store. Caching: default public, max-age=15, s-maxage=15 on success, no-store on error.

Market-maker tools (v0, read-only)

GET /api/token/:addr/reference

Depth-weighted price reference across a token's known venues (its bonding-curve/internal pool and its HartiiSwap WQUAI pair today; typed null placeholders for an external venue and a QUAI/Qi conversion venue, reserved for later). This is the primitive the /api/mm/* routes below are built on — fetch it directly if you only want the reference price, not the quote preview.

No query params. Caching: public, max-age=5, s-maxage=5 on success, plus an internal 5-second Cloudflare edge-cache entry keyed per token address (a cache hit skips the chain reads entirely and returns byte-identical JSON). Errors: no HARTII_LABS_DB binding → 404 { "error": "not indexed", "address": "<param>", "note": "HARTII_LABS_DB binding not configured" } (still max-age=5 — this shape is itself cached as a short-TTL "partial" response); malformed address or unknown/hidden token → 404 { "error": "not indexed", "address": "<param>" } at the normal max-age=15; a chain read throwing mid-request → 503 { "error": "temporarily unavailable", "retryable": true, "address": "<param>" }, no-store.

Example — computed by this repo's own computeReference/weiDecimal from the recorded QAXE mainnet fixture (test/mmReferenceRoute.test.mjs; QAXE curve 0x004bc407903a51506bcf0b1ab423958c5991c237 graduated, HartiiSwap pair 0x00052a890c39ff5592ca88a89924d6083e4e3179, chain head block 10,455,895):

json
{
  "token": "0x0035187a7660f595d93cd53a4d16c635d6cffc8f",
  "blockNumber": 10455895,
  "at": "2026-10-04T12:00:00.000Z",
  "reference": {
    "mid": "0.002239658519168809",
    "low": "0.002216964295947177",
    "high": "0.003423928017629067",
    "midWei": "2239658519168809",
    "lowWei": "2216964295947177",
    "highWei": "3423928017629067",
    "confidenceBps": 5288,
    "flags": ["drift:curve", "drift:hartiiswap"]
  },
  "venues": [
    { "name": "curve", "address": "0x004bc407903a51506bcf0b1ab423958c5991c237",
      "price": "0.002216964295947177", "depthQuai": "141560.130971721715110791",
      "weight": 0.9811972615133717, "ageBlocks": 0,
      "priceWei": "2216964295947177", "depthQuaiWei": "141560130971721715110791" },
    { "name": "hartiiswap", "address": "0x00052a890c39ff5592ca88a89924d6083e4e3179",
      "price": "0.003423928017629067", "depthQuai": "2712.724777369203045966",
      "weight": 0.01880273848662829, "ageBlocks": 0,
      "priceWei": "3423928017629067", "depthQuaiWei": "2712724777369203045966" },
    { "name": "external", "address": null, "price": null, "depthQuai": null,
      "weight": 0, "ageBlocks": null, "priceWei": null, "depthQuaiWei": null },
    { "name": "conversion", "address": null, "price": null, "depthQuai": null,
      "weight": 0, "ageBlocks": null, "priceWei": null, "depthQuaiWei": null }
  ],
  "version": 1
}
  • reference.mid is the depth-weighted average of every venue that has a price, weighted by each venue's depthQuaiWei (equal-weighted instead if every priced venue reports zero depth). low/high widen per venue by a staleness penalty (ageBlocks × 5 bps/block by default, capped at 5,000 bps) before taking the overall min/max — a fresher read narrows the band, a stale one widens it. confidenceBps is the wider of (mid−low)/(high−mid) as a fraction of mid, in bps.
  • flags can include drift:<venue> (that venue's price sits more than 100 bps from mid) and thin (combined venue depth is below a 1,000 QUAI floor). The example above is genuinely flagged drift on both venues — QAXE's curve and HartiiSwap pool were priced noticeably apart at this recorded block; that is the fixture being honest, not a display bug.
  • venues[].weight is each venue's share of the mid weighted average (always sums to 1 across priced venues; an unpriced venue like external/conversion above gets 0). ageBlocks is null for a venue whose price couldn't be read.
  • Pricing a token documents the underlying curve/pool spot-price math this endpoint reads from; this endpoint adds the cross-venue aggregation on top.

GET /api/mm/pairs

The market-maker pair registry, each entry enriched with a live reference and a deterministic quote preview. No query params.

json
{
  "pairs": [{
    "pair": "QAXE/WQUAI",
    "token": "0x0035187a7660f595d93cd53a4d16c635d6cffc8f",
    "depthTargetQuai": 25000,
    "targetRatio": 0.5,
    "band": 0.15,
    "limits": { "minSpreadBps": 40, "maxSpreadBps": 300, "maxConfidenceBps": 400 },
    "status": "planned",
    "reference": { "...": "same shape as reference.reference above, or null if unreadable" },
    "venues": [{ "...": "same shape as the venues array above, or [] if unreadable" }],
    "blockNumber": 10455895,
    "at": "2026-10-04T12:00:00.000Z",
    "preview": true,
    "maker": null,
    "quote": {
      "bid": "0.001640773831143069", "ask": "0.002838543207194549", "mid": "0.002239658519168809",
      "size": "12500", "sizes": { "bid": "12500", "ask": "12500" },
      "spreadBps": 5348, "skewBps": 0,
      "reasons": ["confidence-too-wide", "spread-too-wide"], "ok": false
    }
  }],
  "at": "2026-10-04T12:00:00.000Z",
  "version": 1
}
  • pair/token/depthTargetQuai/targetRatio/band/limits/status come straight from the pair registry (src/data/mmPairs.json) — see Market-maker tools › pair registry. status stays "planned" for every pair in v0.
  • preview: true and maker: null are constant in v0 — there is no code path that sets either to anything else yet.
  • quote is the deterministic bid/ask preview computed from the pair's own reference and an always-empty (quai: 0, token: 0) inventory — it is never computed from a real funded position. ok: false plus a non-empty reasons array is an honest "would not quote this" result, not an error — the worked example above is a real one: this recorded QAXE snapshot's confidence (5,288 bps) and resulting spread (5,348 bps) both exceed the pair's configured maxConfidenceBps/maxSpreadBps (400 / 300), so the preview correctly flags itself as unexecutable. size/sizes.bid/sizes.ask are decimal-QUAI depth caps per side (half of depthTargetQuai each when skewBps is 0, shifted toward the lighter side otherwise). If the reference read fails for a pair, reference/venues/blockNumber fall back to null/[]/ null and quote reflects an empty, unpriced reference (reasons: ["reference-missing"]).
  • Caching: public, max-age=5, s-maxage=5, plus a 5-second Cloudflare edge-cache entry for the whole response (single cache key — there's one registry, not one per token).

GET /api/mm/positions

Live on-chain balances for the configured market-maker vault, one entry per registered pair. No query params.

json
{
  "positions": [{
    "pair": "QAXE/WQUAI",
    "token": "0x0035187a7660f595d93cd53a4d16c635d6cffc8f",
    "vault": null,
    "funded": false,
    "readable": false,
    "quai": "0",
    "tokenBalance": "0",
    "reference": { "...": "same reference shape, or null" },
    "inventory": { "valueQuai": "0", "ratio": 0.5, "inBand": true, "breach": null, "rebalance": null },
    "error": null
  }],
  "vault": null,
  "at": "2026-10-04T12:00:00.000Z",
  "version": 1
}
  • HARTII_LABS_MM_VAULT is an optional environment variable — the address of the wallet acting as the market-maker vault. Unset, empty, or not a well-formed 0x + 40-hex address → every position is honestly zeroed: vault: null, funded: false, quai: "0", tokenBalance: "0", and inventory reflects an empty, in-band 50/50 book. This is the default in v0 — no vault is configured on hartiilabs.com today, so this is what a live call returns.
  • When the vault is configured, funded: true and the route does two live reads per pair (quai_getBalance on the vault, and the token's balanceOf(vault)): on success readable: true and quai/tokenBalance are the real decimal-QUAI balances; on a chain-read failure readable: false and error: "Could not read the configured market-maker vault right now." — quai/tokenBalance from the last-known shape are not substituted, so a reader never confuses a stale balance for a fresh one.
  • inventory is the same 50/50 ± band computation documented in Market-maker tools › inventory bands, marked to the pair's own reference mid.
  • Caching: public, max-age=5, s-maxage=5, plus a single edge-cache entry for the whole response (one vault, one cache key).

GET /api/mm/fills

Paged, append-only read of the mm_fills D1 table — the FIFO accounting ledger's raw input. No fills exist in v0 (nothing quotes yet), so a live call returns an honestly empty page.

Query paramTypeDefaultNotes
limitint50Clamped 1–100.
cursorint0Offset-based (not the nextOffset idiom the holders route uses — this one's field is literally named cursor, and it's an offset either way).
json
{ "items": [], "nextCursor": null, "limit": 50, "at": "2026-10-04T12:00:00.000Z" }

A populated row (shape only — not a real fill):

json
{
  "id": 1, "pair": "QAXE/WQUAI", "token": "0x0035187a...", "side": "buy",
  "txHash": "0x00...", "blockNumber": 10455900, "blockTime": "2026-10-04T12:00:05.000Z",
  "priceQuai": "2239658519168809", "amountToken": "1000000000000000000000",
  "amountQuai": "2239658519168809000", "feeQuai": "6718975557506427",
  "inventoryQuaiAfter": "...", "inventoryTokenAfter": "...",
  "maker": null, "source": null, "createdAt": "2026-10-04T12:00:05.100Z"
}

priceQuai/amountToken/amountQuai/feeQuai/inventoryQuaiAfter/inventoryTokenAfter are raw wei integer strings (straight from mm_fills TEXT columns) — see the note at the top of this section. No DB binding → 200 { items: [], nextCursor: null, limit, at, note: "HARTII_LABS_DB binding not configured" } (not an error — an unconfigured binding is an honest empty ledger, same pattern as the rest of this API). D1 failure → 503 { "error": "temporarily unavailable", "retryable": true, items: [], nextCursor: null, at }, no-store. Caching: public, max-age=5, s-maxage=5 on success (edge-cached only for the default cursor=0&limit=50 call).

GET /api/mm/pnl

FIFO profit-and-loss computed in-request from every row in mm_fills, marked to each pair's current reference mid. No query params.

json
{
  "realisedQuai": "0", "unrealisedQuai": "0", "feesQuai": "0",
  "inventory": { "quai": "0", "token": "0" },
  "byPair": {},
  "pairs": [{
    "pair": "QAXE/WQUAI", "token": "0x0035187a...", "...": "rest of the pair-registry fields",
    "pnl": { "realisedQuai": "0", "unrealisedQuai": "0", "feesQuai": "0",
             "inventory": { "quai": "0", "token": "0" } },
    "reference": { "...": "same reference shape, or null" }
  }],
  "at": "2026-10-04T12:00:00.000Z",
  "version": 1
}
  • FIFO: a buy opens a token lot at its QUAI principal cost; a sell consumes the oldest open lots first and books realisedQuai on the amount consumed. Fees are tracked in feesQuai and already netted out of inventory.quai, but not subtracted from realisedQuai/unrealisedQuai — show the gross trading result and the fee drag as two separate numbers, never fold one into the other.
  • unrealisedQuai is null (not 0) for any pair still holding a non-zero token balance whose current reference price couldn't be read — "unknown," never fabricated. The top-level unrealisedQuai is null the moment any one pair's is null.
  • Every amount here is a wei integer string, same exception called out at the top of this section — realisedQuai, unrealisedQuai, feesQuai, and inventory.quai/inventory.token are not decimal-scaled despite the naming.
  • No DB binding → 200 with every total zeroed as above (an honest empty ledger, not an error). D1 failure → 503 { "error": "temporarily unavailable", "retryable": true, pairs: [], realisedQuai: null, unrealisedQuai: null, feesQuai: null, inventory: null, byPair: {}, at }, no-store — note this is the one place in this section where the error shape nulls the totals instead of zeroing them, precisely so a reader can't mistake "could not compute" for "computed to zero." Caching: public, max-age=5, s-maxage=5 on success, plus a single whole-response edge-cache entry.

Burns, ecosystem stats, and charts

GET /api/burns

Single source of truth for "total burned" — any Transfer to the zero address (treasury buybacks, creator burns, holder burns, curve auto-buyback), across all visible tokens.

json
{
  "totalWei": "584258400700235134958323460",
  "events": 130,
  "tokenCount": 10,
  "lastBurnAt": "2026-09-23T14:28:33.457Z",
  "tokens": [
    { "address": "0x0076bc5b3a8ee996bef290a57d9a4624c5f9f9aa", "symbol": "POEM",
      "burnedWei": "344412894805529134470188070" }
  ],
  "at": "2026-09-23T14:37:15.035Z"
}

tokens sorted descending by burnedWei. No query params. Caching: public, max-age=300, s-maxage=300 (5 minutes — burns are rare and the total moves slowly). No DB / D1 refusal → totalWei: null, events: null (never a fake "0"); D1 refusal is 503, no-store.

GET /api/ecosystem-stats

Site-wide dashboard numbers.

json
{
  "launches": 32, "totalLaunches": 32, "activeTokens": 30, "graduatedTokens": 2,
  "totalHolders": 235,
  "volume24hQuai": "101333640741254648971358", "volume24hWei": "101333640741254648971358",
  "volume7dWei": "718597279224112168832477", "totalVolumeWei": "1075083212255358381423920",
  "topTokens": [ { "address": "0x0076bc...", "name": "POEM", "symbol": "POEM", "updatedAt": "..." } ],
  "topToken": { "address": "0x0035187a...", "name": "QuaiAxe", "symbol": "QAXE",
                "volume24hWei": "59620483275532255828565" },
  "isPartial": false,
  "treasuryFees7d": "3592986396120560844151", "creationFees7d": "55000000000000000000",
  "launches7d": 11, "feesPartial": false,
  "burnedWei": "584258400700235134958323460", "burnEvents": 130, "burnedTokens": 10,
  "lastBurnAt": "2026-09-23T14:28:33.457Z",
  "at": "2026-09-23T14:37:13.893Z"
}
  • treasuryFees7d/creationFees7d are computed from indexed trade volume × each curve's live feeBps (owner-adjustable per curve, 0–300 bps) and the factory's live creationFee() — never a hardcoded assumption. feesPartial: true means at least one curve's fee rate couldn't be read, so the totals are a floor, not the exact figure.
  • burnedWei/burnEvents/burnedTokens/lastBurnAt mirror /api/burns.
  • No query params. Caching: public, max-age=300, s-maxage=300. D1 refusal → 503, no-store.

GET /api/volume-series

Per-UTC-day traded volume for the dashboard chart.

Query paramTypeDefaultNotes
daysint14Clamped 1–90.
json
{
  "network": "mainnet",
  "days": [
    { "day": "2026-09-21", "quaiWei": "93375527322478099743270" },
    { "day": "2026-09-22", "quaiWei": "324034706977607660248126" },
    { "day": "2026-09-23", "quaiWei": "33757681123253474128401" }
  ],
  "available": true,
  "truncated": false,
  "at": "2026-09-23T14:37:38.046Z"
}

available: false means the series couldn't be read at all (render "unavailable," not a zero-line); a day genuinely worth "0" is a real fact and is shown as such. truncated: true adds a note field warning the earliest days may be undercounted (more trades fell in the window than the query's row cap). Caching: public, max-age=120, s-maxage=120.

Four curated lists in one call: new launches, about-to-graduate, short-term movers, and 24h volume leaders.

json
{
  "network": "mainnet",
  "new": [ { "...": "token fields", "sparkline": [{ "t": 1790121600, "priceQuai": 0.0011 }], "badge": "NEW" } ],
  "graduating": [ { "...": "token fields", "liquidityProgress": 0.62, "sparkline": [...], "badge": "SOON" } ],
  "snipers": [ { "...": "token fields", "priceChange1h": 8.4, "sparkline": [...], "badge": "HOT" } ],
  "movers": [ { "...": "token fields", "volume24hQuai": "101333...", "sparkline": [...], "badge": "MOVER" } ],
  "partial": false,
  "at": "..."
}
  • new: launched in the last 24h, newest first, top 20.
  • graduating: status !== 'graduated' and sellout progress ≥ 40%, sorted by progress, top 20. liquidityProgress is a float 0..1.
  • snipers: positive 1h price change from candles, sorted descending, top 20. priceChange1h is a percent number.
  • movers: ranked by the indexer-maintained volume24hWei, top 20. volume24hQuai is a decimal QUAI string (not wei — already divided).
  • Each list is capped at 20 regardless of total token count. sparkline entries are { t: <unix seconds>, priceQuai: <float> } — plotting only.
  • No query params. Caching: public, max-age=60, s-maxage=60. D1 refusal → 503, no-store.

GET /api/graduations

Monotonic event feed (sequence-cursor, not timestamp) — built for sync consumers, not display.

Query paramTypeDefaultNotes
afternon-negative int (as string)0Sequence cursor from a previous response.
limitint50Hard-capped at 50.
json
{
  "items": [
    { "sequence": 1, "address": "0x0035187a...", "name": "QuaiAxe", "symbol": "QAXE",
      "blockNumber": 10064744, "graduatedAt": "2026-09-12T21:34:06.013Z", "txHash": null }
  ],
  "hasMore": false,
  "at": "2026-09-23T14:37:45.232Z"
}

sequence is a monotonic integer independent of block number (multiple graduations can share a block) — poll with after=<last item's sequence>. graduatedAt is when this app's indexer first observed the event (or, for a backfilled gap, when the heal ran) — not necessarily the exact on-chain block timestamp. Caching: always no-store — this route is never cached, by design, so a client's cursor position stays exact.

GET /api/categories

Query paramTypeNotes
categorystringOptional; must be one of memecoin, utility, art, gaming, defi, social or it's ignored. When valid, adds addresses (token list) and filterCategory to the response.
json
{
  "categories": [ { "category": "memecoin", "count": 0 }, "..." ],
  "defaults": ["memecoin", "utility", "art", "gaming", "defi", "social"],
  "at": "2026-09-23T14:37:59.442Z"
}

Caching: public, max-age=15, s-maxage=15. (POST /api/categories exists to assign a category to a token but requires the admin bearer token — see operator-only routes.)

Comments, reactions, flags (wallet-signed writes)

These are public routes, but writes require an EIP-191 personal_sign from the acting wallet — no accounts, no sessions, no API key. The message format is fixed per route (see below) so a client can construct and sign it identically to the frontend. Use quais, not ethers, to sign and to recover.

GET /api/token/:addr/comments / POST /api/token/:addr/comments

GET returns the latest 50 comments, newest first, no-store. Hidden/unknown token → 200 { items: [] }.

POST body: { wallet, text, ts, signature }. Server-side rules:

  • text ≤ 280 chars, non-empty.
  • ts (ms epoch) must be within 10 minutes in the past / 60 seconds in the future of the server clock, or the request is rejected as expired (closes the replay window).
  • Signed message: `hartiilabs:comment:<tokenAddress lowercase>:<ts>:<text>`.
  • The recovered signer must equal the claimed wallet — the client's wallet field is never trusted alone.
  • Rate limits (per isolate, sliding window): 10 requests/min per IP, 4/min per wallet.
  • Success: 201 { ok: true, comment: {...} }.

GET /api/comment-reactions?token=0x… / POST / DELETE /api/comment-reactions

Emoji reactions on a comment. Allowed emoji: 👍 ❤️ 🔥 🚀 😂 👀 — any other value is rejected.

POST/DELETE body: { tokenAddress, commentWallet, commentCreatedAt, wallet, emoji, ts, signature }. Signed message: `hartiilabs:react:<token>:<commentWallet>:<commentCreatedAt>:<emoji>:<ts>`. Same 10 min/60 s timestamp skew rule as comments. Rate limits: 20/min per IP, 10/min per wallet. DELETE toggles a reaction off. Both respond { ok: true } (DELETE also returns deleted). no-store throughout.

POST /api/flag-token

Community moderation — anyone with a wallet can flag a token for human review (never auto-hides).

Body: { wallet, tokenAddress, reason, description, ts, signature }. reason must be one of scam, rug, misleading, spam, other; description ≤ 500 chars. Signed message: `hartiilabs:flag:<tokenAddress>:<ts>:<reason>`. Same timestamp-skew rule. Rate limits: 10/min per IP, 4/min per wallet, and a separate cap of 5 flags per wallet per day. Success: 201 { ok: true, createdAt }.

Creator-only edits (recovered signer must equal the token's on-chain-indexed creator, not an admin token). update-meta edits description/socials/style; set-logo uploads a new logo image (binds the signature to a SHA-256 digest of the uploaded bytes, so a captured signature can't be replayed with different image bytes). Both use the same 10 min/60 s timestamp rule and 10/min-IP + 4/min-wallet rate limits. See Integration recipes if you need the exact message formats — these are creator-tooling routes, not typically needed by a read-only integration.

POST /api/token/announce

Public, rate-limited (5/min/IP) insert-only route: given a launch txHash (/^0x[0-9a-f]{64}$/i), verifies the receipt succeeded and contains a TokenLaunched log from the factory, then inserts the token row if it doesn't already exist (never overwrites creator-edited metadata on a re-announce). Exists so a freshly-launched token appears immediately rather than waiting for the next indexer tick.

Media

GET /api/token-logo/:addr

Serves a token's logo image. :addr must be a 0x + 40-hex address.

Content-Type image/png (or whatever was stored). Resolution order: edge cache → KV bytes → IPFS gateway fetch (self-heals into KV on success). Verified live:

text
curl -sI https://hartiilabs.com/api/token-logo/0x0035187a7660f595d93cd53a4d16c635d6cffc8f
# Content-Type: image/png
# Cache-Control: public, max-age=86400, s-maxage=604800, immutable

A hit is cached immutable for a week at the edge — the ?v=<updatedAt> query string on the logoUrl field elsewhere in this API is what busts that cache on a logo replacement, so always use the logoUrl the API gives you rather than constructing this path yourself. A miss (hidden, nonexistent, or no bytes anywhere) is a cached 404, public, max-age=60, s-maxage=300.

GET /api/og/:addressOrTicker

Live 1200×630 PNG share card (candlestick price chart rendered server-side). Accepts an address or ticker, with or without a .png suffix. Content-Type image/png, Cache-Control: public, max-age=300. On any failure (unknown token, render error) it redirects (302) to a static fallback image rather than ever returning a broken image response — safe to embed in an <img src> unconditionally.

GET /api/badge/:ticker

Compact ~220×40 SVG price badge (name, price in QUAI, 24h change%). Ticker only, no address form.

text
curl -sI https://hartiilabs.com/api/badge/QAXE
# Content-Type: image/svg+xml; charset=utf-8
# Cache-Control: public, max-age=60, s-maxage=300

Unknown ticker → 404 with a "not found" placeholder SVG body (still an image/svg+xml response, so it renders as an image, not a broken link); a real backend failure → 500 with an "unavailable" placeholder SVG, so the two failure modes are visually distinguishable if you look closely, but both are still safe to drop into an <img> tag.

POST /api/token-meta

Public draft-metadata endpoint used by the launch flow before a token exists on-chain (name, symbol, description, socials, logo upload, optional Turnstile bot-check, optional AI moderation screening that is informational only and never blocks). Not typically needed by a read-only integration — see the source or Contracts → Launching a token if you're building your own launch UI.

Live push

GET /api/live/ws

WebSocket upgrade — push channel for new trades/burns, an alternative to polling. Subscribe by connecting to wss://hartiilabs.com/api/live/ws?channel=<channel> and sending {"type":"subscribe","channel":"<channel>"} frames for additional channels on the same socket; {"type":"unsubscribe","channel":"..."} to drop one. Every frame carries a protocol version v (1, unchanged by the 1.1.0 latency programme). The server sends a hello frame with a per-channel seq on subscribe, then heartbeat frames, then event frames carrying an incrementing seq per channel. Heartbeat cadence depends on what's subscribed: roughly every 25 seconds while only non-curve channels are watched (e.g. just the sitewide trade feed), or on every watcher tick — at most 15 seconds, often sooner — once at least one curve channel has a subscriber. If a client's last-seen seq and an incoming frame's seq aren't consecutive, treat it as a gap and reconcile from the indexed REST API (a message was missed — the socket does not replay history). A silent socket (no heartbeat for ~45s) should be treated as dead and reconnected with backoff.

Trade event frames carry an additive blockTime field as of 1.1.0: the block's unix timestamp in seconds (a number), or absent/null when the block header hasn't been seen yet (it fills in on a later tick once it has). This is a different value from the ISO-string blockTime returned by the REST trade routes above — don't conflate the two formats. Pushed (unconfirmed) trade frames also carry hubAt — the hub's own clock (ms) when the chain's log notification reached it — so a client can measure hub→client fan-out without caring about block timestamps.

As of 1.2.0 the global channel additionally carries head frames — one per chain block the hub sees ({ "blockNumber": 10410125, "timestamp": 1790974238, "hubAt": 1790974238110 }, cadence ≈ 2–8 s). They are what hartiilabs.com uses to check a pending trade's receipt the moment its block lands instead of waiting for the next poll; an integrator can do the same (call GET /api/token/<address>/trades or your own receipt lookup on each head). Unknown frame types must be ignored — v stays 1. Also as of 1.2.0 the hub pushes trades for every launched curve to the global channel (not only curves some open token page watches) and lands each pushed trade in the indexed REST routes within ~1 s (POST /api/live/ingest, bearer-gated, internal — not a public write route), so a fresh GET /api/token/<address>/trades is at most a block or so behind the chain while the hub's upstream subscription is healthy. Rows that arrived this way and have not yet been confirmed by the indexer carry provisional: true (1.3.0; the field is absent on confirmed rows); the indexer removes a provisional row only if the transaction's own receipt shows it was dropped by a reorg.

Server kill switch: if HARTII_LABS_LIVE_HUB_ENABLED=false, the endpoint answers 503 and every consumer should fall back to polling the REST routes above — build your integration to do this gracefully rather than depending on push being available.

GET /api/live/publish — health check (unauthenticated)

json
{ "enabled": true, "ok": true, "network": "mainnet", "sockets": 2, "curves": 1, "lastScannedBlock": 10410005,
  "upstream": { "enabled": true, "state": "open", "lastHeadAt": 1790974248865, "lastFrameAt": 1790974248865,
                "curves": 1, "lastError": null },
  "knownCurves": { "count": 138, "loadedAt": 1790975000000 },
  "lastIngestError": null }

Cache-Control: no-store. upstream (1.1.0) is the hub's own subscription to the Quai RPC websocket — state: "open" with an advancing lastHeadAt means trades are being pushed per block; "closed" while curves > 0 means the hub is on its 10 s safety poll. knownCurves (1.2.0) is the full launched-curve set the hub watches for the sitewide feed; lastIngestError (1.2.0) is the last failure of the D1 fast lane, null when healthy. POST /api/live/publish (bearer-gated, operator only) is the manual publish path for ops/smoke-testing — normal indexer publishing doesn't go through this HTTP route.

Status

GET /api/health

Liveness check — ok: true always means the Function itself ran; d1Reachable is the separate, honest signal for the database dependency.

json
{ "ok": true, "version": "1.1.0", "d1Reachable": true, "network": "mainnet",
  "indexer": { "present": true, "factoryAddr": "0x001AF1BbB40807fcb99C9Eeaa49dF5E91e7Efd42" },
  "at": "2026-10-02T21:40:20.046Z" }

Cache-Control: no-store — always live. version (added in 1.1.0) is the deployed app's SemVer version — the same number as the <meta name="application-version"> tag on every page and the vX.Y.Z git tag of the commit that is live. The app's CHANGELOG.md (in the hartii-labs repo) lists what each version changed; MAJOR bumps are the only ones that can break an integration (public API / live-protocol changes), MINOR adds capability, PATCH fixes.

GET /api/status

Richer status surface — deliberately only directly observable facts, no risk verdicts.

json
{
  "status": "ok",
  "at": "2026-09-23T14:37:21.440Z",
  "d1Reachable": true,
  "network": "mainnet",
  "indexer": { "present": true, "factoryAddr": "0x001AF1BbB40807fcb99C9Eeaa49dF5E91e7Efd42",
               "lastIndexedAt": "2026-09-23T14:36:40.818Z", "lastIndexedBlock": 10249946 },
  "lastTradeAt": "2026-09-23T14:28:32.246Z",
  "rpc": { "ok": true, "chainId": "0x9", "latencyMs": 500 }
}

status is "ok" only when D1 is reachable, the RPC health-check succeeds, and the indexer is configured — otherwise "degraded". Useful as a single check before trusting freshness-sensitive data. Caching: public, max-age=30 (no s-maxage).

GET /api/quai-price

QUAI/USD reference price — MEXC primary, CoinGecko fallback.

json
{ "usd": 0.010293, "source": "mexc", "at": 1790174223533 }

Caching: public, s-maxage=120, max-age=60. Both sources failing → 503 { "error": "price unavailable" }, no-store, with no CORS header on that specific error branch.

Operator-only routes

These exist and are documented here for completeness; they require a bearer secret you don't have and don't need for read integration. Three separate secrets, each compared with a constant-time check, each accepted as either Authorization: Bearer <token> or the fallback header x-hartii-labs-indexer-token: <token>:

SecretGuardsGate
HARTII_LABS_INDEXER_TOKENPOST /api/indexer-run, POST /api/admin/repin-logosrequireIndexerAuth
HARTII_LABS_ADMIN_TOKENPOST /api/admin/hidden, GET /api/admin/hidden, POST /api/admin/comments, POST /api/admin/set-logo, POST /api/categories, GET /api/beacon?admin=1requireAdminAuth
HARTII_LABS_LIVE_TOKENPOST /api/live/publishrequireLiveAuth

Leaking one never grants the others — they're intentionally independent secrets. An unauthorized request gets 401 { "error": "Unauthorized <kind> request." }; an unconfigured secret (missing or under 16 chars) gets 503 { "error": "<Kind> token is not configured." } (functions/_lib/auth.js). Verified live on a route this doc's research never had to POST to (GET /api/admin/hidden is itself bearer-gated, so a plain GET with no Authorization header already exercises the same 401 path — POST /api/indexer-run//api/admin/* were never called, per instructions):

text
curl https://hartiilabs.com/api/admin/hidden
# 401 {"error":"Unauthorized admin request."}

The 401 shape for POST /api/indexer-run and the other bearer-gated POST routes above is taken from functions/_lib/auth.js (the same requireIndexerAuth/requireAdminAuth/requireLiveAuth code path GET /api/admin/hidden exercises above) rather than a live POST — those routes were deliberately never called.

POST /api/indexer-run is the indexer scan itself — see Indexer for what it does. POST /api/admin/hidden hides/unhides a token from every read path at once (the single moderation gate — see Indexer → freshness). POST /api/admin/set-logo lets the operator replace any token's logo. GET /api/beacon?admin=1 lists aggregated client-error fingerprints from the site's own error beacon (POST /api/beacon, public, fire-and-forget, always 204, strips wallet addresses before storing — not a data API, not documented further here).

Quai Network mainnet · chain 9 · Cyprus-1. Figures marked "read on" a date were read from the chain that day; re-read before relying on them.