openapi: 3.1.0
info:
  title: PredictAsiaX Trader Track API
  version: "1.0.0"
  summary: Web3-native prediction market REST API
  description: |
    PAX Trader Track = 4-surface REST API for wallets, aggregators, partners, and end-user apps.

    - **Discovery** (`/v1/markets`, `/v1/markets/templates`) — read-only market catalog
    - **Market Data** (`/v1/candles`, `/v1/orderbook`, `/v1/trades`) — price + depth + fills
    - **Account** (`/v1/account/*`) — balance, positions, orders, trades (per API key owner)
    - **Trading** (`/v1/orders`, `POST /v1/markets`) — order entry + creator markets

    Operator Track (RGS + Seamless Wallet for iGaming operators) is a separate spec: see
    `docs.predictasiax.com/operator`. FIX 4.4 gateway for institutional order entry: see
    `docs.predictasiax.com/fix`.

    ### Design principles (from Polymarket + Kalshi + Vertex research)
    - Engine-agnostic: response never leaks AMM vs CLOB (`meta.fill_venue` optional debug)
    - Deterministic IDs: `m_<sha256[0:16]>` for markets (idempotent create)
    - Web3 privacy first: email/phone/IP stripped from external responses by default
    - Envelope: `{ok, data, meta}` on every canonical response
    - snake_case everywhere; money as decimal string (6 decimals for USDT, 8 for BTC)
    - `*_at_ms` sibling for every `*_at` timestamp (ms epoch, easier for clients)

  contact:
    name: PAX Partners
    email: partners@predictasiax.com
    url: https://docs.predictasiax.com
  license:
    name: PAX API Terms
    url: https://predictasiax.com/legal/api-terms

servers:
  - url: https://predictasiax.com/api
    description: Production (mainnet analog — real money)
  - url: https://sandbox.predictasiax.com/api
    description: Sandbox (testnet analog — fake USDT, resettable state, mock oracle)

security:
  - bearerAuth: []
  - hmacAuth: [POLY_ACCESS_KEY, POLY_TIMESTAMP, POLY_PASSPHRASE, POLY_SIGNATURE]
  - apiKeyAuth: []

