Public API
Base URL: https://hartiigallery.com. All endpoints are functions/api/** Cloudflare Pages
Functions. Responses are JSON unless noted. Examples below are trimmed real responses captured
live against production.
GET /api/collections
The whole catalogue in one response — factory-deployed collections plus externally-listed ones, each enriched with live floor price and mint stats.
{
"network": "mainnet",
"collections": [
{
"address": "0x000b37b2f7eac95224b81cc10f55de5db7d016d6",
"factory": "0x00265a93d7Bf94147d88e27EA28A2AC9dAd9eF04",
"name": "First",
"symbol": "FRST",
"totalSupply": 1,
"totalMinted": 1,
"maxSupply": 1,
"mintPrice": "1000000000000000000",
"creator": "0x0055555555555555555555555555555555555555",
"creatorThreshold": "1000000000000000000",
"creatorEarned": "950000000000000000",
"isPhaseTwo": false,
"royaltyBps": 100,
"coverImage": "/api/img?u=ipfs%3A%2F%2FQm...",
"marketStats": null,
"sampleItems": [{ "tokenId": "0", "name": "First #0", "image": "/api/img?u=..." }]
}
]
}Caching: the code path for a warm index hit sets s-maxage=30, stale-while-revalidate=120; a
live check at doc time observed Cache-Control: no-store (the response taken was presumably off
a cold/rebuilding index path). Treat the header on the actual response you get as authoritative
rather than assuming a cache hit. Field availability is a whitelist on the frontend side
(useCollections) — a new field added here needs adding there too before it's used.
GET /api/floor-prices
Live floor per collection, computed from on-chain marketplace state (not the KV index).
{
"collections": {
"0x0010e039dfd4d44681cc984121dfd42a001ba45a": {
"floor": 6666.6666,
"floorWei": "6666666600000000000000",
"count": 1
}
},
"totalListings": "…",
"partial": false,
"failedMarketplaces": []
}floorWei is the exact on-chain value (string, to avoid float precision loss); floor is the
display-rounded float derived from it. Honesty contract: if all marketplace reads fail, the
endpoint returns 503 { error, partial: true } rather than a fabricated empty 200; a partial
failure sets partial: true and lists which venues failed in failedMarketplaces instead of
silently omitting their collections.
GET /api/sales
Durable sales history, backed by a D1 table fed by a discovery script — not a live getLogs
scan, so it has complete history rather than just a recent window.
| Param | Meaning |
|---|---|
limit | page size |
collection | filter to one collection address |
cursor | opaque pagination cursor from a previous response |
summary=1 | (with collection) return aggregate stats instead of a list |
{ "network": "mainnet", "collection": null, "count": 0, "sales": [], "nextCursor": null }Honesty contract: a store/read failure is 503 { error }, never a 200 with an empty array —
"no sales yet" and "could not read" are distinguishable.
GET /api/img?u=<encoded-url>[&w=N]
Caching image proxy for NFT media. u is a URL-encoded ipfs:// or https:// image URL;
optional w requests a resized variant. Hedges across several public IPFS gateways server-side
(gateways like gateway.pinata.cloud 429 under load) and caches the result at Cloudflare's edge
as immutable (IPFS content is content-addressed, so the cache never needs to invalidate).
GET /api/meta?u=<encoded-url>
Same proxy/caching strategy as /api/img, for JSON token metadata instead of image bytes.
GET /api/health
Operator-facing status page, public and read-only, ~30s edge cache.
{
"status": "ok",
"network": "mainnet",
"build": "561c703a",
"index": { "store": "d1", "rows": 124, "cap": 50000, "capPct": 0.2, "nearCap": false },
"heartbeats": {
"quaiscan-scan": { "ageHours": 3.6, "stale": false },
"resolve-media": { "ageHours": 2.8 }
}
}status is ok / degraded / error — every check reports its own failure rather than being
silently skipped, so a component that can't be reached shows up as a finding, not a gap. A live
check at doc time observed Cache-Control: no-store on this endpoint despite the "~30s edge
cache" comment in source — verify the header on your own response rather than assuming.
GET /api/event-config
Public read of the currently-active promotional event's display config (name, dates, contract, hero image) — no auth. Example (trimmed, live):
{
"eventId": "money-moves",
"name": "MONEY MOVES",
"active": true,
"contract": "0x0070BE7e66246aC24eDA1E1D1E0a294d3B090837",
"chainId": 9,
"domain": "hartiigallery.com",
"maxSupply": 300
}GET/POST /api/click
Lightweight click/attribution tracking. POST with an address in the body; returns
{ ok: false, error: "invalid address" } for a malformed one (confirmed live) rather than a
generic 500.
GET /api/profile?address=0x…
Reads a stored social profile document for a wallet (profile:<address> in KV). CORS-restricted
to https://hartiigallery.com. Address must match ^0x[a-fA-F0-9]{40}$.
POST /api/upload, POST /api/upload-json, POST /api/upload-folder
Creator-facing uploads (collection image/metadata assets → IPFS pin). Requires the creator's own request context (wallet-gated on the frontend); not documented further here since it's a write path meant to be used through the Gallery UI's own flow, not integrated against directly.
POST /api/discord-alert, POST /api/discord-member-verify
Server-side Discord webhook/verification glue for the community server. Internal use by the frontend; no public contract beyond "exists and requires the site's own request shape."
GET /api/faucet, POST /api/faucet-drip
Testnet-only (Orchard) faucet drip. No-op / not meaningful on mainnet — FAUCET_ADDRESS is
null in the mainnet config by design.
Admin endpoints — operator only
functions/api/admin/event-config.js, event-drain.js, event-report.js manage the
promotional-event system's write side (create/edit an event, drain claims, pull a report).
Operator only — gated behind a constant-time-compared shared secret checked server-side; not
reachable without it. No further detail here per this doc's no-secrets policy.
Caching summary
| Endpoint class | Cache-Control (source intent) | Observed live at doc time |
|---|---|---|
/api/collections | s-maxage=30, stale-while-revalidate=120 on a warm index hit | no-store |
/api/floor-prices | short edge cache; no-store on partial/total failure | no-store |
/api/img, /api/meta | immutable (content-addressed IPFS) | not re-checked live per URL |
/api/health | ~30s edge cache | no-store |
/api/event-config | ~30s edge cache | not re-checked live |
/api/profile | no-store | matches |
The gap between source comments and the live headers observed above wasn't chased further — don't hard-code a caching assumption into an integration; read the response's own header.