openapi: 3.1.0
info:
  title: LivePricePoly API
  version: "1"
  description: |
    Developer API for Polymarket BTC Up/Down markets. Subscribe via SSE or WebSocket;
    poll REST for snapshots. Payloads use stable JSON field names across all intervals.

    **Authentication:** API keys (`lpp_…`) are required for market data. Create keys at `/admin`.
    Send via `Authorization: Bearer`, `X-API-Key`, or `?api_key=` (SSE/EventSource only).

    **Public (no key):** `GET /api`, `GET /openapi.yaml`, `GET /llms.txt`, `GET /health`.

  x-notes:
    - API key required for market data. Create keys at /admin.
    - Public without key — GET /api, GET /openapi.yaml, GET /llms.txt, GET /health, GET /health/live, GET /health/ready.
    - SSE sends an initial snapshot per market, then pushes on change (live/target immediate; book ~20 Hz max).
    - "target.source: past_results | rtds_window_open | gamma_finalPrice"
    - live.source is always polymarket_rtds (Chainlink BTC/USD).
    - book.* values are outcome token prices in [0,1], not USD.
    - One SSE or WebSocket connection per marketId. Open multiple connections for multiple intervals.
    - Snapshots stored in LogBull (~1 GB retention). Set LOGBULL_HOST, LOGBULL_PROJECT_ID, LOGBULL_QUERY_TOKEN for history.

servers:
  - url: /
    description: Same host as this document

tags:
  - name: docs
    description: Documentation (no API key)
  - name: markets
    description: Live market data (API key required)
  - name: history
    description: Historical snapshots (API key required)

security:
  - bearerAuth: []
  - apiKeyHeader: []

