# RareIndex Public API (v1) Base URL (production): `https://www.rareindex.io/api/v1` — the web app proxies `/api/v1/*` to the Fastify service (`apps/api`, port `API_PORT`, default 8211). Direct access in development: `http://localhost:8211/v1`. Machine-readable contract: `GET /v1/openapi.json` (also `docs/openapi.json`). ## Authentication & tiers `Authorization: Bearer ri_live__`. Requests without a key use the **public** tier. Only the SHA-256 hash of a key is stored (`api_keys.key_hash`); mint keys with ``` pnpm --filter @rareindex/api create-key -- --name "Research desk" --tier professional [--user usr_…] ``` | Tier | req/min | req/day | |---|---|---| | public (no key) | 20 | 500 | | free | 60 | 1 000 | | hobby | 120 | 10 000 | | professional | 600 | 100 000 | | research | 600 | 250 000 | | enterprise | 3 000 | 5 000 000 | Rate-limit headers: `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-reset`; every response carries `x-request-id`. Usage is counted per key/day/endpoint in `api_usage`. ## Conventions - Envelope: `{ "data": …, "meta": { "count", "cursor", "as_of", "attribution", … } }`. - Pagination: `limit` (≤ 200) + opaque `cursor` (from `meta.cursor`, `null` when exhausted). - Errors: RFC 9457 `application/problem+json` (`title`, `status`, `detail`, `instance`, `request_id`). - Export: add `?format=csv` on list endpoints for a flat CSV download. - Caching: read endpoints send `cache-control` and weak ETags; gzip/brotli negotiated. - Money: `price` + `currency` are the native transaction values; `price_usd` uses the FX rate of the transaction date. Valuations (`riv_usd`, `riv_low_usd`, `riv_high_usd`) always come with `riv_confidence` and `riv_sample_size`. Listing prices are asks, never transactions. - Attribution is required (see `meta.attribution`). ## Endpoints | Method & path | Purpose | |---|---| | `GET /v1/assets/search?q&category&limit&cursor` | Search canonical assets (FTS + trigram + identifiers) | | `GET /v1/assets/{idOrSlug}` | Asset detail: attributes, stats, variants with their own valuations, latest valuation, sources | | `GET /v1/assets/{id}/sales?variant&include_flagged` | Observed sales (valid only by default); `all_in_usd` = buyer-pays price (hammer + premium per `fee_basis`) | | `GET /v1/assets/{id}/listings?availability` | Listings (asks) with `discount_to_riv` | | `GET /v1/assets/{id}/history?variant&from&to` | Daily series: RIV, latest sale, median, sales count, volume, listings, min ask | | `GET /v1/assets/{id}/depth?variant` | Market depth (asks within ±5/10/20 % of RIV, below/above, best & median ask, RareIndex spread), days on market (sold vs withdrawn), median time-to-sale by asking band (≤ 90 %, 90–100 %, 100–110 %, > 110 % of RIV; null under 5 observations), fair buy/sell ladder. `liquidation.level` is `category` when the asset has < 10 completed lifecycles. Model estimates, not advice | | `GET /v1/assets/{id}/auctions?status` | Auction lots for an asset with native, USD and all-in (buyer-pays) amounts, `fee_basis`, `bid_vs_riv` / `estimate_vs_riv` and `assessment_verdict` | | `GET /v1/auctions/lots?status&ending_within_hours&category&house&below_riv&asset&sort` | Live/upcoming lots across houses: current bid or low estimate + the house's buyer premium (published, ≈ estimated or default 22 %) compared with the RIV of the lot's own variant through the ask gates. `below_riv=true` keeps verdict `deal` only. Taxes, duties and shipping excluded; bids are not transactions | | `GET /v1/categories` | Taxonomy with tracked asset counts | | `GET /v1/indices` | RARE + subindices: latest value, 1d/7d/30d/YTD/1y changes, breadth, market cap estimate + confidence; `published=false` until breadth threshold | | `GET /v1/indices/{ticker}/history?from&to` | Index history | | `GET /v1/markets` | Latest category snapshot per family | | `GET /v1/markets/{slug}` | Market detail: counts, gainers/losers (≥ 3 sales), most valuable, most liquid, recent sales, history | | `GET /v1/trending?category` | Assets ranked by trending score | | `GET /v1/sales/latest?category&min_usd` | Latest observed sales across the platform | | `GET /v1/records` | Highest verified sale per family (bundles and low-confidence excluded) | | `GET /v1/stats` | Platform counts | | `GET /healthz` | Liveness (DB ping) | ## Internal endpoints (loopback only) `POST /internal/lookup { url }` and `GET /internal/lookup/sources` are used by the web Scanner to resolve a marketplace URL through the connector framework (connector modules are loaded dynamically, which the Next.js bundler cannot do). Auth: header `x-internal-token` = HMAC-SHA256(`SESSION_SECRET`, `"internal"`), and the caller must be 127.0.0.1. ## Running ``` pnpm --filter @rareindex/api dev # tsx watch, port 8211 pnpm --filter @rareindex/api start pnpm --filter @rareindex/api test # fastify.inject integration tests against DATABASE_URL pnpm --filter @rareindex/api exec tsx scripts/gen-openapi.ts # refresh docs/openapi.json ``` Environment: `DATABASE_URL`, `API_PORT`, `API_HOST` (default 127.0.0.1 — put it behind the web proxy), `SESSION_SECRET` (shared with the web app for internal calls), `FIRECRAWL_API_KEY`/`SCRAPFLY_API_KEY` (URL lookups), `LOG_LEVEL`.