{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "Published /v1/ rate limits (LIM-01, LIM-03): part of the contract, not merely an implementation detail. Every response carries RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset (the IETF RateLimit header family); a limited response also carries Retry-After.",
  "rules": [
    {
      "rule": "token",
      "requirement": "LIM-01",
      "applies_to": "Every /v1/ request, keyed per token.",
      "window_seconds": 900,
      "limit": 600,
      "failure_posture": "fail_open",
      "failure_posture_detail": "A published courtesy, not a security control (LIM-01's own resolved wording): a counter failure admits the request rather than refusing it, so an unhealthy rate-limit table never becomes a total /v1/ outage.",
      "on_limited": {
        "status": 429,
        "problem_type_code": "rate_limited",
        "headers": ["Retry-After"]
      }
    },
    {
      "rule": "address",
      "requirement": "LIM-03",
      "applies_to": "The team-surface magic-link request endpoint, keyed per normalized email address.",
      "window_seconds": 3600,
      "limit": 5,
      "failure_posture": "fail_closed",
      "failure_posture_detail": "Review fold (M4): this rule guards the team-operator authentication surface, not merely a published courtesy the way the token rule is, so a counter failure now refuses the request rather than admitting it. The response is still uniform whether or not the address is known (LIM-03) and whichever of the request-link action's rate-limit checks refused it: a limited or a failed-closed response leaks nothing about the address's standing.",
      "on_limited": {
        "status": 429,
        "problem_type_code": "rate_limited",
        "headers": ["Retry-After"]
      }
    },
    {
      "rule": "assertion",
      "requirement": "DOB-assertion limit (pass 5a conductor amendment)",
      "applies_to": "POST /v1/people/{id}/dob-assertion, keyed per (team, person) pair, never per token (AUTH-04 lets a team hold two live tokens, and a per-token bucket would double the budget).",
      "window_seconds": 3600,
      "limit": 10,
      "failure_posture": "fail_closed",
      "failure_posture_detail": "This rule alone fails closed: the endpoint is an oracle over an athlete's birth date (ID-07), so a broken counter must not let it run with no budget enforced at all. A counter failure here answers the catalogued service_unavailable rather than admitting the request.",
      "on_limited": {
        "status": 429,
        "problem_type_code": "rate_limited",
        "headers": ["Retry-After"]
      }
    },
    {
      "rule": "login_ip",
      "requirement": "Review fold, M3/M4",
      "applies_to": "The team-surface login request and confirm actions, keyed per client IP, one shared budget across both actions.",
      "window_seconds": 3600,
      "limit": 20,
      "failure_posture": "fail_closed",
      "failure_posture_detail": "A counter failure refuses the request: this rule is the one budget that still bounds an attacker rotating through many addresses (the request action) or many guessed codes (the confirm action) from a single IP, so it must not silently disappear the moment the counter table is unhealthy.",
      "on_limited": {
        "status": 429,
        "problem_type_code": "rate_limited",
        "headers": ["Retry-After"]
      }
    }
  ],
  "recommended_poll_cadence": {
    "endpoint": "GET /v1/changes",
    "interval_seconds": 300,
    "detail": "The conformant sync mechanism (SYNC-01); the ?updated_since= list filters are an at-least-once convenience only (SYNC-06), not a substitute."
  },
  "page_size": {
    "default": 100,
    "max": 500,
    "applies_to": "Every /v1/ list read's ?limit= (LIM-02 lists, GET /v1/changes, GET /v1/rollups)."
  }
}
