openapi: 3.1.0
info:
  title: Tickory API Key Auth Surface
  version: 0.1.0
  description: >
    API key auth for agents and bots. Use the X-API-Key header. Scope requirements
    are described per operation with x-tickory-required-scopes.
servers:
  - url: http://localhost:8080
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: User-scoped API key for server-to-server integrations.
  schemas:
    ErrorResponse:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
        message:
          type: string
        code:
          type: string
        details:
          type: object
          additionalProperties: true
    ScanMatch:
      type: object
      required: [symbol, exchange, contract_type, timestamp, price, volume_quote, rsi_14, ma_50]
      properties:
        symbol:
          type: string
        exchange:
          type: string
        contract_type:
          type: string
        timestamp:
          type: string
          format: date-time
        price:
          type: number
        volume_quote:
          type: number
        rsi_14:
          type: number
        ma_50:
          type: number
        ma_200:
          type: number
        scan_expression:
          type: string
        chart_url:
          type: string
        matched_metrics:
          type: object
          additionalProperties: true
        timeframe:
          type: string
    ExecuteScanResponse:
      type: object
      required: [run_id, scan_id, started_at, status, matches]
      properties:
        run_id:
          type: string
        scan_id:
          type: string
          description: >
            Saved scan identifier when the run came from /api/crypto/scans/execute.
            Ad hoc executions return an empty string here because no reusable scans row exists.
        started_at:
          type: string
          format: date-time
        status:
          type: string
        matches:
          type: array
          items:
            $ref: '#/components/schemas/ScanMatch'
      example:
        run_id: "22222222-2222-2222-2222-222222222222"
        scan_id: "crypto_oversold"
        started_at: "2026-03-12T12:00:00Z"
        status: "completed"
        matches:
          - symbol: "BTCUSDT"
            exchange: "binance"
            contract_type: "perp"
            timestamp: "2026-03-12T12:00:00Z"
            price: 50000
            volume_quote: 1500000
            rsi_14: 25
            ma_50: 49000
            timeframe: "1m"
    HardGates:
      type: object
      description: Hard filters applied before CEL evaluation.
      additionalProperties: true
      properties:
        min_volume_quote:
          type: number
        max_volume_quote:
          type: number
        max_daily_move:
          type: number
        min_daily_move:
          type: number
        min_price:
          type: number
        max_price:
          type: number
        require_rsi_14:
          type: boolean
        require_ma_50:
          type: boolean
        allowed_exchanges:
          type: array
          items:
            type: string
        allowed_contracts:
          type: array
          items:
            type: string
            enum: [perp, spot]
      example:
        min_volume_quote: 1000000
        require_rsi_14: true
        require_ma_50: true
        allowed_exchanges: ["binance"]
        allowed_contracts: ["spot"]
    ScanResponse:
      type: object
      required: [id, name, description, expression, timeframe, is_shared, created_at, updated_at]
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        expression:
          type: string
        timeframe:
          type: string
          enum: [1m, 5m, 15m, 1h]
        symbol_scope:
          type: array
          description: >
            Saved subset of symbols for this scan. At execution time Tickory revalidates
            this list against the current active supported symbols for the scan contract
            type and silently drops inactive entries.
          items:
            type: string
        visibility:
          type: string
        market:
          type: string
        explain:
          type: string
        hard_gates:
          $ref: '#/components/schemas/HardGates'
        builder_config:
          type: object
          additionalProperties: true
        is_shared:
          type: boolean
        share_token:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        validation_warnings:
          type: array
          items:
            type: string
        notification_status:
          type: object
          additionalProperties: true
      example:
        id: "scan-user-1"
        name: "Agent Oversold Pullback"
        description: "Agent-generated from intent: liquid 15m spot pullbacks"
        expression: "has_rsi_14 && rsi_14 < 30 && close > ma_50"
        timeframe: "15m"
        symbol_scope: ["BTCUSDT", "ETHUSDT"]
        hard_gates:
          min_volume_quote: 1000000
          require_rsi_14: true
          require_ma_50: true
          allowed_exchanges: ["binance"]
          allowed_contracts: ["spot"]
        builder_config:
          source: "agent_quickstart"
        is_shared: false
        created_at: "2026-03-12T12:00:00Z"
        updated_at: "2026-03-12T12:00:00Z"
    ListScansResponse:
      type: object
      required: [scans, count]
      properties:
        scans:
          type: array
          items:
            $ref: '#/components/schemas/ScanResponse'
        count:
          type: integer
    CreateScanRequest:
      type: object
      required: [name, cel_expression]
      properties:
        name:
          type: string
        description:
          type: string
        cel_expression:
          type: string
        timeframe:
          type: string
          default: 1m
          enum: [1m, 5m, 15m, 1h]
        symbol_scope:
          type: array
          description: >
            Optional saved symbol subset. Values must be active supported symbols for
            the selected contract type when the scan is saved.
          items:
            type: string
        hard_gates:
          $ref: '#/components/schemas/HardGates'
        builder_config:
          type: object
          additionalProperties: true
        is_public:
          type: boolean
      example:
        name: "Agent Oversold Pullback"
        description: "Agent-generated from intent: liquid 15m spot pullbacks"
        cel_expression: "has_rsi_14 && rsi_14 < 30 && close > ma_50"
        timeframe: "15m"
        symbol_scope: ["BTCUSDT", "ETHUSDT"]
        hard_gates:
          min_volume_quote: 1000000
          require_rsi_14: true
          require_ma_50: true
          allowed_exchanges: ["binance"]
          allowed_contracts: ["spot"]
        builder_config:
          source: "agent_quickstart"
    UpdateScanRequest:
      type: object
      required: [name, cel_expression]
      properties:
        name:
          type: string
        description:
          type: string
        cel_expression:
          type: string
        timeframe:
          type: string
          default: 1m
          enum: [1m, 5m, 15m, 1h]
        symbol_scope:
          type: array
          description: >
            Optional saved symbol subset. Values must be active supported symbols for
            the selected contract type when the scan is updated.
          items:
            type: string
        hard_gates:
          $ref: '#/components/schemas/HardGates'
        builder_config:
          type: object
          additionalProperties: true
      example:
        name: "Agent Oversold Pullback"
        description: "Agent-generated from intent: liquid 15m spot pullbacks"
        cel_expression: "has_rsi_14 && rsi_14 < 28 && close > ma_50"
        timeframe: "15m"
        symbol_scope: ["BTCUSDT"]
        hard_gates:
          min_volume_quote: 1000000
          require_rsi_14: true
          require_ma_50: true
          allowed_exchanges: ["binance"]
          allowed_contracts: ["spot"]
        builder_config:
          source: "agent_quickstart"
    ExecuteScanRequest:
      type: object
      required: [scan_id]
      properties:
        scan_id:
          type: string
        symbols:
          type: array
          description: >
            Optional one-off override. When omitted, Tickory executes the scan against
            the saved symbol_scope after runtime revalidation, or the full active
            universe if no saved scope exists.
          items:
            type: string
      example:
        scan_id: "scan-user-1"
        symbols: ["SOLUSDT"]
    ExecuteAdHocScanRequest:
      type: object
      required: [expression]
      properties:
        name:
          type: string
          description: Optional display name stored on the run snapshot for debugging.
        expression:
          type: string
        symbols:
          type: array
          description: >
            Optional one-off symbol subset for this execution. When omitted, Tickory
            executes the ad hoc scan against the full active default universe. When
            provided, values must be active supported symbols for the selected contract type.
          items:
            type: string
        hard_gates:
          $ref: '#/components/schemas/HardGates'
        builder_config:
          type: object
          additionalProperties: true
      example:
        name: "Agent spot trial"
        expression: "has_rsi_14 && rsi_14 < 30"
        symbols: ["BTCUSDT", "ETHUSDT"]
        hard_gates:
          require_rsi_14: true
          allowed_contracts: ["spot"]
        builder_config:
          source: "agent_quickstart"
    AlertEventDeliverySummary:
      type: object
      required: [channel, status, attempts]
      properties:
        channel:
          type: string
        status:
          type: string
        attempts:
          type: integer
        error_message:
          type: string
          nullable: true
        sent_at:
          type: string
          format: date-time
          nullable: true
    AlertEventFreshness:
      type: object
      required: [event_created_at]
      properties:
        source_timestamp:
          type: string
          format: date-time
          nullable: true
        event_created_at:
          type: string
          format: date-time
        lag_seconds:
          type: integer
          nullable: true
    AlertEventHardGateSummary:
      type: object
      required: [configured, result]
      properties:
        configured:
          type: object
          additionalProperties: true
        result:
          type: string
    AlertEventCELSummary:
      type: object
      required: [result]
      properties:
        expression:
          type: string
        result:
          type: string
    AlertEventEvidence:
      type: object
      required: [indicator_values, hard_gate_summary, cel_summary, data_freshness]
      properties:
        indicator_values:
          type: object
          additionalProperties: true
        hard_gate_summary:
          $ref: '#/components/schemas/AlertEventHardGateSummary'
        cel_summary:
          $ref: '#/components/schemas/AlertEventCELSummary'
        data_freshness:
          $ref: '#/components/schemas/AlertEventFreshness'
    AlertEvent:
      type: object
      required:
        - payload_version
        - alert_event_id
        - event_type
        - scan_id
        - created_at
        - status
        - evidence
        - delivery_summary
      properties:
        payload_version:
          type: string
        alert_event_id:
          type: string
        event_type:
          type: string
        scan_id:
          type: string
        run_id:
          type: string
          nullable: true
        symbol:
          type: string
          nullable: true
        timeframe:
          type: string
        created_at:
          type: string
          format: date-time
        status:
          type: string
        evidence:
          $ref: '#/components/schemas/AlertEventEvidence'
        delivery_summary:
          type: array
          items:
            $ref: '#/components/schemas/AlertEventDeliverySummary'
        suppression_reason_codes:
          type: array
          items:
            type: string
    ListAlertEventsResponse:
      type: object
      required: [payload_version, events, count]
      properties:
        payload_version:
          type: string
        events:
          type: array
          items:
            $ref: '#/components/schemas/AlertEvent'
        count:
          type: integer
        next_cursor:
          type: string
          nullable: true
    GetAlertEventResponse:
      type: object
      required: [payload_version, event]
      properties:
        payload_version:
          type: string
        event:
          $ref: '#/components/schemas/AlertEvent'
    AlertEventReceiptChannelOutcome:
      type: object
      required: [channel, status, attempts]
      properties:
        channel:
          type: string
        status:
          type: string
        attempts:
          type: integer
        last_error:
          type: string
          nullable: true
        last_attempt_at:
          type: string
          format: date-time
          nullable: true
    AlertEventReceiptSourceTimestamps:
      type: object
      required: [event_created_at]
      properties:
        source_timestamp:
          type: string
          format: date-time
          nullable: true
        event_created_at:
          type: string
          format: date-time
        latest_delivery_attempt_at:
          type: string
          format: date-time
          nullable: true
    AlertEventReceiptFreshness:
      type: object
      properties:
        lag_seconds:
          type: integer
          nullable: true
    AlertEventReceiptEvidence:
      type: object
      required: [indicator_values, top_contributing_metrics, gates_passed, gates_failed, cel_evaluation]
      properties:
        indicator_values:
          type: object
          additionalProperties: true
        top_contributing_metrics:
          type: array
          items:
            $ref: '#/components/schemas/ExplainMetric'
        gates_passed:
          type: array
          items:
            $ref: '#/components/schemas/ExplainGateResult'
        gates_failed:
          type: array
          items:
            $ref: '#/components/schemas/ExplainGateResult'
        cel_evaluation:
          $ref: '#/components/schemas/ExplainCELEvaluation'
        suppression:
          $ref: '#/components/schemas/ExplainSuppressionContext'
        suppression_reason_codes:
          type: array
          items:
            type: string
    AlertEventReceiptVerification:
      type: object
      required: [method, canonicalization, payload_hash]
      properties:
        method:
          type: string
        canonicalization:
          type: string
        payload_hash:
          type: string
    AlertEventReceipt:
      type: object
      required:
        - schema_version
        - receipt_id
        - alert_event_id
        - scan_id
        - rule_version
        - engine_version
        - event_type
        - event_status
        - evaluated_at
        - source_timestamps
        - freshness
        - evidence
        - delivery_outcomes
        - verification
      properties:
        schema_version:
          type: string
        receipt_id:
          type: string
        alert_event_id:
          type: string
        run_id:
          type: string
          nullable: true
        scan_id:
          type: string
        rule_version:
          type: string
        engine_version:
          type: string
        event_type:
          type: string
        event_status:
          type: string
        symbol:
          type: string
          nullable: true
        timeframe:
          type: string
        evaluated_at:
          type: string
          format: date-time
        source_timestamps:
          $ref: '#/components/schemas/AlertEventReceiptSourceTimestamps'
        freshness:
          $ref: '#/components/schemas/AlertEventReceiptFreshness'
        evidence:
          $ref: '#/components/schemas/AlertEventReceiptEvidence'
        delivery_outcomes:
          type: array
          items:
            $ref: '#/components/schemas/AlertEventReceiptChannelOutcome'
        verification:
          $ref: '#/components/schemas/AlertEventReceiptVerification'
    GetAlertEventReceiptResponse:
      type: object
      required: [payload_version, receipt]
      properties:
        payload_version:
          type: string
        receipt:
          $ref: '#/components/schemas/AlertEventReceipt'
      example:
        payload_version: "1.0"
        receipt:
          schema_version: "1.0"
          receipt_id: "dc64d405-8587-5874-8fe0-4a55139d4c48"
          alert_event_id: "11111111-1111-1111-1111-111111111111"
          run_id: "22222222-2222-2222-2222-222222222222"
          scan_id: "crypto_oversold"
          rule_version: "sha256:8d94b97d6dbe5d5b6f4f42d8b3f7d2bfa9f4f8f0f7f05b8d2cc9c4c4318e29f8"
          engine_version: "scanner-evidence/1.0"
          event_type: "alert"
          event_status: "completed"
          symbol: "BTCUSDT"
          timeframe: "15m"
          evaluated_at: "2026-03-10T09:04:30Z"
          source_timestamps:
            source_timestamp: "2026-03-10T09:04:30Z"
            event_created_at: "2026-03-10T09:05:00Z"
            latest_delivery_attempt_at: "2026-03-10T09:05:45Z"
          freshness:
            lag_seconds: 30
          evidence:
            indicator_values:
              price: 100.25
              rsi_14: 24.5
              volume_quote: 2100000
            top_contributing_metrics:
              - rank: 1
                name: "price"
                value: 100.25
                source: "payload"
                captured_at: "2026-03-10T09:04:30Z"
            gates_passed:
              - gate: "min_volume_quote"
                reason_code: "volume_gate_satisfied"
                summary: "configured minimum volume gate passed"
                configured_value: 1000000
                observed_value: 2100000
            gates_failed: []
            cel_evaluation:
              expression: "rsi_14 < 30"
              result: "matched"
              reason_code: "cel_expression_matched"
              summary: "stored alert evidence indicates the CEL expression matched"
          delivery_outcomes:
            - channel: "email"
              status: "sent"
              attempts: 2
              last_attempt_at: "2026-03-10T09:05:30Z"
            - channel: "webhook"
              status: "failed"
              attempts: 1
              last_error: "webhook timeout"
              last_attempt_at: "2026-03-10T09:05:45Z"
          verification:
            method: "deterministic_sha256"
            canonicalization: "json_sorted_keys_v1"
            payload_hash: "sha256:34a59d4a0f258874eec7178a4f0d88d8a7464af691f5a526a4fdd8347cf0d53f"
    MarketDataSnapshot:
      type: object
      required: [symbol, exchange, contract_type, timeframe, timestamp, price]
      properties:
        symbol:
          type: string
        exchange:
          type: string
          description: Exchange identifier for the cached Tickory snapshot.
        contract_type:
          type: string
        timeframe:
          type: string
        timestamp:
          type: string
          format: date-time
        price:
          type: number
        rsi_14:
          type: number
          nullable: true
        ma_50:
          type: number
          nullable: true
        ma_200:
          type: number
          nullable: true
        volume_quote:
          type: number
          nullable: true
        funding_rate:
          type: number
          nullable: true
        daily_move_pct:
          type: number
          nullable: true
    MarketDataListResponse:
      type: object
      required: [market_data, count]
      properties:
        market_data:
          type: array
          items:
            $ref: '#/components/schemas/MarketDataSnapshot'
        count:
          type: integer
    ExplainGateResult:
      type: object
      required: [gate, reason_code, summary]
      properties:
        gate:
          type: string
        reason_code:
          type: string
        summary:
          type: string
        configured_value:
          nullable: true
        observed_value:
          nullable: true
        count:
          type: integer
          nullable: true
    ExplainCELEvaluation:
      type: object
      required: [result, reason_code, summary]
      properties:
        expression:
          type: string
        result:
          type: string
        reason_code:
          type: string
        summary:
          type: string
    ExplainMetric:
      type: object
      required: [rank, name, value, source]
      properties:
        rank:
          type: integer
        name:
          type: string
        value:
          nullable: true
        source:
          type: string
        captured_at:
          type: string
          format: date-time
          nullable: true
    ExplainTimestamps:
      type: object
      properties:
        source_timestamp:
          type: string
          format: date-time
          nullable: true
        event_created_at:
          type: string
          format: date-time
          nullable: true
        run_started_at:
          type: string
          format: date-time
          nullable: true
        reset_at:
          type: string
          format: date-time
          nullable: true
        lag_seconds:
          type: integer
          nullable: true
    ExplainSuppressionContext:
      type: object
      required: [suppressed, reason_codes]
      properties:
        suppressed:
          type: boolean
        reason_codes:
          type: array
          items:
            type: string
        suppressed_count:
          type: integer
          nullable: true
        daily_limit:
          type: integer
          nullable: true
        sent_today:
          type: integer
          nullable: true
        reset_at:
          type: string
          format: date-time
          nullable: true
    ExplainResponseBody:
      type: object
      required: [gates_passed, gates_failed, cel_evaluation, top_contributing_metrics, timestamps]
      properties:
        gates_passed:
          type: array
          items:
            $ref: '#/components/schemas/ExplainGateResult'
        gates_failed:
          type: array
          items:
            $ref: '#/components/schemas/ExplainGateResult'
        cel_evaluation:
          $ref: '#/components/schemas/ExplainCELEvaluation'
        top_contributing_metrics:
          type: array
          items:
            $ref: '#/components/schemas/ExplainMetric'
        timestamps:
          $ref: '#/components/schemas/ExplainTimestamps'
        suppression:
          $ref: '#/components/schemas/ExplainSuppressionContext'
    AlertEventExplainResponse:
      type: object
      required: [payload_version, alert_event_id, scan_id, event_type, explanation]
      properties:
        payload_version:
          type: string
        alert_event_id:
          type: string
        scan_id:
          type: string
        run_id:
          type: string
          nullable: true
        event_type:
          type: string
        symbol:
          type: string
          nullable: true
        timeframe:
          type: string
        explanation:
          $ref: '#/components/schemas/ExplainResponseBody'
      example:
        payload_version: "1.0"
        alert_event_id: "11111111-1111-1111-1111-111111111111"
        scan_id: "crypto_oversold"
        run_id: "22222222-2222-2222-2222-222222222222"
        event_type: "throttle_summary"
        timeframe: "15m"
        explanation:
          gates_passed:
            - gate: "min_volume_quote"
              reason_code: "volume_gate_satisfied"
              summary: "configured minimum volume gate passed"
              configured_value: 1000000
              observed_value: 1500000
          gates_failed: []
          cel_evaluation:
            expression: "rsi_14 < 30"
            result: "suppressed"
            reason_code: "daily_limit_reached"
            summary: "stored run evidence matched, but delivery was suppressed by the daily alert limit"
          top_contributing_metrics:
            - rank: 1
              name: "suppressed_count"
              value: 5
              source: "payload"
              captured_at: "2026-03-10T14:04:30Z"
            - rank: 2
              name: "daily_limit"
              value: 20
              source: "payload"
              captured_at: "2026-03-10T14:04:30Z"
          timestamps:
            source_timestamp: "2026-03-10T14:04:30Z"
            event_created_at: "2026-03-10T14:05:00Z"
            reset_at: "2026-03-11T00:00:00Z"
            lag_seconds: 30
          suppression:
            suppressed: true
            reason_codes: ["daily_limit"]
            suppressed_count: 5
            daily_limit: 20
            sent_today: 20
            reset_at: "2026-03-11T00:00:00Z"
    ScanRunExplainNoMatchResponse:
      type: object
      required: [payload_version, run_id, scan_id, scan_name, symbols_scanned, symbols_matched, started_at, explanation]
      properties:
        payload_version:
          type: string
        run_id:
          type: string
        scan_id:
          type: string
        scan_name:
          type: string
        timeframe:
          type: string
        symbols_scanned:
          type: integer
        symbols_matched:
          type: integer
        started_at:
          type: string
          format: date-time
        explanation:
          $ref: '#/components/schemas/ExplainResponseBody'
      example:
        payload_version: "1.0"
        run_id: "33333333-3333-3333-3333-333333333333"
        scan_id: "scan-user-1"
        scan_name: "Low RSI Scan"
        timeframe: "15m"
        symbols_scanned: 24
        symbols_matched: 0
        started_at: "2026-03-10T12:00:00Z"
        explanation:
          gates_passed:
            - gate: "require_rsi_14"
              reason_code: "rsi_14_ready"
              summary: "required RSI-14 data recorded no readiness failures"
              configured_value: true
              observed_value: true
          gates_failed:
            - gate: "cel_expression"
              reason_code: "cel_expression_not_matched"
              summary: "symbols passed hard gates but evaluated false for the CEL expression"
              count: 12
            - gate: "min_volume_quote"
              reason_code: "volume_gate_rejected"
              summary: "symbols were rejected by the configured volume gate"
              configured_value: 1000000
              count: 4
          cel_evaluation:
            expression: "rsi_14 < 30"
            result: "no_match"
            reason_code: "cel_expression_not_matched"
            summary: "Most symbols cleared liquidity but failed the CEL expression."
          top_contributing_metrics:
            - rank: 1
              name: "cel_expression"
              value: 12
              source: "rejection_stats"
              captured_at: "2026-03-10T12:00:00Z"
            - rank: 2
              name: "min_volume_quote"
              value: 4
              source: "rejection_stats"
              captured_at: "2026-03-10T12:00:00Z"
          timestamps:
            run_started_at: "2026-03-10T12:00:00Z"
