openapi: 3.1.0
info:
  title: Taifoon CLOB API
  version: "1.0.0"
  description: |
    Machine-readable API for the Taifoon CLOB at https://clob.taifoon.dev — for
    agents and integrations building on top of the order book + replay system.

    ## Core model
    - **Candles** are window-pull history (`from`/`to`, unix seconds). Each response
      echoes `t_logical` (clock now), `speed_x`, `mode` (`wall`|`warped`).
    - **speed_x** is the replay multiplier and lives on the SESSION clock, not on the
      candle endpoint. A session created with `speed_x=N` advances logical time at N
      market-seconds per wall-second; a `tf` bar closes every `tf_seconds/N` wall-seconds.
      `speed_x=1` = real time. The candle/clock responses carry `speed_x` so a consumer
      knows the scale. Replay (history → live) all flows through the session clock.
    - **Account**: every actor (algo or user) has a profile + full history derived from
      the fill log. `GET /book/v1/account/{attribution}` returns it in one call.
    - **Dispatch**: `POST /book/v1/orders` places an order; replay dispatch stamps
      `tape_now` (decision-candle close) + `tape_speed_x` so fills land on the causal bar.

    ## Execution model — there is no paper mode
    The CLOB has ONE execution path. `POST /book/v1/orders` ALWAYS places a real order
    against the single matcher — there is no paper / observe / dry-run mode and no
    simulated book to opt into. An order either lands on the book or it does not.

    - **`speed_x` is the only prod/dev knob.** `speed_x=1` is the live book at real time.
      `speed_x>1` is a server-paced *session* book (the warped-clock venue): the SAME
      matcher and the SAME dispatch path, replaying history faster — real fills, persisted
      history, but you cannot post into a future candle. `speed_x=0` is flat-out backfill
      (no live follow). Replay is not a different mode; it is the same engine on a warped
      clock. An equivalence test proves the warped and wall books are byte-identical at
      `speed_x=100`.
    - **Consequence for backtests:** a replay walk and a production run go through the
      identical matcher + fill model, so a strategy's replay PnL is its production PnL on
      the same bars (modulo live queue position / fees). There is no
      optimistic-fill "paper" backtest that production then fails to reproduce — closing
      any residual replay↔production gap (fill timing, fees, slippage) is a first-class
      correctness goal, not an accepted simulation artifact.
    - **Account / PnL** is derived from the realized fill log (FIFO closes), the same for
      a leased agent bot and a human — see `GET /book/v1/account/{attribution}`.

    ## Time-travel in 6 calls (verified turnkey recipe)
    Drive a full history→live replay end-to-end. All against this server.

    ```
    # 1. Plain candles for a market (window-pull). Echoes speed_x=1.0, mode=wall.
    GET /tape/v1/candles/NQ/1m?from=1772000000&to=1772003600
        → { candles:[{t,o,h,l,c,v}], t_logical, speed_x, mode }

    # 2. Create a time-travel session at speed_x (history→live). speed_x=1 is real time;
    #    1000 replays ~1000 market-seconds per wall-second.
    POST /tape/v1/sessions
        { "from":1772000000, "to":1772086400, "speed_x":1000,
          "markets":["NQ-PERP"], "tf":"1m" }   → { session_id }

    # 3. The session clock now advances at speed_x — poll it.
    GET /tape/v1/sessions/{session_id}
        → clock.now climbs ~speed_x logical-seconds per wall-second

    # 4. Candles SCOPED to the session = the time-travel view (mode=warped).
    GET /tape/v1/candles/NQ/1m?from=1772000000&to=1772086400&session={session_id}
        → { candles:[…], mode:"warped", speed_x:1000 }

    # 5. Start the algo replay — it steps the cubicle and synth-fills as the clock moves.
    POST /tape/v1/sessions/{session_id}/replay/start
        { "from":…, "to":…, "speed_x":1000, "markets":["NQ-PERP"], "tf":"1m" }
    GET  /tape/v1/sessions/{session_id}/decisions   → fires emitted
    #    (or stream live: WS /tape/v1/sessions/{session_id}/ws)

    # 6. Read the result — the actor's account / closed trades.
    GET /book/v1/account/algotrada-skydweller-v1   → profile + positions + closes + equity
    DELETE /tape/v1/sessions/{session_id}          → clean up (per-IP session cap)
    ```
    Notes: candles accept BOTH path-style `/candles/{market}/{tf}` (above) and a flat
    `/candles?market=&tf=` form. Sessions are capped per-IP — DELETE when done.
servers:
  - url: https://clob.taifoon.dev
