openapi: 3.1.0
info:
  title: MazeLedger Bot Trading API
  version: 1.0.0
  description: >
    Public API for offering spot (INR) and futures (USDT-M) trading bots to your users.
    Organizations onboard, register end-users with their own ZebPay keys (BYO), and place
    & manage orders. Auth via x-api-key (ml_test_* sandbox, ml_live_* live).
servers:
  - url: https://mazeledger.ai
paths:
  /api/v1/futures/orders:
    post:
      tags:
        - Trading API
      operationId: placeFuturesOrder
      summary: Place a futures order (params-driven; sandbox simulates, live trades via BYO keys)
      description: >
        Authenticate with an organization API key in the x-api-key header
        (ml_test_* sandbox or ml_live_* live). Provide either quantity or
        notionalUsd. Enforces the org's risk caps and MIN_NOTIONAL before submit.
      security:
        - orgApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [symbol, side]
              properties:
                symbol: { type: string, example: DOGEUSDT }
                side: { type: string, enum: [BUY, SELL] }
                type: { type: string, enum: [MARKET, LIMIT], default: MARKET }
                quantity: { type: number, description: Base-asset quantity (or use notionalUsd) }
                notionalUsd: { type: number, description: USD notional, converted at live price }
                leverage: { type: integer }
                price: { type: number, description: Required for LIMIT }
                reduceOnly: { type: boolean, default: false }
                clientOrderId: { type: string, description: Idempotency key }
      responses:
        '201': { description: Order placed (sandbox simulated or live submitted). }
        '401': { description: Missing/invalid API key. }
        '403': { description: Missing scope or live not enabled. }
        '409': { description: No connected ZebPay credentials. }
        '422': { description: Rejected by risk caps (RISK_CAP_REJECTED). }
        '502': { description: Exchange error. }
  /api/v1/spot/orders:
    post:
      tags:
        - Trading API
      operationId: placeSpotOrder
      summary: Place a spot order (INR-denominated; sandbox simulates, live trades via BYO keys)
      description: >
        Market buys use amountInr (Rs.99 minimum); sells/limits use quantity (+ priceInr for
        LIMIT). Pass endUserId to route to a registered end-user's credentials.
      security:
        - orgApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [symbol, side]
              properties:
                symbol: { type: string, example: BTC }
                side: { type: string, enum: [buy, sell] }
                type: { type: string, enum: [MARKET, LIMIT], default: MARKET }
                amountInr: { type: number, description: INR amount for market buys }
                quantity: { type: number, description: Base-asset quantity for sells/limits }
                priceInr: { type: number, description: Required for LIMIT }
                endUserId: { type: string, description: Registered end-user to trade for }
                clientOrderId: { type: string }
      responses:
        '201': { description: Order placed (sandbox simulated or live submitted). }
        '401': { description: Missing/invalid API key. }
        '422': { description: Rejected by risk caps. }
        '502': { description: Exchange error. }
  /api/v1/users:
    post:
      tags:
        - Trading API
      operationId: registerEndUser
      summary: Register an end-user with their own ZebPay credentials
      security:
        - orgApiKey: []
      responses:
        '201': { description: End-user registered (keys validated). }
    get:
      tags:
        - Trading API
      operationId: listEndUsers
      summary: List registered end-users + count
      security:
        - orgApiKey: []
      responses:
        '200': { description: List of end-users. }
  /api/v1/status:
    get:
      tags:
        - Partner API
      operationId: getApiV1Status
      summary: Get partner API status and validated token context
      security:
        - oauth2ClientCredentials:
            - api:read
      responses:
        '200':
          description: Service status with authenticated client context.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusSuccessResponse'
        '401':
          description: Missing/invalid bearer token, or inactive token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Bearer token is valid but missing required scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: OAuth2 introspection environment is not configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Internal unexpected error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/users/{externalUserId}:
    get:
      tags: [Trading API]
      operationId: getEndUser
      summary: Get a registered end-user (scope users:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: externalUserId, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: End-user details. }
        '404': { description: End-user not found. }
    delete:
      tags: [Trading API]
      operationId: deleteEndUser
      summary: Remove a registered end-user (scope users:write)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: externalUserId, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: End-user removed. }
  /api/v1/users/{externalUserId}/status:
    get:
      tags: [Trading API]
      operationId: getEndUserStatus
      summary: End-user trading snapshot — balances, positions, open orders, risk caps (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: externalUserId, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: Status snapshot. }
        '404': { description: End-user not found. }
  /api/v1/users/{externalUserId}/orders:
    get:
      tags: [Trading API]
      operationId: listEndUserOrders
      summary: List an end-user's orders placed via the API (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: externalUserId, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: Orders for the end-user. }
  /api/v1/users/{externalUserId}/readiness:
    get:
      tags: [Trading API]
      operationId: getEndUserReadiness
      summary: Live-money readiness checklist for an end-user (scope account:read)
      description: >
        Returns whether the end-user is connected, keys validated, within caps, and which
        live gates remain — the handoff checklist before real-money trading (UF-11/34).
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: externalUserId, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: Readiness checklist. }
        '404': { description: End-user not found. }
  /api/v1/bots:
    post:
      tags: [Bots]
      operationId: createBot
      summary: Create a managed bot for an end-user (scope orders:write)
      description: >
        Provide a natural-language `prompt` (parsed server-side into a Strategy Spec V2 via
        Bedrock, with a deterministic fallback) or a ready `spec`. If the prompt is incomplete
        the response is 200 with `clarification.questions` and NO bot is created. Bots are
        created PAUSED in PAPER mode; autonomous live execution is gated separately.
      security: [{ orgApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [endUserId]
              properties:
                endUserId: { type: string, description: A registered end-user external id }
                prompt: { type: string, description: 'Natural-language strategy (e.g. "DCA $50 BTC daily")' }
                spec: { type: object, description: A ready Strategy Spec V2 (alternative to prompt) }
                name: { type: string }
                tokens: { type: array, items: { type: string } }
                clarificationAnswers: { type: object, description: Answers to a prior clarification round }
      responses:
        '201': { description: Bot created (paper, paused). }
        '200': { description: Clarification needed — body has clarification.questions; no bot created. }
        '403': { description: Missing scope. }
        '404': { description: End-user not found. }
        '422': { description: Unsupported or invalid strategy. }
    get:
      tags: [Bots]
      operationId: listBots
      summary: List the org's managed bots (scope orders:read)
      security: [{ orgApiKey: [] }]
      responses:
        '200': { description: List of managed bots. }
  /api/v1/bots/validate:
    post:
      tags: [Bots]
      operationId: validateBot
      summary: Validate/derive a Strategy Spec V2 from natural language without creating a bot (scope orders:read)
      security: [{ orgApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prompt]
              properties:
                prompt: { type: string }
                tokens: { type: array, items: { type: string } }
      responses:
        '200': { description: '{ valid, spec } or { valid:false, questions } when incomplete.' }
  /api/v1/bots/pause-all:
    post:
      tags: [Bots]
      operationId: pauseAllBots
      summary: Kill-switch — pause all of the org's active bots (scope orders:write)
      security: [{ orgApiKey: [] }]
      responses:
        '200': { description: All active bots paused; returns count. }
  /api/v1/bots/{id}:
    get:
      tags: [Bots]
      operationId: getBot
      summary: Get a managed bot (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Bot details. }
        '404': { description: Bot not found. }
    delete:
      tags: [Bots]
      operationId: deleteBot
      summary: Delete a managed bot (scope orders:write)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Bot deleted. }
        '404': { description: Bot not found. }
  /api/v1/bots/{id}/pause:
    post:
      tags: [Bots]
      operationId: pauseBot
      summary: Pause a managed bot (scope orders:write)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Bot paused. }
        '404': { description: Bot not found. }
  /api/v1/bots/{id}/resume:
    post:
      tags: [Bots]
      operationId: resumeBot
      summary: Resume (activate) a managed bot — paper mode (scope orders:write)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Bot activated. }
        '404': { description: Bot not found. }
  /api/v1/market/klines:
    get:
      tags: [Market Data]
      operationId: getKlines
      summary: Futures OHLCV candles for charts (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: symbol, in: query, required: true, schema: { type: string }, example: DOGEUSDT }
        - { name: interval, in: query, schema: { type: string, default: '1h' } }
        - { name: limit, in: query, schema: { type: integer, default: 100, maximum: 1000 } }
      responses:
        '200': { description: Candle array. }
        '502': { description: Exchange error. }
  /api/v1/market/ticker:
    get:
      tags: [Market Data]
      operationId: getTicker
      summary: Last price + 24h stats for a symbol (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: symbol, in: query, required: true, schema: { type: string }, example: BTCUSDT }
        - { name: market, in: query, schema: { type: string, enum: [futures, spot], default: futures } }
      responses:
        '200': { description: Ticker. }
  /api/v1/market/compare:
    get:
      tags: [Market Data]
      operationId: compareSymbols
      summary: Last price + 24h change for up to 10 symbols (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: symbols, in: query, required: true, schema: { type: string }, example: 'BTC,ETH,DOGE' }
        - { name: market, in: query, schema: { type: string, enum: [futures, spot], default: futures } }
      responses:
        '200': { description: Per-symbol last + change (per-symbol errors reported inline). }
  /api/v1/screener:
    get:
      tags: [Market Data]
      operationId: screenMarket
      summary: Conventional signal/indicator screener — Opportunity Room (scope orders:read)
      description: >
        Computes the conventional technical panel per symbol — RSI(14), MACD(12,26,9) + cross,
        SMA(20/50/200) + golden/death cross, Bollinger(20,2) %B / bandwidth / squeeze / breakout,
        ADX/DMI(14), Stochastic(14,3,3), ATR%(14), volume vs 20-MA, and range position — then filters +
        ranks by the requested criteria (or a named `screen` preset; explicit filters override the preset).
        Returns candidates with human-readable signals, or an empty list + reason (UF-32). Read-only —
        never trades and never auto-creates bots.
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: screen, in: query, schema: { type: string, enum: [oversold, overbought, momentum, breakout, squeeze, golden_cross, death_cross, uptrend, downtrend, high_volume] }, description: named preset }
        - { name: symbols, in: query, schema: { type: string }, example: 'BTC,ETH,SOL' }
        - { name: timeframe, in: query, schema: { type: string, enum: ['1m', '5m', '15m', '1h', '4h', '1D'], default: '1h' } }
        - { name: lookback, in: query, schema: { type: integer, default: 24 }, description: candles used for the momentum % change }
        - { name: rsi_below, in: query, schema: { type: number } }
        - { name: rsi_above, in: query, schema: { type: number } }
        - { name: macd, in: query, schema: { type: string, enum: [bull, bear] }, description: MACD histogram sign }
        - { name: macd_cross, in: query, schema: { type: string, enum: [bull, bear] }, description: recent MACD crossover }
        - { name: trend, in: query, schema: { type: string, enum: [up, down] }, description: price/SMA20/SMA50 stack }
        - { name: adx_above, in: query, schema: { type: number }, description: min ADX (>=25 = trending) }
        - { name: adx_below, in: query, schema: { type: number } }
        - { name: di, in: query, schema: { type: string, enum: [up, down] }, description: +DI vs -DI direction }
        - { name: stoch_below, in: query, schema: { type: number } }
        - { name: stoch_above, in: query, schema: { type: number } }
        - { name: bb, in: query, schema: { type: string, enum: [squeeze, above, below, breakout_up, breakout_down] } }
        - { name: vol_spike, in: query, schema: { type: number }, description: min volume / 20-period-MA ratio }
        - { name: min_change_pct, in: query, schema: { type: number } }
        - { name: max_change_pct, in: query, schema: { type: number } }
        - { name: min_atr_pct, in: query, schema: { type: number } }
        - { name: max_atr_pct, in: query, schema: { type: number } }
        - { name: golden_cross, in: query, schema: { type: boolean } }
        - { name: death_cross, in: query, schema: { type: boolean } }
        - { name: sort, in: query, schema: { type: string, enum: [change_desc, change_asc, rsi_asc, rsi_desc, adx_desc, volatility_desc, volatility_asc, volume_desc], default: change_desc } }
        - { name: top, in: query, schema: { type: integer, default: 10, maximum: 50 } }
      responses:
        '200': { description: '{ candidates, no_candidates_reason?, criteria, preset, scanned, timeframe, note }' }
  /api/v1/account/webhook-secret:
    get:
      tags: [Alerts]
      operationId: getWebhookSecret
      summary: Get (creating if absent) the org webhook signing secret (scope account:read)
      description: >
        Use to verify alert webhook deliveries — header `x-mazeledger-signature` =
        "sha256=" + HMAC_SHA256(secret, `${x-mazeledger-timestamp}.${rawBody}`).
      security: [{ orgApiKey: [] }]
      responses:
        '200': { description: '{ secret, signature_header, scheme }' }
    post:
      tags: [Alerts]
      operationId: rotateWebhookSecret
      summary: Rotate the org webhook signing secret (scope users:write)
      security: [{ orgApiKey: [] }]
      responses:
        '200': { description: '{ secret, rotated }' }
  /api/v1/alerts:
    post:
      tags: [Alerts]
      operationId: createAlert
      summary: Create a price alert, optionally per end-user (scope orders:write)
      description: >
        Alerts are evaluated every ~5 min; when the condition is met the alert flips to `triggered`
        and we deliver to the configured channel(s): an HMAC-signed `webhookUrl` (HTTPS, public host
        only), a `slackWebhookUrl`, an email `notifyEmail`, and/or a `telegramChatId` (via the platform
        bot). Failed deliveries retry with backoff (≤5). Webhook signature: header
        `x-mazeledger-signature` = "sha256=" + HMAC_SHA256(secret, `${x-mazeledger-timestamp}.${rawBody}`)
        — fetch the secret from GET /api/v1/account/webhook-secret. You can also poll
        GET /api/v1/alerts?status=triggered.
      security: [{ orgApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [symbol, condition, price]
              properties:
                symbol: { type: string, example: BTCUSDT }
                condition: { type: string, enum: [above, below] }
                price: { type: number }
                market: { type: string, enum: [futures, spot], default: futures }
                externalUserId: { type: string, description: Attach to a registered end-user }
                note: { type: string }
                webhookUrl: { type: string, format: uri, description: 'HTTPS URL POSTed (HMAC-signed) when the alert triggers (public host only)' }
                notifyEmail: { type: string, format: email, description: Email notified when the alert triggers }
                slackWebhookUrl: { type: string, format: uri, description: Slack incoming-webhook URL }
                telegramChatId: { type: string, description: Telegram chat id (delivered via the platform bot) }
      responses:
        '201': { description: Alert created. }
        '404': { description: End-user not found. }
    get:
      tags: [Alerts]
      operationId: listAlerts
      summary: List alerts (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [active, triggered, cancelled] } }
        - { name: externalUserId, in: query, schema: { type: string } }
      responses:
        '200': { description: Alerts + total. }
  /api/v1/alerts/{id}:
    get:
      tags: [Alerts]
      operationId: getAlert
      summary: Get an alert (scope orders:read)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Alert. }
        '404': { description: Alert not found. }
    delete:
      tags: [Alerts]
      operationId: cancelAlert
      summary: Cancel an alert (scope orders:write)
      security: [{ orgApiKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Alert cancelled. }
        '404': { description: Alert not found. }
components:
  securitySchemes:
    orgApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: Organization API key (ml_test_* sandbox or ml_live_* live).
    oauth2ClientCredentials:
      type: oauth2
      description: OAuth2 Client Credentials bearer token.
      flows:
        clientCredentials:
          tokenUrl: https://auth.example.com/oauth/token
          scopes:
            api:read: Read partner API resources.
  schemas:
    ApiErrorResponse:
      type: object
      additionalProperties: false
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: string
        code:
          type: string
        details:
          type: string
    StatusAuthContext:
      type: object
      additionalProperties: false
      required:
        - client_id
        - subject
        - scopes
      properties:
        client_id:
          type:
            - string
            - 'null'
        subject:
          type:
            - string
            - 'null'
        scopes:
          type: array
          items:
            type: string
    StatusPayload:
      type: object
      additionalProperties: false
      required:
        - api_version
        - status
        - timestamp
        - auth
      properties:
        api_version:
          type: string
          enum:
            - v1
        status:
          type: string
          enum:
            - ok
        timestamp:
          type: string
          format: date-time
        auth:
          $ref: '#/components/schemas/StatusAuthContext'
    StatusSuccessResponse:
      type: object
      additionalProperties: false
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          $ref: '#/components/schemas/StatusPayload'