paths:
  /api/auth/api-keys:
    get:
      summary: List API keys for the authenticated user
      security:
        - bearerAuth: []
    post:
      summary: Create a new API key
      security:
        - bearerAuth: []
  /api/auth/api-keys/{keyId}:
    delete:
      summary: Revoke an API key
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: keyId
          required: true
          schema:
            type: string
  /api/auth/api-keys/{keyId}/rotate:
    post:
      summary: Rotate an API key and return a replacement secret once
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: keyId
          required: true
          schema:
            type: string
  /api/crypto/scans:
    get:
      summary: List scans
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
      parameters:
        - in: query
          name: include_public
          schema:
            type: boolean
            default: true
          description: Include public preset scans alongside the caller's own scans.
        - in: query
          name: include_notification_status
          schema:
            type: boolean
            default: false
          description: Include per-scan notification delivery status for owned scans.
      responses:
        '200':
          description: Scan list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListScansResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      summary: Create a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateScanRequest'
      responses:
        '201':
          description: Scan created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResponse'
        '400':
          description: Invalid scan payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Authenticated user cannot create the scan under the current plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/crypto/scans/{scanId}:
    get:
      summary: Get a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
      parameters:
        - in: path
          name: scanId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Scan detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Authenticated user does not own the private scan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Scan not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      summary: Update a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
      parameters:
        - in: path
          name: scanId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateScanRequest'
      responses:
        '200':
          description: Scan updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResponse'
        '400':
          description: Invalid scan payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Authenticated user cannot modify the scan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Scan not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Delete a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
      parameters:
        - in: path
          name: scanId
          required: true
          schema:
            type: string
  /api/crypto/scans/execute:
    post:
      summary: Execute a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExecuteScanRequest'
      responses:
        '200':
          description: Scan executed successfully and returned the immediate run payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExecuteScanResponse'
        '400':
          description: Invalid scan request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Authenticated user does not own the scan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/crypto/scans/execute-ad-hoc:
    post:
      summary: Execute an ad hoc scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExecuteAdHocScanRequest'
      responses:
        '200':
          description: Ad hoc scan executed successfully and returned the immediate run payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExecuteScanResponse'
        '400':
          description: Invalid ad hoc scan request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/crypto/scans/validate:
    post:
      summary: Validate a scan expression
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
  /api/crypto/scans/variables:
    get:
      summary: List scan builder variables
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
  /api/crypto/scans/runs:
    get:
      summary: List scan runs
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/crypto/scans/runs/{runId}:
    get:
      summary: Get a scan run
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/crypto/scans/runs/{runId}/explain-no-match:
    get:
      summary: Explain why a scan run produced no usable trigger
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: path
          name: runId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Structured no-match explanation for agents
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanRunExplainNoMatchResponse'
        '400':
          description: Invalid run identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Authenticated user does not own the scan run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Scan run not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/crypto/scans/{scanId}/schedule:
    get:
      summary: Get a schedule
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
    post:
      summary: Create a schedule
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
    put:
      summary: Update a schedule
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
    delete:
      summary: Delete a schedule
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
  /api/crypto/scans/{scanId}/share:
    post:
      summary: Create a share link for a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
    delete:
      summary: Revoke a share link for a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
  /api/crypto/scans/{scanId}/alerts:
    get:
      summary: Get alert config
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
    post:
      summary: Create alert config
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
    put:
      summary: Update alert config
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
    delete:
      summary: Delete alert config
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
  /api/crypto/scans/{scanId}/alerts/test:
    post:
      summary: Send a test alert
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_scans
  /api/crypto/scans/{scanId}/alerts/limits:
    get:
      summary: Get alert limit status
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/crypto/scans/{scanId}/alerts/rejections:
    get:
      summary: Get alert rejections for a scan
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/crypto/market-data:
    get:
      summary: Get current Tickory market indicator snapshots
      description: >
        Returns the latest cached Tickory snapshot for tracked symbols only. This is a
        narrow product endpoint for current indicator context, not a general exchange
        market-data API. It does not expose historical ranges, raw trades, order books,
        or arbitrary exchange coverage.
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: query
          name: symbols
          required: true
          schema:
            type: string
          description: Comma-separated tracked symbols, for example `BTCUSDT,ETHUSDT`.
        - in: query
          name: exchange
          schema:
            type: string
            default: binance
          description: Optional exchange filter. Only `binance` is supported today.
        - in: query
          name: contract_type
          schema:
            type: string
            default: perp
            enum: [perp, spot]
          description: Optional contract type filter for the cached snapshot lookup.
        - in: query
          name: timeframe
          schema:
            type: string
            default: 1m
            enum: [1m, 5m, 15m, 1h]
          description: Optional cached timeframe to read.
      responses:
        '200':
          description: Current market data snapshots
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketDataListResponse'
        '400':
          description: Invalid symbol or filter request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No current snapshot available for the requested tracked symbols
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/crypto/alert-events:
    get:
      summary: List alert events
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: query
          name: since
          schema:
            type: string
            format: date-time
          description: Return events created at or after this RFC3339 timestamp.
        - in: query
          name: scan_id
          schema:
            type: string
          description: Filter events to a specific scan.
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - in: query
          name: cursor
          schema:
            type: string
          description: Opaque pagination cursor returned by the previous page.
      responses:
        '200':
          description: Alert event page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAlertEventsResponse'
  /api/crypto/alert-events/{eventId}:
    get:
      summary: Get a single alert event
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: path
          name: eventId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Alert event detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetAlertEventResponse'
  /api/crypto/alert-events/{eventId}/receipt:
    get:
      summary: Get a machine-verifiable execution receipt for an alert event
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: path
          name: eventId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Canonical alert-event receipt with delivery proof and verification metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetAlertEventReceiptResponse'
        '400':
          description: Invalid event identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Authenticated user cannot access this alert event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Alert event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/crypto/alert-events/{eventId}/explain:
    get:
      summary: Explain why an alert event triggered or was suppressed
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: path
          name: eventId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Structured alert-event explanation for agents
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AlertEventExplainResponse'
        '400':
          description: Invalid event identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Authenticated user cannot access this alert event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Alert event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/crypto/alerts/history:
    get:
      summary: List alert delivery history
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/crypto/alerts/rejections:
    get:
      summary: List alert rejections across scans
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/activity:
    get:
      summary: List activity feed entries
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/alert-sources:
    get:
      summary: List alert sources
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
    post:
      summary: Create an alert source
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
  /api/alert-sources/{sourceId}:
    get:
      summary: Get an alert source
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
    patch:
      summary: Update an alert source
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
    delete:
      summary: Delete an alert source
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
  /api/alert-sources/{sourceId}/routes:
    get:
      summary: List routes for an alert source
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
  /api/alert-sources/{sourceId}/activity:
    get:
      summary: List source activity
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
  /api/alert-sources/{sourceId}/events:
    get:
      summary: List source event lifecycle traces
      description: Additive external alert lifecycle feed introduced by migration 053. Historical route activity is not backfilled, and persisted lifecycle traces survive later source or route deletion.
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: path
          name: sourceId
          required: true
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Recent external alert events for the source with route, enqueue, delivery, and receipt linkage
        '401':
          description: Missing or invalid credentials
        '403':
          description: Authenticated user cannot access this source
        '404':
          description: Source not found
  /api/alert-sources/{sourceId}/events/{sourceEventId}:
    get:
      summary: Get a source event lifecycle trace
      description: Returns the stored inbound payload plus downstream route, delivery, and receipt linkage for one external source event, including traces for deleted routes or deleted sources.
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - read_events
      parameters:
        - in: path
          name: sourceId
          required: true
          schema:
            type: string
        - in: path
          name: sourceEventId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Full lifecycle trace for one external source event
        '400':
          description: Invalid source event identifier
        '401':
          description: Missing or invalid credentials
        '403':
          description: Authenticated user cannot access this source event
        '404':
          description: Source or source event not found
  /api/alert-sources/{sourceId}/events/{sourceEventId}/replay:
    post:
      summary: Replay one failed route from a source event trace
      description: Requeues the downstream alert event captured for one route trace when its current status is `failed` or `dead_letter`. Replay is only allowed while the underlying route still exists on the source, and it preserves accumulated delivery attempts instead of refunding retry counts. Direct routes reuse the snapshotted delivery settings stored on the alert event payload, while legacy scan-backed routes still require the referenced alert config to exist and remain deliverable.
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
      parameters:
        - in: path
          name: sourceId
          required: true
          schema:
            type: string
        - in: path
          name: sourceEventId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Replay accepted and queued for retry
        '400':
          description: Invalid request body or malformed source event identifier
        '401':
          description: Missing or invalid credentials
        '403':
          description: Authenticated user cannot access this source event
        '404':
          description: Source event or route trace not found
        '409':
          description: Route is not replayable because it has no downstream alert event or is not currently failed
  /api/alert-sources/{sourceId}/test:
    post:
      summary: Send a test event through a source
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
  /api/alert-sources/{sourceId}/test-payload:
    post:
      summary: Preview routing with a test payload
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
  /api/alert-routes:
    post:
      summary: Create an alert route
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
  /api/alert-routes/{routeId}:
    patch:
      summary: Update an alert route
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
    delete:
      summary: Delete an alert route
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      x-tickory-required-scopes:
        - manage_routing
