openapi: '3.0.3'
info:
  title: PriceDepth API
  description: >
    Real-time and historical pricing data for 64,000+ alternative assets
    across collectible cards, watches, sneakers, and LEGO.
    Provides fair-market-value estimates, risk analytics, cross-platform
    arbitrage detection, portfolio management, and signed index feeds.
    Register for a free API key at /api/developer/register.
  version: '1.1.0'
  contact:
    url: https://pricedepth.com/docs.html

servers:
  - url: https://pricedepth.com
    description: Production

tags:
  - name: Cards
    description: Card catalog and detail lookups
  - name: Pricing
    description: Current, historical, and live price data
  - name: Analytics
    description: Risk analytics and market indices
  - name: Alerts
    description: Price alert management (v1 API-key auth)
  - name: Portfolio
    description: Batch portfolio valuation
  - name: Premium
    description: Tier-gated endpoints (Collector+, Dealer+, Enterprise)
  - name: Public
    description: Unauthenticated endpoints for search, trends, embeds, and charts
  - name: Portal
    description: Customer self-service portal (JWT auth)
  - name: Webhooks
    description: Stripe webhook receiver
  - name: Export
    description: Bulk data export endpoints (Enterprise tier)
  - name: Checkout
    description: Stripe checkout session creation
  - name: Health
    description: Health and documentation endpoints
  - name: Indexes
    description: Signed price indexes and benchmark data. Each response includes a cryptographic signature for tamper-evident verification.
  - name: Developer
    description: Self-serve API key registration, email verification, and usage dashboard. No API key required for registration.
  - name: Funds
    description: Fund NAV computation for institutional clients. Provides auditable, deterministic pricing with SHA-256 audit hashes.
  - name: Resolver
    description: Prediction market resolver API. Register price condition contracts with HMAC-signed callbacks on resolution or expiry.

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: API key issued per user. Prefix varies by tier (e.g. tp_collector_...).
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Supabase JWT token for portal endpoints.

  schemas:
    # ── Core domain objects ──────────────────────────────────────────────────

    Card:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        name:
          type: string
          example: 'Charizard'
        set_name:
          type: string
          example: 'Base Set'
        card_number:
          type: string
          example: '4/102'
        grade:
          type: string
          nullable: true
          example: '10'
        image_url:
          type: string
          nullable: true
          example: 'https://images.trueprice.cards/psa10-charizard-base-4.webp'
        product_type:
          type: string
          example: 'single'
        category:
          type: string
          nullable: true
          example: 'pokemon-tcg'
        sport:
          type: string
          nullable: true
          description: 'Sport/vertical identifier'
          example: 'pokemon-tcg'
          enum: [pokemon-tcg, baseball, basketball, football]

    CardFull:
      allOf:
        - $ref: '#/components/schemas/Card'
        - type: object
          properties:
            slug:
              type: string
              nullable: true
              example: 'charizard-base-set-psa-10'
            seo_title:
              type: string
              nullable: true
            seo_description:
              type: string
              nullable: true
            tcgplayer_url:
              type: string
              nullable: true
            distribution:
              type: string
              nullable: true
            created_at:
              type: string
              format: date-time

    PricePoint:
      type: object
      description: >
        Comprehensive pricing snapshot for a single card. Includes current FMV,
        historical extremes, trailing averages, momentum indicators, risk analytics,
        and sales-based medians — all in one call.
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        price_usd:
          type: number
          format: float
          example: 42000.00
        price_date:
          type: string
          format: date
          example: '2026-03-29'
        all_time_high_usd:
          type: number
          format: float
          nullable: true
          example: 52000.00
          description: Highest daily FMV ever recorded
        all_time_high_date:
          type: string
          format: date
          nullable: true
          example: '2021-02-15'
        all_time_low_usd:
          type: number
          format: float
          nullable: true
          example: 8500.00
          description: Lowest daily FMV ever recorded
        all_time_low_date:
          type: string
          format: date
          nullable: true
          example: '2019-06-03'
        high_52w_usd:
          type: number
          format: float
          nullable: true
          example: 48000.00
          description: 52-week high price
        high_52w_date:
          type: string
          format: date
          nullable: true
          example: '2025-11-20'
        low_52w_usd:
          type: number
          format: float
          nullable: true
          example: 31000.00
          description: 52-week low price
        low_52w_date:
          type: string
          format: date
          nullable: true
          example: '2026-01-15'
        change_pct_24h:
          type: number
          format: float
          nullable: true
          example: -1.2
          description: Price change vs 24 hours ago (%)
        change_pct_7d:
          type: number
          format: float
          nullable: true
          example: 3.5
          description: Price change vs 7 days ago (%)
        change_pct_30d:
          type: number
          format: float
          nullable: true
          example: -8.1
          description: Price change vs 30 days ago (%)
        change_pct_90d:
          type: number
          format: float
          nullable: true
          example: 12.4
          description: Price change vs 90 days ago (%)
        change_pct_ytd:
          type: number
          format: float
          nullable: true
          example: 15.7
          description: Year-to-date price change (%)
        twap_7d:
          type: number
          format: float
          nullable: true
          example: 41850.00
          description: 7-day time-weighted average price
        twap_30d:
          type: number
          format: float
          nullable: true
          example: 43020.00
          description: 30-day time-weighted average price
        vwap_7d:
          type: number
          format: float
          nullable: true
          example: 42010.00
          description: 7-day volume-weighted average price
        vwap_30d:
          type: number
          format: float
          nullable: true
          example: 42875.00
          description: 30-day volume-weighted average price
        sma_7d:
          type: number
          format: float
          nullable: true
          example: 41900.00
          description: 7-day simple moving average
        sma_30d:
          type: number
          format: float
          nullable: true
          example: 42530.00
          description: 30-day simple moving average
        sma_90d:
          type: number
          format: float
          nullable: true
          example: 41050.00
          description: 90-day simple moving average
        roc_7d:
          type: number
          format: float
          nullable: true
          example: 3.5
          description: 7-day rate of change (%)
        roc_30d:
          type: number
          format: float
          nullable: true
          example: -8.1
          description: 30-day rate of change (%)
        roc_90d:
          type: number
          format: float
          nullable: true
          example: 12.4
          description: 90-day rate of change (%)
        volatility_annual:
          type: number
          format: float
          nullable: true
          example: 34.21
          description: Annualized volatility (%)
        sharpe_ratio:
          type: number
          format: float
          nullable: true
          example: 1.42
          description: Sharpe ratio
        max_drawdown_pct:
          type: number
          format: float
          nullable: true
          example: 12.5
          description: Maximum drawdown from peak (%)
        return_90d_pct:
          type: number
          format: float
          nullable: true
          example: 8.73
          description: 90-day return (%)
        data_points:
          type: integer
          example: 87
          description: Number of daily price data points used
        median_sale_7d:
          type: number
          format: float
          nullable: true
          example: 43000.00
          description: Median sale price over last 7 days
        median_sale_30d:
          type: number
          format: float
          nullable: true
          example: 41500.00
          description: Median sale price over last 30 days
        sales_count_7d:
          type: integer
          example: 8
          description: Number of sales in last 7 days
        sales_count_30d:
          type: integer
          example: 23
          description: Number of sales in last 30 days

    PriceHistory:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        days:
          type: integer
          example: 90
        prices:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
                example: '2026-01-01'
              price:
                type: number
                format: float
                example: 39500.00

    AnalyticsResult:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        data_points:
          type: integer
          example: 87
        volatility_annual:
          type: number
          format: float
          example: 34.21
          description: Annualized volatility (%)
        sharpe_ratio:
          type: number
          format: float
          nullable: true
          example: 1.42
          description: Sharpe ratio
        max_drawdown_pct:
          type: number
          format: float
          example: 12.5
          description: Maximum drawdown (%)
        return_90d_pct:
          type: number
          format: float
          nullable: true
          example: 8.73
          description: 90-day return (%)
        avg_daily_return:
          type: number
          format: float
          nullable: true
          example: 0.035
        sample_days:
          type: integer
          example: 87

    IndexListItem:
      type: object
      description: Summary of an index as returned by GET /v1/indexes.
      properties:
        slug:
          type: string
          example: 'pdi-pokemon'
        name:
          type: string
          example: 'PDI PSA-10 Index'
        current_value:
          type: number
          format: float
          nullable: true
          example: 1042.37
        change_24h_pct:
          type: number
          format: float
          nullable: true
          example: 0.58
          description: Percentage change vs previous daily close.
        methodology_version:
          type: string
          nullable: true
          example: 'v1.0.0'
        as_of_timestamp:
          type: string
          format: date-time
          nullable: true
          example: '2026-04-11T00:00:00Z'

    Index:
      type: object
      description: Full index detail including 30-day inline history and ECDSA signature.
      properties:
        slug:
          type: string
          example: 'pdi-pokemon'
        name:
          type: string
          example: 'PDI PSA-10 Index'
        description:
          type: string
          nullable: true
          example: 'Benchmark index of liquid PSA 10-graded Pokemon cards, rebalanced monthly.'
        methodology_version:
          type: string
          nullable: true
          example: 'v1.0.0'
        as_of_timestamp:
          type: string
          format: date-time
          nullable: true
          example: '2026-04-11T00:00:00Z'
        value:
          type: number
          format: float
          nullable: true
          example: 1042.37
        constituent_count:
          type: integer
          nullable: true
          example: 100
        history_30d:
          type: array
          description: Daily closes for the past 30 days (precomputed, ascending order).
          items:
            type: object
            properties:
              as_of_date:
                type: string
                format: date
                example: '2026-03-12'
              value:
                type: number
                format: float
                example: 1010.00
        signature:
          type: string
          format: byte
          nullable: true
          description: >
            Base64-encoded DER ECDSA secp256k1 signature. Signed over the canonical JSON
            of exactly 4 fields in key-sorted order: as_of_timestamp, methodology_version,
            slug, value — whitespace-stripped. Public key at
            https://pricedepth.com/.well-known/pricedepth-oracle.pub.
          example: 'MEUCIQD3...base64=='

    IndexHistoryPoint:
      type: object
      properties:
        as_of_date:
          type: string
          format: date
          example: '2026-04-10'
        value:
          type: number
          format: float
          example: 1038.91
        constituent_count:
          type: integer
          nullable: true
          example: 100

    IndexConstituent:
      type: object
      properties:
        asset_id:
          type: string
          example: 'psa10-charizard-base-4'
        weight:
          type: number
          format: float
          description: Fractional weight (sum of all weights = 1.0).
          example: 0.01
        meta:
          type: object
          nullable: true
          description: Additional metadata (price, name, etc.) captured at rebalance time.

    AlertObject:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        card_name:
          type: string
          example: 'Charizard'
        alert_type:
          type: string
          enum: [above, below, percent_change]
          example: 'below'
        threshold_value:
          type: number
          format: float
          example: 35000.00
        email:
          type: string
          nullable: true
          example: 'collector@example.com'
        is_active:
          type: boolean
          example: true
        last_triggered_at:
          type: string
          format: date-time
          nullable: true
        trigger_count:
          type: integer
          example: 2
        created_at:
          type: string
          format: date-time

    AlertHistoryEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        alert_id:
          type: string
          format: uuid
        triggered_at:
          type: string
          format: date-time
        price_at_trigger:
          type: number
          format: float
          example: 34800.00
        threshold_value:
          type: number
          format: float
          example: 35000.00
        notification_type:
          type: string
          example: 'email'
        notification_status:
          type: string
          example: 'sent'

    ArbitrageOpportunity:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        name:
          type: string
          example: 'Charizard'
        set_name:
          type: string
          nullable: true
          example: 'Base Set'
        grade:
          type: string
          nullable: true
          example: '10'
        image_url:
          type: string
          nullable: true
        buy:
          type: object
          properties:
            source:
              type: string
              example: 'tcgplayer'
            avg_price:
              type: number
              format: float
              example: 38500.00
            sale_count:
              type: integer
              example: 4
        sell:
          type: object
          properties:
            source:
              type: string
              example: 'ebay'
            avg_price:
              type: number
              format: float
              example: 44200.00
            sale_count:
              type: integer
              example: 7
            fee_pct:
              type: number
              format: float
              example: 13.25
        net_profit_usd:
          type: number
          format: float
          example: 345.50
        spread_pct:
          type: number
          format: float
          example: 8.97

    GradePriceEntry:
      type: object
      properties:
        card_id:
          type: string
        grade:
          type: string
          nullable: true
          example: '9'
        price_usd:
          type: number
          format: float
          nullable: true
          example: 12500.00
        price_date:
          type: string
          format: date
          nullable: true
        psa_pop:
          type: integer
          nullable: true
          example: 1247

    IndexSnapshot:
      type: object
      properties:
        category:
          type: string
          example: 'pokemon-tcg'
        total_market_cap:
          type: number
          format: float
          nullable: true
          example: 28450000.00
        avg_price:
          type: number
          format: float
          nullable: true
          example: 478.32
        median_price:
          type: number
          format: float
          nullable: true
          example: 125.00
        card_count:
          type: integer
          example: 59500
        return_30d:
          type: number
          format: float
          nullable: true
          example: 2.4
        return_90d:
          type: number
          format: float
          nullable: true
          example: 8.7
        snapshot_date:
          type: string
          format: date
          example: '2026-03-29'

    Mover:
      type: object
      properties:
        card_id:
          type: string
        name:
          type: string
        set_name:
          type: string
          nullable: true
        grade:
          type: string
          nullable: true
        image_url:
          type: string
          nullable: true
        current_price:
          type: number
          format: float
          example: 850.00
        previous_price:
          type: number
          format: float
          example: 720.00
        return_7d:
          type: number
          format: float
          example: 18.06
        abs_change:
          type: number
          format: float
          example: 130.00

    PortfolioCard:
      type: object
      properties:
        id:
          type: string
          format: uuid
        card_id:
          type: string
        name:
          type: string
        set_name:
          type: string
          nullable: true
        card_number:
          type: string
          nullable: true
        grade:
          type: string
          nullable: true
        image_url:
          type: string
          nullable: true
        quantity:
          type: integer
          example: 2
        purchase_price:
          type: number
          format: float
          nullable: true
          example: 350.00
        purchase_date:
          type: string
          format: date
          nullable: true
        current_price:
          type: number
          format: float
          nullable: true
          example: 420.00
        position_value:
          type: number
          format: float
          nullable: true
          example: 840.00
        cost_basis:
          type: number
          format: float
          nullable: true
          example: 700.00
        gain_loss:
          type: number
          format: float
          nullable: true
          example: 140.00
        gain_loss_percent:
          type: number
          format: float
          nullable: true
          example: 20.00
        notes:
          type: string
          nullable: true

    UserProfile:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          example: 'collector@example.com'
        name:
          type: string
          example: 'CardCollector42'
        tier:
          type: string
          enum: [free, collector, dealer, api, enterprise]
          example: 'collector'
        stripe_customer_id:
          type: boolean
          description: Whether user has a Stripe customer ID (boolean only, no leak)
          example: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ApiKeyInfo:
      type: object
      nullable: true
      properties:
        masked_key:
          type: string
          example: '••••••••••••••••••••••••••••ab12'
        tier:
          type: string
          example: 'collector'
        rate_limit_per_min:
          type: integer
          example: 30
        monthly_quota:
          type: integer
          nullable: true
          example: 10000
        created_at:
          type: string
          format: date-time

    ReportSubscription:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
        is_active:
          type: boolean
        subscribed_at:
          type: string
          format: date-time
        unsubscribed_at:
          type: string
          format: date-time
          nullable: true

    # ── Wrapper / envelope schemas ───────────────────────────────────────────

    PaginatedResponse:
      type: object
      properties:
        data:
          type: array
          items: {}
          description: The page of results (cards, alerts, etc.)
        total:
          type: integer
          example: 59928
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 50
        pages:
          type: integer
          example: 1199

    # ── Wave 2-3 schemas ────────────────────────────────────────────────

    BatchPriceRequest:
      type: object
      required: [card_ids]
      properties:
        card_ids:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 500
          description: Array of card IDs to look up (max 500)
          example:
            - 'psa10-charizard-base-4'
            - 'psa9-pikachu-jungle-60'
            - 'psa10-lugia-neo-genesis-9'

    BatchPriceResponse:
      type: object
      properties:
        prices:
          type: object
          additionalProperties:
            type: object
            properties:
              price_usd:
                type: number
                format: float
                example: 42000.00
              price_date:
                type: string
                format: date
                example: '2026-03-29'
          description: Map of card_id to latest price data
          example:
            psa10-charizard-base-4:
              price_usd: 42000.00
              price_date: '2026-03-29'
            psa9-pikachu-jungle-60:
              price_usd: 285.00
              price_date: '2026-03-28'
        found:
          type: integer
          description: Number of card IDs with price data
          example: 2
        missing:
          type: array
          items:
            type: string
          description: Card IDs that had no price data
          example:
            - 'psa10-lugia-neo-genesis-9'

    CompareCardEntry:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        name:
          type: string
          nullable: true
          example: 'Charizard'
        set_name:
          type: string
          nullable: true
          example: 'Base Set'
        grade:
          type: string
          nullable: true
          example: '10'
        image_url:
          type: string
          nullable: true
        current_price:
          type: number
          format: float
          nullable: true
          example: 42000.00
        price_date:
          type: string
          format: date
          nullable: true
          example: '2026-03-29'
        analytics:
          type: object
          properties:
            volatility_annual:
              type: number
              format: float
              nullable: true
              example: 34.21
              description: Annualized volatility (%)
            sharpe_ratio:
              type: number
              format: float
              nullable: true
              example: 1.42
            max_drawdown_pct:
              type: number
              format: float
              nullable: true
              example: 12.5
            return_90d_pct:
              type: number
              format: float
              nullable: true
              example: 8.73
        liquidity:
          type: object
          properties:
            sales_30d:
              type: integer
              example: 14
              description: Number of sales in the last 30 days
            avg_daily_volume:
              type: number
              format: float
              example: 0.5
              description: Average daily sales volume (sales_30d / 30)

    CompareResponse:
      type: object
      properties:
        cards:
          type: array
          items:
            $ref: '#/components/schemas/CompareCardEntry'
        compared_at:
          type: string
          format: date-time
          example: '2026-03-30T12:00:00.000Z'

    PopulationEntry:
      type: object
      properties:
        report_date:
          type: string
          format: date
          example: '2026-03-15'
        grader:
          type: string
          example: 'PSA'
        grade:
          type: string
          example: '10'
        pop_count:
          type: integer
          example: 121
          description: Number of copies at this grade
        total_graded:
          type: integer
          example: 3456
          description: Total copies graded by this grader

    ExportPriceRow:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        price_date:
          type: string
          format: date
          example: '2026-03-29'
        price_usd:
          type: number
          format: float
          nullable: true
          example: 42000.00
        source:
          type: string
          nullable: true
          example: 'ebay'
        sale_count:
          type: integer
          nullable: true
          example: 8

    ExportSaleRow:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        sale_date:
          type: string
          format: date
          example: '2026-03-28'
        price_usd:
          type: number
          format: float
          nullable: true
          example: 41800.00
        source:
          type: string
          nullable: true
          example: 'tcgplayer'
        listing_url:
          type: string
          nullable: true
          example: 'https://tcgplayer.com/product/12345'
        grade:
          type: string
          nullable: true
          example: '10'
        raw_title:
          type: string
          nullable: true
          example: 'PSA 10 Charizard Base Set 4/102 GEM MINT'

    ExportCardRow:
      type: object
      properties:
        card_id:
          type: string
          example: 'psa10-charizard-base-4'
        name:
          type: string
          example: 'Charizard'
        set_name:
          type: string
          example: 'Base Set'
        card_number:
          type: string
          example: '4/102'
        grade:
          type: string
          nullable: true
          example: '10'
        product_type:
          type: string
          example: 'single'
        image_url:
          type: string
          nullable: true
          example: 'https://images.trueprice.cards/psa10-charizard-base-4.webp'

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: 'Card not found'
        code:
          type: string
          example: 'CARD_NOT_FOUND'

