{"openapi":"3.1.0","info":{"title":"Gold API","description":"Developer API for gold prices and precious metals pricing. XAU spot across 31 currencies (live FX cross via exchangerate.dev, ECB fallback), COMEX futures (GC=F), GLD ETF, and 10+ year daily XAU/USD history. Published methodology; aggregated from public reference data per Path B Revision 2.","contact":{"name":"goldprice.dev","url":"https://goldprice.dev/"},"license":{"name":"Commercial API license","url":"https://goldprice.dev/terms"},"version":"0.1.0"},"servers":[{"url":"https://api.goldprice.dev","description":"Production"}],"paths":{"/v1/health":{"get":{"tags":["system"],"summary":"Liveness probe","description":"Minimal 200 OK if the process is up. No downstream checks.\n\nGET is the documented public operation. HEAD is registered separately\nbelow so uptime monitors work without creating duplicate OpenAPI\noperation IDs for generated clients.","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health V1 Health Get"}}}}}}},"/v1/status":{"get":{"tags":["system"],"summary":"Readiness probe — Redis + per-scraper freshness","description":"Returns Redis reachability + per-scraper freshness from scraper_runs.\n\nOverall `status` is `ok` only when Redis is up AND every tracked scraper's\nlatest successful run is within 2 * expected_interval_s. Otherwise\n`degraded`. Dead-man thresholds are read from scraper_runs rather than\nhardcoded constants (C1).\n\nThe `value_staleness` block (F1 per audit 2026-04-23) surfaces WGC\nper-currency mids that have stopped moving while the scraper keeps\nfetching. Per scope §3.2 Q2, entries in this block are\nINFORMATIONAL-ONLY — they do not flip overall `status` to `degraded`,\nbecause INR is currently upstream-stuck and coupling would leave\n`/v1/status` permanently degraded, drowning out other alerts.\nMonitoring consumers alert on the presence of entries directly.","operationId":"status_v1_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Status V1 Status Get"}}}}}}},"/v1/prices":{"get":{"tags":["prices"],"summary":"Latest aggregated price per (symbol, currency, contract_type)","operationId":"get_prices_v1_prices_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Symbol"}},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication.","title":"Include"},"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpotListResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/spot/{symbol}":{"get":{"tags":["prices"],"summary":"Latest spot price for one symbol — flat shape, no envelope (parity convenience)","operationId":"get_spot_v1_spot__symbol__get","parameters":[{"name":"symbol","in":"path","required":true,"schema":{"type":"string","title":"Symbol"}},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication.","title":"Include"},"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpotResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/prices/stream":{"get":{"tags":["prices"],"summary":"Live oracle SSE stream — Pro-tier (XAU-USD-SPOT and XAG-USD-SPOT)","operationId":"stream_prices_v1_prices_stream_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","description":"Streamable symbol; supports XAU-USD-SPOT and XAG-USD-SPOT","title":"Symbol"},"description":"Streamable symbol; supports XAU-USD-SPOT and XAG-USD-SPOT"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/v1/prices/divergence":{"get":{"tags":["prices"],"summary":"Symbols whose cross-source price spread exceeds a threshold","operationId":"get_prices_divergence_v1_prices_divergence_get","parameters":[{"name":"threshold","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Spread floor in basis points. Default 50 bps — matches the tokenized PAXG/XAUT divergence threshold for consistency across transparency signals.","default":50,"title":"Threshold"},"description":"Spread floor in basis points. Default 50 bps — matches the tokenized PAXG/XAUT divergence threshold for consistency across transparency signals."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/prices/history":{"get":{"tags":["prices"],"summary":"[DEPRECATED — use /v1/bars] Aggregated close history for a symbol — interval ∈ {1d, 1h}.","description":"Dispatch to the per-interval handler. The handler is the Module\nInterface; per-interval helpers are Deep Modules hiding the\ninterval-specific policy + storage shape.\n\nDEPRECATED in favour of /v1/bars (C4). This endpoint retains the\nlegacy close-only series[] wire shape for backward compatibility.\nDeprecation + Sunset + Link headers are injected on every 200 response\n(and on 403 plan_gated, where the caller may need to upgrade before\nmigrating). The sunset DATE is founder-controlled (see config\nprices_history_sunset_date) — NOT hardcoded here.","operationId":"get_history_v1_prices_history_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"title":"Symbol"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","maxLength":32,"title":"From"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","maxLength":32,"title":"To"}},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1d|1h)$","default":"1d","title":"Interval"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Success. See Deprecation + Sunset + Link headers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/settlements":{"get":{"tags":["prices"],"summary":"Futures settlement series for GC / SI / HG (Pro+)","operationId":"get_settlements_v1_settlements_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":8,"title":"Symbol"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"To"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/forward-view/{asset}":{"get":{"tags":["prices"],"summary":"Multi-horizon forward view: futures settlement curve","operationId":"get_forward_view_v1_forward_view__asset__get","parameters":[{"name":"asset","in":"path","required":true,"schema":{"type":"string","title":"Asset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/convert":{"get":{"tags":["convert"],"summary":"Convert between metals, currencies, and units","description":"Convert an amount between metals, currencies, and units.\n\n- metal → currency: spot price in that currency via FX cross through USD\n- currency → metal: inverse of the above\n- metal → metal: ratio of USD spot prices\n- currency → currency: FX cross through USD (live raw_exchangerate_fx\n  first, raw_frankfurter daily-ECB fallback)\n\nTwo-leg source skew: currency → currency conversions resolve EACH leg\nindependently through the live-first/daily-fallback chain, so the two\nlegs can come from different sources and ages (e.g. a live EUR rate\ncrossed with a daily-ECB THB rate). The response `timestamp` reflects\nthe `to` leg only — the `from` leg's timestamp is discarded — so it\ncan understate (or overstate) the age of the other half of the cross.\nBoth legs share the USD anchor, so cross drift is bounded by the\nstaleness of the older leg — never compounded.\n\nThe `unit` param applies to the metal side only (oz / gram / kg).\nHistorical conversion via `date` is not supported; use /v1/bars +\n/v1/prices/history for historical spot data.","operationId":"get_convert_v1_convert_get","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","description":"Source symbol: XAU, XAG, USD, EUR, …","title":"From"},"description":"Source symbol: XAU, XAG, USD, EUR, …"},{"name":"to","in":"query","required":true,"schema":{"type":"string","description":"Target symbol: USD, EUR, XAU, …","title":"To"},"description":"Target symbol: USD, EUR, XAU, …"},{"name":"amount","in":"query","required":false,"schema":{"type":"number","description":"Amount to convert (default 1).","default":1.0,"title":"Amount"},"description":"Amount to convert (default 1)."},{"name":"unit","in":"query","required":false,"schema":{"type":"string","description":"Unit for the metal side: oz | gram | kg (default oz).","default":"oz","title":"Unit"},"description":"Unit for the metal side: oz | gram | kg (default oz)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/carat":{"get":{"tags":["convert"],"summary":"Per-gram gold price by karat purity","description":"Per-gram XAU price for karats 24 / 22 / 21 / 20 / 18 / 16 / 14 / 10.\n\nLogic: per_gram_24k = spot_xau_usd_per_oz / GRAMS_PER_TROY_OUNCE * fx_rate;\nper_gram_Nk = per_gram_24k * N/24.\n\nHistorical `date` support is not implemented; omit the param or use\n/v1/prices/history + do the math client-side.","operationId":"get_carat_v1_carat_get","parameters":[{"name":"currency","in":"query","required":false,"schema":{"type":"string","description":"Quote currency (default USD).","default":"USD","title":"Currency"},"description":"Quote currency (default USD)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaratResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/physical":{"get":{"tags":["physical"],"summary":"Physical gold — live country index","description":"§5.1 — which countries are live, how many sources, freshest fetch.","operationId":"physical_index_v1_physical_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Physical Index V1 Physical Get"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/physical/{country}":{"get":{"tags":["physical"],"summary":"Physical gold — dealer quotes for one country","description":"§5.2 — latest dealer quotes + premium_over_spot_bps for one country.","operationId":"physical_country_v1_physical__country__get","parameters":[{"name":"country","in":"path","required":true,"schema":{"type":"string","title":"Country"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Physical Country V1 Physical  Country  Get"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/surface/{metal}":{"get":{"tags":["surface"],"summary":"Probability surface — options x prediction markets","description":"§5.3 — latest snapshot for one metal at one horizon (or the default\nhorizon from the §5.3b index when `horizon` is omitted). Stale-engine\nhonesty: a snapshot older than surface_serve_max_age_seconds is still\nserved with its true snapshot_age_seconds — never 404 on staleness.","operationId":"surface_v1_surface__metal__get","parameters":[{"name":"metal","in":"path","required":true,"schema":{"type":"string","title":"Metal"}},{"name":"horizon","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Horizon"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Surface V1 Surface  Metal  Get"}}}},"403":{"description":"plan_gated — requires the Pro tier or higher."},"404":{"description":"unknown_metal, unknown_horizon, or no data yet."},"422":{"description":"invalid_horizon — must be an ISO date (YYYY-MM-DD)."}}}},"/v1/surface/{metal}/expiries":{"get":{"tags":["surface"],"summary":"Probability surface — horizon index","description":"§5.3b — the live horizon index for one metal.","operationId":"surface_expiries_v1_surface__metal__expiries_get","parameters":[{"name":"metal","in":"path","required":true,"schema":{"type":"string","title":"Metal"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Surface Expiries V1 Surface  Metal  Expiries Get"}}}},"403":{"description":"plan_gated — requires the Pro tier or higher."},"404":{"description":"unknown_metal, or no snapshot ever written for this metal."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/v1/signals/arbitrage":{"get":{"tags":["surface"],"summary":"Cross-venue executable-edge signals","description":"§5.4 — cross-venue arb signals swept across EVERY live horizon's\nlatest snapshot (not just the default: real edge can live at a\nnon-default horizon, and the public \"zero executable arb\" claim is\nmeasured against everything we serve). Each signal carries the\nhorizon_target where the edge was found. Populated rarely; empty array\nis the honest, normal case.\n\nNo Age header: signals span horizons of different cycle ages —\nfreshness is per-signal via detected_at (+ horizon_target); a single\nresponse-level age would misstate the stale ones. Age stays on\n/surface and /expiries, whose responses are single snapshots.","operationId":"signals_arbitrage_v1_signals_arbitrage_get","parameters":[{"name":"metal","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Metal"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Signals Arbitrage V1 Signals Arbitrage Get"}}}},"403":{"description":"plan_gated — requires the Pro tier or higher."},"404":{"description":"unknown_metal, or no snapshot ever written for this metal."},"422":{"description":"metal query param required and must be xau|xag."}}}},"/v1/bars":{"get":{"tags":["bars"],"summary":"OHLCV bar history — cursor-paginated, real data, is_closed label","description":"Paginated OHLCV bar history from the canonical price_bars store.\n\nResults are ordered DESC by bar_start (newest first). Paginate via\nnext_cursor — pass it as ?cursor= on the next request. When next_cursor\nis null the result is exhausted. Empty range returns 200 with bars: [].\n\nTier depth gates:\n- Free:  30 days\n- Basic: 1 year\n- Pro+:  full history (daily back to 1996 for XAU/USD; intraday from 2006)\n\nIntraday intervals (1m, 5m, 15m, 30m, 1h, 2h, 4h) require Pro+.","operationId":"get_bars_endpoint_v1_bars_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\".","title":"Symbol"},"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\"."},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1m|5m|15m|30m|1h|2h|4h|1d)$","description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d.","default":"1d","title":"Interval"},"description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d."},{"name":"from","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 start (inclusive).","title":"From"},"description":"ISO-8601 start (inclusive)."},{"name":"to","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 end (inclusive).","title":"To"},"description":"ISO-8601 end (inclusive)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Max bars per page (1-10000). Default 100.","default":100,"title":"Limit"},"description":"Max bars per page (1-10000). Default 100."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns bars older than the cursor within the from/to window.","title":"Cursor"},"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns bars older than the cursor within the from/to window."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BarsResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/bars/latest":{"get":{"tags":["bars"],"summary":"Latest bar for a symbol+interval — the most-recent settled or forming bar","description":"Return the single most-recent bar for a symbol+interval.\n\nOrdered by bar_start DESC, returns the first row. Provisional (forming)\nbars are returned with is_closed=False. Empty → 200 with bar: null, never\n404. No tier gate on this endpoint (latest bar is the lightest read;\ndepth-gating only applies to ranges).","operationId":"get_latest_bar_v1_bars_latest_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"GLD\".","title":"Symbol"},"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"GLD\"."},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1m|5m|15m|30m|1h|2h|4h|1d)$","description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d.","default":"1d","title":"Interval"},"description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestBarResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/ratios":{"get":{"tags":["ratios"],"summary":"Cross-commodity ratio time-series — date-joined daily closes (Pro+)","description":"Paginated cross-commodity ratio series from the canonical price_bars store.\n\nResults are ordered DESC by date (newest first). Paginate via\nnext_cursor — pass it as ?cursor= on the next request. When next_cursor\nis null the result is exhausted. Empty range returns 200 with points: [].\n\nA date where either leg has no bar is absent — no forward-fill, no\nfabrication (Invariant #1 honesty).\n\nTier depth gates (same as /v1/bars 1d):\n- Free:  30 days (403 plan_gated)\n- Basic: 1 year  (403 plan_gated)\n- Pro+:  full history\nAll pairs require Pro+ (all pairs involve a non-XAU or non-spot leg).","operationId":"get_ratios_v1_ratios_get","parameters":[{"name":"pair","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Pair slug. Supported: gold-silver, gold-platinum, silver-platinum, gold-palladium, gld-xau.","title":"Pair"},"description":"Pair slug. Supported: gold-silver, gold-platinum, silver-platinum, gold-palladium, gld-xau."},{"name":"from","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 start (inclusive).","title":"From"},"description":"ISO-8601 start (inclusive)."},{"name":"to","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 end (inclusive).","title":"To"},"description":"ISO-8601 end (inclusive)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Max points per page (1-10000). Default 100.","default":100,"title":"Limit"},"description":"Max points per page (1-10000). Default 100."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns points older than the cursor within the from/to window.","title":"Cursor"},"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns points older than the cursor within the from/to window."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RatiosResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/ratios/latest":{"get":{"tags":["ratios"],"summary":"Latest ratio point for a pair — the most-recent shared-date ratio (Pro+)","description":"Return the single most-recent shared-date ratio point for a pair.\n\nReturns 200 with point: null when no shared date exists (e.g. before\neither leg is backfilled) — never 404 on empty (mirrors /v1/bars/latest).","operationId":"get_latest_ratio_v1_ratios_latest_get","parameters":[{"name":"pair","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Pair slug (e.g. 'gold-silver', 'gold-platinum', 'gld-xau').","title":"Pair"},"description":"Pair slug (e.g. 'gold-silver', 'gold-platinum', 'gld-xau')."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestRatioResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/download":{"get":{"tags":["download"],"summary":"Bulk OHLCV download — presigned parquet from R2 cold tier (Basic+, flag-gated)","description":"Bulk download of OHLCV bars for a symbol+interval+year from R2 cold tier.\n\nReturns a 307 redirect to a time-limited presigned URL (1 hour TTL).\nFree and anonymous callers are blocked — bulk download is the paid depth moat.\n\nTier gates:\n  Free/anonymous: 403 plan_gated.\n  Basic: 365-day window — years within the last year only.\n  Pro+: full history (back to 1996 for XAU/USD daily).\n\nFlag gate: 404 when core_ohlc_serving_enabled=False.","operationId":"download_bars_v1_download_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Symbol to download. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\". Only symbols with R2-exported data are available.","title":"Symbol"},"description":"Symbol to download. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\". Only symbols with R2-exported data are available."},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1m|5m|15m|30m|1h|2h|4h|1d)$","description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d.","default":"1d","title":"Interval"},"description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d."},{"name":"year","in":"query","required":true,"schema":{"type":"integer","maximum":2026,"minimum":1996,"description":"Calendar year to download (1996-2026). Each year is one parquet file partitioned from the R2 store.","title":"Year"},"description":"Calendar year to download (1996-2026). Each year is one parquet file partitioned from the R2 store."},{"name":"fmt","in":"query","required":false,"schema":{"type":"string","pattern":"^(parquet|csv)$","description":"Download format. \"parquet\" (default) = presigned R2 URL. \"csv\" = same parquet file (CSV transcode is a future enhancement; both formats serve the parquet object today).","default":"parquet","title":"Fmt"},"description":"Download format. \"parquet\" (default) = presigned R2 URL. \"csv\" = same parquet file (CSV transcode is a future enhancement; both formats serve the parquet object today)."}],"responses":{"307":{"description":"Redirect to presigned R2 URL (parquet file)."},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}},"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/public/xau-history":{"get":{"tags":["public"],"summary":"Public XAU/USD daily-close history (1 year, hourly refresh, no auth)","description":"Year-back daily-close series for XAU-USD-SPOT. No auth, no tier\ngate, no per-IP cap. Refreshes once per hour to deter scraping —\ncallers within the same hour receive identical bytes.\n\nUsed by the /data/gold forward-view chart (left-half history) and\nany future marketing surface that needs price context without\nasking the visitor for an API key.","operationId":"get_public_xau_history_v1_public_xau_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/metadata/tiers":{"get":{"tags":["system"],"summary":"Public tier catalog","description":"Ordered list of tiers (Free → Basic → Physical → Pro → Realtime Pro).\n\nStatic catalog — same response for every caller. 1h edge-cache is safe\nbecause tier definitions only change on redeploy.","operationId":"public_tiers_v1_metadata_tiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TierPublic"},"type":"array","title":"Response Public Tiers V1 Metadata Tiers Get"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/keys":{"get":{"tags":["system"],"summary":"List active API keys for the authed organization","operationId":"list_keys_v1_account_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/KeyPublic"},"type":"array","title":"Response List Keys V1 Account Keys Get"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]},"post":{"tags":["system"],"summary":"Mint a new API key; old key enters a 24h grace period","operationId":"create_key_v1_account_keys_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyMintResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}},"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/keys/{key_id}":{"delete":{"tags":["system"],"summary":"Hard-revoke a key immediately (revoked_at = now)","operationId":"delete_key_v1_account_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"204":{"description":"Successful Response"},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/checkout":{"post":{"tags":["system"],"summary":"Create Checkout","operationId":"create_checkout_v1_account_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The requested account action cannot be completed in its current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/usage":{"get":{"tags":["system"],"summary":"Authed key's day + month call counters","operationId":"get_usage_v1_account_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsagePublic"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-minute, anonymous, or monthly quota exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/internal/account/usage":{"get":{"tags":["internal"],"summary":"Proxy: day + month call counters for an org (Next.js dashboard)","description":"Validates the end-user's Supabase token via auth.get_user (catches revocation; no new dep or secret). Derives the org server-side from the verified user id — never trusts a caller-supplied org_id (closes the IDOR). Still gated by X-Internal-Secret as defense-in-depth (only the Vercel proxy reaches this).","operationId":"internal_account_usage_internal_account_usage_get","parameters":[{"name":"X-User-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Token"}},{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsagePublic"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/account/funnel-event":{"post":{"tags":["internal"],"summary":"Internal Funnel Event","description":"Record one allowlisted dashboard funnel fact for the verified org.","operationId":"internal_funnel_event_internal_account_funnel_event_post","parameters":[{"name":"X-User-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Token"}},{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FunnelEventInput"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"boolean"},"title":"Response Internal Funnel Event Internal Account Funnel Event Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/account/test-key":{"post":{"tags":["internal"],"summary":"Internal Test Key","description":"Execute one public authenticated sample request without returning a key.","operationId":"internal_test_key_internal_account_test_key_post","parameters":[{"name":"X-User-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Token"}},{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Internal Test Key Internal Account Test Key Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/wgc-spot":{"post":{"tags":["internal"],"summary":"Trigger continuous spot reference scrape cycle","operationId":"cron_wgc_spot_internal_cron_wgc_spot_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Wgc Spot Internal Cron Wgc Spot Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/billing-reconciliation":{"post":{"tags":["internal"],"summary":"Recover verified paid checkouts that missed webhook provisioning","operationId":"cron_billing_reconciliation_internal_cron_billing_reconciliation_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Billing Reconciliation Internal Cron Billing Reconciliation Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/frankfurter-fx":{"post":{"tags":["internal"],"summary":"Trigger ECB reference rates FX scrape cycle","operationId":"cron_frankfurter_fx_internal_cron_frankfurter_fx_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Frankfurter Fx Internal Cron Frankfurter Fx Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/exchangerate-fx":{"post":{"tags":["internal"],"summary":"Trigger exchangerate.dev live FX scrape cycle","operationId":"cron_exchangerate_fx_internal_cron_exchangerate_fx_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Exchangerate Fx Internal Cron Exchangerate Fx Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/yahoo-futures":{"post":{"tags":["internal"],"summary":"Trigger secondary GC=F + SI=F + HG=F multi-symbol futures scrape cycle","operationId":"cron_yahoo_futures_internal_cron_yahoo_futures_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Yahoo Futures Internal Cron Yahoo Futures Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/cme-settlements":{"post":{"tags":["internal"],"summary":"Trigger futures settlement capture (GC / SI / HG via daily-close series)","operationId":"cron_cme_settlements_internal_cron_cme_settlements_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Cme Settlements Internal Cron Cme Settlements Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/settlements-proxy":{"post":{"tags":["internal"],"summary":"Derive daily GC/SI/HG settlement proxies from raw_yahoo_futures","operationId":"cron_settlements_proxy_internal_cron_settlements_proxy_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Settlements Proxy Internal Cron Settlements Proxy Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/cmc-gold-tokens":{"post":{"tags":["internal"],"summary":"Trigger tokenized gold (PAXG + XAUT) snapshot cycle","operationId":"cron_cmc_gold_tokens_internal_cron_cmc_gold_tokens_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Cmc Gold Tokens Internal Cron Cmc Gold Tokens Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/pyth-xau-usd":{"post":{"tags":["internal"],"summary":"Trigger live oracle XAU/USD scrape cycle","operationId":"cron_pyth_xau_usd_internal_cron_pyth_xau_usd_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Pyth Xau Usd Internal Cron Pyth Xau Usd Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/pyth-xag-usd":{"post":{"tags":["internal"],"summary":"Trigger live oracle XAG/USD (silver spot) scrape cycle","operationId":"cron_pyth_xag_usd_internal_cron_pyth_xag_usd_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Pyth Xag Usd Internal Cron Pyth Xag Usd Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/aggregator":{"post":{"tags":["internal"],"summary":"Trigger aggregator cycle (derived_prices)","operationId":"cron_aggregator_internal_cron_aggregator_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Aggregator Internal Cron Aggregator Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/observability-sweep":{"post":{"tags":["internal"],"summary":"Periodic health sweep: scrapers + freshness + cron drift [B-91]","description":"Five-minute sweep that emits Sentry events on anomalies. row_count\nis the anomaly count for that cycle — queryable via scraper_runs to\ntrack baseline noise post-launch.","operationId":"cron_observability_sweep_internal_cron_observability_sweep_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Observability Sweep Internal Cron Observability Sweep Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/indexnow-ping":{"post":{"tags":["internal"],"summary":"Fetch sitemap.xml + submit all URLs to IndexNow for fast indexing","description":"Daily cron (02:00 UTC): pings IndexNow with every URL in our\npublic sitemap. Idempotent — IndexNow de-duplicates server-side and\nre-submitting unchanged URLs is cheap + harmless.\n\nSitemap source: https://goldprice.dev/sitemap.xml (served by Next.js\nfrom web/src/app/sitemap.ts).","operationId":"cron_indexnow_ping_internal_cron_indexnow_ping_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Indexnow Ping Internal Cron Indexnow Ping Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/daily-usage-snapshot":{"post":{"tags":["internal"],"summary":"Snapshot yesterday's per-org request counts from Redis to daily_usage","description":"Read Redis `quota_day:org:<uuid>:<YYYY-MM-DD>` counters and UPSERT\ninto `public.daily_usage` for yesterday (UTC). Runs 00:05 UTC daily\nper 0015_daily_usage_pg_cron.sql. The Redis counters live 48h so\nyesterday's bucket is still readable when this fires.\n\nAnonymous (`ip:*`) buckets are intentionally skipped — they have no\nforeign key target in `organizations`. Orgs with zero daily count\nstill get a row so the dashboard can distinguish \"quiet day\" from\n\"no data yet.\"","operationId":"cron_daily_usage_snapshot_internal_cron_daily_usage_snapshot_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Daily Usage Snapshot Internal Cron Daily Usage Snapshot Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/monthly-usage-digest":{"post":{"tags":["internal"],"summary":"Send monthly-usage-summary email to paid orgs for previous month","description":"Aggregate previous month `daily_usage` per paid org, send digest.\n\nFires from `pg_cron` at 13:00 UTC on the 1st of each month (see\n0025_monthly_usage_digest_cron.sql). Audience per PMO Q4 = basic +\npro only; free-tier orgs excluded (low signal + template has\ntier_quota_monthly that free tier barely touches).\n\nMonth boundaries: previous-month (first day → last day inclusive).\nE.g. firing on 2026-05-01 aggregates 2026-04-01 → 2026-04-30. The\n0-usage case still sends (`calls_used=0`) — the digest is the only\nrecurring touchpoint for quiet orgs; silent skip would be invisible.\n\nIdempotency: cron firing once per month + Resend message_id in log\nis sufficient dedup for V1. org_email_log backing lands in B-19-d\nand will cover the \"cron fires twice on the 1st due to DST or\nmanual trigger\" case. Known limitation for launch.","operationId":"cron_monthly_usage_digest_internal_cron_monthly_usage_digest_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Monthly Usage Digest Internal Cron Monthly Usage Digest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/quota-warnings":{"post":{"tags":["internal"],"summary":"Daily sweep: send quota-80/quota-100 emails when MTD usage crosses thresholds","description":"Daily cron scanning month-to-date `daily_usage` per active org.\n\nThresholds: quota-100 at ratio >= 1.0, quota-80 at ratio >= 0.8.\nFires for all tiers. Period-scoped org_email_log keys make each 80% and\n100% notice a once-per-billing-month action.\n\nMonth boundaries read from _mtd_bounds; daily_usage lag is ~1 day\nbecause daily-usage-snapshot writes yesterday's bucket at 00:05 UTC\nand this cron fires at 00:15 UTC. Today's usage will not appear\nuntil tomorrow's snapshot, which is fine — a customer crossing 80%\non day N gets the email on day N+1 at worst.","operationId":"cron_quota_warnings_internal_cron_quota_warnings_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Quota Warnings Internal Cron Quota Warnings Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/winback":{"post":{"tags":["internal"],"summary":"Weekly sweep: send win-back emails to inactive Basic/Pro orgs (30-90d stale)","description":"Weekly cron targeting paid orgs whose most-recent `api_keys.last_used_at`\nis between 30 and 90 days ago.\n\nAudience (audit R15):\n  - status='active' (skip canceled / past_due / payment_at_risk)\n  - tier IN ('basic', 'pro')  — free-tier excluded: free cooling-off\n    is evaluation behavior, not churn, and over-emailing degrades\n    sender reputation.\n  - max(api_keys.last_used_at) between 30 and 90 days ago.\n\nUpper bound (90d) prevents cold-contacting fully abandoned trials\nindefinitely. Frequency cap (30d) in send.py means a user stuck\nin the 30-90 band gets at most one win-back every 30 days, not\nonce per weekly cron fire.","operationId":"cron_winback_internal_cron_winback_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Winback Internal Cron Winback Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/activation-first-call":{"post":{"tags":["internal"],"summary":"Daily sweep: nudge new orgs 24h-7d after signup that never made a call","description":"Daily cron targeting recent active orgs that have NEVER used their API key.\n\nAudience (2026-07-10, real-data-driven — found via a live org/usage\ncross-reference that a paying customer sat unactivated for 2 weeks\nwith no template able to reach them):\n  - status='active' (skip canceled / past_due / payment_at_risk)\n  - every active tier, including Free. The observed activation drop-off\n    happens after Free users receive a key, so they need this nudge too.\n  - created_at at least 24h ago (matches dino.markets' proven\n    activation-first-call cadence).\n  - created_at no more than 7d ago. This keeps the message relevant to\n    new users and prevents a first rollout from emailing a historical\n    backlog months after signup.\n  - NO api_keys row for the org has a non-null last_used_at — i.e.\n    every key they've ever held (including a rotated-away one) has\n    never been used even once. A rotated key's OLD row keeps its\n    original last_used_at (rotation never touches it), so this\n    correctly reads \"activated\" for an org that used a key once\n    then rotated.\n\nOnce-ever via FREQUENCY_CAPS (same _WELCOME_ONCE_EVER sentinel as\nwelcome) — a daily re-scan while still unactivated is a cheap no-op\nonce the first send lands. Structurally disjoint from win-back's\naudience: win-back requires a real last_used_at in the 30-90d band,\nwhich an org with zero usage can never have.","operationId":"cron_activation_first_call_internal_cron_activation_first_call_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Activation First Call Internal Cron Activation First Call Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/pyth-live-flush":{"post":{"tags":["internal"],"summary":"D3: flush settled 1m oracle bars + trigger incremental coarse rollup","description":"Per-minute cron flush: drains settled minute bars from the accumulator\ninto price_bars, then re-derives the affected 15m/1h/4h/1d windows.\n\npg_cron schedule: every minute. Dead-man 2x = 120s.\nFOUNDER-GATED to have effect: PYTH_LIVE_ROLLUP_ENABLED must be True;\nuntil then the accumulator stays empty and this flush is a no-op.","operationId":"cron_pyth_live_flush_internal_cron_pyth_live_flush_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Pyth Live Flush Internal Cron Pyth Live Flush Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/prune-live-1m":{"post":{"tags":["internal"],"summary":"D3: prune pyth_live 1m bars older than retention window (dry-run by default)","description":"Daily retention prune: removes pyth_live 1m bars older than\npyth_live_1m_retention_days (default 30d). Coarser rollups (15m+) are\nNEVER pruned.\n\nDRY-RUN BY DEFAULT (execute=False) until the founder explicitly enables\nautomated pruning by flipping the lambda to execute=True. Flipping to\nexecute=True is a FOUNDER DECISION — it is irreversible for the deleted\nrows. Do not change execute=True without explicit founder go-ahead.\n\npg_cron schedule: daily at 03:00 UTC (0003_d3_live_rollup_jobs.sql).\nDead-man 2x = 48h.","operationId":"cron_prune_live_1m_internal_cron_prune_live_1m_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Prune Live 1M Internal Cron Prune Live 1M Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/prune-derived":{"post":{"tags":["internal"],"summary":"C3: prune derived_prices rows older than the hot window (dry-run by default)","description":"Daily retention prune: removes derived_prices rows older than\nderived_prices_hot_window_days (default 90d) across all known symbols.\n\nDRY-RUN BY DEFAULT (execute=False) until the founder explicitly enables\nautomated pruning by flipping the lambda to execute=True. Flipping to\nexecute=True is a FOUNDER DECISION and is gated by C3 cutover preconditions\n(core_ohlc_serving_enabled=True + R2 export verified) enforced inside\nprune_derived.run — so even a mistaken flip raises before deleting if the\ncutover isn't complete. Deleted rows are irreversible.\n\npg_cron schedule: daily at 03:50 UTC (0004_schedule_prune_derived.sql).\nDead-man 2x = 48h.","operationId":"cron_prune_derived_internal_cron_prune_derived_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Prune Derived Internal Cron Prune Derived Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/lse-confirmation":{"post":{"tags":["internal"],"summary":"D3: LSE cross-check — latest oracle close vs LSE close, emit divergence signal","description":"Hourly cross-check: fetches LSE daily close for each covered metal,\ncompares against the most recent settled Pyth bar, emits divergence signal\nand stale/high-divergence alerts. FOUNDER-GATED: needs LSE_API_KEY.\n\npg_cron schedule: every 60 minutes. Dead-man 2x = lse_confirmation_interval_minutes * 2.","operationId":"cron_lse_confirmation_internal_cron_lse_confirmation_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Lse Confirmation Internal Cron Lse Confirmation Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/surface-snapshot-xau":{"post":{"tags":["internal"],"summary":"TIERS_V2 Surface (PR-2b-core): compute + write XAU surface snapshots (flag-gated)","operationId":"cron_surface_snapshot_xau_internal_cron_surface_snapshot_xau_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Surface Snapshot Xau Internal Cron Surface Snapshot Xau Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/surface-snapshot-xag":{"post":{"tags":["internal"],"summary":"TIERS_V2 Surface (PR-2b-core): compute + write XAG surface snapshots (flag-gated)","operationId":"cron_surface_snapshot_xag_internal_cron_surface_snapshot_xag_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Surface Snapshot Xag Internal Cron Surface Snapshot Xag Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/forward-view-futures-curve":{"post":{"tags":["internal"],"summary":"Trigger primary per-expiry GC curve scrape (secondary backup chained)","operationId":"cron_forward_view_futures_curve_internal_cron_forward_view_futures_curve_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Forward View Futures Curve Internal Cron Forward View Futures Curve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/physical-gold-id":{"post":{"tags":["internal"],"summary":"Trigger all four Indonesia physical-gold scrapers (per-source isolation)","description":"Fans out to treasury_id, indogold, pegadaian, anekalogam —\nconcurrently via _run_physical_dispatch, each under its own\nrun_and_record so scraper_runs telemetry + Healthchecks pings are\nper-source (matches every other scraper's instrumentation) and one\nsource failing does NOT prevent the other three from running.\n\nCadence: 15 minutes (see 20260703120200_physical_gold_id_pg_cron.sql).\nDead-man 2x per source = 30 min.\n\nDark launch: this route has no tier/pricing gating — it exists purely\nto populate raw_physical_gold ahead of the TIERS_V2 flip (§6b PR-3).","operationId":"cron_physical_gold_id_internal_cron_physical_gold_id_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Physical Gold Id Internal Cron Physical Gold Id Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/physical-gold-intl":{"post":{"tags":["internal"],"summary":"Trigger the 6 international physical-gold scrapers (per-source isolation)","description":"Fans out to bnm_my, vangtoday, sge_cn, sarafa_pk, forexpk,\ndubaicityofgold — one route per wave, concurrently via\n_run_physical_dispatch, same per-source isolation shape as\n/physical-gold-id (Indonesia).\n\nCadence: 15 minutes (see 20260703120300_physical_gold_intl_pg_cron.sql).\nDead-man 2x per source = 30 min.\n\nDark launch: this route has no tier/pricing gating — it exists purely\nto populate raw_physical_gold ahead of the TIERS_V2 flip (§6b PR-3),\nsame posture as /physical-gold-id.","operationId":"cron_physical_gold_intl_internal_cron_physical_gold_intl_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Physical Gold Intl Internal Cron Physical Gold Intl Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/physical-gold-w3":{"post":{"tags":["internal"],"summary":"Trigger the 3 wave-3 physical-gold scrapers (per-source isolation)","description":"Fans out to tanaka_jp (Japan), fenegosida_np (Nepal), isagha_eg\n(Egypt) — one route per wave, concurrently via\n_run_physical_dispatch, same per-source isolation shape as\n/physical-gold-id and /physical-gold-intl.\n\nCadence: 15 minutes (see 20260704120000_physical_gold_w3_pg_cron.sql).\nDead-man 2x per source = 30 min.\n\nDark launch: this route has no tier/pricing gating — it exists purely\nto populate raw_physical_gold ahead of the TIERS_V2 flip (§6b PR-3),\nsame posture as the earlier physical waves.","operationId":"cron_physical_gold_w3_internal_cron_physical_gold_w3_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Physical Gold W3 Internal Cron Physical Gold W3 Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/cron/physical-gold-w4":{"post":{"tags":["internal"],"summary":"Trigger the 3 wave-4 physical-gold scrapers (per-source isolation)","description":"Fans out to ibja_in (India), goldpricedata_sa (Saudi Arabia),\nhkgoldrate_hk (Hong Kong) — one route per wave, concurrently via\n_run_physical_dispatch, same per-source isolation shape as the\nearlier physical routes.\n\nCadence: 15 minutes (see 20260706120000_physical_gold_w4_pg_cron.sql).\nDead-man 2x per source = 30 min.\n\nDark launch: this route has no tier/pricing gating — it exists purely\nto populate raw_physical_gold ahead of the TIERS_V2 flip (§6b PR-3),\nsame posture as the earlier physical waves.","operationId":"cron_physical_gold_w4_internal_cron_physical_gold_w4_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cron Physical Gold W4 Internal Cron Physical Gold W4 Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/internal/email/send":{"post":{"tags":["internal"],"summary":"Send one email through the standard api-side pipeline","description":"Dispatch via `send_email`. Errors short-circuit with 502 so the\nNext caller can decide whether to surface or swallow. Welcome sends\nfrom provision-org are fire-and-forget on the Next side — a 502\nfrom here becomes a Sentry breadcrumb, not a user-visible failure.","operationId":"internal_email_send_internal_email_send_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Internal-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendEmailRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Internal Email Send Internal Email Send Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}}},"components":{"schemas":{"Bar":{"properties":{"bar_start":{"type":"string","title":"Bar Start","description":"Bar open timestamp, ISO-8601 UTC with Z suffix (e.g. \"2026-06-15T00:00:00Z\"). The bar covers the half-open interval [bar_start, bar_start + interval)."},"open":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Open","description":"Open price as decimal string. Null only on data gaps (never fabricated)."},"high":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"High","description":"High price as decimal string."},"low":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Low","description":"Low price as decimal string."},"close":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Close","description":"Close price as decimal string."},"volume":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Volume","description":"Trading volume as decimal string. Null for spot instruments (XAU/USD, XAG/USD — no consolidated spot volume). Real for futures (GC, SI, HG) and ETFs (GLD). Never 0, never fabricated."},"is_closed":{"type":"boolean","title":"Is Closed","description":"True if the bar's time window has ended and the OHLC values are final (settled). False for the currently-forming bar (provisional)."}},"additionalProperties":false,"type":"object","required":["bar_start","is_closed"],"title":"Bar","description":"One OHLCV bar in the /v1/bars wire shape.\n\nPrices are Decimal-as-string to match the existing /v1/prices/history\ncontract and avoid float drift on the consumer side."},"BarsMeta":{"properties":{"tier":{"type":"string","title":"Tier"},"count":{"type":"integer","title":"Count"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id","description":"Provenance tag from the price_bars store. Examples: 'csv_xau_15m_v1', 'csv_xau_rollup_v1', 'lse_gld', 'pyth_live', 'live_xau_canonical'. Null when bars list is empty."}},"additionalProperties":false,"type":"object","required":["tier","count"],"title":"BarsMeta","description":"Metadata block in BarsResponse.\n\nsource_id is a PAGE-SCOPED provenance summary derived from the bars in this\npage — NOT a per-bar attribute.  It equals the source_id of the first bar\nin the page, or \"mixed\" if the page spans multiple source_ids (e.g. a range\nthat crosses from CSV history into a live Pyth feed).  It is non-authoritative\nfor mixed pages: consumers wanting per-bar provenance should wait for a future\nversion that exposes bar.source_id per-bar.  That per-bar field is a reserved\nfuture decision (flagged in the §9.4 public-surface audit); do NOT add it to\nthe current Bar wire shape without a version bump.\n\nExamples: 'csv_xau_15m_v1', 'csv_xau_rollup_v1', 'lse_gld', 'mixed'.\nNull when bars list is empty."},"BarsResponse":{"properties":{"symbol":{"type":"string","title":"Symbol","description":"The requested symbol (e.g. 'XAU/USD', 'GLD')."},"interval":{"type":"string","title":"Interval","description":"The requested interval (e.g. '1d', '15m')."},"bars":{"items":{"$ref":"#/components/schemas/Bar"},"type":"array","title":"Bars","description":"OHLCV bars, newest-first (DESC by bar_start)."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page. Pass as ?cursor= on the next request. Null when the result is exhausted. Encoding: base64url of the bar_start_utc ISO-8601 string of the last bar in this page."},"meta":{"$ref":"#/components/schemas/BarsMeta"}},"additionalProperties":false,"type":"object","required":["symbol","interval","bars","meta"],"title":"BarsResponse","description":"Response envelope for GET /v1/bars.\n\nKeyset pagination via next_cursor. When next_cursor is null, the result\nis exhausted (no more pages). An empty bars list with next_cursor=null\nis the canonical empty-range response (200, never 404)."},"CaratResponse":{"properties":{"currency":{"type":"string","title":"Currency"},"timestamp":{"type":"string","title":"Timestamp"},"price_gram_24k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 24K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_22k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 22K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_21k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 21K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_20k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 20K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_18k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 18K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_16k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 16K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_14k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 14K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_10k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 10K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."}},"type":"object","required":["currency","timestamp","price_gram_24k","price_gram_22k","price_gram_21k","price_gram_20k","price_gram_18k","price_gram_16k","price_gram_14k","price_gram_10k"],"title":"CaratResponse","description":"Response for GET /v1/carat."},"CheckoutRequest":{"properties":{"plan":{"type":"string","enum":["physical","pro","realtime"],"title":"Plan"}},"additionalProperties":false,"type":"object","required":["plan"],"title":"CheckoutRequest"},"CheckoutResponse":{"properties":{"result":{"type":"string","const":"success","title":"Result","default":"success"},"action":{"type":"string","enum":["checkout","manage_subscription"],"title":"Action"},"url":{"type":"string","title":"Url"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["action","url"],"title":"CheckoutResponse"},"ConvertResponse":{"properties":{"from":{"type":"string","title":"From","description":"Source symbol (e.g. XAU, USD)."},"to":{"type":"string","title":"To","description":"Target symbol (e.g. USD, EUR)."},"amount":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Amount","description":"Input amount."},"rate":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Rate","description":"Conversion rate (1 from-unit = rate to-units)."},"result":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Result","description":"amount x rate."},"unit":{"type":"string","title":"Unit","description":"Unit applied to the metal side (oz, gram, kg)."},"timestamp":{"type":"string","title":"Timestamp","description":"UTC ISO-8601 timestamp of the source data. For currency-to-currency conversions the two FX legs resolve independently (live-first with daily fallback) and may differ in source/age; this reflects one leg only."}},"type":"object","required":["from","to","amount","rate","result","unit","timestamp"],"title":"ConvertResponse","description":"Response for GET /v1/convert."},"ErrorResponse":{"properties":{"error":{"type":"string","title":"Error","description":"Machine-readable error code, e.g. 'not_found'."},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Human-readable explanation, when the error path provides one."}},"type":"object","required":["error"],"title":"ErrorResponse","description":"Canonical error envelope for documented failure responses.\n\nMirrors the runtime shape emitted by the HTTPException/validation\nexception handlers and the rate/quota middleware: a machine-readable\n`error` code plus an optional human `message`. Individual error paths may\ncarry additional keys (e.g. `upgrade_url`, `retry_after_seconds`); this\nmodel documents the stable floor, not an exhaustive contract."},"FunnelEventInput":{"properties":{"event":{"type":"string","title":"Event"}},"type":"object","required":["event"],"title":"FunnelEventInput"},"HistoryMeta":{"properties":{"tier":{"type":"string","title":"Tier"},"count":{"type":"integer","title":"Count"},"ohlc_note":{"type":"string","title":"Ohlc Note"}},"additionalProperties":true,"type":"object","required":["tier","count","ohlc_note"],"title":"HistoryMeta","description":"Meta block in HistoryResponse."},"HistoryResponse":{"properties":{"symbol":{"type":"string","title":"Symbol"},"interval":{"type":"string","title":"Interval","description":"One of \"1d\", \"1h\"."},"from":{"type":"string","title":"From","description":"ISO-8601 date or timestamp."},"to":{"type":"string","title":"To"},"series":{"items":{"$ref":"#/components/schemas/HistorySeriesEntry"},"type":"array","title":"Series"},"meta":{"$ref":"#/components/schemas/HistoryMeta"}},"additionalProperties":true,"type":"object","required":["symbol","interval","from","to","series","meta"],"title":"HistoryResponse","description":"Response envelope for GET /v1/prices/history (DEPRECATED — use /v1/bars).\n\nIMPORTANT: This model is for OpenAPI documentation only. The handler\nreturns JSONResponse directly; Pydantic validation does not run on the\nactual response (preserves byte-identical wire contract). Do not add\nfields here and expect them to appear in production responses without\nalso updating the handler."},"HistorySeriesEntry":{"properties":{"date":{"type":"string","title":"Date","description":"Daily bars: \"YYYY-MM-DD\". Intraday bars: ISO-8601 UTC timestamp with Z suffix."},"open":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Open","description":"Open price as decimal string."},"high":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"High","description":"High price as decimal string."},"low":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Low","description":"Low price as decimal string."},"close":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Close","description":"Close price as decimal string."},"volume":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Volume","description":"Always null in the current implementation (close-only legacy path)."},"is_stale":{"type":"boolean","title":"Is Stale","description":"True if the underlying data row was stale when fetched."},"is_closed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Closed","description":"Present only when core_ohlc_serving_enabled=True (real-bars path). True = settled bar; False = currently-forming (provisional)."}},"additionalProperties":true,"type":"object","required":["date","is_stale"],"title":"HistorySeriesEntry","description":"One element of the series[] array in /v1/prices/history.\n\nvolume is always null in the current implementation (close-only path);\nis_closed is present only when core_ohlc_serving_enabled=True (the C2\nreal-bars path). Both are included in the schema for forward-compat."},"KeyMintResponse":{"properties":{"id":{"type":"string","title":"Id"},"plaintext":{"type":"string","title":"Plaintext"},"key_prefix":{"type":"string","title":"Key Prefix"},"created_at":{"type":"string","title":"Created At"},"previous_key_revokes_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Key Revokes At"}},"type":"object","required":["id","plaintext","key_prefix","created_at"],"title":"KeyMintResponse"},"KeyPublic":{"properties":{"id":{"type":"string","title":"Id"},"key_prefix":{"type":"string","title":"Key Prefix"},"name":{"type":"string","title":"Name"},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At"}},"type":"object","required":["id","key_prefix","name","created_at"],"title":"KeyPublic"},"LatestBarResponse":{"properties":{"symbol":{"type":"string","title":"Symbol"},"interval":{"type":"string","title":"Interval"},"bar":{"anyOf":[{"$ref":"#/components/schemas/Bar"},{"type":"null"}],"description":"The most-recent bar for this symbol+interval. Null when no bar exists (e.g. before the backfill or for an unsupported interval)."},"meta":{"$ref":"#/components/schemas/BarsMeta"}},"additionalProperties":false,"type":"object","required":["symbol","interval","meta"],"title":"LatestBarResponse","description":"Response envelope for GET /v1/bars/latest.\n\nbar is null (not present) when no bar exists for the given symbol+interval.\nThe response is always 200 — never 404 on empty."},"LatestRatioResponse":{"properties":{"pair":{"type":"string","title":"Pair"},"numerator":{"type":"string","title":"Numerator"},"denominator":{"type":"string","title":"Denominator"},"interval":{"type":"string","title":"Interval","default":"1d"},"point":{"anyOf":[{"$ref":"#/components/schemas/RatioPoint"},{"type":"null"}],"description":"The most-recent shared-date ratio point. Null when no shared date exists (e.g. before either leg is backfilled)."},"meta":{"$ref":"#/components/schemas/RatiosMeta"}},"additionalProperties":false,"type":"object","required":["pair","numerator","denominator","meta"],"title":"LatestRatioResponse","description":"Response envelope for GET /v1/ratios/latest.\n\npoint is null (not present) when no shared-date bar exists for both legs.\nThe response is always 200 — never 404 on empty (mirrors /v1/bars/latest)."},"RatioPoint":{"properties":{"date":{"type":"string","title":"Date","description":"Bar date in YYYY-MM-DD format (UTC midnight = bar_start_utc). Only emitted when both numerator and denominator have a settled bar for this date — no forward-fill, no fabricated rows."},"ratio":{"type":"string","title":"Ratio","description":"numerator_close / denominator_close as a decimal string (4 dp). Example: '88.1234'. Never null — a date without both legs is simply absent from the series."},"numerator_close":{"type":"string","title":"Numerator Close","description":"Closing price of the numerator instrument as a decimal string."},"denominator_close":{"type":"string","title":"Denominator Close","description":"Closing price of the denominator instrument as a decimal string."}},"additionalProperties":false,"type":"object","required":["date","ratio","numerator_close","denominator_close"],"title":"RatioPoint","description":"One data point in the ratios series.\n\ndate is YYYY-MM-DD (daily). ratio/numerator_close/denominator_close are\nDecimal-as-string (4 dp), matching the price_bars NUMERIC(14,4) store.\nA point is only emitted when BOTH legs have a bar for that date — no\nforward-fill, no fabrication (Invariant #1 honesty)."},"RatiosMeta":{"properties":{"tier":{"type":"string","title":"Tier"},"count":{"type":"integer","title":"Count"}},"additionalProperties":false,"type":"object","required":["tier","count"],"title":"RatiosMeta","description":"Metadata block in RatiosResponse. Mirrors BarsMeta."},"RatiosResponse":{"properties":{"pair":{"type":"string","title":"Pair","description":"The pair slug (e.g. 'gold-silver', 'gold-platinum', 'gld-xau'). Echoed from the request for self-describing responses."},"numerator":{"type":"string","title":"Numerator","description":"Resolved numerator instrument symbol (e.g. 'XAU/USD')."},"denominator":{"type":"string","title":"Denominator","description":"Resolved denominator instrument symbol (e.g. 'XAG/USD')."},"interval":{"type":"string","title":"Interval","description":"Bar interval. Currently always '1d' (intraday ratios out of scope at launch).","default":"1d"},"points":{"items":{"$ref":"#/components/schemas/RatioPoint"},"type":"array","title":"Points","description":"Ratio data points, newest-first (DESC by date)."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page. Pass as ?cursor= on the next request. Null when the result is exhausted. Encoding: base64url of the date string ('YYYY-MM-DD') of the last point in this page."},"meta":{"$ref":"#/components/schemas/RatiosMeta"}},"additionalProperties":false,"type":"object","required":["pair","numerator","denominator","points","meta"],"title":"RatiosResponse","description":"Response envelope for GET /v1/ratios.\n\nKeyset pagination via next_cursor. When next_cursor is null, the result\nis exhausted (no more pages). An empty points list with next_cursor=null\nis the canonical empty-range response (200, never 404).\n\nResults are ordered DESC by date (newest first), mirroring /v1/bars."},"SendEmailRequest":{"properties":{"to":{"type":"string","format":"email","title":"To","description":"Recipient email address"},"template":{"type":"string","enum":["subscription-confirmed","monthly-usage-summary","welcome","quota-80","quota-100","win-back","activation-first-call"],"title":"Template","description":"Template slug"},"subject":{"type":"string","title":"Subject","description":"Subject line"},"variables":{"additionalProperties":true,"type":"object","title":"Variables","description":"Template-specific {placeholder} substitutions"},"organization_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization Id","description":"Supabase org uuid. When present, send.py applies frequency caps and appends to org_email_log."}},"type":"object","required":["to","template","subject"],"title":"SendEmailRequest","description":"Shape accepted by POST /internal/email/send.\n\n`variables` is opaque at this layer — the template decides which\n`{placeholder}` keys it needs and render.py raises\nTemplateRenderError if any required key is missing."},"SourceEntry":{"properties":{"source":{"type":"string","title":"Source"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"price":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price"},"unit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit"},"license":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License"},"is_stale":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Stale"},"informational":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Informational"}},"additionalProperties":true,"type":"object","required":["source"],"title":"SourceEntry","description":"One row inside `sources[]`. Shape varies across feeds (Pyth,\nStooq, WGC, Yahoo, CMC, Frankfurter); only `source` is mandatory.\n`extra=\"allow\"` keeps the model forward-compatible with new feed\nmetadata without a schema bump."},"SpotListResponse":{"properties":{"symbols":{"items":{"$ref":"#/components/schemas/SpotResponse"},"type":"array","title":"Symbols"}},"type":"object","required":["symbols"],"title":"SpotListResponse","description":"Envelope for `/v1/prices` — array of zero-or-more SpotResponse\nrows, one per (symbol, quote_currency, contract_type) latest."},"SpotResponse":{"properties":{"symbol":{"type":"string","title":"Symbol"},"quote_currency":{"type":"string","title":"Quote Currency"},"unit":{"type":"string","title":"Unit"},"contract_type":{"type":"string","title":"Contract Type"},"price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Price","description":"Decimal-as-string (avoids float drift)."},"bid":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Bid"},"ask":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Ask"},"is_stale":{"type":"boolean","title":"Is Stale"},"divergence_flag":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Divergence Flag","description":"Present only when requested via ?include= (stats or all). True when a tokenized-gold source diverges > 0.5% from primary."},"computed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Computed At"},"sources":{"anyOf":[{"items":{"$ref":"#/components/schemas/SourceEntry"},"type":"array"},{"type":"null"}],"title":"Sources","description":"Present only when requested via ?include= (sources or all). Tier-filtered provenance array; vendor keys obfuscated."},"divergence_bps":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Divergence Bps"},"value_stale":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Value Stale"},"live_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Live Quality","description":"Selected live quote quality when a live source is served."},"price_gram_24k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 24K"},"price_gram_22k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 22K"},"price_gram_21k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 21K"},"price_gram_20k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 20K"},"price_gram_18k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 18K"},"price_gram_16k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 16K"},"price_gram_14k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 14K"},"price_gram_10k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 10K"},"open_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Open Price"},"high_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"High Price"},"low_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Low Price"},"prev_close_price":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prev Close Price"},"ch":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ch"},"chp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chp"},"open_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Open Time"}},"type":"object","required":["symbol","quote_currency","unit","contract_type","is_stale"],"title":"SpotResponse","description":"Single tier-filtered spot row. Shared shape between the\nenvelope (`/v1/prices` returns `{\"symbols\": [SpotResponse, ...]}`)\nand the convenience endpoint (`/v1/spot/{symbol}` returns the\nSpotResponse unwrapped)."},"TierPublic":{"properties":{"name":{"type":"string","title":"Name"},"monthly_price_usd":{"type":"string","title":"Monthly Price Usd"},"yearly_price_usd":{"type":"string","title":"Yearly Price Usd"},"rate_limit_per_minute":{"type":"integer","title":"Rate Limit Per Minute"},"monthly_quota":{"type":"integer","title":"Monthly Quota"},"allowed_bases":{"items":{"type":"string"},"type":"array","title":"Allowed Bases"},"allowed_contracts":{"items":{"type":"string"},"type":"array","title":"Allowed Contracts"},"includes_mcp":{"type":"boolean","title":"Includes Mcp"},"commercial_use":{"type":"boolean","title":"Commercial Use"},"support_sla_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Support Sla Hours"},"max_concurrent_streams":{"type":"integer","title":"Max Concurrent Streams"},"hidden":{"type":"boolean","title":"Hidden"}},"type":"object","required":["name","monthly_price_usd","yearly_price_usd","rate_limit_per_minute","monthly_quota","allowed_bases","allowed_contracts","includes_mcp","commercial_use","support_sla_hours","max_concurrent_streams","hidden"],"title":"TierPublic"},"UsagePublic":{"properties":{"calls_today":{"type":"integer","title":"Calls Today"},"calls_this_month":{"type":"integer","title":"Calls This Month"},"active_streams":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Streams"},"max_streams":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Streams"}},"type":"object","required":["calls_today","calls_this_month"],"title":"UsagePublic"},"RequestValidationErrorResponse":{"type":"object","required":["error","message","details"],"properties":{"error":{"type":"string","const":"validation_error"},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","required":["loc","msg","type"],"properties":{"loc":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"integer"}]}},"msg":{"type":"string"},"type":{"type":"string"}}}}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"goldprice.dev API key"},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}},"tags":[{"name":"system","description":"Liveness + readiness probes"},{"name":"prices","description":"Latest Assessment per symbol"},{"name":"internal","description":"pg_cron dispatch endpoints — shared-secret gated, not for client use"}]}