paths:
  /tape/v1/candles/{market}/{tf}:
    get:
      summary: Historical OHLCV candles (window-pull)
      parameters:
        - { name: market, in: path, required: true, schema: { type: string }, example: NQ }
        - { name: tf, in: path, required: true, schema: { type: string, enum: [1m,5m,15m,30m,1h,4h,1d] } }
        - { name: from, in: query, schema: { type: integer }, description: unix seconds (window start) }
        - { name: to, in: query, schema: { type: integer }, description: unix seconds (window end) }
        - { name: limit, in: query, schema: { type: integer, default: 500 } }
        - { name: session, in: query, schema: { type: string }, description: replay session id (clamps to its clock) }
      responses:
        "200":
          description: candles + clock context
          content:
            application/json:
              schema:
                type: object
                properties:
                  candles:
                    type: array
                    items: { $ref: '#/components/schemas/Candle' }
                  market: { type: string }
                  tf: { type: string }
                  t_logical: { type: integer, description: clock now (unix s) }
                  speed_x: { type: number, description: replay multiplier (1.0 = real time) }
                  mode: { type: string, enum: [wall, warped] }
  /book/v1/account/{attribution}:
    get:
      summary: Full account rollup — profile + positions + closes + equity curve
      parameters:
        - { name: attribution, in: path, required: true, schema: { type: string }, example: algotrada-skydweller-v1 }
      responses:
        "200":
          description: one-call account
          content:
            application/json:
              schema:
                type: object
                properties:
                  attribution: { type: string }
                  profile: { type: object, description: lifetime + today PnL/WR/by_market }
                  positions: { type: array, items: { $ref: '#/components/schemas/Position' } }
                  closes: { type: array, items: { $ref: '#/components/schemas/ClosedTrade' } }
                  daily: { type: array, description: daily equity/PnL series }
                  t_logical: { type: integer }
                  speed_x: { type: number }
                  mode: { type: string }
  /book/v1/closes:
    get:
      summary: Per-round-trip closed-trade history (the replay-film source)
      parameters:
        - { name: attribution, in: query, schema: { type: string } }
        - { name: market, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 1000 } }
      responses:
        "200":
          description: closes
          content:
            application/json:
              schema:
                type: object
                properties:
                  closes: { type: array, items: { $ref: '#/components/schemas/ClosedTrade' } }
                  count: { type: integer }
  /book/v1/positions:
    get:
      summary: Open positions (native realized_pnl + win/loss per actor/market)
      responses: { "200": { description: positions } }
  /book/v1/orders:
    post:
      summary: Place an order (dispatch). Replay sets tape_now/tape_speed_x.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [side, qty, market, attribution]
              properties:
                side: { type: string, enum: [buy, sell] }
                qty: { type: string }
                price: { type: string }
                tif: { type: string, enum: [gtc, ioc, fok], default: gtc }
                market: { type: string }
                attribution: { type: string }
                tape_now: { type: integer, description: "replay: decision-candle close (unix s) → fill.t_logical" }
                tape_speed_x: { type: number }
      responses: { "200": { description: "{ order_id }" } }
  /tape/v1/sessions:
    post:
      summary: Create a replay session (history → live, at speed_x)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [from, to, markets]
              properties:
                from: { type: integer }
                to: { type: integer }
                speed_x: { type: number, default: 100 }
                markets: { type: array, items: { type: string } }
                tf: { type: string }
                attribution: { type: string }
      responses: { "200": { description: "{ session_id }" } }
  /tape/v1/sessions/{id}/replay/start:
    post: { summary: Start the algo replay (steps the cubicle, synth-fills), responses: { "200": { description: started } } }
  /tape/v1/sessions/{id}/ws:
    get: { summary: Per-session WebSocket — live replay tick/fill stream, responses: { "101": { description: switching protocols } } }

  # ── Agentic clients — register + lease a default bot (off-chain, live-path) ──
  # An agent registers, leases one of the two default algos (skydweller|bender) on a
  # market subset, and runs it through the SAME live dispatch path (no paper mode).
  # On-chain custody/LIVE registration stays the gated Phase-B block below.
  /users/v1/bots:
    get:
      summary: Catalog of the two default algos + their live track record
      description: |
        Returns `skydweller` (SMT×P3 selective FSM — the NQ algo) and `bender`
        (corridor-breakout FSM — the crypto algo), each with markets, style, and a
        track_record (n_closes / win_rate / realized_pnl from TimescaleDB closes).
      responses: { "200": { description: "{ bots: { skydweller, bender } }" } }
  /users/v1/clients/register:
    post:
      summary: Register an agent client → client_id + api_key
      description: |
        Off-chain client identity for leasing replay/live bots. NOT on-chain custody
        (that is the gated /book/v1/users/register below). Returns an opaque api_key.
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { display_name: { type: string }, kind: { type: string, default: agent } } }
      responses: { "200": { description: "{ client_id, api_key, display_name, kind }" } }
  /users/v1/clients/{client_id}/lease:
    post:
      summary: Lease a default bot (skydweller|bender) on a market subset
      description: |
        Maps the lease to an attribution (`client-<id>-<bot>`) the fills/closes pipeline
        already tracks, so GET /users/v1/account/<attr> works for the leased bot. The bot
        dispatches through the one live path; `mode` is informational (replay vs live is
        a function of speed_x + consent, not a separate paper book).
      parameters:
        - { name: client_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bot]
              properties:
                bot: { type: string, enum: [skydweller, bender] }
                markets: { type: array, items: { type: string, enum: [NQ, BTC, ETH, SOL] } }
      responses:
        "200": { description: "{ lease_id, bot, markets, attribution, account_url }" }
        "400": { description: unknown bot / client / no valid markets }
  /users/v1/clients/{client_id}:
    get:
      summary: Client profile + leases + per-lease account rollups
      parameters:
        - { name: client_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: "{ client_id, display_name, leases:[{bot,markets,attribution,account}] }" }
        "404": { description: unknown client_id }
  /users/v1/account/{attribution}:
    get:
      summary: TimescaleDB account rollup (source of truth for history + realized PnL)
      description: |
        FIFO closes computed in TimescaleDB, byte-equal to the venue's native
        /book/v1/closes. `?prefix=1` matches by attribution prefix (includes -shadow /
        warp- variants), mirroring the venue's starts_with semantics.
      parameters:
        - { name: attribution, in: path, required: true, schema: { type: string } }
        - { name: prefix, in: query, schema: { type: string, enum: ["0", "1"] } }
      responses: { "200": { description: "{ attribution, profile, closes, source: timescaledb }" } }

  # ── PLANNED, custody-gated — NOT yet wired (see x-status) ───────────────────
  /book/v1/users/register:
    post:
      x-status: planned-custody-gated
      summary: "[PLANNED] Register a user/agent → on-chain TournamentRegistry attribution"
      description: |
        Identity is the on-chain TournamentRegistry (chain 36927, rpc.taifoon.dev),
        not a REST row. Registration = an on-chain isActive(epoch, attributionTag).
        NOT yet exposed as REST — requires KYC/identity design + operator sign-off.
      responses: { "501": { description: not implemented (custody-gated) } }
  /book/v1/deposit:
    post:
      x-status: planned-custody-gated
      summary: "[PLANNED] Deposit funds — on-chain via the Reactor settlement contract"
      description: |
        The book has NO server-side balance ledger; real funds move ON-CHAIN through
        the Reactor (0xD186…, chain 36927). Deposit = an on-chain token transfer into
        the settlement contract. Wiring requires custody model + private-key signing +
        security sign-off (CLAUDE.md key-rotation risk). Documented, NOT wired.
      responses: { "501": { description: not implemented (custody-gated) } }
  /book/v1/withdraw:
    post:
      x-status: planned-custody-gated
      summary: "[PLANNED] Withdraw funds — on-chain via the Reactor settlement contract"
      responses: { "501": { description: not implemented (custody-gated) } }
components:
  schemas:
    Candle:
      type: object
      properties:
        t: { type: integer, description: candle open, unix seconds }
        o: { type: number }
        h: { type: number }
        l: { type: number }
        c: { type: number }
        v: { type: number }
    Position:
      type: object
      properties:
        market: { type: string }
        attribution: { type: string }
        qty: { type: string }
        avg_price: { type: string }
        side: { type: string, enum: [long, short, flat] }
        realized_pnl: { type: string }
        n_fills: { type: integer }
        n_wins: { type: integer }
        n_losses: { type: integer }
        mark: { type: string }
        unrealized_pnl: { type: string }
    ClosedTrade:
      type: object
      properties:
        attribution: { type: string }
        market: { type: string }
        side: { type: string, enum: [long, short] }
        qty: { type: string }
        entry_price: { type: string }
        exit_price: { type: string }
        entry_ts: { type: integer }
        exit_ts: { type: integer }
        realized_pnl: { type: string }
        win: { type: boolean }
        entry_fill_id: { type: string }
        exit_fill_id: { type: string }