# ═══════════════════════════════════════════════════════════════════════════
# PATHS
# ═══════════════════════════════════════════════════════════════════════════

paths:

  # ── Health & Docs ──────────────────────────────────────────────────────

  /api/health:
    get:
      summary: Health check
      tags: [Health]
      security: []
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: 'ok'
                  service:
                    type: string
                    example: 'trueprice-api'
                  version:
                    type: string
                    example: '1.0.0'

  /api/openapi.yaml:
    get:
      summary: OpenAPI 3.0 spec (YAML)
      tags: [Health]
      security: []
      responses:
        '200':
          description: YAML specification file
          content:
            text/yaml:
              schema:
                type: string

  # ── v1 Cards ───────────────────────────────────────────────────────────

  /v1/cards:
    get:
      summary: List cards
      description: >
        Paginated card catalog. Returns card metadata without prices. Supports
        filtering by set name, grade, category, product type, and free-text
        name search.
      tags: [Cards]
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 100 }
        - name: set_name
          in: query
          description: Exact-match filter on card set name
          schema: { type: string }
          example: 'Base Set'
        - name: grade
          in: query
          description: Exact-match filter on grade (e.g. "10", "9", "BGS 9.5")
          schema: { type: string }
          example: '10'
        - name: q
          in: query
          description: Free-text search across card name (case-insensitive ILIKE)
          schema: { type: string }
          example: 'charizard'
        - name: category
          in: query
          description: Exact-match filter on category
          schema: { type: string }
          example: 'pokemon-tcg'
        - name: product_type
          in: query
          description: Exact-match filter on product type (e.g. "single", "sealed")
          schema: { type: string }
          example: 'single'
        - name: sport
          in: query
          description: Filter by sport/vertical (e.g. "pokemon-tcg", "baseball", "basketball", "football")
          schema: { type: string, enum: [pokemon-tcg, baseball, basketball, football] }
          example: 'baseball'
        - name: language
          in: query
          description: Filter by card language (e.g. "EN", "JP")
          schema: { type: string }
          example: 'EN'
      responses:
        '200':
          description: Paginated card catalog
          content:
            application/json:
              schema:
                type: object
                properties:
                  cards:
                    type: array
                    items:
                      $ref: '#/components/schemas/Card'
                  total:
                    type: integer
                    example: 59928
                  page:
                    type: integer
                    example: 1
                  limit:
                    type: integer
                    example: 50
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/cards/autocomplete:
    get:
      summary: Autocomplete card search
      description: Returns up to 8 card suggestions matching a prefix query.
      tags: [Cards]
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2, maxLength: 50 }
          description: Search prefix (min 2 characters)
      responses:
        '200':
          description: Autocomplete suggestions
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestions:
                    type: array
                    items:
                      type: object
                      properties:
                        card_id: { type: string }
                        name: { type: string }
                        set_name: { type: string }
                        category: { type: string }
                        grade: { type: string }
                        image_url: { type: string }

  /v1/cards/{id}:
    get:
      summary: Get card detail
      description: Full card record including all metadata fields.
      tags: [Cards]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
      responses:
        '200':
          description: Card details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardFull'
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Pricing ─────────────────────────────────────────────────────────

  /v1/cards/{id}/price:
    get:
      summary: Full pricing snapshot
      description: >
        Comprehensive pricing endpoint. Returns current FMV, all-time and 52-week
        highs/lows, price change deltas (24h/7d/30d/90d/YTD), TWAP, VWAP, simple
        moving averages (7d/30d/90d), rate of change, risk analytics (volatility,
        Sharpe, max drawdown), and sales-based medians. Replaces the need to call
        /analytics separately. Optionally pass `date` for historical lookup.
      tags: [Pricing]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
        - name: date
          in: query
          description: Return price on or before this date
          schema: { type: string, format: date }
          example: '2026-01-15'
      responses:
        '200':
          description: Price data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PricePoint'
        '404':
          description: No price data available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/cards/{id}/history:
    get:
      summary: Price history time series
      description: Daily price series for a card over a configurable window (1-365 days).
      tags: [Pricing]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
        - name: days
          in: query
          schema: { type: integer, default: 90, maximum: 365 }
      responses:
        '200':
          description: Daily price series
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceHistory'

  /v1/cards/{id}/live:
    get:
      summary: Live price and WebSocket channel info
      description: >
        Returns the most recent price, freshness indicator, and the WebSocket
        channel name to subscribe for real-time updates.
      tags: [Pricing]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
      responses:
        '200':
          description: Live price with WS channel info
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id:
                    type: string
                    example: 'psa10-charizard-base-4'
                  price_usd:
                    type: number
                    format: float
                    nullable: true
                    example: 42000.00
                  price_date:
                    type: string
                    format: date
                    nullable: true
                    example: '2026-03-29'
                  ws_channel:
                    type: string
                    example: 'card:psa10-charizard-base-4'
                  freshness:
                    type: string
                    enum: [daily, unavailable]
                    example: 'daily'

  # ── v1 Batch Pricing ──────────────────────────────────────────────────

  /v1/prices/batch:
    post:
      summary: Batch price lookup
      description: >
        Look up the latest price for up to 500 cards in a single request.
        Returns a map of card_id to price data, plus a list of any card_ids
        that had no price data.
      tags: [Pricing]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchPriceRequest'
            example:
              card_ids:
                - 'psa10-charizard-base-4'
                - 'psa9-pikachu-jungle-60'
                - 'psa10-lugia-neo-genesis-9'
      responses:
        '200':
          description: Batch price results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchPriceResponse'
        '400':
          description: >
            Invalid request — card_ids must be a non-empty array of strings
            (max 500)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                empty:
                  value:
                    error: 'card_ids must not be empty'
                    code: 'INVALID_CARD_IDS'
                too_many:
                  value:
                    error: 'Maximum 500 card_ids per request'
                    code: 'TOO_MANY_CARD_IDS'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/prices/all:
    get:
      summary: All latest prices (Enterprise)
      description: >
        Paginated endpoint returning the latest price per card across the
        entire catalog. Requires Enterprise tier. Useful for building local
        price caches or data warehouse syncs.
      tags: [Pricing, Premium]
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          description: Results per page (max 1000)
          schema: { type: integer, default: 100, maximum: 1000 }
      responses:
        '200':
          description: Paginated latest prices
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PricePoint'
                  total:
                    type: integer
                    example: 45200
                  page:
                    type: integer
                    example: 1
                  limit:
                    type: integer
                    example: 100
                  pages:
                    type: integer
                    example: 452
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Requires Enterprise tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Analytics ───────────────────────────────────────────────────────

  /v1/cards/{id}/analytics:
    get:
      summary: Risk analytics
      description: >
        Computes annualized volatility, max drawdown, Sharpe ratio, and 90-day
        return from daily price data. Requires at least 2 data points.
      tags: [Analytics]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
      responses:
        '200':
          description: Volatility, drawdown, Sharpe ratio
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsResult'

  /v1/index/{category}:
    get:
      summary: Category market index
      description: Returns the latest snapshot and up to 90 historical snapshots for a market category.
      tags: [Analytics]
      parameters:
        - name: category
          in: path
          required: true
          schema: { type: string }
          example: 'pokemon-tcg'
      responses:
        '200':
          description: Index snapshots
          content:
            application/json:
              schema:
                type: object
                properties:
                  category:
                    type: string
                    example: 'pokemon-tcg'
                  latest:
                    $ref: '#/components/schemas/IndexSnapshot'
                  snapshots:
                    type: array
                    items:
                      $ref: '#/components/schemas/IndexSnapshot'

  # ── v1 Compare ──────────────────────────────────────────────────────────

  /v1/compare:
    get:
      summary: Compare cards side-by-side
      description: >
        Compare 2-10 cards with current prices, risk analytics (volatility,
        Sharpe, drawdown, 90-day return), and 30-day liquidity metrics.
        Useful for evaluating investment alternatives.
      tags: [Analytics]
      parameters:
        - name: cards
          in: query
          required: true
          description: Comma-separated list of card IDs (2-10)
          schema: { type: string }
          example: 'psa10-charizard-base-4,psa9-pikachu-jungle-60,psa10-blastoise-base-2'
      responses:
        '200':
          description: Comparison data for all requested cards
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompareResponse'
        '400':
          description: Missing or invalid cards parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missing:
                  value:
                    error: 'cards query parameter is required (comma-separated card_ids)'
                    code: 'MISSING_CARDS'
                count:
                  value:
                    error: 'cards must contain 2-10 card IDs'
                    code: 'INVALID_CARDS_COUNT'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Population ───────────────────────────────────────────────────────

  /v1/cards/{id}/population:
    get:
      summary: Population data for a card
      description: >
        Returns grading population data for a card — how many copies have been
        graded at each grade level by each grading company (PSA, BGS, CGC, etc.).
        Results are ordered by report date descending.
      tags: [Cards]
      parameters:
        - name: id
          in: path
          required: true
          description: Card ID
          schema: { type: string }
          example: 'psa10-charizard-base-4'
        - name: grader
          in: query
          description: Filter by grading company (e.g. PSA, BGS, CGC)
          schema: { type: string }
          example: 'PSA'
        - name: limit
          in: query
          description: Maximum number of population entries to return (max 200)
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        '200':
          description: Population data
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id:
                    type: string
                    example: 'psa10-charizard-base-4'
                  population:
                    type: array
                    items:
                      $ref: '#/components/schemas/PopulationEntry'
                  total:
                    type: integer
                    description: Total number of population records for this card
                    example: 42
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Comps ──────────────────────────────────────────────────────────

  /v1/cards/{id}/comps:
    get:
      summary: Unified comparable sales data
      description: >
        Returns recent sold comps, pricing statistics, population data, and
        liquidity metrics for a card. Primary endpoint for oracle consumers
        institutional data consumers. Supports grade filtering and a lookback
        window (1–90 days, default 30). When no explicit grade filter is set,
        the endpoint attempts to match the card's own grade and falls back to
        all-grade comps if fewer than 3 matches exist.
      tags: [Pricing]
      parameters:
        - name: id
          in: path
          required: true
          description: Card ID
          schema: { type: string }
        - name: days
          in: query
          description: Lookback window in days (1–90, default 30)
          schema: { type: integer, default: 30, minimum: 1, maximum: 90 }
        - name: grade
          in: query
          description: Filter comps to a specific grade (e.g. "PSA 10")
          schema: { type: string }
      responses:
        '200':
          description: Comp data with pricing, liquidity, population, and raw sales
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id: { type: string }
                  card: { type: object }
                  pricing: { type: object }
                  liquidity: { type: object }
                  population: { type: object }
                  sales: { type: array, items: { type: object } }
                  sales_capped: { type: boolean }
                  meta: { type: object }
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Screener ────────────────────────────────��───────────────────────

  /v1/screener:
    get:
      summary: Oracle-grade card screener
      description: >
        Returns cards that meet minimum population, sales volume, and liquidity
        thresholds — suitable for oracle index construction. Requires dealer+
        tier. Supports filtering by category and sorting by sales count,
        liquidity score, volatility, population, or manipulation resistance.
      tags: [Premium]
      parameters:
        - name: min_pop
          in: query
          description: Minimum PSA 10 population (default 1000)
          schema: { type: integer, default: 1000 }
        - name: min_sales_30d
          in: query
          description: Minimum 30-day sold comp count (default 50)
          schema: { type: integer, default: 50 }
        - name: min_liquidity
          in: query
          description: Minimum liquidity score 0–100 (default 40)
          schema: { type: integer, default: 40, minimum: 0, maximum: 100 }
        - name: limit
          in: query
          description: Max results (1–100, default 25)
          schema: { type: integer, default: 25, minimum: 1, maximum: 100 }
        - name: category
          in: query
          description: Filter by card category
          schema: { type: string }
        - name: sort
          in: query
          description: Sort field
          schema:
            type: string
            enum: [sales_count_30d, liquidity_score, volatility, pop, manipulation_resistance]
            default: sales_count_30d
      responses:
        '200':
          description: Screened candidates with computed metrics
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates: { type: array, items: { type: object } }
                  count: { type: integer }
                  filters: { type: object }
        '401':
          description: Invalid or missing API key
        '403':
          description: Tier requirement not met (dealer+)

  # ── v1 Bounty Matching ────────────────────────────────────────────────

  /v1/bounty/match:
    post:
      summary: Match bounty — find eBay listings for a collector bounty
      description: >
        Searches eBay active listings via Browse API for items matching the
        bounty query. Returns scored matches with confidence and recent sold
        comps from the PriceDepth database. Enterprise tier required.
      tags: [Premium]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                  description: Search query (e.g. "1935 National Chicle Bronko Nagurski PSA 8")
                  maxLength: 500
                max_price:
                  type: number
                  description: Maximum price in USD
                limit:
                  type: integer
                  description: Max results to return (1-25, default 10)
                  minimum: 1
                  maximum: 25
                  default: 10
      responses:
        '200':
          description: Bounty match results
          content:
            application/json:
              schema:
                type: object
                properties:
                  matches:
                    type: array
                    items:
                      type: object
                      properties:
                        source: { type: string }
                        title: { type: string }
                        price: { type: number }
                        currency: { type: string }
                        url: { type: string }
                        listing_id: { type: string }
                        image_url: { type: string }
                        condition: { type: string }
                        seller:
                          type: object
                          properties:
                            username: { type: string }
                            feedback_score: { type: number }
                            feedback_pct: { type: number }
                        listed_at: { type: string }
                        confidence_score: { type: number }
                  comps:
                    type: array
                    items:
                      type: object
                      properties:
                        source: { type: string }
                        title: { type: string }
                        price: { type: number }
                        sold_date: { type: string }
                        url: { type: string }
                  meta:
                    type: object
                    properties:
                      query: { type: string }
                      sources_queried: { type: array, items: { type: string } }
                      total_found: { type: integer }
                      request_id: { type: string }
                      ebay_calls_remaining: { type: integer }
        '400':
          description: Invalid request (missing query, invalid max_price, etc.)
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (enterprise+)

  # ── v1 Inventory Search ──────────────────────────────────────────────

  /v1/inventory/search:
    get:
      summary: Search active marketplace listings
      description: >
        Searches active listings across supported marketplaces (currently eBay
        Browse API). Returns normalized listing data with seller info, pricing,
        and shipping details. Enterprise tier required.
      tags: [Premium]
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          description: Search query string
        - name: sources
          in: query
          schema: { type: string, default: 'ebay' }
          description: Comma-separated sources (currently only "ebay")
        - name: status
          in: query
          schema: { type: string, default: 'active' }
          description: Listing status filter
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 25 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: min_price
          in: query
          schema: { type: number }
        - name: max_price
          in: query
          schema: { type: number }
        - name: sort
          in: query
          schema: { type: string, enum: [price_asc, price_desc, date_desc] }
      responses:
        '200':
          description: Listing search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  listings:
                    type: array
                    items:
                      type: object
                      properties:
                        source: { type: string }
                        title: { type: string }
                        price: { type: number }
                        currency: { type: string }
                        condition: { type: string }
                        url: { type: string }
                        listing_id: { type: string }
                        image_url: { type: string }
                        listed_at: { type: string }
                        seller:
                          type: object
                          properties:
                            username: { type: string }
                            feedback_score: { type: number }
                            feedback_pct: { type: number }
                        shipping:
                          type: object
                          nullable: true
                          properties:
                            cost: { type: number }
                            type: { type: string }
                  meta:
                    type: object
                    properties:
                      query: { type: string }
                      sources_queried: { type: array, items: { type: string } }
                      total_found: { type: integer }
                      limit: { type: integer }
                      offset: { type: integer }
                      request_id: { type: string }
        '400':
          description: Invalid request parameters
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (enterprise+)
        '429':
          description: Per-key daily rate limit exceeded

  # ── v1 Multi-Marketplace Availability ──────────────────────────────────

  /v1/cards/{id}/availability:
    get:
      summary: Get multi-marketplace availability for a card
      description: >
        Returns live availability across all supported marketplaces (eBay live,
        Tradible cached) for a single card. Enterprise tier required.
      tags: [Premium]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: Card ID
      responses:
        '200':
          description: Card availability across marketplaces
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id:
                    type: string
                  availability:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        listings:
                          type: array
                          items:
                            type: object
                            properties:
                              title: { type: string }
                              price_usd: { type: number, nullable: true }
                              currency: { type: string }
                              condition: { type: string }
                              url: { type: string }
                              image_url: { type: string }
                              seller_name: { type: string }
                              listing_id: { type: string }
                        count: { type: integer }
                        as_of: { type: string, format: date-time }
                        source: { type: string, enum: [live, cached] }
                  total_listings:
                    type: integer
                  partial:
                    type: boolean
                    description: True if one or more sources failed
                  errors:
                    type: object
                    nullable: true
                    additionalProperties: { type: string }
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (enterprise+)
        '404':
          description: Card not found

  /v1/cards/availability/batch:
    post:
      summary: Batch multi-marketplace availability
      description: >
        Returns availability across all supported marketplaces for up to 25 cards
        in a single request. Enterprise tier required.
      tags: [Premium]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [card_ids]
              properties:
                card_ids:
                  type: array
                  items: { type: string }
                  maxItems: 25
                  description: Array of card IDs (max 25)
      responses:
        '200':
          description: Batch availability results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        card_id: { type: string }
                        availability: { type: object }
                        total_listings: { type: integer }
                        partial: { type: boolean }
                  errors:
                    type: object
                    additionalProperties: { type: string }
        '400':
          description: Invalid request (non-array, empty, too many, invalid IDs)
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (enterprise+)

  # ── v1 Momentum ────────────────────────────────────────────────────────

  /v1/cards/{id}/momentum:
    get:
      summary: Card momentum breakdown
      description: >
        Returns momentum score, label, component values, and supply shock data
        for a specific card. Requires collector+ tier.
      tags: [Analytics]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Momentum data
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id: { type: string }
                  momentum_score: { type: number }
                  label: { type: string }
                  trading_velocity: { type: number }
                  price_momentum: { type: number }
                  computed_at: { type: string, format: date-time }
        '404':
          description: Card not found
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (collector+)

  /v1/cards/{id}/supply-analysis:
    get:
      summary: Card supply analysis
      description: >
        Returns population data and active supply shocks for a card.
        Requires collector+ tier.
      tags: [Analytics]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Supply analysis data
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id: { type: string }
                  population: { type: object }
                  active_shocks: { type: array, items: { type: object } }
        '404':
          description: Card not found
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (collector+)

  # ── v1 AI Projections ──────────────────────────────────────────────────

  /v1/cards/{id}/forecast:
    get:
      summary: AI price forecast
      description: >
        Returns AI-powered price forecast with confidence intervals using
        exponential smoothing (SES, Holt's Linear, or Seasonal Holt).
        Requires collector+ tier.
      tags: [AI]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: days
          in: query
          schema: { type: integer, enum: [7, 30, 90], default: 30 }
          description: Forecast horizon in days
      responses:
        '200':
          description: Forecast data or graceful degradation
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id: { type: string }
                  model: { type: string, enum: [ses, holt_linear, seasonal_holt] }
                  trend:
                    type: object
                    properties:
                      direction: { type: string, enum: [up, down, flat] }
                      slope: { type: number }
                      slope_pct_per_day: { type: number }
                  forecast:
                    type: array
                    nullable: true
                    items:
                      type: object
                      properties:
                        date: { type: string, format: date }
                        price: { type: number }
                        upper: { type: number }
                        lower: { type: number }
                  accuracy:
                    type: object
                    properties:
                      rmse: { type: number }
                      mape: { type: number, nullable: true }
                  seasonality:
                    type: object
                    nullable: true
                    properties:
                      detected: { type: boolean }
                      period: { type: integer }
                      strength: { type: number }
                  data_points: { type: integer }
                  last_price: { type: number }
                  generated_at: { type: string, format: date-time }
        '404':
          description: Card not found
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (collector+)

  /v1/cards/{id}/anomalies:
    get:
      summary: Price anomaly detection
      description: >
        Detects price anomalies using dual Z-score + IQR methods.
        A price is flagged only when both methods agree.
        Requires collector+ tier.
      tags: [AI]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: days
          in: query
          schema: { type: integer, default: 90 }
          description: Lookback window in days (7-365)
      responses:
        '200':
          description: Anomaly detection results
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id: { type: string }
                  card_title: { type: string }
                  stats:
                    type: object
                    properties:
                      count: { type: integer }
                      mean: { type: number }
                      std: { type: number }
                  anomalies:
                    type: array
                    nullable: true
                    items:
                      type: object
                      properties:
                        price: { type: number }
                        direction: { type: string, enum: [high, low] }
                        zscore: { type: number }
                        source: { type: string, nullable: true }
                  total_prices: { type: integer }
                  anomaly_count: { type: integer }
                  lookback_days: { type: integer }
                  generated_at: { type: string, format: date-time }
        '404':
          description: Card not found
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (collector+)

  /v1/market/momentum-leaders:
    get:
      summary: Momentum leaderboard
      description: >
        Returns top cards ranked by momentum score. Filterable by category,
        vertical, and direction. Requires collector+ tier.
      tags: [Analytics]
      parameters:
        - name: category
          in: query
          schema: { type: string }
        - name: vertical
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 20 }
        - name: direction
          in: query
          schema: { type: string, enum: [up, down], default: up }
      responses:
        '200':
          description: Momentum leaders list
          content:
            application/json:
              schema:
                type: object
                properties:
                  leaders:
                    type: array
                    items:
                      type: object
                      properties:
                        card_id: { type: string }
                        name: { type: string }
                        category: { type: string }
                        momentum_score: { type: number }
                        label: { type: string }
                  count: { type: integer }
        '401':
          description: Missing or invalid API key
        '403':
          description: Tier requirement not met (collector+)

  # ── v1 Alerts ──────────────────────────────────────────────────────────

  /v1/alerts:
    get:
      summary: List price alerts
      description: Returns all price alerts associated with the authenticated API key.
      tags: [Alerts]
      responses:
        '200':
          description: Alert list
          content:
            application/json:
              schema:
                type: object
                properties:
                  alerts:
                    type: array
                    items:
                      $ref: '#/components/schemas/AlertObject'
                  count:
                    type: integer
                    example: 3
    post:
      summary: Create price alert
      description: >
        Creates a price alert that fires when the card's FMV crosses the
        specified threshold. Supports above, below, and percent_change types.
      tags: [Alerts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [card_id, alert_type, threshold_value]
              properties:
                card_id:
                  type: string
                  example: 'psa10-charizard-base-4'
                alert_type:
                  type: string
                  enum: [above, below, percent_change]
                  example: 'below'
                threshold_value:
                  type: number
                  example: 35000.00
                email:
                  type: string
                  example: 'alerts@example.com'
                webhook_url:
                  type: string
                  example: 'https://example.com/hooks/price'
      responses:
        '201':
          description: Alert created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Alert created for "Charizard"'
                  alert:
                    $ref: '#/components/schemas/AlertObject'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/alerts/{id}:
    delete:
      summary: Delete alert
      tags: [Alerts]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Alert deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Alert deleted'
                  deleted_id:
                    type: string
        '404':
          description: Alert not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/alerts/{id}/history:
    get:
      summary: Alert trigger history
      description: Returns up to 50 most recent trigger events for a specific alert.
      tags: [Alerts]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Alert trigger history
          content:
            application/json:
              schema:
                type: object
                properties:
                  alert_id:
                    type: string
                    format: uuid
                  history:
                    type: array
                    items:
                      $ref: '#/components/schemas/AlertHistoryEntry'
        '404':
          description: Alert not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Portfolio ───────────────────────────────────────────────────────

  /v1/portfolio/value:
    post:
      summary: Batch portfolio valuation
      description: >
        Accepts up to 500 card positions and returns current FMV for each,
        plus a total portfolio value.
      tags: [Portfolio]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [positions]
              properties:
                positions:
                  type: array
                  items:
                    type: object
                    required: [card_id]
                    properties:
                      card_id:
                        type: string
                        example: 'psa10-charizard-base-4'
                      quantity:
                        type: integer
                        default: 1
                        example: 2
            example:
              positions:
                - card_id: 'psa10-charizard-base-4'
                  quantity: 1
                - card_id: 'psa9-pikachu-jungle-60'
                  quantity: 3
      responses:
        '200':
          description: Portfolio valuation
          content:
            application/json:
              schema:
                type: object
                properties:
                  positions:
                    type: array
                    items:
                      type: object
                      properties:
                        card_id:
                          type: string
                        quantity:
                          type: integer
                        price_usd:
                          type: number
                          format: float
                          nullable: true
                        value_usd:
                          type: number
                          format: float
                          nullable: true
                  total_value_usd:
                    type: number
                    format: float
                    example: 42350.00
        '400':
          description: Missing or invalid positions array
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/portfolio/pnl:
    post:
      summary: Portfolio P&L summary
      description: >
        Accepts up to 500 card positions with cost basis (purchase_price) and
        returns per-position and aggregate profit & loss analysis including
        winners/losers, best/worst performer, and total return percentage.
      tags: [Portfolio]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [positions]
              properties:
                positions:
                  type: array
                  maxItems: 500
                  items:
                    type: object
                    required: [card_id, purchase_price]
                    properties:
                      card_id:
                        type: string
                        maxLength: 200
                        example: '1999 Pokemon Jungle Eevee #51 PSA 10|10'
                      quantity:
                        type: number
                        default: 1
                        minimum: 0
                        exclusiveMinimum: true
                        maximum: 1000000
                        example: 2
                      purchase_price:
                        type: number
                        format: float
                        minimum: 0
                        example: 150.00
            example:
              positions:
                - card_id: '1999 Pokemon Jungle Eevee #51 PSA 10|10'
                  quantity: 2
                  purchase_price: 150.00
                - card_id: 'psa10-charizard-base-4'
                  quantity: 1
                  purchase_price: 50000.00
      responses:
        '200':
          description: Portfolio P&L analysis
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      total_invested:
                        type: number
                        format: float
                        example: 300.00
                      total_current_value:
                        type: number
                        format: float
                        example: 340.00
                      total_pnl:
                        type: number
                        format: float
                        example: 40.00
                      total_pnl_pct:
                        type: number
                        format: float
                        nullable: true
                        example: 13.33
                      winners:
                        type: integer
                        example: 1
                      losers:
                        type: integer
                        example: 0
                      best_performer:
                        type: object
                        nullable: true
                        properties:
                          card_id:
                            type: string
                          pnl_pct:
                            type: number
                            format: float
                      worst_performer:
                        type: object
                        nullable: true
                        properties:
                          card_id:
                            type: string
                          pnl_pct:
                            type: number
                            format: float
                  positions:
                    type: array
                    items:
                      type: object
                      properties:
                        card_id:
                          type: string
                        quantity:
                          type: number
                        purchase_price:
                          type: number
                          format: float
                        current_price:
                          type: number
                          format: float
                          nullable: true
                        cost_basis:
                          type: number
                          format: float
                        current_value:
                          type: number
                          format: float
                          nullable: true
                        pnl:
                          type: number
                          format: float
                          nullable: true
                        pnl_pct:
                          type: number
                          format: float
                          nullable: true
        '400':
          description: Missing or invalid positions / purchase_price
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/portfolio/analytics:
    post:
      summary: Portfolio analytics
      description: >
        Comprehensive portfolio analysis including HHI diversification index,
        benchmark comparison against cat-all index, per-category P&L breakdown,
        top 5 momentum positions, and aggregate P&L summary.
      tags: [Portfolio]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [positions]
              properties:
                positions:
                  type: array
                  maxItems: 500
                  items:
                    type: object
                    required: [card_id, purchase_price]
                    properties:
                      card_id:
                        type: string
                        maxLength: 200
                      quantity:
                        type: number
                        default: 1
                        minimum: 0
                        exclusiveMinimum: true
                        maximum: 1000000
                      purchase_price:
                        type: number
                        format: float
                        minimum: 0
      responses:
        '200':
          description: Portfolio analytics
          content:
            application/json:
              schema:
                type: object
                properties:
                  diversification:
                    type: object
                    properties:
                      hhi:
                        type: number
                        format: float
                      category_weights:
                        type: object
                        additionalProperties:
                          type: number
                      assessment:
                        type: string
                        enum: [diversified, moderately_diversified, moderately_concentrated, concentrated]
                  benchmark:
                    type: object
                    properties:
                      index_name:
                        type: string
                      index_7d_change_pct:
                        type: number
                        format: float
                        nullable: true
                      portfolio_return_pct:
                        type: number
                        format: float
                        nullable: true
                      alpha_vs_index_7d:
                        type: number
                        format: float
                        nullable: true
                  category_pnl:
                    type: array
                    items:
                      type: object
                      properties:
                        category:
                          type: string
                        invested:
                          type: number
                          format: float
                        current_value:
                          type: number
                          format: float
                        pnl:
                          type: number
                          format: float
                        pnl_pct:
                          type: number
                          format: float
                          nullable: true
                        position_count:
                          type: integer
                  top_momentum:
                    type: array
                    items:
                      type: object
                      properties:
                        card_id:
                          type: string
                        name:
                          type: string
                        momentum_score:
                          type: number
                          format: float
                        label:
                          type: string
                        pnl_pct:
                          type: number
                          format: float
                          nullable: true
                  pnl_summary:
                    type: object
                    properties:
                      total_invested:
                        type: number
                        format: float
                      total_current_value:
                        type: number
                        format: float
                      total_pnl:
                        type: number
                        format: float
                      total_pnl_pct:
                        type: number
                        format: float
                        nullable: true
                      winners:
                        type: integer
                      losers:
                        type: integer
        '400':
          description: Missing or invalid positions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/bounties:
    get:
      summary: List bounties
      description: Returns Yabe bounties with optional filters for status, category, graded, and text search.
      tags: [Bounties]
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [open, matched, fulfilled, expired]
        - name: category
          in: query
          schema:
            type: string
        - name: graded
          in: query
          schema:
            type: boolean
        - name: q
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of bounties
          content:
            application/json:
              schema:
                type: object
                properties:
                  bounties:
                    type: array
                    items:
                      type: object
                  count:
                    type: integer
                  meta:
                    type: object

  /v1/bounties/stats:
    get:
      summary: Bounty summary stats
      description: Returns aggregate counts of bounties by status.
      tags: [Bounties]
      responses:
        '200':
          description: Bounty statistics
          content:
            application/json:
              schema:
                type: object

  /v1/bounties/reconcile:
    post:
      summary: Reconcile bounties to cards
      description: >-
        Fuzzy-matches unlinked Yabe bounties to PriceDepth card_ids using
        token-based confidence scoring. Supports dry-run (default) and apply modes.
      tags: [Bounties]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                bounty_number:
                  type: integer
                  minimum: 1
                  description: Reconcile only this bounty (optional)
                auto_link_threshold:
                  type: number
                  minimum: 0
                  maximum: 1
                  default: 0.8
                  description: Minimum confidence for auto-linking
                apply:
                  type: boolean
                  default: false
                  description: Actually update card_id (false = dry-run)
      responses:
        '200':
          description: Reconciliation results
          content:
            application/json:
              schema:
                type: object
                properties:
                  auto_matched:
                    type: array
                    items:
                      type: object
                  needs_review:
                    type: array
                    items:
                      type: object
                  no_match:
                    type: array
                    items:
                      type: object
                  summary:
                    type: object
                    properties:
                      total:
                        type: integer
                      auto_matched:
                        type: integer
                      needs_review:
                        type: integer
                      no_match:
                        type: integer
                      applied:
                        type: integer
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/bounties/{bountyNumber}:
    get:
      summary: Get bounty by number
      description: Returns a single Yabe bounty by its bounty number.
      tags: [Bounties]
      parameters:
        - name: bountyNumber
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Bounty details
          content:
            application/json:
              schema:
                type: object
                properties:
                  bounty:
                    type: object
        '400':
          description: Invalid bounty number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Bounty not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Premium ─────────────────────────────────────────────────────────

  /v1/cards/{id}/grade-prices:
    get:
      summary: Grade-specific pricing (Collector+)
      description: >
        Returns prices across all known grade variants of a card, including
        PSA population data where available. Requires Collector tier or above.
      tags: [Premium]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
      responses:
        '200':
          description: Prices across all grades
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                    example: 'Charizard'
                  set_name:
                    type: string
                    example: 'Base Set'
                  grades:
                    type: array
                    items:
                      $ref: '#/components/schemas/GradePriceEntry'
                  grade_count:
                    type: integer
                    example: 5
        '403':
          description: Requires Collector tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/arbitrage:
    get:
      summary: Cross-platform arbitrage (Dealer+)
      description: >
        Identifies cards with meaningful price spreads across platforms (eBay,
        TCGPlayer, StockX, PWCC). Accounts for seller fees to show net profit.
        Requires Dealer tier or above.
      tags: [Premium]
      parameters:
        - name: min_spread
          in: query
          description: Minimum spread percentage to include
          schema: { type: number, default: 5 }
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 50 }
        - name: category
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Arbitrage opportunities
          content:
            application/json:
              schema:
                type: object
                properties:
                  opportunities:
                    type: array
                    items:
                      $ref: '#/components/schemas/ArbitrageOpportunity'
                  count:
                    type: integer
                    example: 12
                  min_spread_pct:
                    type: number
                    example: 5
                  platform_fees:
                    type: object
                    additionalProperties:
                      type: number
                    example:
                      ebay: 0.1325
                      tcgplayer: 0.1115
                      stockx: 0.10
                      pwcc: 0.10
                  as_of:
                    type: string
                    format: date-time
        '403':
          description: Requires Dealer tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/movers:
    get:
      summary: Price movers
      description: >
        Top gainers or losers by 7-day price change percentage.
        Uses the search_cards_v2 RPC with change_7d sort.
      operationId: getMovers
      tags: [Cards]
      security:
        - ApiKeyAuth: []
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: ['7d']
            default: '7d'
        - name: direction
          in: query
          schema:
            type: string
            enum: ['up', 'down']
            default: 'up'
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
        - name: category
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of top movers
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CardSummary'
                  period:
                    type: string
                  direction:
                    type: string
                  limit:
                    type: integer
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/sets/ev:
    get:
      summary: Set expected value rankings
      description: >
        Returns aggregated expected value data for each card set, ranked by
        total value. Useful for breakers deciding which sealed product to open.
      tags: [Sets]
      parameters:
        - name: vertical
          in: query
          description: Filter by vertical (e.g. pokemon-tcg, sports)
          schema: { type: string }
        - name: sort
          in: query
          description: Sort field
          schema:
            type: string
            enum: [total_value, avg_card_value, coverage_pct]
            default: total_value
        - name: limit
          in: query
          description: Maximum number of sets to return
          schema: { type: integer, default: 25, maximum: 100 }
      responses:
        '200':
          description: Ranked set valuations
          content:
            application/json:
              schema:
                type: object
                properties:
                  sets:
                    type: array
                    items:
                      type: object
                      properties:
                        set_name: { type: string, example: 'Prismatic Evolutions' }
                        vertical: { type: string, example: 'pokemon-tcg' }
                        valuation_date: { type: string, format: date, example: '2026-04-27' }
                        card_count: { type: integer, example: 186 }
                        priced_card_count: { type: integer, example: 142 }
                        total_value: { type: number, example: 1850.00 }
                        avg_card_value: { type: number, example: 13.03 }
                        median_card_value: { type: number, example: 4.50 }
                        top_card:
                          type: object
                          properties:
                            name: { type: string, example: 'Umbreon ex SAR' }
                            value: { type: number, example: 285.00 }
                        coverage_pct: { type: number, example: 76.34 }
                  generated_at:
                    type: string
                    format: date-time

  /v1/export:
    get:
      summary: Data export (Enterprise)
      description: Export card catalog data as JSON or CSV. Requires Enterprise tier.
      tags: [Premium]
      parameters:
        - name: format
          in: query
          schema: { type: string, enum: [json, csv], default: json }
        - name: limit
          in: query
          schema: { type: integer, default: 100, maximum: 1000 }
      responses:
        '200':
          description: Export data (JSON or CSV depending on format parameter)
          content:
            application/json:
              schema:
                type: object
                properties:
                  cards:
                    type: array
                    items:
                      $ref: '#/components/schemas/Card'
                  count:
                    type: integer
            text/csv:
              schema:
                type: string
        '403':
          description: Requires Enterprise tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── v1 Export (Enterprise) ──────────────────────────────────────────────

  /v1/export/prices:
    get:
      summary: Export price data (Enterprise)
      description: >
        Bulk export of daily price data within a date range. Supports JSON and
        CSV output formats with optional card_id and source filters. Paginated
        with up to 10,000 rows per page. Requires Enterprise tier.
      tags: [Export]
      parameters:
        - name: start
          in: query
          required: true
          description: Start date (inclusive, YYYY-MM-DD)
          schema: { type: string, format: date }
          example: '2026-01-01'
        - name: end
          in: query
          description: End date (inclusive, YYYY-MM-DD). Defaults to today.
          schema: { type: string, format: date }
          example: '2026-03-29'
        - name: format
          in: query
          description: Output format
          schema: { type: string, enum: [json, csv], default: json }
        - name: card_id
          in: query
          description: Filter to a specific card
          schema: { type: string }
          example: 'psa10-charizard-base-4'
        - name: source
          in: query
          description: Filter by price source (e.g. ebay, tcgplayer)
          schema: { type: string }
          example: 'ebay'
        - name: limit
          in: query
          description: Rows per page (max 10,000)
          schema: { type: integer, default: 10000, maximum: 10000 }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
      responses:
        '200':
          description: Price export data (JSON or CSV depending on format param)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExportPriceRow'
                  total:
                    type: integer
                    example: 125000
                  page:
                    type: integer
                    example: 1
                  limit:
                    type: integer
                    example: 10000
                  pages:
                    type: integer
                    example: 13
            text/csv:
              schema:
                type: string
                description: CSV with columns card_id, price_date, price_usd, source, sale_count
        '400':
          description: Invalid parameters (missing start date, bad format, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Requires Enterprise tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/export/sales:
    get:
      summary: Export sales data (Enterprise)
      description: >
        Bulk export of individual sale records within a date range. Includes
        listing URLs, grades, and raw titles. Supports JSON and CSV output
        with optional card_id and source filters. Requires Enterprise tier.
      tags: [Export]
      parameters:
        - name: start
          in: query
          required: true
          description: Start date (inclusive, YYYY-MM-DD)
          schema: { type: string, format: date }
          example: '2026-01-01'
        - name: end
          in: query
          description: End date (inclusive, YYYY-MM-DD). Defaults to today.
          schema: { type: string, format: date }
          example: '2026-03-29'
        - name: format
          in: query
          description: Output format
          schema: { type: string, enum: [json, csv], default: json }
        - name: card_id
          in: query
          description: Filter to a specific card
          schema: { type: string }
          example: 'psa10-charizard-base-4'
        - name: source
          in: query
          description: Filter by sale source (e.g. ebay, tcgplayer)
          schema: { type: string }
          example: 'tcgplayer'
        - name: limit
          in: query
          description: Rows per page (max 10,000)
          schema: { type: integer, default: 10000, maximum: 10000 }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
      responses:
        '200':
          description: Sales export data (JSON or CSV depending on format param)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExportSaleRow'
                  total:
                    type: integer
                    example: 87000
                  page:
                    type: integer
                    example: 1
                  limit:
                    type: integer
                    example: 10000
                  pages:
                    type: integer
                    example: 9
            text/csv:
              schema:
                type: string
                description: CSV with columns card_id, sale_date, price_usd, source, listing_url, grade, raw_title
        '400':
          description: Invalid parameters (missing start date, bad format, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Requires Enterprise tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/export/cards:
    get:
      summary: Export card catalog (Enterprise)
      description: >
        Bulk export of the card catalog. Supports JSON and CSV output with
        optional category filter. Paginated with up to 10,000 rows per page.
        Requires Enterprise tier.
      tags: [Export]
      parameters:
        - name: format
          in: query
          description: Output format
          schema: { type: string, enum: [json, csv], default: json }
        - name: category
          in: query
          description: Filter by product type / category
          schema: { type: string }
          example: 'single'
        - name: limit
          in: query
          description: Rows per page (max 10,000)
          schema: { type: integer, default: 10000, maximum: 10000 }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
      responses:
        '200':
          description: Card catalog export (JSON or CSV depending on format param)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExportCardRow'
                  total:
                    type: integer
                    example: 59928
                  page:
                    type: integer
                    example: 1
                  limit:
                    type: integer
                    example: 10000
                  pages:
                    type: integer
                    example: 6
            text/csv:
              schema:
                type: string
                description: CSV with columns card_id, name, set_name, card_number, grade, product_type, image_url
        '400':
          description: Invalid format parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Requires Enterprise tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── Public Endpoints (no auth) ─────────────────────────────────────────

  /api/search:
    get:
      summary: Search cards (public)
      description: >
        DB-side ILIKE search across card name, set name, and card number.
        Returns paginated results with latest price and 7-day change.
      tags: [Public]
      security: []
      parameters:
        - name: q
          in: query
          description: Search query (name, set, or card number)
          schema: { type: string }
          example: 'charizard'
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 50 }
      responses:
        '200':
          description: Search results with prices
          content:
            application/json:
              schema:
                type: object
                properties:
                  cards:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Card'
                        - type: object
                          properties:
                            fmv:
                              type: number
                              format: float
                              nullable: true
                              example: 42000.00
                            change_7d_pct:
                              type: number
                              format: float
                              nullable: true
                              example: 3.2
                  total:
                    type: integer
                    example: 47
                  page:
                    type: integer
                    example: 1
                  limit:
                    type: integer
                    example: 20
                  pages:
                    type: integer
                    example: 3
                  query:
                    type: string
                    nullable: true
                    example: 'charizard'

  /api/search/{cardId}:
    get:
      summary: Card detail (public)
      description: >
        Returns full card detail including pricing breakdown, source analysis,
        price history (90d), and recent sales. No authentication required.
      tags: [Public]
      security: []
      parameters:
        - name: cardId
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
      responses:
        '200':
          description: Card detail with pricing
          content:
            application/json:
              schema:
                type: object
                properties:
                  card:
                    $ref: '#/components/schemas/Card'
                  pricing:
                    type: object
                    properties:
                      fmv:
                        type: number
                        format: float
                        nullable: true
                        example: 42000.00
                      change_7d_pct:
                        type: number
                        format: float
                        nullable: true
                        example: 3.2
                      sale_count_30d:
                        type: integer
                        example: 14
                      psa10_pop:
                        type: integer
                        nullable: true
                        example: 121
                      sources:
                        type: array
                        items:
                          type: object
                          properties:
                            source:
                              type: string
                              example: 'ebay'
                            count:
                              type: integer
                              example: 8
                            avg_price:
                              type: number
                              format: float
                              example: 43200.00
                  price_history:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        price:
                          type: number
                          format: float
                  recent_sales:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        price:
                          type: number
                          format: float
                        source:
                          type: string
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/cards/{slug}/seo:
    get:
      summary: SEO card page data (public)
      description: >
        Returns card detail enriched with confidence scores, liquidity tiers,
        source breakdown, price range, and related cards for SEO card pages.
      tags: [Public]
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          description: Card slug or card_id
          schema: { type: string }
          example: 'charizard-base-set-psa-10'
      responses:
        '200':
          description: SEO card data with confidence, sources, and related cards
          content:
            application/json:
              schema:
                type: object
                properties:
                  card:
                    allOf:
                      - $ref: '#/components/schemas/Card'
                      - type: object
                        properties:
                          slug:
                            type: string
                          seo_title:
                            type: string
                            nullable: true
                          seo_description:
                            type: string
                            nullable: true
                  pricing:
                    type: object
                    properties:
                      price_usd:
                        type: number
                        format: float
                        nullable: true
                      price_range:
                        type: object
                        properties:
                          low:
                            type: number
                            format: float
                            nullable: true
                          high:
                            type: number
                            format: float
                            nullable: true
                      sale_count_window:
                        type: integer
                      psa10_pop:
                        type: integer
                        nullable: true
                      confidence:
                        type: object
                        properties:
                          score:
                            type: integer
                            example: 4
                          label:
                            type: string
                            enum: [very_low, low, medium, high, very_high]
                            example: 'high'
                      liquidity:
                        type: object
                        properties:
                          tier:
                            type: integer
                            example: 2
                          label:
                            type: string
                            enum: [very_low, low, moderate, high]
                            example: 'moderate'
                      price_change_pct:
                        type: number
                        format: float
                        nullable: true
                  sources:
                    type: array
                    items:
                      type: object
                      properties:
                        source:
                          type: string
                        count:
                          type: integer
                        avg_price:
                          type: number
                          format: float
                        last_sale_date:
                          type: string
                          format: date
                  related:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Card'
                        - type: object
                          properties:
                            slug:
                              type: string
                            price_usd:
                              type: number
                              format: float
                              nullable: true
                  generated_at:
                    type: string
                    format: date-time
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/cards/{cardId}/chart:
    get:
      summary: Chart data for card (public)
      description: >
        Returns price + market cap chart data for a card over a configurable
        window. Supports daily (aggregated) and hourly (individual sale) granularity.
      tags: [Public]
      security: []
      parameters:
        - name: cardId
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
        - name: days
          in: query
          schema: { type: integer, default: 90 }
        - name: granularity
          in: query
          description: "'daily' (default) aggregates to daily averages; 'hourly' returns individual sale points"
          schema: { type: string, enum: [daily, hourly], default: daily }
      responses:
        '200':
          description: Chart data with card info and stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  card:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      set:
                        type: string
                      number:
                        type: string
                      grade:
                        type: string
                      image_url:
                        type: string
                      tcgplayer_url:
                        type: string
                        nullable: true
                      distribution:
                        type: string
                        nullable: true
                  stats:
                    type: object
                    properties:
                      current_price:
                        type: number
                        format: float
                        nullable: true
                      psa10_population:
                        type: integer
                        nullable: true
                      market_cap:
                        type: integer
                        nullable: true
                      price_change_90d_pct:
                        type: number
                        format: float
                        nullable: true
                      total_sales_tracked:
                        type: integer
                      sources:
                        type: array
                        items:
                          type: string
                  granularity:
                    type: string
                    enum: [daily, hourly]
                  chart_data:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        time:
                          type: string
                          description: Only present in hourly granularity
                        datetime:
                          type: string
                          description: Only present in hourly granularity
                        price:
                          type: number
                          format: float
                          nullable: true
                        psa10_pop:
                          type: integer
                          nullable: true
                        market_cap:
                          type: integer
                          nullable: true
                        source:
                          type: string
                          description: Only present in hourly granularity
                  chart_points:
                    type: array
                    description: Compact format for charting libraries
                    items:
                      type: object
                      properties:
                        d:
                          type: string
                          description: Short date label (e.g. "Mar 29")
                        p:
                          type: number
                          format: float
                        m:
                          type: integer
                          nullable: true
                  generated_at:
                    type: string
                    format: date-time

  /api/embed/card/{cardId}:
    get:
      summary: Embed card data (public, CORS)
      description: >
        Lightweight endpoint for embedding card price widgets on external
        sites. Returns minimal card info, current price, and 7-day change.
        Aggressive cache headers (15 min). CORS allow-all.
      tags: [Public]
      security: []
      parameters:
        - name: cardId
          in: path
          required: true
          schema: { type: string }
          example: 'psa10-charizard-base-4'
      responses:
        '200':
          description: Card price for widget embedding
          content:
            application/json:
              schema:
                type: object
                properties:
                  card_id:
                    type: string
                    example: 'psa10-charizard-base-4'
                  name:
                    type: string
                    example: 'Charizard'
                  set_name:
                    type: string
                    example: 'Base Set'
                  grade:
                    type: string
                    example: '10'
                  slug:
                    type: string
                    nullable: true
                  image_url:
                    type: string
                    nullable: true
                  price_usd:
                    type: number
                    format: float
                    nullable: true
                    example: 42000.00
                  price_date:
                    type: string
                    format: date
                    nullable: true
                  change_7d_pct:
                    type: number
                    format: float
                    nullable: true
                    example: 3.2
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── Public Trends ──────────────────────────────────────────────────────

  /api/trends/indices:
    get:
      summary: Market indices (public)
      description: Returns the latest index snapshot per category. Cached 15 minutes.
      tags: [Public]
      security: []
      responses:
        '200':
          description: Category indices
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items:
                      $ref: '#/components/schemas/IndexSnapshot'
                  count:
                    type: integer
                    example: 6
                  as_of:
                    type: string
                    format: date-time

  /api/trends/index/{category}:
    get:
      summary: Category index time series (public)
      description: >
        Returns the latest snapshot and up to 90 historical snapshots for a
        single category. Valid categories: pokemon-tcg, sports-baseball,
        sports-basketball, sports-football, one-piece, sealed, all.
      tags: [Public]
      security: []
      parameters:
        - name: category
          in: path
          required: true
          schema:
            type: string
            enum: [pokemon-tcg, sports-baseball, sports-basketball, sports-football, one-piece, sealed, all]
          example: 'pokemon-tcg'
      responses:
        '200':
          description: Category index with time series
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/IndexSnapshot'
                  - type: object
                    properties:
                      snapshots:
                        type: array
                        items:
                          type: object
                          properties:
                            date:
                              type: string
                              format: date
                            total_market_cap:
                              type: number
                              format: float
                              nullable: true
                            avg_price:
                              type: number
                              format: float
                              nullable: true
                            card_count:
                              type: integer
                      as_of:
                        type: string
                        format: date-time
        '400':
          description: Invalid category
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/trends/movers:
    get:
      summary: Top movers (public)
      description: Top gainers and losers by 7-day price change. Cached 15 minutes.
      tags: [Public]
      security: []
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 10, maximum: 50 }
      responses:
        '200':
          description: Gainers and losers
          content:
            application/json:
              schema:
                type: object
                properties:
                  gainers:
                    type: array
                    items:
                      $ref: '#/components/schemas/Mover'
                  losers:
                    type: array
                    items:
                      $ref: '#/components/schemas/Mover'
                  count:
                    type: integer
                    example: 20
                  as_of:
                    type: string
                    format: date-time

  /api/trends/summary:
    get:
      summary: Market summary (public)
      description: >
        Market-wide aggregate stats: total cards, cards with prices,
        total market value, and average price. Cached 15 minutes.
      tags: [Public]
      security: []
      responses:
        '200':
          description: Market-wide stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_cards:
                    type: integer
                    example: 59928
                  cards_with_prices:
                    type: integer
                    example: 45200
                  total_market_value:
                    type: number
                    format: float
                    example: 28450000.00
                  avg_price:
                    type: number
                    format: float
                    example: 629.54
                  as_of:
                    type: string
                    format: date-time

  # ── Checkout ───────────────────────────────────────────────────────────

  /api/checkout/session:
    post:
      summary: Create Stripe checkout session
      description: >
        Creates a Stripe Checkout session for the specified plan (startup or growth).
        Returns the session ID and checkout URL to redirect the user.
      tags: [Checkout]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan]
              properties:
                plan:
                  type: string
                  enum: [startup, growth]
                  example: 'startup'
                userId:
                  type: string
                  description: Supabase auth user ID to link checkout to profile
                  example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
      responses:
        '200':
          description: Checkout session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id:
                    type: string
                    example: 'cs_test_a1b2c3d4'
                  checkout_url:
                    type: string
                    example: 'https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4'
        '400':
          description: Invalid plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/checkout/abandoned:
    post:
      summary: Report abandoned checkout
      description: Frontend analytics endpoint that logs when a user starts but does not complete checkout.
      tags: [Checkout]
      security: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                plan:
                  type: string
                  example: 'startup'
                step:
                  type: string
                  example: 'payment_info'
                timestamp:
                  type: string
                  format: date-time
      responses:
        '200':
          description: Event received
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    example: true

  # ── Stripe Webhooks ────────────────────────────────────────────────────

  /webhooks/stripe:
    post:
      summary: Stripe webhook receiver
      description: >
        Receives Stripe webhook events (checkout.session.completed,
        customer.subscription.updated, customer.subscription.deleted).
        Verifies the signature using STRIPE_WEBHOOK_SECRET. Raw body required.
      tags: [Webhooks]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Raw Stripe event payload (signature verified)
      parameters:
        - name: stripe-signature
          in: header
          required: true
          schema: { type: string }
          description: Stripe webhook signature header
      responses:
        '200':
          description: Event processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    example: true
                  duplicate:
                    type: boolean
                    description: True if the event was already processed (idempotency)
        '400':
          description: Missing signature or invalid signature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Stripe not configured or handler error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── Portal Endpoints (JWT auth) ────────────────────────────────────────

  /portal/me:
    get:
      summary: Get current user profile
      description: Returns user profile, tier, and masked API key for the authenticated user.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: User profile with API key info
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/UserProfile'
                  api_key:
                    $ref: '#/components/schemas/ApiKeyInfo'
        '401':
          description: Invalid or missing JWT
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /portal/usage:
    get:
      summary: API usage statistics
      description: Returns API call counts and quota usage for the current billing period.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Usage stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  usage:
                    type: object
                    properties:
                      calls_this_month:
                        type: integer
                        example: 2450
                      monthly_quota:
                        type: integer
                        nullable: true
                        example: 10000
                      quota_percent:
                        type: integer
                        example: 25
                      last_request_at:
                        type: string
                        format: date-time
                        nullable: true
                      period_start:
                        type: string
                        format: date-time
                      period_end:
                        type: string
                        format: date-time

  /portal/plan:
    get:
      summary: Current plan details
      description: Returns plan name, tier, and feature list for the authenticated user.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Plan details
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    type: object
                    properties:
                      name:
                        type: string
                        example: 'Collector'
                      tier:
                        type: string
                        example: 'collector'
                      features:
                        type: array
                        items:
                          type: string
                        example:
                          - '10,000 API calls / month'
                          - '30 requests / minute'
                          - 'Historical price data'
                          - 'Portfolio tracking'
                          - 'Price alerts'
                          - 'Email support'

  /portal/billing/session:
    post:
      summary: Create Stripe billing portal session
      description: >
        Creates a Stripe Customer Portal session so the user can manage their
        subscription, payment methods, and invoices.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Billing portal URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    example: 'https://billing.stripe.com/session/cus_abc123'
                  message:
                    type: string
        '400':
          description: No billing account (subscribe to a plan first)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /portal/portfolio:
    get:
      summary: Get user portfolio with valuations
      description: >
        Returns the user's default portfolio with all card positions, current
        valuations, gain/loss calculations, and 24h/7d change tracking.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Portfolio with valuations
          content:
            application/json:
              schema:
                type: object
                properties:
                  portfolio:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: string
                        format: uuid
                      name:
                        type: string
                        example: 'My Portfolio'
                  cards:
                    type: array
                    items:
                      $ref: '#/components/schemas/PortfolioCard'
                  summary:
                    type: object
                    properties:
                      total_value_usd:
                        type: number
                        format: float
                        example: 85400.00
                      total_cost_usd:
                        type: number
                        format: float
                        nullable: true
                        example: 72000.00
                      total_gain_usd:
                        type: number
                        format: float
                        nullable: true
                        example: 13400.00
                      change_24h_usd:
                        type: number
                        format: float
                        nullable: true
                      change_24h_pct:
                        type: number
                        format: float
                        nullable: true
                      change_7d_usd:
                        type: number
                        format: float
                        nullable: true
                      change_7d_pct:
                        type: number
                        format: float
                        nullable: true
                      card_count:
                        type: integer
                        example: 12
                      as_of:
                        type: string
                        format: date-time
    post:
      summary: Add card to portfolio
      description: >
        Adds a card to the user's default portfolio (auto-creates portfolio on
        first use). Maximum 500 positions per portfolio.
      tags: [Portal]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [card_id]
              properties:
                card_id:
                  type: string
                  example: 'psa10-charizard-base-4'
                quantity:
                  type: integer
                  default: 1
                  example: 2
                purchase_price:
                  type: number
                  format: float
                  nullable: true
                  example: 38000.00
                purchase_date:
                  type: string
                  format: date
                  nullable: true
                  example: '2025-12-15'
                grade:
                  type: string
                  nullable: true
                  example: '10'
                notes:
                  type: string
                  nullable: true
                  example: 'Purchased at PWCC auction'
      responses:
        '201':
          description: Card added to portfolio
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Card added to portfolio'
                  card:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      card_id:
                        type: string
                      quantity:
                        type: integer
                      purchase_price:
                        type: number
                        format: float
                        nullable: true
                      purchase_date:
                        type: string
                        format: date
                        nullable: true
                      grade:
                        type: string
                        nullable: true
                      notes:
                        type: string
                        nullable: true
        '400':
          description: Missing card_id or position limit reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /portal/portfolio/{cardEntryId}:
    delete:
      summary: Remove card from portfolio
      description: Removes a specific card entry from the user's portfolio. Verifies ownership.
      tags: [Portal]
      security:
        - BearerAuth: []
      parameters:
        - name: cardEntryId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Card removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Card removed from portfolio'
                  id:
                    type: string
        '403':
          description: Not authorized to modify this portfolio
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Entry not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /portal/alerts:
    get:
      summary: List user's price alerts
      description: Returns all price alerts for the authenticated portal user.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Alert list
          content:
            application/json:
              schema:
                type: object
                properties:
                  alerts:
                    type: array
                    items:
                      $ref: '#/components/schemas/AlertObject'
                  count:
                    type: integer
    post:
      summary: Create price alert (portal)
      description: Creates a price alert scoped to the authenticated portal user.
      tags: [Portal]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [card_id, alert_type, threshold_value]
              properties:
                card_id:
                  type: string
                  example: 'psa10-charizard-base-4'
                alert_type:
                  type: string
                  enum: [above, below, percent_change]
                threshold_value:
                  type: number
                  example: 35000.00
                email:
                  type: string
                  description: Defaults to user's email if omitted
                webhook_url:
                  type: string
      responses:
        '201':
          description: Alert created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  alert:
                    $ref: '#/components/schemas/AlertObject'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /portal/alerts/{id}:
    delete:
      summary: Delete price alert (portal)
      description: Deletes a price alert. Only the owning user can delete their alerts.
      tags: [Portal]
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Alert deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Alert deleted'
                  deleted_id:
                    type: string
        '404':
          description: Alert not found or not authorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /portal/api-key:
    post:
      summary: Provision API key
      description: >
        Generates a new API key for the authenticated user. Key prefix and rate
        limits are determined by the user's tier. Only one active key per user.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '201':
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'API key created successfully. Store it securely — it will not be shown again.'
                  api_key:
                    type: string
                    example: 'tp_collector_a1b2c3d4e5f6...'
                  tier:
                    type: string
                    example: 'collector'
                  rate_limit_per_min:
                    type: integer
                    example: 30
                  monthly_quota:
                    type: integer
                    nullable: true
                    example: 10000
        '200':
          description: User already has an active key
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'You already have an active API key'
                  api_key:
                    type: string
                  tier:
                    type: string
    delete:
      summary: Revoke API key
      description: Deactivates the user's active API key. A new one can be provisioned afterward.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Key revoked (or no active key found)
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'API key revoked. Generate a new one with POST /portal/api-key'

  /portal/report-subscription:
    get:
      summary: Check report subscription status
      description: Returns whether the user is subscribed to the weekly market report.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Subscription status
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscribed:
                    type: boolean
                    example: true
                  subscription:
                    $ref: '#/components/schemas/ReportSubscription'
    post:
      summary: Subscribe to weekly market report
      description: Subscribes (or resubscribes) the user to the weekly market report email.
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '201':
          description: Subscribed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Subscribed to weekly market report'
                  subscription:
                    $ref: '#/components/schemas/ReportSubscription'
        '200':
          description: Already subscribed or resubscribed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  subscription:
                    $ref: '#/components/schemas/ReportSubscription'
    delete:
      summary: Unsubscribe from weekly market report
      tags: [Portal]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Unsubscribed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Unsubscribed from weekly market report'

  /v1/indexes:
    get:
      tags: [Indexes]
      summary: List all public indexes
      description: Returns all active indexes with current value and 24h change. No authentication required.
      security: []
      responses:
        '200':
          description: List of indexes
          content:
            application/json:
              schema:
                type: object
                properties:
                  indexes:
                    type: array
                    items:
                      $ref: '#/components/schemas/IndexListItem'
              example:
                indexes:
                  - slug: 'pdi-pokemon'
                    name: 'PDI PSA-10 Index'
                    current_value: 1042.37
                    change_24h_pct: 0.58
                    methodology_version: 'v1.0.0'
                    as_of_timestamp: '2026-04-11T00:00:00Z'
    post:
      tags: [Indexes]
      summary: Create a new index (admin-only)
      description: >
        Creates a new index from a configuration object. Requires JWT auth with admin role.
        The `config.weighting` field must be a valid weighting method.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug, name, config]
              properties:
                slug:
                  type: string
                  example: 'pdi-pokemon'
                name:
                  type: string
                  example: 'PDI PSA-10 Index'
                description:
                  type: string
                  nullable: true
                  example: 'Benchmark index of liquid PSA 10-graded Pokemon cards, rebalanced monthly.'
                methodology_version:
                  type: string
                  example: 'v1.0.0'
                config:
                  type: object
                  required: [inclusion_rules]
                  description: Index configuration including inclusion rules and weighting method.
                  properties:
                    inclusion_rules:
                      type: object
                      example: { category: 'pokemon', grade: 'PSA 10', basket_size: 100 }
                    weighting:
                      type: string
                      example: 'equal'
                    rebalance_cadence:
                      type: string
                      example: 'monthly'
                    base_value:
                      type: number
                      example: 1000
                    base_date:
                      type: string
                      format: date
                      example: '2026-03-01'
      responses:
        '201':
          description: Index created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Index'
        '400':
          description: Validation error (missing fields or invalid weighting)
        '409':
          description: Slug already exists

  /v1/indexes/summary:
    get:
      tags: [Indexes]
      summary: Market-wide summary combining index data and category snapshots
      description: >
        Returns a consolidated market summary: all indexes with change percentages (24h, 7d, 30d),
        market-wide aggregate stats (total market cap, tracked cards, avg price), and a per-category
        breakdown from the latest index snapshots.
      security: []
      responses:
        '200':
          description: Market summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  indexes:
                    type: array
                    items:
                      type: object
                      properties:
                        slug:
                          type: string
                        name:
                          type: string
                        current_value:
                          type: number
                          nullable: true
                        change_24h_pct:
                          type: number
                          nullable: true
                        change_7d_pct:
                          type: number
                          nullable: true
                        change_30d_pct:
                          type: number
                          nullable: true
                        constituent_count:
                          type: integer
                          nullable: true
                        as_of_date:
                          type: string
                          format: date
                          nullable: true
                  market:
                    type: object
                    nullable: true
                    properties:
                      total_market_cap:
                        type: number
                      total_tracked_cards:
                        type: integer
                      avg_price:
                        type: number
                      as_of_date:
                        type: string
                        format: date
                  categories:
                    type: array
                    items:
                      type: object
                      properties:
                        category:
                          type: string
                        total_market_cap:
                          type: number
                        avg_price:
                          type: number
                        median_price:
                          type: number
                        card_count:
                          type: integer
                        snapshot_date:
                          type: string
                          format: date
                  computed_at:
                    type: string
                    format: date-time
              example:
                indexes:
                  - slug: 'pdi-pokemon'
                    name: 'PDI Pokemon'
                    current_value: 1042.50
                    change_24h_pct: 1.23
                    change_7d_pct: -0.45
                    change_30d_pct: 5.67
                    constituent_count: 100
                    as_of_date: '2026-05-07'
                market:
                  total_market_cap: 1234567.89
                  total_tracked_cards: 3500
                  avg_price: 352.73
                  as_of_date: '2026-05-07'
                categories:
                  - category: 'Pokemon'
                    total_market_cap: 800000
                    avg_price: 425.50
                    median_price: 275.00
                    card_count: 1200
                    snapshot_date: '2026-05-07'
                computed_at: '2026-05-08T02:00:00Z'

  /v1/indexes/{slug}:
    get:
      tags: [Indexes]
      summary: Current index value + metadata
      description: >
        Returns the most-recent computed value for the index, along with 30-day inline history
        and an ECDSA signature. Falls back to on-the-fly computation if no history row exists yet.
        No authentication required.
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          example: 'pdi-pokemon'
      responses:
        '200':
          description: Index detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Index'
              example:
                slug: 'pdi-pokemon'
                name: 'PDI PSA-10 Index'
                description: 'Benchmark index of liquid PSA 10-graded Pokemon cards, rebalanced monthly.'
                methodology_version: 'v1.0.0'
                as_of_timestamp: '2026-04-11T00:00:00Z'
                value: 1042.37
                constituent_count: 100
                history_30d:
                  - as_of_date: '2026-03-12'
                    value: 1000.00
                  - as_of_date: '2026-04-11'
                    value: 1042.37
                signature: 'MEUCIQDexampleBase64=='
        '404':
          description: Index not found

  /v1/indexes/{slug}/history:
    get:
      tags: [Indexes]
      summary: Index time-series history
      description: >
        Returns historical daily closes for the index. Default window is 30 days ending today.
        Maximum window is 365 days. Supports 1d / 1w / 1m downsampling.
        No authentication required.
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          example: 'pdi-pokemon'
        - name: from
          in: query
          schema:
            type: string
            format: date
          description: Start date inclusive (YYYY-MM-DD). Defaults to 30 days before `to`.
          example: '2026-03-01'
        - name: to
          in: query
          schema:
            type: string
            format: date
          description: End date inclusive (YYYY-MM-DD). Defaults to today.
          example: '2026-04-11'
        - name: interval
          in: query
          schema:
            type: string
            enum: ['1d', '1w', '1m']
            default: '1d'
          description: Downsampling interval. 1d = daily (default), 1w = weekly, 1m = monthly.
      responses:
        '200':
          description: Time-series history
          content:
            application/json:
              schema:
                type: object
                properties:
                  slug:
                    type: string
                    example: 'pdi-pokemon'
                  from:
                    type: string
                    format: date
                    example: '2026-03-12'
                  to:
                    type: string
                    format: date
                    example: '2026-04-11'
                  interval:
                    type: string
                    example: '1d'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/IndexHistoryPoint'
        '400':
          description: Invalid date range or interval
        '404':
          description: Index not found

  /v1/indexes/{slug}/constituents:
    get:
      tags: [Indexes]
      summary: Index constituent basket
      description: >
        Returns the most-recent constituent snapshot — all asset_ids with their fractional weights,
        sorted by weight descending. No authentication required.
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          example: 'pdi-pokemon'
      responses:
        '200':
          description: Constituent basket
          content:
            application/json:
              schema:
                type: object
                properties:
                  slug:
                    type: string
                    example: 'pdi-pokemon'
                  as_of_date:
                    type: string
                    format: date
                    nullable: true
                    example: '2026-04-11'
                  as_of_timestamp:
                    type: string
                    format: date-time
                    nullable: true
                    example: '2026-04-11T00:00:00Z'
                  methodology_version:
                    type: string
                    nullable: true
                    example: 'v1.0.0'
                  constituents:
                    type: array
                    items:
                      $ref: '#/components/schemas/IndexConstituent'
                  warning:
                    type: string
                    nullable: true
                    description: Set when no constituent snapshot is available yet (pre-backfill).
                    example: 'No constituent snapshot available yet. Backfill pending.'
        '404':
          description: Index not found

  # ── Developer Self-Serve ────────────────────────────────────────────────────

  /api/developer/register:
    post:
      tags: [Developer]
      summary: Register for a free API key
      description: >
        Create a free-tier API key. The key is inactive until the email
        address is verified via the link sent to the provided email.
        Rate limited to 3 registrations per email per day.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, name]
              properties:
                email:
                  type: string
                  format: email
                  description: Developer email address (used for verification and key recovery)
                  example: 'dev@example.com'
                name:
                  type: string
                  maxLength: 100
                  description: Developer or company name
                  example: 'Jane Developer'
                company:
                  type: string
                  maxLength: 200
                  nullable: true
                  description: Company name (optional)
                  example: 'Acme Corp'
      responses:
        '201':
          description: API key created (inactive until email verified)
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'API key created. Check your email to verify and activate it.'
                  key:
                    type: string
                    example: 'tp_free_a1b2c3d4e5f6...'
                  tier:
                    type: string
                    example: 'free'
                  rate_limit_per_min:
                    type: integer
                    example: 10
                  monthly_quota:
                    type: integer
                    example: 100
                  is_active:
                    type: boolean
                    example: false
                  verification_required:
                    type: boolean
                    example: true
                  email_sent:
                    type: boolean
                    example: true
                  docs_url:
                    type: string
                    example: 'https://pricedepth.com/docs.html'
                  dashboard_url:
                    type: string
                    example: 'https://pricedepth.com/developer.html'
        '400':
          description: Invalid email or missing name
        '429':
          description: Rate limited (too many registrations)

  /api/developer/verify:
    get:
      tags: [Developer]
      summary: Verify email and activate API key
      description: >
        Verify the developer's email address using the token from the
        welcome email. Activates the API key on success and redirects
        to the documentation page.
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: Verification token from the welcome email
      responses:
        '302':
          description: Redirects to docs page on success
        '400':
          description: Invalid token format
        '404':
          description: Token not found or expired

  /api/developer/usage:
    get:
      tags: [Developer]
      summary: Get API usage dashboard data
      description: >
        Returns usage statistics for the authenticated API key including
        requests today, this month, quota info, and a 30-day daily chart.
      parameters:
        - name: X-Api-Key
          in: header
          required: true
          schema:
            type: string
          description: Your PriceDepth API key
      responses:
        '200':
          description: Usage statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  key_name:
                    type: string
                    example: 'dev-jane-developer'
                  tier:
                    type: string
                    example: 'free'
                  requests_today:
                    type: integer
                    example: 42
                  requests_this_month:
                    type: integer
                    example: 850
                  monthly_quota:
                    type: integer
                    nullable: true
                    example: 100
                  quota_remaining:
                    type: integer
                    nullable: true
                    example: 50
                  quota_used_pct:
                    type: integer
                    nullable: true
                    example: 50
                  rate_limit_per_min:
                    type: integer
                    example: 10
                  daily_usage:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                          example: '2026-05-13'
                        requests:
                          type: integer
                          example: 28
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                        format: date
                      to:
                        type: string
                        format: date
        '401':
          description: Invalid or missing API key

  /api/developer/usage/endpoints:
    get:
      tags: [Developer]
      summary: Get per-endpoint usage breakdown
      description: >
        Returns a breakdown of API requests by endpoint for the current
        month, sorted by request count (most-used first).
      parameters:
        - name: X-Api-Key
          in: header
          required: true
          schema:
            type: string
          description: Your PriceDepth API key
      responses:
        '200':
          description: Endpoint usage breakdown
          content:
            application/json:
              schema:
                type: object
                properties:
                  endpoints:
                    type: array
                    items:
                      type: object
                      properties:
                        method:
                          type: string
                          example: 'GET'
                        endpoint:
                          type: string
                          example: '/v1/cards'
                        requests:
                          type: integer
                          example: 340
                  total_endpoints:
                    type: integer
                    example: 8
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                        format: date
                      to:
                        type: string
                        format: date
        '401':
          description: Invalid or missing API key

  # ── Funds (Enterprise / Index tier) ─────────────────────────────────────

  /v1/funds/nav:
    post:
      tags: [Funds]
      summary: Compute fund NAV
      description: >
        Compute a deterministic Net Asset Value for a portfolio of positions.
        Returns per-position pricing with confidence labels, a total NAV, and
        a SHA-256 audit hash for compliance verification. Requires Index
        (Enterprise) tier.
  # ── SLA Monitoring ──────────────────────────────────────────────────────────

  /v1/sla/status:
    get:
      tags: [SLA]
      summary: Current SLA metrics
      description: Returns current uptime, latency percentiles, and data freshness.
      parameters:
        - name: X-Api-Key
          in: header
          required: true
          schema:
            type: string
          description: Enterprise / Index tier API key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [positions]
              properties:
                positions:
                  type: array
                  description: Portfolio positions (max 1000)
                  items:
                    type: object
                    required: [card_id, quantity]
                    properties:
                      card_id:
                        type: string
                        example: 'psa10-charizard-base-4'
                      quantity:
                        type: number
                        format: float
                        example: 2
                        description: Must be a positive number
                pricing_method:
                  type: string
                  enum: [median, vwap]
                  default: median
                  description: Pricing methodology to use
      responses:
        '200':
          description: NAV computation result
          content:
            application/json:
              schema:
                type: object
                properties:
                  nav:
                    type: number
                    format: float
                    example: 84000.00
                    description: Total Net Asset Value in USD
                  positions:
                    type: array
                    items:
                      type: object
                      properties:
                        card_id:
                          type: string
                          example: 'psa10-charizard-base-4'
                        quantity:
                          type: number
                          example: 2
                        unit_price:
                          type: number
                          format: float
                          example: 42000.00
                        total:
                          type: number
                          format: float
                          example: 84000.00
                        confidence:
                          type: string
                          example: 'high'
                          description: Price confidence label
                        price_date:
                          type: string
                          format: date
                          example: '2026-05-13'
                  pricing_timestamp:
                    type: string
                    format: date-time
                    description: ISO timestamp of when prices were fetched
                  pricing_method:
                    type: string
                    enum: [median, vwap]
                    example: 'median'
                  audit_hash:
                    type: string
                    example: 'sha256:a1b2c3d4...'
                    description: Deterministic SHA-256 hash over sorted positions and NAV for compliance
                  missing:
                    type: array
                    items:
                      type: string
                    description: Card IDs with no available pricing data
        '400':
          description: Invalid request (bad positions, pricing method, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Requires Index (Enterprise) tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── Resolver (Enterprise tier) ──────────────────────────────────────────

  /v1/resolver/register:
    post:
      tags: [Resolver]
      summary: Register a price condition contract
      description: >
        Register a prediction market contract that monitors a card's price against
        a threshold condition. When the condition is met (or the contract expires),
        an HMAC-signed callback is delivered to the specified URL. Resolution is
        checked every 15 minutes via cron. Requires Enterprise tier.
      parameters:
        - name: X-Api-Key
          in: header
          required: true
          schema:
            type: string
          description: Enterprise tier API key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [card_id, condition, operator, threshold, callback_url, callback_secret, expires_at]
              properties:
                card_id:
                  type: string
                  example: '2023 Pokemon 151 Charizard ex #199|10'
                  description: Card identifier to monitor
                condition:
                  type: string
                  maxLength: 200
                  example: 'price > 500'
                  description: Human-readable description of the condition
                operator:
                  type: string
                  enum: ['>', '<', '>=', '<=', '==']
                  example: '>'
                  description: Comparison operator for price evaluation
                threshold:
                  type: number
                  format: float
                  example: 500
                  description: Price threshold in USD
                callback_url:
                  type: string
                  format: uri
                  example: 'https://example.com/webhook'
                  description: HTTPS URL to receive resolution callbacks (no private/internal addresses)
                callback_secret:
                  type: string
                  minLength: 16
                  maxLength: 256
                  example: 'a-secret-key-with-enough-length'
                  description: Secret used to generate HMAC-SHA256 signature on callbacks
                expires_at:
                  type: string
                  format: date-time
                  example: '2026-06-01T00:00:00Z'
                  description: Contract expiry (must be in the future)
      responses:
        '201':
          description: Contract registered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  contract:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      card_id:
                        type: string
                      condition:
                        type: string
                      operator:
                        type: string
                      threshold:
                        type: number
                      status:
                        type: string
                        enum: [active, resolved, expired, failed]
                        example: 'active'
                      expires_at:
                        type: string
                        format: date-time
                      created_at:
                        type: string
                        format: date-time
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Requires Enterprise tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Card not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/resolver/{id}:
    get:
      tags: [Resolver]
      summary: Get contract status
      description: >
        Retrieve the current status and details of a resolver contract.
        Only returns contracts owned by the caller's API key.
        Requires Enterprise tier.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Contract ID
      responses:
        '200':
          description: SLA status metrics
        '401':
          description: Missing or invalid API key
        '403':
          description: Requires collector tier or higher

  /v1/sla/history:
    get:
      tags: [SLA]
      summary: Daily SLA snapshots
      description: Returns historical daily SLA snapshots.
      parameters:
        - name: X-Api-Key
          in: header
          required: true
          schema:
            type: string
          description: Enterprise tier API key
      responses:
        '200':
          description: Contract details
          content:
            application/json:
              schema:
                type: object
                properties:
                  contract:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      card_id:
                        type: string
                      condition:
                        type: string
                      operator:
                        type: string
                      threshold:
                        type: number
                      status:
                        type: string
                        enum: [active, resolved, expired, failed]
                      resolution_price:
                        type: number
                        format: float
                        nullable: true
                        description: Price at resolution (null if not yet resolved)
                      resolved_at:
                        type: string
                        format: date-time
                        nullable: true
                      expires_at:
                        type: string
                        format: date-time
                      created_at:
                        type: string
                        format: date-time
        '403':
          description: Requires Enterprise tier
        '404':
          description: Contract not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/resolver/contracts:
    get:
      tags: [Resolver]
      summary: List caller's contracts
      description: >
        Returns a paginated list of resolver contracts owned by the caller's
        API key. Supports optional status filter. Requires Enterprise tier.
        - name: days
          in: query
          schema:
            type: integer
            default: 30
      responses:
        '200':
          description: SLA snapshot history
        '401':
          description: Missing or invalid API key
        '403':
          description: Requires collector tier or higher

  /v1/sla/incidents:
    get:
      tags: [SLA]
      summary: SLA breach incidents
      description: Returns detected SLA breaches and degradation events.
      parameters:
        - name: X-Api-Key
          in: header
          required: true
          schema:
            type: string
          description: Enterprise tier API key
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
          description: Page number
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
          description: Results per page
        - name: status
          in: query
          schema:
            type: string
            enum: [active, resolved, expired, failed]
          description: Filter by contract status
      responses:
        '200':
          description: Paginated list of contracts
          content:
            application/json:
              schema:
                type: object
                properties:
                  contracts:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        card_id:
                          type: string
                        condition:
                          type: string
                        operator:
                          type: string
                        threshold:
                          type: number
                        status:
                          type: string
                          enum: [active, resolved, expired, failed]
                        resolution_price:
                          type: number
                          format: float
                          nullable: true
                        resolved_at:
                          type: string
                          format: date-time
                          nullable: true
                        expires_at:
                          type: string
                          format: date-time
                        created_at:
                          type: string
                          format: date-time
                  total:
                    type: integer
                    example: 42
                  page:
                    type: integer
                    example: 1
                  limit:
                    type: integer
                    example: 20
                  pages:
                    type: integer
                    example: 3
        '403':
          description: Requires Enterprise tier

  # ── Card Match API ──
  /v1/match:
    post:
      summary: Match a card title to PriceDepth card_ids
      tags: [Match]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                grade: { type: string }
                year: { type: integer }
                set: { type: string }
                category: { type: string }
      responses:
        '200':
          description: Match results with confidence scores
  /v1/match/batch:
    post:
      summary: Batch match up to 25 card titles (dealer+)
      tags: [Match]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  maxItems: 25
                  items:
                    type: object
                    required: [title]
                    properties:
                      title: { type: string }
      responses:
        '200':
          description: Batch match results keyed by title
        '403':
          description: Requires dealer tier

  # ── Portfolio Tracking (API-key scoped) ──
  /v1/portfolio/track:
    post:
      summary: Create or update an API-key portfolio
      tags: [Portfolio]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                positions:
                  type: array
                  items:
                    type: object
                    properties:
                      card_id: { type: string }
                      quantity: { type: integer }
                      cost_basis_usd: { type: number }
      responses:
        '201':
          description: Portfolio created
    get:
      summary: List all portfolios for this API key
      tags: [Portfolio]
      security: [{ ApiKeyAuth: [] }]
      responses:
        '200':
          description: Array of portfolios
  /v1/portfolio/track/{id}:
    get:
      summary: Get portfolio with current prices and P&L
      tags: [Portfolio]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Portfolio with live prices
    delete:
      summary: Delete a portfolio
      tags: [Portfolio]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Portfolio deleted

  # ── ML Predictions ──
  /v1/cards/{id}/prediction:
    get:
      summary: Get ML price movement prediction
      tags: [Predictions]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Prediction with direction probability
  /v1/cards/{id}/ml-price:
    get:
      summary: Get ML-derived fair price estimate
      tags: [Predictions]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: ML price estimate with confidence
  /v1/predictions/movers:
    get:
      summary: Top predicted price movers
      tags: [Predictions]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: direction
          in: query
          schema: { type: string, enum: [up, down] }
        - name: limit
          in: query
          schema: { type: integer, default: 20 }
      responses:
        '200':
          description: Cards with highest predicted movement
  /v1/predictions/undervalued:
    get:
      summary: Most undervalued cards by ML estimate
      tags: [Predictions]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20 }
      responses:
        '200':
          description: Undervalued cards ranked by expected return
  /v1/predictions/risk-signals:
    get:
      summary: Cards with high-risk ML signals
      tags: [Predictions]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20 }
      responses:
        '200':
          description: Cards with elevated risk indicators

  # ── Collection Completeness ──
  /v1/collections/sets:
    get:
      summary: List all sets with card counts
      tags: [Collections]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: category
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
      responses:
        '200':
          description: Array of sets with total_cards
  /v1/collections/progress:
    get:
      summary: Collection progress across all sets
      tags: [Collections]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: user_id
          in: query
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Progress summary per set
  /v1/collections/progress/{setName}:
    get:
      summary: Detailed progress for one set
      tags: [Collections]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: setName
          in: path
          required: true
          schema: { type: string }
        - name: user_id
          in: query
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Per-card owned/missing status
  /v1/collections/missing/{setName}:
    get:
      summary: Missing cards with current prices
      tags: [Collections]
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - name: setName
          in: path
          required: true
          schema: { type: string }
        - name: user_id
          in: query
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Missing cards with price_usd
