{
  "openapi": "3.1.0",
  "info": {
    "title": "AnalystMarkets API",
    "version": "1.0.0",
    "description": "Market data, AI-enriched news, and the verification record behind every published accuracy figure.\n\nTwo things are worth reading before integrating. Freshness is stated, not implied: every payload carrying market data also carries the timestamp of that data and a status, and \"live\" is only ever returned when both a timestamp and an age inside the expected interval support it. And accuracy over models is reported separately from accuracy including the deterministic fallback, because blending them would report a keyword counter's score as a model's.",
    "license": {
      "name": "Terms of Service",
      "url": "https://www.analystmarkets.xyz/terms/"
    }
  },
  "servers": [
    {
      "url": "https://www.analystmarkets.xyz"
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    }
  },
  "paths": {
    "/api/news/": {
      "get": {
        "summary": "Recent news articles with AI enrichment",
        "description": "Articles newest first, each with its AI sentiment, confidence, affected symbols and source. Enrichment fields are absent on articles the pipeline has not reached yet rather than defaulted, so a missing sentiment is distinguishable from a neutral one.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nPaging: No cursor. `limit` caps the page and articles come back newest first; poll with the newest `published_at` you already hold as your watermark.\n\nResponse fields:\n- `articles[].id` (integer) \u2014 Stable article id. Use it against /api/accuracy/article/{id}/.\n- `articles[].title` (string) \u2014 Headline as published.\n- `articles[].summary` (string) \u2014 Summary, empty when the pipeline has not produced one.\n- `articles[].url` (string) \u2014 The publisher's URL, not ours.\n- `articles[].source` (string) \u2014 Publisher name.\n- `articles[].category` (string) \u2014 crypto, stocks, commodities, macro or general.\n- `articles[].published_at` (string (ISO 8601)) \u2014 Publication time, UTC. This is the freshness signal for news: there is no separate status field on this endpoint.\n- `articles[].sentiment` (string) \u2014 AI sentiment, or \"\" when the article has not been analysed.\n- `articles[].tags` (array) \u2014 Symbols and themes named in the piece.\n- `articles[].is_featured` (boolean) \u2014 Whether editorial selection picked it up.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Maximum articles to return."
          },
          {
            "name": "symbol",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only articles naming this symbol."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "articles": [
                    {
                      "id": 41,
                      "title": "Nvidia beats on data-centre revenue as AI demand holds",
                      "summary": "",
                      "url": "https://example.com/wire/nvidia-q3",
                      "source": "Reuters",
                      "category": "general",
                      "published_at": "2026-09-01T06:40:18.218215+00:00",
                      "time_ago": "16h ago",
                      "tags": [
                        "NVDA"
                      ],
                      "sentiment": "bullish",
                      "is_featured": false
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "News"
        ]
      }
    },
    "/api/market-data/": {
      "get": {
        "summary": "Current prices across asset classes",
        "description": "Latest stored price per symbol, with the timestamp of the price itself rather than of the request. Read `data_status` before treating a value as current: it carries one of live, delayed, refreshing, stale, partial, historical, unavailable, error or offline.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `crypto.charts.<SYMBOL>[]` (array) \u2014 OHLCV candles, oldest first.\n- `crypto.charts.<SYMBOL>[].timestamp` (string (ISO 8601)) \u2014 The candle's own time, not the time of your request. This is the value freshness is judged from.\n- `crypto.charts.<SYMBOL>[].open/high/low/close` (number) \u2014 Prices in USD.\n- `crypto.charts.<SYMBOL>[].volume` (number) \u2014 Volume in USD.\n- `crypto.analyses.<SYMBOL>` (string) \u2014 One-line stored reading for the symbol, absent when none exists.\n- `stocks` (object) \u2014 Same two keys, for equities.",
        "parameters": [
          {
            "name": "symbols",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated symbols, e.g. BTC,AAPL. Defaults to BTC,ETH,TSLA,AAPL when omitted."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "crypto": {
                    "charts": {
                      "BTC": [
                        {
                          "timestamp": "2026-09-01T00:00:00Z",
                          "open": 76980.0,
                          "high": 77650.0,
                          "low": 76410.0,
                          "close": 77140.0,
                          "volume": 29846188636.0
                        }
                      ]
                    },
                    "analyses": {
                      "BTC": "BTC: Bullish trend"
                    }
                  },
                  "stocks": {
                    "charts": {
                      "AAPL": []
                    },
                    "analyses": {}
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Market Data"
        ]
      }
    },
    "/api/heatmap/": {
      "get": {
        "summary": "Market breadth as a heatmap dataset",
        "description": "Symbols with their change and a size weight, shaped for a treemap. Returns the whole tracked universe, not a page.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nPaging: None, deliberately: a treemap of half a market is a different picture, not a first page. Expect the whole tracked universe in one response and size your client for it.\n\nResponse fields:\n- `items[].label` (string) \u2014 Ticker, as displayed in the tile.\n- `items[].name` (string) \u2014 Full instrument name.\n- `items[].size` (number) \u2014 Area weight \u2014 market capitalisation.\n- `items[].change` (number) \u2014 Percent change driving the tile colour.\n- `items[].price` (number) \u2014 Latest stored price.\n- `items[].sector / industry` (string) \u2014 Grouping for the treemap.\n- `items[].type` (string) \u2014 crypto or stock.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "items": [
                    {
                      "label": "BTC",
                      "name": "Bitcoin",
                      "size": 1551334515862.0,
                      "change": -1.60013,
                      "price": 77271.0,
                      "volume": 29846188636.0,
                      "sector": "Mega Cap (Top 10)",
                      "industry": "Digital Asset",
                      "type": "crypto"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Market Data"
        ]
      }
    },
    "/api/global-markets/": {
      "get": {
        "summary": "Index and regional market summary",
        "description": "One row per tracked index, with its latest price, change and the timestamp of that reading.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `last_updated` (string (ISO 8601)) \u2014 When this snapshot was assembled. Compare it against the per-row `timestamp` values before calling anything current.\n- `data_status` (object) \u2014 Availability counters for the snapshot: which sub-datasets were present and how many rows each carried. It reports coverage, not age \u2014 age comes from the timestamps.\n- `data_sources` (object) \u2014 Which upstream provided each block.\n- `rate_limit_status` (object) \u2014 Whether an upstream throttled us and whether a cached fallback was served, plus that cache's age in minutes.\n- `crypto` (object) \u2014 Total market cap, 24h volume, dominance and the Fear & Greed reading, each with its own timestamp.\n- `stocks[] / indices[] / commodities[]` (array) \u2014 One row per instrument: symbol, name, price, change_percent and the timestamp of that reading.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "crypto": {
                    "market_cap": {
                      "value": 2729426102416.08,
                      "delta": null,
                      "timestamp": "2026-08-27T17:25:37.737074+00:00"
                    },
                    "fear_greed": {
                      "value": 71.0,
                      "classification": "Greed",
                      "timestamp": "2026-08-27T00:00:00+00:00"
                    }
                  },
                  "indices": [
                    {
                      "symbol": "SP500",
                      "name": "S&P 500",
                      "price": 5431.2,
                      "change_percent": 0.81,
                      "timestamp": "2026-09-01T06:56:18.218215+00:00"
                    }
                  ],
                  "last_updated": "2026-09-01T23:22:14.321930+00:00",
                  "data_status": {
                    "crypto_available": true,
                    "stocks_count": 10,
                    "indices_count": 7,
                    "commodities_count": 8
                  },
                  "data_sources": {
                    "crypto": "CoinGecko",
                    "indices": "Yahoo Finance"
                  },
                  "rate_limit_status": {
                    "crypto_rate_limited": false,
                    "fallback_used": false,
                    "cache_age_minutes": 0
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Market Data"
        ]
      }
    },
    "/api/symbols/autocomplete/": {
      "get": {
        "summary": "Symbol search",
        "description": "Symbols matching a partial name or ticker, for an autocomplete control. Only symbols the platform can price are returned.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `symbols[]` (array of string) \u2014 Up to fifteen tickers beginning with the query, crypto first, then equities. Only instruments the platform can price.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Partial symbol or name."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "symbols": [
                    "BTC",
                    "BTCB"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Market Data"
        ]
      }
    },
    "/api/search/palette/": {
      "get": {
        "summary": "Command-palette search across assets, news and pages",
        "description": "Grouped results for one query: matching assets, recent articles naming them, and the platform pages that answer the question. Every item carries the URL it resolves to, so a client can act on a result without a second call. Queries shorter than the minimum return 400 with `error: query_too_short` rather than an empty list, because an empty list reads as \"nothing matches\".\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `query` (string) \u2014 The query, echoed back.\n- `total` (integer) \u2014 Result count across every group.\n- `groups[].id` (string) \u2014 assets, news or pages.\n- `groups[].label` (string) \u2014 Localised group heading.\n- `groups[].items[].title` (string) \u2014 What to display.\n- `groups[].items[].subtitle` (string) \u2014 Instrument name, or the publisher for an article.\n- `groups[].items[].url` (string) \u2014 Where the result resolves to.\n- `groups[].items[].actions[]` (array) \u2014 Follow-on destinations for an asset \u2014 dossier, news, accuracy, compare, backtest \u2014 so a client need not construct them.\n- `error` (string) \u2014 Present only on 400: `query_too_short`.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The query. Below the minimum length the endpoint returns 400."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "query": "bitcoin",
                  "total": 6,
                  "groups": [
                    {
                      "id": "assets",
                      "label": "Assets",
                      "items": [
                        {
                          "kind": "asset",
                          "symbol": "BTC",
                          "title": "BTC",
                          "subtitle": "Bitcoin",
                          "url": "/symbol/BTC/",
                          "actions": [
                            {
                              "title": "Open asset dossier",
                              "url": "/symbol/BTC/"
                            }
                          ]
                        }
                      ],
                      "more_url": ""
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Market Data"
        ]
      }
    },
    "/api/accuracy/stats/": {
      "get": {
        "summary": "Benchmark totals",
        "description": "Scored, pending and excluded prediction counts, and accuracy. Accuracy over model providers excludes the rule-based fallback; the two are separate figures because the fallback is a keyword counter and not a model.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `total_evaluated_predictions` (integer) \u2014 Calls that have been scored against a later price.\n- `correct_predictions` (integer) \u2014 Of those, how many were right.\n- `overall_accuracy` (number) \u2014 Percentage, one decimal.\n- `pending_evaluation` (integer) \u2014 Scored-eligible calls still in the queue. Read it beside the total: a small sample means something different when the queue behind it is large.\n- `failed_evaluation` (integer) \u2014 Calls scoring could not resolve, usually a missing later price.\n- `active_providers` (integer) \u2014 Distinct providers with calls.\n- `top_performer` (object or null) \u2014 Null until at least one provider clears the minimum sample.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "total_evaluated_predictions": 1539,
                  "correct_predictions": 525,
                  "overall_accuracy": 34.11,
                  "pending_evaluation": 64,
                  "failed_evaluation": 1,
                  "active_providers": 5,
                  "top_performer": null
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Accuracy"
        ]
      }
    },
    "/api/accuracy/leaderboard/": {
      "get": {
        "summary": "Provider accuracy leaderboard",
        "description": "Providers ranked by accuracy. A provider below the minimum sample carries its figure but no rank, so a percentage from three calls cannot be read as a ranking. Every provider appears twice over: `ai_provider`/`provider` is the stable tag to key on, and `display_name` is the name to print. Both are sent because a consumer given only the tag builds its own lookup table, and three such tables inside this project each went stale at a different time -- which is how a raw storage id, carrying the provider name doubled onto a model id that already began with it, reached a page as a model name.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nPaging: Unpaginated \u2014 the provider set is small and bounded. `min_predictions` is a floor on evidence, not a page size: raising it removes providers whose figure rests on too few calls.\n\nResponse fields:\n- `period` (string) \u2014 The window the figures cover, echoed back.\n- `source` (string) \u2014 all, news or technical.\n- `leaderboard[]` (array) \u2014 Providers in rank order. Empty when nobody clears the minimum sample \u2014 an empty board is the honest answer, not a zero.\n- `message` (string) \u2014 Why the board is empty, when it is.",
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "daily, weekly, monthly or all_time."
          },
          {
            "name": "asset_class",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict to one asset class."
          },
          {
            "name": "min_predictions",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Minimum scored calls to include."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "period": "all_time",
                  "source": "all",
                  "leaderboard": [],
                  "message": "No providers with at least 10 predictions in all_time period"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Accuracy"
        ]
      }
    },
    "/api/accuracy/provider/{provider_name}/": {
      "get": {
        "summary": "One provider's accuracy record",
        "description": "Scored calls, hit rate and sample size for a single provider over a chosen window. Every provider appears twice over: `ai_provider`/`provider` is the stable tag to key on, and `display_name` is the name to print. Both are sent because a consumer given only the tag builds its own lookup table, and three such tables inside this project each went stale at a different time -- which is how a raw storage id, carrying the provider name doubled onto a model id that already began with it, reached a page as a model name.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `provider` (string) \u2014 The provider tag, as requested.\n- `period` (string) \u2014 Window covered.\n- `total_predictions / correct_predictions` (integer) \u2014 Sample size, and hits within it.\n- `accuracy` (number) \u2014 Percentage over the sample.",
        "parameters": [
          {
            "name": "provider_name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Path segment. Supplied as a string."
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "daily, weekly, monthly or all_time."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Accuracy"
        ]
      }
    },
    "/api/accuracy/symbol/{symbol}/": {
      "get": {
        "summary": "Accuracy on one asset",
        "description": "How each provider has performed on this symbol, with the sample size behind every figure. Both the per-provider breakdown and the recent predictions carry the naming pair. Every provider appears twice over: `ai_provider`/`provider` is the stable tag to key on, and `display_name` is the name to print. Both are sent because a consumer given only the tag builds its own lookup table, and three such tables inside this project each went stale at a different time -- which is how a raw storage id, carrying the provider name doubled onto a model id that already began with it, reached a page as a model name.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `symbol` (string) \u2014 The asset, uppercased.\n- `providers[]` (array) \u2014 One row per provider: hit rate and the sample size behind it.",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Path segment. Supplied as a string."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Accuracy"
        ]
      }
    },
    "/api/accuracy/article/{article_id}/": {
      "get": {
        "summary": "The calls one article produced",
        "description": "Every prediction generated from an article, with its verification state. This is the evidence trail behind a published reading. Every provider appears twice over: `ai_provider`/`provider` is the stable tag to key on, and `display_name` is the name to print. Both are sent because a consumer given only the tag builds its own lookup table, and three such tables inside this project each went stale at a different time -- which is how a raw storage id, carrying the provider name doubled onto a model id that already began with it, reached a page as a model name.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `article_id` (integer) \u2014 The article the calls came from.\n- `predictions[]` (array) \u2014 Every call the article produced, with its verification state \u2014 the evidence trail behind a published reading.",
        "parameters": [
          {
            "name": "article_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Path segment. Supplied as a integer."
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Accuracy"
        ]
      }
    },
    "/api/accuracy/diagnostics/": {
      "get": {
        "summary": "Evaluation pipeline diagnostics",
        "description": "Queue depth, exclusion reasons and when scoring last ran. Published because a benchmark that hides its own backlog is not auditable: a small sample means something different when the queue behind it is large.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nResponse fields:\n- `total_pending / total_evaluated / total_failed` (integer) \u2014 The queue, in three numbers.\n- `by_source` (object) \u2014 Split between news and symbol analysis.\n- `by_timeframe` (object) \u2014 Split across 6h, 24h and 7d horizons.\n- `oldest_pending / newest_pending` (string (ISO 8601)) \u2014 The age of the backlog.\n- `ready_for_evaluation` (integer) \u2014 Calls whose horizon has elapsed and which the next scoring run will pick up.\n- `sample_predictions[] / recent_evaluated[]` (array) \u2014 Rows from each end of the queue, for spot-checking.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "total_pending": 64,
                  "total_evaluated": 1539,
                  "total_failed": 1,
                  "by_source": {
                    "news": 1543,
                    "symbol_analysis": 61
                  },
                  "by_timeframe": {
                    "6h": 518,
                    "24h": 567,
                    "7d": 519
                  },
                  "oldest_pending": "2026-08-30T14:58:18.218215+00:00",
                  "ready_for_evaluation": 2
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Accuracy"
        ]
      }
    },
    "/api/health/": {
      "get": {
        "summary": "Service health",
        "description": "Liveness, plus the status of each dependency the application needs. Intended for monitoring rather than for display.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nStaff only: this route is behind a staff session and answers 302 to the sign-in page for everyone else. It is published here because operators integrate against it, not because an API key opens it.\n\nResponse fields:\n- `overall_health` (string) \u2014 healthy, degraded or critical.\n- `api_status` (object) \u2014 Per-upstream state.\n- `critical_issues[]` (array) \u2014 Open circuit breakers, named.\n- `recommendations[]` (array) \u2014 What the state implies.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "overall_health": "healthy",
                  "api_status": {
                    "coingecko": "healthy",
                    "yahoo": "healthy"
                  },
                  "critical_issues": [],
                  "recommendations": [
                    "All APIs are operating normally."
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Status"
        ]
      }
    },
    "/api/metrics/": {
      "get": {
        "summary": "Every public number the platform states about itself",
        "description": "The canonical metric set: provider counts, the four prediction counts, both accuracy figures, coverage, and the state of the evaluation pipeline. Each carries the sentence that defines it and, where it is a share, the population it is a share of. Served from the same service that renders /methodology/ and the site header, so a figure cannot mean one thing on a page and another over the API. The payload also carries computed_at, the methodology_version the scoring rules were at, a partial flag with the keys that could not be computed, and headline_accuracy_key \u2014 the one accuracy figure any surface may present as \"the\" accuracy. Cached one minute.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nPaging: None. The whole set is one object; there is no page to walk.\n\nResponse fields:\n- `metrics` (object) \u2014 Metric key to {value, definition, unit, denominator}. `value` is null for a figure that is not yet measurable \u2014 never 0, which would state something false where null states nothing.\n- `computed_at` (string) \u2014 When this set was computed. Cached five minutes and dropped early by any job that moves one of these numbers.\n- `methodology_version` (string) \u2014 The scoring ruleset the accuracy figures were produced under.\n- `headline_accuracy_key` (string) \u2014 Which accuracy metric is the published headline. Two are served, over two populations; this names the one to show.\n- `accuracy_note` (string) \u2014 One sentence on why there are two accuracy figures and what each is measured over.\n- `partial` (boolean) \u2014 True when a metric could not be computed. The others are still correct.\n- `failures[]` (array) \u2014 The keys that could not be computed.\n- `definitions_url` (string) \u2014 Where every definition is published in prose.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "metrics": {
                    "predictions_evaluated": {
                      "value": 5875,
                      "unit": "count",
                      "denominator": "",
                      "definition": "Predictions whose timeframe closed and which were scored against the realised price move."
                    },
                    "rolling_accuracy": {
                      "value": 29.6,
                      "unit": "percent",
                      "denominator": "scored predictions from model providers",
                      "definition": "The headline accuracy figure. \u2026"
                    }
                  },
                  "computed_at": "2026-09-02T09:00:00+00:00",
                  "methodology_version": "v1.0",
                  "headline_accuracy_key": "rolling_accuracy",
                  "partial": false,
                  "failures": [],
                  "definitions_url": "https://www.analystmarkets.xyz/methodology/"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Status"
        ]
      }
    },
    "/api/rate-limits/": {
      "get": {
        "summary": "Rate limits in force",
        "description": "The limits currently applied to the calling client, and how much of each remains.\n\nRate limited per IP. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After when throttled.\n\nStaff only: this route is behind a staff session and answers 302 to the sign-in page for everyone else. It is published here because operators integrate against it, not because an API key opens it.\n\nResponse fields:\n- `success` (boolean) \u2014 Whether the snapshot could be taken.\n- `rate_limiting.failure_counts` (object) \u2014 Consecutive upstream failures per provider.\n- `rate_limiting.last_failure_times` (object) \u2014 When each last failed.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "success": true,
                  "rate_limiting": {
                    "failure_counts": {},
                    "last_failure_times": {},
                    "timestamp": "2026-09-02T09:00:00+00:00"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the interval in Retry-After."
          },
          "503": {
            "description": "A dependency is unavailable. The response body names which."
          }
        },
        "tags": [
          "Status"
        ]
      }
    }
  }
}