﻿{
  "openapi": "3.0.3",
  "info": {
    "title": "PMAxis API",
    "description": "Real-time prediction market data platform. Provides market data, prices, orderbooks, trades, signals, and analytics from Polymarket.\n\n## Authentication\n\nAll `/v1/*` endpoints require an API key passed as the `X-API-Key` header:\n\n```\nX-API-Key: pmx_live_your_key_here\n```\n\nGet a free key at [/register](/register).\n\n## Credits (2026-08-10 — replaces the old flat req/min limits)\n\nEvery request costs a per-endpoint number of credits (most reads cost 1; heavier computed\nendpoints like wallet PnL/calibration cost more), checked against two independent windows:\na per-second burst cap and a monthly quota.\n\n| Tier | Credits / month | Burst / sec | API keys | Webhooks | WS connections |\n|------|-----------------|-------------|----------|----------|-----------------|\n| Free | 5,000 | 5 | 1 | 5 | 5 |\n| Pro (Coming Soon) | 100,000 | 25 | 3 | 2,500 | 50 |\n| Enterprise (Coming Soon) | Custom | Custom | Custom | Custom | Custom |\n\nCredit headers are returned on every response: `X-Credits-Limit`, `X-Credits-Remaining`,\n`X-Credits-Used`, `X-Credits-Reset`. Exceeding the burst cap returns `429`; exhausting the\nmonthly quota returns `402`.\n\n**Debug endpoints** require `X-Debug-Token` header (set `DEBUG_TOKEN` env var on the server).",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.pmaxis.trade",
      "description": "Production"
    },
    {
      "url": "http://localhost:8088",
      "description": "Local development"
    }
  ],
  "tags": [
    { "name": "Keys", "description": "API key registration and rotation (self-service)" },
    { "name": "Health", "description": "Service health check" },
    { "name": "Stats", "description": "Platform-wide aggregate statistics" },
    { "name": "Markets", "description": "Market listing, search, and detail" },
    { "name": "Prices", "description": "Live prices and batch lookups" },
    { "name": "Trades", "description": "Trade history with wallet addresses" },
    { "name": "Orderbook", "description": "Live orderbook state" },
    { "name": "Price History", "description": "OHLCV candles and price history" },
    { "name": "Taxonomy", "description": "Categories, tags, series, and events" },
    { "name": "Positions", "description": "Wallet positions" },
    { "name": "Signals", "description": "Trading signals" },
    { "name": "Wallet Activity", "description": "On-chain wallet tracking, activity history, and live alerts" },
    { "name": "WebSocket", "description": "Real-time streaming — documented here for discoverability; OpenAPI has no native WebSocket support, so this describes the protocol upgrade and message shapes rather than a request/response pair" },
    { "name": "Debug", "description": "Raw internal state (requires X-Debug-Token header)" }
  ],
  "paths": {
    "/register": {
      "post": {
        "tags": ["Keys"],
        "summary": "Register a free API key",
        "description": "Public endpoint — no auth required. Submit your app name and email to receive a free-tier key (5,000 credits/month, 5 credits/sec burst). One active key per email on Free tier. The key is returned **once** and cannot be retrieved again.",
        "operationId": "registerKey",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["app_name", "email"],
                "properties": {
                  "app_name": { "type": "string", "example": "My Trading Bot" },
                  "email":    { "type": "string", "example": "dev@example.com" },
                  "use_case": { "type": "string", "example": "algo trading" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created — shown once, store it securely",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key":        { "type": "string", "example": "pmx_live_a1b2c3d4e5f6..." },
                    "app_name":   { "type": "string" },
                    "tier":       { "type": "string", "example": "free" },
                    "credits_month": { "type": "integer", "example": 5000 },
                    "credits_burst": { "type": "integer", "example": 5 },
                    "message":    { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "app_name and email are required / invalid email" }
        }
      }
    },
    "/v1/keys/rotate": {
      "post": {
        "tags": ["Keys"],
        "summary": "Rotate your API key",
        "description": "Generate a new key and immediately invalidate the old one. Authenticate with your current key via `X-API-Key`. The new key inherits the same tier and metadata. **The new key is shown once — save it immediately.**",
        "operationId": "rotateKey",
        "responses": {
          "200": {
            "description": "New key issued, old key is now invalid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key":     { "type": "string", "example": "pmx_live_new_key..." },
                    "message": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing or invalid X-API-Key" },
          "404": { "description": "Key not found in Postgres" }
        }
      }
    },
    "/health": {
      "get": {
        "tags": ["Health"],
        "summary": "Health check",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" },
                    "service": { "type": "string", "example": "pmaxis-api" },
                    "project": { "type": "string" },
                    "instance": { "type": "string" },
                    "timestamp": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/time": {
      "get": {
        "tags": ["Stats"],
        "summary": "Server current time  🆕",
        "description": "Returns the server's authoritative current UTC time. Call this before reasoning about 'the current market window' - don't infer 'now' from how fresh a market looks in a result set. Polymarket market titles/windows are in US Eastern Time (ET), not UTC.",
        "operationId": "getServerTime",
        "responses": {
          "200": {
            "description": "Current time in UTC and IST",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "utc": { "type": "string" }, "ist": { "type": "string" }, "unix": { "type": "integer" }, "unix_ms": { "type": "integer" }, "note": { "type": "string" } } } } }
          }
        }
      }
    },
    "/v1/stats": {
      "get": {
        "tags": ["Stats"],
        "summary": "Platform-wide statistics",
        "description": "Returns aggregate platform numbers: total markets, active markets, 24h volume, 24h trades, total onchain trades, and last ingestion timestamp. Cached for 30 seconds.",
        "operationId": "getPlatformStats",
        "responses": {
          "200": {
            "description": "Platform statistics",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PlatformStats" }
              }
            }
          }
        }
      }
    },
    "/v1/status/pipeline": {
      "get": {
        "tags": ["Stats"],
        "summary": "Pipeline data freshness  🆕",
        "description": "Reports data freshness across discovery/ingestion/trade-settlement as a proxy for Kafka consumer lag - inferred from how recently each downstream table received new data, not a direct Kafka admin API query.",
        "operationId": "getPipelineStatus",
        "responses": {
          "200": {
            "description": "Per-stage freshness with overall status",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "redis_up": { "type": "boolean" }, "stages": { "type": "array", "items": { "type": "object" } }, "checked_at": { "type": "string" } } } } }
          }
        }
      }
    },
    "/v1/leaderboard": {
      "get": {
        "tags": ["Wallets"],
        "summary": "Wallet volume leaderboard  🆕",
        "description": "Top wallets ranked by traded volume over a window, aggregated from wallet_profiles. Ranks by volume, not realized P&L - cost basis and resolution outcomes aren't tracked per position.",
        "operationId": "getLeaderboard",
        "parameters": [
          { "name": "window", "in": "query", "schema": { "type": "string", "default": "7d", "enum": ["1d", "7d", "30d", "all"] } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Ranked wallets by traded volume",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "window": { "type": "string" }, "data": { "type": "array", "items": { "type": "object" } }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/trades/recent": {
      "get": {
        "tags": ["Trades"],
        "summary": "Recent trades with wallet addresses",
        "description": "Returns the latest trades enriched with maker/taker wallet addresses. Joins the `trades` table (price, size, side from WebSocket feed) with `onchain_trades` (maker/taker from Polygon blockchain) on `tx_hash`. Trades without a blockchain tx_hash will have empty wallet fields.",
        "operationId": "getRecentTrades",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 500 }, "description": "Max results" },
          { "name": "market_id", "in": "query", "schema": { "type": "string" }, "description": "Filter to a specific market" }
        ],
        "responses": {
          "200": {
            "description": "Recent trades with optional wallet info",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RecentTradesResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/markets": {
      "get": {
        "tags": ["Markets"],
        "summary": "List markets",
        "description": "Returns paginated markets ranked by recent activity. Supports filtering by status, category, tag, and series. Use `cursor` for pagination.",
        "operationId": "getMarkets",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 }, "description": "Number of results to return" },
          { "name": "cursor", "in": "query", "schema": { "type": "string" }, "description": "Pagination cursor from previous response `next_cursor`" },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 }, "description": "Offset (use cursor instead for reliable pagination)" },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["ACTIVE", "CLOSED", "RESOLVED"] }, "description": "Filter by market status" },
          { "name": "category", "in": "query", "schema": { "type": "string", "example": "politics" }, "description": "Filter by category slug" },
          { "name": "tag", "in": "query", "schema": { "type": "string", "example": "trump" }, "description": "Filter by tag slug" },
          { "name": "series", "in": "query", "schema": { "type": "string", "example": "England" }, "description": "Filter by series name" },
          { "name": "exchange", "in": "query", "schema": { "type": "string", "default": "polymarket" }, "description": "Exchange source" },
          { "name": "source", "in": "query", "schema": { "type": "string", "enum": ["local", "gamma"] }, "description": "Use `gamma` to proxy directly to Polymarket Gamma API" }
        ],
        "responses": {
          "200": {
            "description": "Paginated market list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } },
                    "next_cursor": { "type": "string" },
                    "has_more": { "type": "boolean" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/markets/search": {
      "get": {
        "tags": ["Markets"],
        "summary": "Search markets",
        "description": "Full-text search on market title and question. Case-insensitive substring match.",
        "operationId": "searchMarkets",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "example": "bitcoin" }, "description": "Search query" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["ACTIVE", "CLOSED", "RESOLVED"] } },
          { "name": "exchange", "in": "query", "schema": { "type": "string", "default": "polymarket" } }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } }
              }
            }
          }
        }
      }
    },
    "/v1/markets/top": {
      "get": {
        "tags": ["Markets"],
        "summary": "Top markets by volume or trades",
        "description": "Returns the highest-activity markets over a time window, ranked by volume or trade count.",
        "operationId": "getTopMarkets",
        "parameters": [
          { "name": "by", "in": "query", "schema": { "type": "string", "enum": ["volume", "trades"], "default": "volume" }, "description": "Rank by volume (USDC) or trade count" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 10, "maximum": 50 } },
          { "name": "period", "in": "query", "schema": { "type": "string", "enum": ["1h", "24h", "7d"], "default": "24h" }, "description": "Time window for aggregation" }
        ],
        "responses": {
          "200": {
            "description": "Top markets",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } }
              }
            }
          }
        }
      }
    },
    "/v1/markets/resolving": {
      "get": {
        "tags": ["Markets"],
        "summary": "Markets resolving soon  🆕",
        "description": "Active markets ordered by resolution time (soonest first). Use `within` to filter to a specific window.",
        "operationId": "getResolvingMarkets",
        "parameters": [
          { "name": "within", "in": "query", "schema": { "type": "string", "enum": ["1h", "6h", "24h", "7d"] }, "description": "Only include markets resolving within this window" },
          { "name": "exchange", "in": "query", "schema": { "type": "string", "default": "polymarket" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "List of resolving markets with `resolves_at` (unix timestamp) and `time_to_resolution` fields",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } }, "count": { "type": "integer" }, "within": { "type": "string" } } } } }
          }
        }
      }
    },
    "/v1/markets/trending": {
      "get": {
        "tags": ["Markets"],
        "summary": "Trending markets  🆕",
        "description": "Markets with the highest volume spike ratio: last-1h volume vs 24h hourly average. A ratio >1 means the market is trading faster than its daily baseline.",
        "operationId": "getTrendingMarkets",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Trending markets with `vol_1h`, `vol_24h`, and `spike_ratio` fields",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/markets/breaking": {
      "get": {
        "tags": ["Markets"],
        "summary": "Breaking markets  🆕",
        "description": "Markets with the sharpest absolute price move over the last hour. Distinct from /markets/trending (volume spike) - a market can move sharply on thin volume without tripping the volume-spike signal.",
        "operationId": "getBreakingMarkets",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Breaking markets with `price_now`, `price_baseline` (earliest sample available in the last 65 minutes — a young market's own start, not a fixed '1h ago' point), and `price_move` fields",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/markets/new": {
      "get": {
        "tags": ["Markets"],
        "summary": "Newly listed markets  🆕",
        "description": "Markets created within the last N hours, newest first.",
        "operationId": "getNewMarkets",
        "parameters": [
          { "name": "hours", "in": "query", "schema": { "type": "integer", "default": 24, "maximum": 168 }, "description": "Look-back window in hours (max 168 = 7 days)" },
          { "name": "exchange", "in": "query", "schema": { "type": "string", "default": "polymarket" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "New markets with `created_at` (unix timestamp)",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } }, "count": { "type": "integer" }, "hours": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/markets/by-timeframe": {
      "get": {
        "tags": ["Markets"],
        "summary": "Markets grouped by duration bucket  🆕",
        "description": "Groups active markets into buckets by minutes until resolution: 5m, 15m, 1h, 4h, 1d, 1w, long. Note: bucketing uses `end_time`, not `start_time` — for recurring short-duration markets (e.g. 5-min crypto up/down) `start_time` reflects market creation, not the trading window.",
        "operationId": "getMarketsByTimeframe",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "default": "ACTIVE" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 }, "description": "Max markets per bucket" },
          { "name": "asset", "in": "query", "schema": { "type": "string", "example": "BTC" }, "description": "Filter to markets whose title/slug contains this string, e.g. BTC, ETH — pulls one asset's markets across every timeframe bucket in a single call" }
        ],
        "responses": {
          "200": {
            "description": "Markets grouped by timeframe bucket, each market includes `mins_until_close`",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "timeframes": { "type": "array", "items": { "type": "object", "properties": { "timeframe": { "type": "string" }, "count": { "type": "integer" }, "markets": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } } } } }, "timestamp": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/markets/compare": {
      "get": {
        "tags": ["Markets"],
        "summary": "Compare markets side-by-side  🆕",
        "description": "Returns price and 24h stats for up to 10 markets in a single call for easy comparison.",
        "operationId": "compareMarkets",
        "parameters": [
          { "name": "ids", "in": "query", "required": true, "schema": { "type": "string", "example": "558937,682705,1654958" }, "description": "Comma-separated market IDs (max 10) — try: 558937 Brazil WC, 682705 Netanyahu, 1654958 Fed Rates" }
        ],
        "responses": {
          "200": {
            "description": "Market summaries with embedded `stats_24h`",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/markets/{id}": {
      "get": {
        "tags": ["Markets"],
        "summary": "Get market detail",
        "description": "Full market detail including live price, orderbook state, and embedded 24h stats.",
        "operationId": "getMarket",
        "parameters": [
          {
            "name": "id", "in": "path", "required": true,
            "schema": { "type": "string", "example": "558937" },
            "description": "Market ID",
            "examples": {
              "brazil":    { "summary": "Brazil – 2026 World Cup ($24M vol)", "value": "558937" },
              "netanyahu": { "summary": "Netanyahu – PM of Israel ($12M vol)", "value": "682705" },
              "fed":       { "summary": "Fed Rates – July 2026 ($10M vol)",   "value": "1654958" }
            }
          }
        ],
        "responses": {
          "200": { "description": "Market detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MarketDetail" } } } },
          "404": { "description": "Market not found" }
        }
      }
    },
    "/v1/markets/{id}/price": {
      "get": {
        "tags": ["Prices"],
        "summary": "Get live price for a market",
        "description": "Returns the latest mid-price, bid, ask, and spread from Redis (updated in real-time by the processor hub).",
        "operationId": "getPrice",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } }
        ],
        "responses": {
          "200": {
            "description": "Live price",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Price" }
              }
            }
          },
          "404": { "description": "Price not found — market may not be actively traded" }
        }
      }
    },
    "/v1/prices": {
      "get": {
        "tags": ["Prices"],
        "summary": "Batch price lookup",
        "description": "Returns live prices for up to 100 market IDs in a single request. Returns `null` for markets with no live price.",
        "operationId": "getBatchPrices",
        "parameters": [
          { "name": "ids", "in": "query", "required": true, "schema": { "type": "string", "example": "558937,682705,1654958" }, "description": "Comma-separated market IDs (max 100)" }
        ],
        "responses": {
          "200": {
            "description": "Map of market_id → price object (null if not found)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": { "$ref": "#/components/schemas/Price" }
                }
              }
            }
          }
        }
      }
    },
    "/v1/markets/{id}/orderbook": {
      "get": {
        "tags": ["Orderbook"],
        "summary": "Get live orderbook",
        "description": "Returns the current L2 orderbook state from Redis (updated in real-time).",
        "operationId": "getOrderbook",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } }
        ],
        "responses": {
          "200": { "description": "Orderbook with bids and asks" },
          "404": { "description": "Orderbook not found" }
        }
      }
    },
    "/v1/markets/{id}/trades": {
      "get": {
        "tags": ["Trades"],
        "summary": "Get trade history for a market",
        "description": "Returns normalized trade events from ClickHouse. Use `source=data-api` to proxy live trades directly from Polymarket.",
        "operationId": "getMarketTrades",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 100, "maximum": 5000 } },
          { "name": "from", "in": "query", "schema": { "type": "string", "example": "2026-06-01T00:00:00Z" }, "description": "Start time (RFC3339 or Unix ms)" },
          { "name": "to", "in": "query", "schema": { "type": "string" }, "description": "End time (RFC3339 or Unix ms)" },
          { "name": "source", "in": "query", "schema": { "type": "string", "enum": ["local", "data-api"] }, "description": "Use `data-api` to proxy directly from Polymarket" },
          { "name": "wallet", "in": "query", "schema": { "type": "string" }, "description": "Filter by wallet address (requires source=data-api)" }
        ],
        "responses": {
          "200": {
            "description": "Trade list",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Trade" } }
              }
            }
          }
        }
      }
    },
    "/v1/markets/{id}/price-history": {
      "get": {
        "tags": ["Price History"],
        "summary": "Price history for a market",
        "description": "Returns OHLCV data at the requested resolution from ClickHouse.",
        "operationId": "getMarketPriceHistory",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } },
          { "name": "resolution", "in": "query", "schema": { "type": "string", "enum": ["1s", "1m", "5m", "1h"], "default": "1m" }, "description": "Candle resolution" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 500, "maximum": 20000 } },
          { "name": "from", "in": "query", "schema": { "type": "string" }, "description": "Start time (RFC3339 or Unix ms)" },
          { "name": "to", "in": "query", "schema": { "type": "string" }, "description": "End time (RFC3339 or Unix ms)" }
        ],
        "responses": {
          "200": { "description": "Price history array" }
        }
      }
    },
    "/v1/markets/{id}/candles": {
      "get": {
        "tags": ["Price History"],
        "summary": "OHLCV candles (TradingView UDF format)",
        "description": "Returns candles compatible with TradingView's UDF datafeed format. `from` and `to` are Unix seconds.",
        "operationId": "getCandles",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } },
          { "name": "resolution", "in": "query", "schema": { "type": "string", "enum": ["1", "60"], "default": "1" }, "description": "1 = 1-minute bars, 60 = 1-hour bars" },
          { "name": "from", "in": "query", "schema": { "type": "integer", "example": 1704067200 }, "description": "Start time as Unix seconds" },
          { "name": "to", "in": "query", "schema": { "type": "integer", "example": 1704153600 }, "description": "End time as Unix seconds" }
        ],
        "responses": {
          "200": { "description": "TradingView UDF candle response" }
        }
      }
    },
    "/v1/markets/{id}/summary": {
      "get": {
        "tags": ["Markets"],
        "summary": "Market summary",
        "description": "Unified view of a market including live price, latest signals, and 24h stats in one response.",
        "operationId": "getMarketSummary",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } }
        ],
        "responses": {
          "200": { "description": "Market summary" }
        }
      }
    },
    "/v1/markets/{id}/stats": {
      "get": {
        "tags": ["Markets"],
        "summary": "Market 24h stats",
        "description": "Returns 24h OHLCV stats and price change % for a market.",
        "operationId": "getMarketStats",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } }
        ],
        "responses": {
          "200": {
            "description": "24h stats",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MarketStats" }
              }
            }
          }
        }
      }
    },
    "/v1/markets/{id}/related": {
      "get": {
        "tags": ["Markets"],
        "summary": "Related markets (same event group)",
        "description": "Returns sibling markets that share the same `event_id`. Useful for multi-outcome events (e.g. all team outcomes in a tournament).",
        "operationId": "getRelatedMarkets",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } }
        ],
        "responses": {
          "200": { "description": "Related markets" }
        }
      }
    },
    "/v1/markets/{id}/positions": {
      "get": {
        "tags": ["Positions"],
        "summary": "Positions for a market",
        "description": "Returns all open positions for a specific market. Proxied from Polymarket Data API.",
        "operationId": "getMarketPositions",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } },
          { "name": "wallet", "in": "query", "schema": { "type": "string" }, "description": "Filter by wallet address" }
        ],
        "responses": {
          "200": { "description": "Positions" }
        }
      }
    },
    "/v1/markets/{id}/signals": {
      "get": {
        "tags": ["Signals"],
        "summary": "Trading signals for a market",
        "description": "Returns the latest trading signals generated by the processor hub for a specific market.",
        "operationId": "getSignalsByMarket",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } },
          { "name": "type", "in": "query", "schema": { "type": "string" }, "description": "Filter by signal type (e.g. momentum, volume_spike)" }
        ],
        "responses": {
          "200": { "description": "Signals array" }
        }
      }
    },
    "/v1/signals/top": {
      "get": {
        "tags": ["Signals"],
        "summary": "Top signals across all markets  🆕",
        "description": "Scans every live signal across all markets and ranks by magnitude (value/threshold), so a bot can ask 'what's firing hardest right now' instead of polling /markets/{id}/signals per market.",
        "operationId": "getTopSignals",
        "parameters": [
          { "name": "type", "in": "query", "schema": { "type": "string" }, "description": "Filter to one signal type, e.g. whale_alert" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 200 } }
        ],
        "responses": {
          "200": {
            "description": "Ranked signals with a `score` field",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object" } }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/markets/{id}/health": {
      "get": {
        "tags": ["Markets"],
        "summary": "Market data freshness  🆕",
        "description": "Reports whether orderbook/price data for this market is fresh, stale, or missing - distinguishes 'market has no activity' from 'our pipeline is stale for this market'.",
        "operationId": "getMarketHealth",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "558937" } }
        ],
        "responses": {
          "200": {
            "description": "Freshness status for orderbook and price data",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "market_id": { "type": "string" }, "status": { "type": "string" }, "orderbook": { "type": "object" }, "price": { "type": "object" }, "signal_count": { "type": "integer" }, "checked_at": { "type": "string" } } } } }
          }
        }
      }
    },
    "/v1/markets/{id}/liquidity": {
      "get": {
        "tags": ["Markets"],
        "summary": "Market orderbook liquidity  🆕",
        "description": "Returns spread, mid price, best bid/ask, and full bids/asks depth from ClickHouse orderbook history.",
        "operationId": "getMarketLiquidity",
        "parameters": [
          {
            "name": "id", "in": "path", "required": true,
            "schema": { "type": "string", "example": "558937" },
            "description": "Market ID",
            "examples": {
              "brazil":    { "summary": "Brazil – 2026 World Cup ($24M vol)", "value": "558937" },
              "netanyahu": { "summary": "Netanyahu – PM of Israel ($12M vol)", "value": "682705" },
              "fed":       { "summary": "Fed Rates – July 2026 ($10M vol)",   "value": "1654958" }
            }
          }
        ],
        "responses": {
          "200": { "description": "Liquidity snapshot with `spread`, `mid_price`, `best_bid`, `best_ask`, `bids`, `asks`" },
          "404": { "description": "Orderbook not found — market may not be actively tracked" }
        }
      }
    },
    "/v1/markets/{id}/sentiment": {
      "get": {
        "tags": ["Markets"],
        "summary": "Market sentiment signal  🆕",
        "description": "Derives a `bullish` / `bearish` / `neutral` sentiment from 24h price momentum and compares last-1h volume against the 24h hourly average to surface a `volume_trend` of `up` / `down` / `flat`.",
        "operationId": "getMarketSentiment",
        "parameters": [
          {
            "name": "id", "in": "path", "required": true,
            "schema": { "type": "string", "example": "558937" },
            "description": "Market ID",
            "examples": {
              "brazil":    { "summary": "Brazil – 2026 World Cup ($24M vol)", "value": "558937" },
              "netanyahu": { "summary": "Netanyahu – PM of Israel ($12M vol)", "value": "682705" },
              "fed":       { "summary": "Fed Rates – July 2026 ($10M vol)",   "value": "1654958" }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sentiment object",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "market_id": { "type": "string" },
              "sentiment": { "type": "string", "enum": ["bullish", "bearish", "neutral"] },
              "price_change_pct": { "type": "number" },
              "volume_trend": { "type": "string", "enum": ["up", "down", "flat"] },
              "vol_1h": { "type": "number" },
              "signals_count": { "type": "integer" },
              "stats_24h": { "type": "object" },
              "timestamp": { "type": "integer" }
            } } } }
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "tags": ["Taxonomy"],
        "summary": "List events",
        "description": "Proxied directly from Polymarket Gamma API `/events`. Supports all Gamma API query params.",
        "operationId": "getEvents",
        "parameters": [
          { "name": "exchange", "in": "query", "schema": { "type": "string", "default": "polymarket" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 100 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } },
          { "name": "active", "in": "query", "schema": { "type": "boolean" } },
          { "name": "closed", "in": "query", "schema": { "type": "boolean" } }
        ],
        "responses": {
          "200": { "description": "Events from Polymarket Gamma API" }
        }
      }
    },
    "/v1/categories": {
      "get": {
        "tags": ["Taxonomy"],
        "summary": "List categories",
        "description": "Returns all distinct categories with market counts, derived from Polymarket event-level tags.",
        "operationId": "getCategories",
        "responses": {
          "200": {
            "description": "Category list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "category": { "type": "string", "example": "politics" },
                      "market_count": { "type": "integer", "example": 42 }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/tags": {
      "get": {
        "tags": ["Taxonomy"],
        "summary": "List tags",
        "description": "Returns all distinct tags with market counts.",
        "operationId": "getTags",
        "responses": {
          "200": {
            "description": "Tag list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "tag": { "type": "string", "example": "trump" },
                      "market_count": { "type": "integer", "example": 18 }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/series": {
      "get": {
        "tags": ["Taxonomy"],
        "summary": "List series",
        "description": "Returns all distinct series (groupItemTitle from Polymarket) with market counts.",
        "operationId": "getSeries",
        "responses": {
          "200": {
            "description": "Series list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "series": { "type": "string", "example": "England" },
                      "market_count": { "type": "integer", "example": 10 }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/positions": {
      "get": {
        "tags": ["Positions"],
        "summary": "Open positions for a wallet",
        "description": "Returns all open (unsettled) positions for a Polymarket wallet address. Pass the full 0x address in the `wallet` param. Proxied from Polymarket Data API.",
        "operationId": "getPositions",
        "parameters": [
          { "name": "wallet", "in": "query", "required": true, "schema": { "type": "string", "example": "0xAdA100Db00Ca00073811820692005400218FcE1f" }, "description": "Full Ethereum wallet address (required)" }
        ],
        "responses": {
          "200": { "description": "Open positions for the wallet" },
          "400": { "description": "wallet is required" }
        }
      }
    },
    "/v1/positions/closed": {
      "get": {
        "tags": ["Positions"],
        "summary": "Closed positions for a wallet",
        "description": "Returns all closed/settled positions for a Polymarket wallet address. Pass the full 0x address in the `wallet` param. Proxied from Polymarket Data API.",
        "operationId": "getClosedPositions",
        "parameters": [
          { "name": "wallet", "in": "query", "required": true, "schema": { "type": "string", "example": "0xAdA100Db00Ca00073811820692005400218FcE1f" }, "description": "Full Ethereum wallet address (required)" }
        ],
        "responses": {
          "200": { "description": "Closed positions for the wallet" },
          "400": { "description": "wallet is required" }
        }
      }
    },
    "/v1/wallets/watch": {
      "post": {
        "tags": ["Wallet Activity"],
        "summary": "Watch a wallet",
        "description": "Adds a wallet address to the watch list. Once watched, all on-chain ERC-1155 settlement trades involving this address will be captured in real-time and stored in `wallet_activity`. Also triggers live WebSocket alerts via `subscribe_wallet`.",
        "operationId": "watchWallet",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["address"],
                "properties": {
                  "address": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B", "description": "Ethereum wallet address to watch" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Wallet added to watch list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "address": { "type": "string" },
                    "watching": { "type": "boolean", "example": true }
                  }
                }
              }
            }
          },
          "400": { "description": "address is required" }
        }
      }
    },
    "/v1/wallets/watched": {
      "get": {
        "tags": ["Wallet Activity"],
        "summary": "List watched wallets",
        "description": "Returns all wallet addresses currently on the watch list.",
        "operationId": "getWatchedWallets",
        "responses": {
          "200": {
            "description": "Watched wallet list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "wallets": { "type": "array", "items": { "type": "string" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/wallets/{address}/watch": {
      "delete": {
        "tags": ["Wallet Activity"],
        "summary": "Unwatch a wallet",
        "description": "Removes a wallet address from the watch list. New trades will no longer be captured, but existing `wallet_activity` history is retained until the 90-day TTL expires.",
        "operationId": "unwatchWallet",
        "parameters": [
          { "name": "address", "in": "path", "required": true, "schema": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B" }, "description": "Wallet address to unwatch" }
        ],
        "responses": {
          "200": {
            "description": "Wallet removed from watch list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "address": { "type": "string" },
                    "watching": { "type": "boolean", "example": false }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/wallets/{address}/activity": {
      "get": {
        "tags": ["Wallet Activity"],
        "summary": "Wallet activity history (watched only)",
        "description": "Returns stored on-chain trade activity for a **watched** wallet from ClickHouse. Data is only captured from the moment the wallet was added to the watch list. For any wallet without pre-watching, use `/onchain` instead.",
        "operationId": "getWalletActivity",
        "parameters": [
          { "name": "address", "in": "path", "required": true, "schema": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } }
        ],
        "responses": {
          "200": {
            "description": "Wallet activity records",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WalletActivityResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/wallets/{address}/onchain": {
      "get": {
        "tags": ["Wallet Activity"],
        "summary": "On-chain trades for any wallet",
        "description": "Returns raw on-chain ERC-1155 settlement trades for **any** wallet address (maker OR taker), directly from the `onchain_trades` table. No pre-watching required — works retroactively for trades within the 14-day retention window.",
        "operationId": "getWalletOnchain",
        "parameters": [
          { "name": "address", "in": "path", "required": true, "schema": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } }
        ],
        "responses": {
          "200": {
            "description": "On-chain trade records",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WalletOnchainResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/wallets/{address}/summary": {
      "get": {
        "tags": ["Wallet Activity"],
        "summary": "Wallet trading summary",
        "description": "Returns aggregated trading stats for a watched wallet: total trades, total volume, buy/sell breakdown, and first/last seen timestamps. Queries the `wallet_activity` table.",
        "operationId": "getWalletSummary",
        "parameters": [
          { "name": "address", "in": "path", "required": true, "schema": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B" } }
        ],
        "responses": {
          "200": {
            "description": "Wallet summary stats",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WalletSummary" }
              }
            }
          }
        }
      }
    },
    "/v1/wallets/{address}/markets": {
      "get": {
        "tags": ["Wallet Activity"],
        "summary": "Markets traded by a wallet  🆕",
        "description": "Returns every market a wallet has interacted with, grouped with `bought`, `sold`, `net_position`, and trade count. Ordered by most-recently-traded first.",
        "operationId": "getWalletMarkets",
        "parameters": [
          { "name": "address", "in": "path", "required": true, "schema": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Markets with `bought`, `sold`, `net_position`, `trades`, `first_trade`, `last_trade`",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "address": { "type": "string" }, "data": { "type": "array" }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/wallets/{address}/positions/open": {
      "get": {
        "tags": ["Wallet Activity"],
        "summary": "Wallet open positions  🆕",
        "description": "Markets where `bought > sold` — an approximation of open long positions derived from `wallet_activity`. Does not require the wallet to be pre-watched.",
        "operationId": "getWalletOpenPositions",
        "parameters": [
          { "name": "address", "in": "path", "required": true, "schema": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B" } }
        ],
        "responses": {
          "200": {
            "description": "Open position markets with `bought`, `sold`, `net_position`",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "address": { "type": "string" }, "data": { "type": "array" }, "count": { "type": "integer" }, "source": { "type": "string" } } } } }
          }
        }
      }
    },
    "/v1/wallets/{address}/pnl": {
      "get": {
        "tags": ["Wallet Activity"],
        "summary": "Wallet profit & loss  🆕",
        "description": "Realized/unrealized profit and loss per market for a watched wallet. Cost basis comes from the per-trade price attached to wallet_activity rows (requires the wallet to have traded after the price-cache join shipped, 2026-07-10); resolved markets are settled using market_metadata.outcome_prices. Rows with no matched price are excluded from cost basis and flagged via has_cost_basis=false — this differs from /v1/wallets/top, which ranks by volume only and does not attempt P&L at all.",
        "operationId": "getWalletPnL",
        "parameters": [
          { "name": "address", "in": "path", "required": true, "schema": { "type": "string", "example": "0xE111180000d2663C0091e4f400237545B87B996B" } }
        ],
        "responses": {
          "200": {
            "description": "Per-market P&L breakdown plus wallet-level totals",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "address": { "type": "string" }, "data": { "type": "array" }, "count": { "type": "integer" }, "total_realized_pnl": { "type": "number" }, "total_unrealized_pnl": { "type": "number" }, "total_pnl": { "type": "number" }, "markets_missing_cost_basis": { "type": "integer" }, "note": { "type": "string" } } } } }
          },
          "400": { "description": "address is required" }
        }
      }
    },
    "/v1/events/{id}/markets": {
      "get": {
        "tags": ["Taxonomy"],
        "summary": "Markets under an event  🆕",
        "description": "Returns all markets that belong to a specific event ID. Useful when you have an event from `/v1/events` and want its constituent markets.",
        "operationId": "getEventMarkets",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Event ID" },
          { "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["resolution", "created"], "default": "resolution" }, "description": "resolution = by end_time ASC; created = newest first" },
          { "name": "status", "in": "query", "schema": { "type": "string", "example": "ACTIVE" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Markets for this event",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "event_id": { "type": "string" }, "data": { "type": "array" }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/v1/categories/{slug}/markets": {
      "get": {
        "tags": ["Taxonomy"],
        "summary": "Markets by category  🆕",
        "description": "Returns markets filtered by category slug with configurable sort. Use `/v1/categories` to discover available slugs.",
        "operationId": "getCategoryMarkets",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "example": "politics" }, "description": "Category slug (lowercase)" },
          { "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["created", "resolution"], "default": "created" }, "description": "created = newest first; resolution = resolving soonest first" },
          { "name": "status", "in": "query", "schema": { "type": "string", "example": "ACTIVE" } },
          { "name": "exchange", "in": "query", "schema": { "type": "string", "default": "polymarket" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Markets in this category",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "category": { "type": "string" }, "data": { "type": "array" }, "count": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/stream": {
      "get": {
        "tags": ["WebSocket"],
        "summary": "Live event stream (WebSocket upgrade)",
        "description": "Upgrades to a WebSocket connection delivering real-time events for all markets by default (send `{\"action\":\"subscribe\",\"markets\":[...]}` to scope to specific market IDs). Auth is passed as a query param since browsers can't set custom headers on the WebSocket handshake. Frames may contain multiple newline-delimited JSON messages — split on `\\n` before calling `JSON.parse`.\n\n**Message types** (discriminated by `type`):\n- `trade` — CLOB order fill (price, size, side; no wallet address)\n- `orderbook` — L2 book snapshot/delta (bids, asks, best_bid/ask)\n- `price` — mid-price, spread, 24h volume\n- `signal` — analyzer output (momentum, volume spikes, `whale_alert` for on-chain settlements over an admin-adjustable USD threshold)\n- `wallet_activity` — activity for a wallet you've registered via `POST /v1/wallets/{address}/watch`\n- `onchain_trade` 🆕 — every on-chain settlement, unfiltered, with real `maker`/`taker` addresses (added 2026-07-10; the only message type carrying an address on every trade rather than just watched/whale-sized ones)",
        "operationId": "streamEvents",
        "parameters": [
          { "name": "api_key", "in": "query", "required": true, "schema": { "type": "string", "example": "pmx_live_your_key_here" }, "description": "API key (query param, not header — required for the WS handshake)" }
        ],
        "responses": {
          "101": { "description": "Switching Protocols — WebSocket connection established" },
          "403": { "description": "Missing or invalid API key" }
        }
      }
    },
    "/debug/markets": {
      "get": {
        "tags": ["Debug"],
        "summary": "All markets in Redis",
        "description": "Returns raw Redis state for all tracked markets. Requires `X-Debug-Token` header.",
        "operationId": "getDebugMarkets",
        "security": [{ "DebugToken": [] }],
        "responses": {
          "200": { "description": "Markets from Redis" },
          "403": { "description": "debug endpoints disabled" }
        }
      }
    },
    "/debug/markets/{id}": {
      "get": {
        "tags": ["Debug"],
        "summary": "Single market Redis state",
        "operationId": "getDebugMarket",
        "security": [{ "DebugToken": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Market Redis state" },
          "403": { "description": "debug endpoints disabled" }
        }
      }
    },
    "/debug/trades": {
      "get": {
        "tags": ["Debug"],
        "summary": "Recent trades from Redis",
        "operationId": "getDebugTrades",
        "security": [{ "DebugToken": [] }],
        "responses": {
          "200": { "description": "Recent trades" },
          "403": { "description": "debug endpoints disabled" }
        }
      }
    },
    "/debug/orderbook/{id}": {
      "get": {
        "tags": ["Debug"],
        "summary": "Raw orderbook from Redis",
        "operationId": "getDebugOrderbook",
        "security": [{ "DebugToken": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Raw orderbook" },
          "403": { "description": "debug endpoints disabled" }
        }
      }
    },
    "/debug/signals": {
      "get": {
        "tags": ["Debug"],
        "summary": "All signals from Redis",
        "operationId": "getDebugSignals",
        "security": [{ "DebugToken": [] }],
        "responses": {
          "200": { "description": "All signals" },
          "403": { "description": "debug endpoints disabled" }
        }
      }
    },
    "/debug/signals/{id}": {
      "get": {
        "tags": ["Debug"],
        "summary": "Signals for a specific market",
        "operationId": "getDebugSignalsByMarket",
        "security": [{ "DebugToken": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "type", "in": "query", "schema": { "type": "string" }, "description": "Signal type filter" }
        ],
        "responses": {
          "200": { "description": "Signals for market" },
          "403": { "description": "debug endpoints disabled" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "DebugToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Debug-Token",
        "description": "Set the `DEBUG_TOKEN` environment variable on the server, then pass that value here to access debug endpoints."
      }
    },
    "schemas": {
      "Market": {
        "type": "object",
        "properties": {
          "market_id": { "type": "string", "example": "558937" },
          "slug": { "type": "string", "example": "will-england-win-the-2026-fifa-world-cup" },
          "question": { "type": "string", "example": "Will England win the 2026 FIFA World Cup?" },
          "status": { "type": "string", "example": "ACTIVE" },
          "exchange": { "type": "string", "example": "polymarket" },
          "event_id": { "type": "string", "example": "30615" },
          "category": { "type": "string", "example": "sports" },
          "series": { "type": "string", "example": "England" },
          "price": { "type": "number", "format": "float", "example": 0.1075 },
          "best_bid": { "type": "number", "format": "float", "example": 0.107 },
          "best_ask": { "type": "number", "format": "float", "example": 0.108 },
          "mid_price": { "type": "number", "format": "float", "example": 0.1075 },
          "no_price": { "type": "number", "format": "float", "example": 0.8925 },
          "spread": { "type": "number", "format": "float", "example": 0.001 },
          "outcomes": { "type": "array", "items": { "type": "string" }, "example": ["Yes", "No"] },
          "start_time": { "type": "string", "format": "date-time" },
          "end_time": { "type": "string", "format": "date-time" }
        }
      },
      "MarketDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/Market" },
          {
            "type": "object",
            "properties": {
              "condition_id": { "type": "string" },
              "stats_24h": { "$ref": "#/components/schemas/MarketStats" }
            }
          }
        ]
      },
      "Price": {
        "type": "object",
        "properties": {
          "market_id": { "type": "string" },
          "price": { "type": "number", "format": "float" },
          "best_bid": { "type": "number", "format": "float" },
          "best_ask": { "type": "number", "format": "float" },
          "mid_price": { "type": "number", "format": "float" },
          "spread": { "type": "number", "format": "float" },
          "volume_24h": { "type": "number", "format": "float" },
          "timestamp": { "type": "integer", "description": "Unix milliseconds" }
        }
      },
      "Trade": {
        "type": "object",
        "properties": {
          "trade_id": { "type": "string" },
          "market_id": { "type": "string" },
          "price": { "type": "number", "format": "float" },
          "size": { "type": "number", "format": "float" },
          "side": { "type": "string", "enum": ["BUY", "SELL"] },
          "token": { "type": "string" },
          "timestamp": { "type": "integer", "description": "Unix milliseconds" }
        }
      },
      "WalletActivityRow": {
        "type": "object",
        "properties": {
          "tx_hash": { "type": "string" },
          "wallet": { "type": "string" },
          "maker": { "type": "string" },
          "taker": { "type": "string" },
          "token_id": { "type": "string" },
          "market_id": { "type": "string" },
          "amount": { "type": "number", "format": "float", "description": "Normalized amount (6 decimal places)" },
          "side": { "type": "string", "enum": ["buy", "sell"], "description": "buy = wallet was taker, sell = wallet was maker" },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "WalletActivityResponse": {
        "type": "object",
        "properties": {
          "address": { "type": "string" },
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/WalletActivityRow" } },
          "count": { "type": "integer" }
        }
      },
      "WalletOnchainRow": {
        "type": "object",
        "properties": {
          "tx_hash": { "type": "string" },
          "maker": { "type": "string" },
          "taker": { "type": "string" },
          "token_id": { "type": "string" },
          "amount": { "type": "string" },
          "timestamp": { "type": "string", "format": "date-time" },
          "side": { "type": "string", "enum": ["buy", "sell"] }
        }
      },
      "WalletOnchainResponse": {
        "type": "object",
        "properties": {
          "address": { "type": "string" },
          "source": { "type": "string", "example": "onchain" },
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/WalletOnchainRow" } },
          "count": { "type": "integer" }
        }
      },
      "WalletSummary": {
        "type": "object",
        "properties": {
          "address": { "type": "string" },
          "total_trades": { "type": "integer" },
          "total_volume": { "type": "number", "format": "float" },
          "buy_count": { "type": "integer" },
          "sell_count": { "type": "integer" },
          "first_seen": { "type": "string", "format": "date-time" },
          "last_seen": { "type": "string", "format": "date-time" }
        }
      },
      "PlatformStats": {
        "type": "object",
        "properties": {
          "total_markets": { "type": "integer", "example": 4821 },
          "active_markets": { "type": "integer", "example": 1203 },
          "volume_24h": { "type": "number", "format": "float", "example": 48293100.5 },
          "trades_24h": { "type": "integer", "example": 182400 },
          "total_onchain_trades": { "type": "integer", "example": 755550 },
          "last_trade_at": { "type": "string", "format": "date-time" },
          "timestamp": { "type": "integer", "description": "Unix milliseconds" }
        }
      },
      "RecentTrade": {
        "type": "object",
        "properties": {
          "trade_id": { "type": "string" },
          "market_id": { "type": "string" },
          "token": { "type": "string" },
          "price": { "type": "number", "format": "float" },
          "size": { "type": "number", "format": "float" },
          "side": { "type": "string", "enum": ["buy", "sell"] },
          "timestamp": { "type": "string", "format": "date-time" },
          "tx_hash": { "type": "string", "description": "Blockchain transaction hash (empty if not from on-chain event)" },
          "maker": { "type": "string", "description": "Maker wallet address (empty if tx_hash unavailable)" },
          "taker": { "type": "string", "description": "Taker wallet address (empty if tx_hash unavailable)" }
        }
      },
      "RecentTradesResponse": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/RecentTrade" } },
          "count": { "type": "integer" }
        }
      },
      "MarketStats": {
        "type": "object",
        "properties": {
          "market_id": { "type": "string" },
          "volume_24h": { "type": "number" },
          "trades_24h": { "type": "integer" },
          "open_price": { "type": "number" },
          "close_price": { "type": "number" },
          "high_24h": { "type": "number" },
          "low_24h": { "type": "number" },
          "price_change_pct": { "type": "number" },
          "timestamp": { "type": "integer" }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key obtained from /register. Required on all /v1/* endpoints."
      }
    }
  },
  "security": [
    { "ApiKeyAuth": [] }
  ]
}
