Hartii developer docs

Gallery

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.

json
{
  "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).

json
{
  "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.

ParamMeaning
limitpage size
collectionfilter to one collection address
cursoropaque pagination cursor from a previous response
summary=1(with collection) return aggregate stats instead of a list
json
{ "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.

json
{
  "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):

json
{
  "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 classCache-Control (source intent)Observed live at doc time
/api/collectionss-maxage=30, stale-while-revalidate=120 on a warm index hitno-store
/api/floor-pricesshort edge cache; no-store on partial/total failureno-store
/api/img, /api/metaimmutable (content-addressed IPFS)not re-checked live per URL
/api/health~30s edge cacheno-store
/api/event-config~30s edge cachenot re-checked live
/api/profileno-storematches

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.

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.