paths:
  /health:
    get:
      tags: [docs]
      summary: Full health report
      description: Returns hub status per market, history config, and upstream connectivity. HTTP 503 when degraded.
      security: []
      responses:
        "200":
          description: All markets healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthReport"
        "503":
          description: One or more markets stale or disconnected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthReport"

  /health/live:
    get:
      tags: [docs]
      summary: Liveness probe
      description: Always 200 when the API process is running. Use for container liveness checks.
      security: []
      responses:
        "200":
          description: Process is alive
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  live: { type: boolean }
                  ts: { type: integer }

  /health/ready:
    get:
      tags: [docs]
      summary: Readiness probe
      description: 200 when all markets are connected and at least one has live price data.
      security: []
      responses:
        "200":
          description: Ready to serve traffic
        "503":
          description: Still starting or upstream disconnected

  /llms.txt:
    get:
      tags: [docs]
      summary: LLM-friendly API documentation
      security: []
      responses:
        "200":
          description: Plain-text llms.txt
          content:
            text/plain:
              schema:
                type: string

  /openapi.yaml:
    get:
      tags: [docs]
      summary: OpenAPI specification (this file)
      security: []
      responses:
        "200":
          description: YAML OpenAPI 3.1 document
          content:
            application/yaml:
              schema:
                type: string

  /openapi.json:
    get:
      tags: [docs]
      summary: OpenAPI specification (JSON)
      security: []
      responses:
        "200":
          description: JSON OpenAPI 3.1 document
          content:
            application/json:
              schema:
                type: object

  /api:
    get:
      tags: [docs]
      summary: Legacy documentation JSON
      description: Generated from this OpenAPI spec plus runtime market list and code examples.
      security: []
      responses:
        "200":
          description: Documentation bundle
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiDocsBundle"

  /api/markets:
    get:
      tags: [markets]
      summary: List all markets with latest snapshots
      responses:
        "200":
          description: Market metadata and snapshots
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/markets/{marketId}:
    get:
      tags: [markets]
      summary: Snapshot for one market
      parameters:
        - $ref: "#/components/parameters/marketId"
      responses:
        "200":
          description: Market config and latest snapshot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketDetailResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/markets/{marketId}/stream:
    get:
      tags: [markets]
      summary: SSE stream for one market
      description: |
        Server-Sent Events (`text/event-stream`). Sends an initial snapshot, then pushes
        `MarketSnapshot` JSON on change. One connection per `marketId`.

        EventSource cannot set headers — pass `api_key` as a query parameter.
      parameters:
        - $ref: "#/components/parameters/marketId"
        - $ref: "#/components/parameters/apiKeyQuery"
      responses:
        "200":
          description: SSE stream of market events
          content:
            text/event-stream:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/markets/{marketId}/history:
    get:
      tags: [history]
      summary: Historical snapshots for one round
      description: Defaults to the active round when `epoch` is omitted.
      parameters:
        - $ref: "#/components/parameters/marketId"
        - name: epoch
          in: query
          schema:
            type: integer
          description: Round start (Unix seconds)
        - name: from
          in: query
          schema:
            type: string
          description: ISO timestamp or ms
        - name: to
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 500
            maximum: 5000
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: sort
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        "200":
          description: Paginated snapshot history
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HistoryResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/history/stats:
    get:
      tags: [history]
      summary: LogBull recorder and storage stats
      responses:
        "200":
          description: Recorder status and LogBull stats
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/markets/{marketId}/ws:
    x-webSocket: true
    get:
      tags: [markets]
      summary: WebSocket stream for one market
      description: |
        Upgrade to WebSocket. Same JSON events as SSE. Pass `api_key` query param if the
        client cannot set headers.
      parameters:
        - $ref: "#/components/parameters/marketId"
        - $ref: "#/components/parameters/apiKeyQuery"
      responses:
        "101":
          description: WebSocket upgrade
        "401":
          $ref: "#/components/responses/Unauthorized"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key as Bearer token (`lpp_…`)
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
    apiKeyQuery:
      type: apiKey
      in: query
      name: api_key
      description: For SSE/EventSource and WebSocket when headers are unavailable

  parameters:
    marketId:
      name: marketId
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/MarketId"
    apiKeyQuery:
      name: api_key
      in: query
      schema:
        type: string
      description: API key (SSE/WS alternative to headers)

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Market not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    MarketId:
      type: string
      enum: [btc-5m, btc-15m, btc-1h, btc-4h, btc-daily]

    BookSide:
      type: object
      properties:
        mid: { type: ["number", "null"] }
        bid: { type: ["number", "null"] }
        ask: { type: ["number", "null"] }

    PricePoint:
      type: object
      properties:
        usd: { type: number }
        source: { type: string }
        obsMs: { type: number }

    TargetPoint:
      type: object
      properties:
        usd: { type: number }
        source: { type: string }

    MarketSnapshot:
      type: object
      required: [event, v, ts, marketId, interval, slug, epoch, book]
      properties:
        event:
          type: string
          const: market
        v: { type: integer }
        ts: { type: integer, description: Emit timestamp (ms) }
        marketId: { $ref: "#/components/schemas/MarketId" }
        marketLabel: { type: string }
        interval: { type: string }
        symbol: { type: string }
        slug: { type: string }
        title: { type: string }
        epoch: { type: integer, description: Round start (Unix seconds) }
        secsLeft: { type: ["integer", "null"] }
        target: { oneOf: [{ $ref: "#/components/schemas/TargetPoint" }, { type: "null" }] }
        live: { oneOf: [{ $ref: "#/components/schemas/PricePoint" }, { type: "null" }] }
        marginUsd: { type: ["number", "null"] }
        favored: { type: ["string", "null"], enum: [up, down, null] }
        book:
          type: object
          properties:
            up: { $ref: "#/components/schemas/BookSide" }
            down: { $ref: "#/components/schemas/BookSide" }

    MarketsResponse:
      type: object
      properties:
        v: { type: integer }
        markets:
          type: array
          items:
            type: object

    MarketDetailResponse:
      type: object
      properties:
        v: { type: integer }
        market: { type: object }
        snapshot: { $ref: "#/components/schemas/MarketSnapshot" }

    HistoryResponse:
      type: object
      properties:
        v: { type: integer }
        marketId: { $ref: "#/components/schemas/MarketId" }
        epoch: { type: ["integer", "null"] }
        total: { type: integer }
        points:
          type: array
          items: { $ref: "#/components/schemas/MarketSnapshot" }

    Error:
      type: object
      properties:
        error: { type: string }
        message: { type: string }
        code: { type: string }

    ApiDocsBundle:
      type: object
      description: Legacy GET /api response (generated from this spec)

    HealthReport:
      type: object
      properties:
        ok: { type: boolean }
        status: { type: string, enum: [healthy, degraded] }
        uptimeSec: { type: integer }
        ts: { type: integer }
        ready: { type: boolean }
        markets:
          type: object
          properties:
            total: { type: integer }
            live: { type: integer }
            stale:
              type: array
              items: { type: string }
        stream:
          type: object
          properties:
            emitMinMs: { type: integer }
        history:
          type: object
          properties:
            recording: { type: boolean }
            query: { type: boolean }
            sampleMs: { type: ["integer", "null"] }

  examples:
    MarketSnapshotEvent:
      value:
        event: market
        v: 1
        ts: 1781543000000
        marketId: btc-15m
        interval: 15m
        slug: btc-updown-15m-1781542800
        epoch: 1781542800
        secsLeft: 420
        target: { usd: 67124.03, source: past_results }
        live: { usd: 67080.5, source: polymarket_rtds, obsMs: 1781543000000 }
        marginUsd: -43.53
        favored: down
        book:
          up: { mid: 0.42, bid: 0.41, ask: 0.43 }
          down: { mid: 0.58, bid: 0.57, ask: 0.59 }