# ─────────────────────────────────────────────────────────────────────────────
# Reusable components — schemas, params, security
# ─────────────────────────────────────────────────────────────────────────────
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Session bearer token (from `POST /api/auth/login`).
        Use this for browser/mobile client flows. NOT for machine-to-machine.
    hmacAuth:
      type: apiKey
      in: header
      name: POLY_SIGNATURE
      description: |
        HMAC-SHA256 signature (Polymarket-compatible pattern).
        Send 5 headers together: `POLY_ACCESS_KEY`, `POLY_TIMESTAMP`, `POLY_PASSPHRASE`,
        `POLY_SIGNATURE`, and body method. Signature = base64(HMAC-SHA256(secret,
        timestamp + method + path + body)).
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: |
        Simple API key for machine-to-machine (creator profiles, aggregators).
        `sk_live_*` on production, `sk_test_*` on sandbox.
        Get one via `POST /v1/keys` (requires session bearer for initial mint).

  parameters:
    MarketId:
      name: market_id
      in: path
      required: true
      description: Deterministic market ID `m_<sha256[0:16]>`
      schema:
        type: string
        pattern: '^m_[a-f0-9]{12,16}$'
        example: m_a1b2c3d4e5f6
    Cursor:
      name: cursor
      in: query
      description: Opaque pagination cursor (from previous response `meta.next_cursor`)
      schema: { type: string }
    Limit:
      name: limit
      in: query
      description: Max rows to return (1-500, default 50)
      schema: { type: integer, minimum: 1, maximum: 500, default: 50 }

  headers:
    XRequestID:
      description: Server-assigned request ID (echoed in error tickets)
      schema: { type: string, example: req_01hjk3n0abcdef }
    XApiVersion:
      description: API version served
      schema: { type: string, example: "1.0.0" }
    XMoneyFormat:
      description: Money field format on this response (`decimal_string` or `float`)
      schema: { type: string, enum: [decimal_string, float] }
    XPiiRedacted:
      description: "`true` if PII (email/phone/ip) was stripped from response"
      schema: { type: boolean }
    XCanonicalPath:
      description: Canonical path if legacy alias was rewritten
      schema: { type: string, example: /api/markets/list }
    RetryAfter:
      description: Seconds to wait before retry (present on 429)
      schema: { type: integer }
    XRateLimitRemaining:
      description: Requests remaining in current window
      schema: { type: integer }

  schemas:
    Envelope:
      type: object
      required: [ok]
      properties:
        ok: { type: boolean }
        data: { description: "Response payload — shape depends on endpoint" }
        meta:
          type: object
          properties:
            request_id: { type: string }
            server_ts_ms: { type: integer, format: int64 }
            next_cursor: { type: string, nullable: true }
            page_size: { type: integer }
            total_count: { type: integer, nullable: true }
        error:
          type: object
          nullable: true
          properties:
            code: { type: string, example: MARKET_NOT_FOUND }
            message: { type: string }
            details: { type: object }

    Money:
      type: string
      pattern: '^-?[0-9]+(\.[0-9]{1,18})?$'
      description: |
        Decimal string (never JSON number). Preserves precision for USDT (6 dp),
        BTC (8 dp), ETH (18 dp). Example: `"1000.234567"`.
      example: "1000.234567"

    Market:
      type: object
      required: [market_id, template_id, title, status, outcomes]
      properties:
        market_id: { type: string, example: m_a1b2c3d4e5f6 }
        template_id: { type: string, example: crypto_price_binary_60s }
        template_version: { type: integer, example: 1 }
        title: { type: string, example: "BTC > $100,000 at 2026-08-09 12:00 UTC?" }
        subtitle: { type: string }
        description: { type: string }
        image_url: { type: string, format: uri }
        category: { type: string, example: crypto }
        market_type: { type: string, enum: [fast, normal, creator] }
        outcome_schema: { type: string, enum: [binary, multi, scalar] }
        outcomes:
          type: array
          items:
            type: object
            properties:
              id: { type: string, example: yes }
              label: { type: string, example: "Yes" }
              price: { $ref: '#/components/schemas/Money' }
              volume_24h: { $ref: '#/components/schemas/Money' }
        status:
          type: string
          enum: [scheduled, active, locked, settling, resolved, cancelled]
        opens_at_ms: { type: integer, format: int64 }
        closes_at_ms: { type: integer, format: int64 }
        settles_at_ms: { type: integer, format: int64, nullable: true }
        total_volume: { $ref: '#/components/schemas/Money' }
        open_interest: { $ref: '#/components/schemas/Money' }
        creator_address:
          type: string
          nullable: true
          description: EVM address (0x-prefixed) or `null` for platform markets
        chain_data:
          type: object
          nullable: true
          description: Reserved for on-chain settlement metadata (future)

    Template:
      type: object
      required: [id, name, category, market_type, params_schema]
      properties:
        id: { type: string, example: crypto_price_binary_60s }
        version: { type: integer }
        name: { type: string, example: "Crypto Price Binary — 60s Fast" }
        category: { type: string, example: crypto }
        market_type: { type: string, enum: [fast, normal, creator] }
        outcome_schema: { type: string, enum: [binary, multi, scalar] }
        min_bet: { $ref: '#/components/schemas/Money' }
        max_bet: { $ref: '#/components/schemas/Money' }
        fee_schedule:
          type: object
          properties:
            maker_bps: { type: integer }
            taker_bps: { type: integer }
            settle_bps: { type: integer }
        params_schema:
          type: object
          description: JSON Schema for user-fillable knobs
          example:
            type: object
            required: [asset, target_price, timeframe_sec]
            properties:
              asset: { type: string, enum: [BTC, ETH, SOL] }
              target_price: { type: number, minimum: 0 }
              timeframe_sec: { type: integer, enum: [60, 300, 900, 1800, 3600] }
        display_defaults:
          type: object
          properties:
            title_template: { type: string, example: "{asset} > ${target_price} in {timeframe_sec}s?" }
            image_url_pattern: { type: string, format: uri }
        round_config:
          type: object
          nullable: true
          description: Only present when market_type=fast
          properties:
            duration_sec: { type: integer }
            lock_offset_ms: { type: integer }
        status: { type: string, enum: [active, deprecated, draft] }

    Candle:
      type: object
      required: [ts_ms, open, high, low, close, volume]
      properties:
        ts_ms: { type: integer, format: int64 }
        open: { $ref: '#/components/schemas/Money' }
        high: { $ref: '#/components/schemas/Money' }
        low: { $ref: '#/components/schemas/Money' }
        close: { $ref: '#/components/schemas/Money' }
        volume: { $ref: '#/components/schemas/Money' }

    OrderBook:
      type: object
      properties:
        market_id: { type: string }
        outcome_id: { type: string }
        bids:
          type: array
          items:
            type: array
            minItems: 2
            maxItems: 2
            items: { $ref: '#/components/schemas/Money' }
          description: "Array of [price, size] pairs, sorted desc by price"
        asks:
          type: array
          items:
            type: array
            minItems: 2
            maxItems: 2
            items: { $ref: '#/components/schemas/Money' }
        ts_ms: { type: integer, format: int64 }

    Trade:
      type: object
      properties:
        trade_id: { type: string }
        market_id: { type: string }
        outcome_id: { type: string }
        side: { type: string, enum: [buy, sell] }
        price: { $ref: '#/components/schemas/Money' }
        size: { $ref: '#/components/schemas/Money' }
        ts_ms: { type: integer, format: int64 }

    Order:
      type: object
      required: [market_id, outcome_id, side, order_type, size]
      properties:
        order_id: { type: string, readOnly: true }
        market_id: { type: string }
        outcome_id: { type: string }
        side: { type: string, enum: [buy, sell] }
        order_type: { type: string, enum: [market, limit] }
        price: { $ref: '#/components/schemas/Money', description: "Required if order_type=limit" }
        size: { $ref: '#/components/schemas/Money' }
        status:
          type: string
          enum: [pending, filled, partially_filled, cancelled, rejected]
          readOnly: true
        filled_size: { $ref: '#/components/schemas/Money', readOnly: true }
        avg_fill_price: { $ref: '#/components/schemas/Money', readOnly: true }
        created_at_ms: { type: integer, format: int64, readOnly: true }
        client_order_id:
          type: string
          maxLength: 64
          description: "Idempotency key — retry-safe. Reject duplicate within 24h."

    Account:
      type: object
      properties:
        account_id: { type: string, example: acc_kL9m8n7p6q5r }
        wallet_address: { type: string, nullable: true, example: "0xabc..." }
        balance_free: { $ref: '#/components/schemas/Money' }
        balance_reserved: { $ref: '#/components/schemas/Money' }
        asset: { type: string, enum: [USDT, CROWN], default: USDT }
        mode: { type: string, enum: [live, sandbox, demo], default: live }
        created_at_ms: { type: integer, format: int64 }

    Position:
      type: object
      properties:
        market_id: { type: string }
        outcome_id: { type: string }
        size: { $ref: '#/components/schemas/Money' }
        avg_price: { $ref: '#/components/schemas/Money' }
        unrealized_pnl: { $ref: '#/components/schemas/Money' }
        realized_pnl: { $ref: '#/components/schemas/Money' }

  responses:
    Envelope:
      description: Successful envelope response
      headers:
        X-Request-ID: { $ref: '#/components/headers/XRequestID' }
        X-API-Version: { $ref: '#/components/headers/XApiVersion' }
        X-Money-Format: { $ref: '#/components/headers/XMoneyFormat' }
        X-PII-Redacted: { $ref: '#/components/headers/XPiiRedacted' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }

    Error400:
      description: Bad request (validation error)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
    Error401:
      description: Unauthorized (missing/invalid credential)
    Error403:
      description: Forbidden (valid credential but insufficient permission)
    Error404:
      description: Not found
    Error429:
      description: Rate limited
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
    Error503:
      description: Service unavailable (e.g., read_only_mode)
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                example:
                  ok: false
                  error: { code: READ_ONLY_MODE, message: "Trading paused" }

# ─────────────────────────────────────────────────────────────────────────────
# Paths
# ─────────────────────────────────────────────────────────────────────────────
paths:
  /v1/health:
    get:
      summary: Health check
      tags: [Discovery]
      security: []  # public
      responses:
        '200':
          $ref: '#/components/responses/Envelope'

  /v1/markets:
    get:
      summary: List active markets
      tags: [Discovery]
      security: []  # public read
      parameters:
        - name: category
          in: query
          schema: { type: string, example: crypto }
        - name: market_type
          in: query
          schema: { type: string, enum: [fast, normal, creator] }
        - name: status
          in: query
          schema: { type: string, enum: [active, locked, settling] }
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Paginated market list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Market' }
    post:
      summary: Create market from template (creator flow)
      description: |
        **Deterministic**: `market_id = m_ + sha256(template_id + params + creator)[0:16]`.
        Idempotent — repeat POST returns existing market. Requires creator deposit
        (per template's `min_creator_deposit`, drawn from account's `balance_free`).
        Blocked if `risk_settings.read_only_mode = 1` → 503.
      tags: [Trading]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [template_id, params]
              properties:
                template_id: { type: string, example: crypto_price_binary_60s }
                params:
                  type: object
                  example: { asset: BTC, target_price: 100000, timeframe_sec: 60 }
                creator_metadata:
                  type: object
                  properties:
                    display_title: { type: string, description: Override template title }
                    display_image_url: { type: string, format: uri }
                idempotency_key:
                  type: string
                  maxLength: 64
                  description: Client-supplied. Server also derives one from content hash.
      responses:
        '200':
          description: Market created (or existing returned if idempotent)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          market: { $ref: '#/components/schemas/Market' }
                          template: { $ref: '#/components/schemas/Template' }
        '400': { $ref: '#/components/responses/Error400' }
        '401': { $ref: '#/components/responses/Error401' }
        '503': { $ref: '#/components/responses/Error503' }

  /v1/markets/templates:
    get:
      summary: List active market templates
      description: Templates define market shape (outcomes, fees, oracle, display).
      tags: [Discovery]
      security: []  # public read
      parameters:
        - name: category
          in: query
          schema: { type: string }
        - name: market_type
          in: query
          schema: { type: string, enum: [fast, normal, creator] }
      responses:
        '200':
          description: Template list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Template' }

  /v1/markets/{market_id}:
    get:
      summary: Get single market detail
      tags: [Discovery]
      security: []
      parameters:
        - $ref: '#/components/parameters/MarketId'
      responses:
        '200':
          description: Market detail
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Market' }
        '404': { $ref: '#/components/responses/Error404' }

  /v1/candles/{market_id}:
    get:
      summary: OHLCV candles for market's outcome price history
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - name: outcome_id
          in: query
          required: true
          schema: { type: string }
        - name: interval
          in: query
          schema: { type: string, enum: [1m, 5m, 15m, 30m, 1h, 4h, 1d], default: 1m }
        - name: from_ms
          in: query
          schema: { type: integer, format: int64 }
        - name: to_ms
          in: query
          schema: { type: integer, format: int64 }
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Candles
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Candle' }

  /v1/orderbook/{market_id}:
    get:
      summary: Top-of-book depth
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - name: outcome_id
          in: query
          required: true
          schema: { type: string }
        - name: depth
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: Orderbook snapshot (subscribe to WS for live updates)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/OrderBook' }

  /v1/trades/{market_id}:
    get:
      summary: Recent trades (public tape)
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - name: outcome_id
          in: query
          schema: { type: string }
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Trades
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Trade' }

  /v1/account:
    get:
      summary: Get authenticated account
      tags: [Account]
      responses:
        '200':
          description: Account snapshot
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Account' }
        '401': { $ref: '#/components/responses/Error401' }

  /v1/account/positions:
    get:
      summary: List open positions
      tags: [Account]
      responses:
        '200':
          description: Positions
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Position' }

  /v1/account/orders:
    get:
      summary: List orders (active + recent)
      tags: [Account]
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [pending, filled, cancelled, all] }
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Orders
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Order' }

  /v1/account/trades:
    get:
      summary: List own trade fills
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Own trades
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Trade' }

  /v1/orders:
    post:
      summary: Place order
      description: |
        Engine-agnostic — server routes to AMM or CLOB per market. If you need
        to know venue, check `meta.fill_venue` in response.
      tags: [Trading]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Order' }
      responses:
        '200':
          description: Order accepted (may be immediately filled)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Order' }
                      meta:
                        type: object
                        properties:
                          fill_venue: { type: string, enum: [amm, clob, hybrid] }
        '400': { $ref: '#/components/responses/Error400' }
        '503': { $ref: '#/components/responses/Error503' }

  /v1/orders/{order_id}:
    delete:
      summary: Cancel order
      tags: [Trading]
      parameters:
        - name: order_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '404': { $ref: '#/components/responses/Error404' }

  # ── Keys ────────────────────────────────────────────────────────────────
  /v1/keys:
    post:
      summary: Mint new API key
      description: |
        Returns key + secret + passphrase ONCE. Store securely — cannot be
        retrieved again. On sandbox, prefix is `sk_test_*`; on prod, `sk_live_*`.
      tags: [Account]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label: { type: string, example: "my-arb-bot" }
                permissions:
                  type: array
                  items: { type: string, enum: [read, trade, withdraw] }
                  default: [read]
      responses:
        '200':
          description: New key material (SHOW ONCE — cannot re-fetch)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          key_id: { type: string, example: sk_live_ABC123 }
                          secret: { type: string, description: "HMAC secret (32 bytes hex)" }
                          passphrase: { type: string }
                          permissions:
                            type: array
                            items: { type: string }
                          created_at_ms: { type: integer, format: int64 }
    get:
      summary: List own API keys (metadata only, no secrets)
      tags: [Account]
      responses:
        '200':
          description: Key list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'

  /v1/keys/{key_id}:
    delete:
      summary: Revoke API key
      tags: [Account]
      parameters:
        - name: key_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '404': { $ref: '#/components/responses/Error404' }

  # ── Sandbox-only ────────────────────────────────────────────────────────
  /v1/sandbox/faucet:
    post:
      summary: (Sandbox only) Mint fake USDT for testing
      description: |
        Delivers 10,000 test USDT to authenticated account. Max 1 call per hour
        per key. Only works on `sandbox.predictasiax.com` — 404 on prod.
      tags: [Sandbox]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '404':
          description: Not sandbox environment
        '429': { $ref: '#/components/responses/Error429' }

  /v1/sandbox/reset:
    post:
      summary: (Sandbox only) Wipe partner state — positions, orders, balance
      description: |
        Resets account to fresh state (0 balance, 0 positions, cancels all orders).
        Idempotent. Only works on sandbox.
      tags: [Sandbox]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '404':
          description: Not sandbox environment

  /v1/sandbox/mock-oracle:
    post:
      summary: (Sandbox only) Force settlement of test market
      description: |
        Set outcome for a sandbox market to test settlement flow without
        waiting for real oracle. Only works on markets created in sandbox with
        `template.oracle_config.source = sandbox_mock`.
      tags: [Sandbox]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [market_id, winning_outcome_id]
              properties:
                market_id: { type: string }
                winning_outcome_id: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/Error400' }
        '404':
          description: Market not sandbox-oracle-eligible

tags:
  - name: Discovery
    description: Public market catalog + templates
  - name: Market Data
    description: Candles, orderbook, trades tape
  - name: Account
    description: Balance, positions, orders, trades (auth required)
  - name: Trading
    description: Order entry + market creation
  - name: Sandbox
    description: Sandbox-only endpoints (faucet, reset, mock oracle)
