openapi: 3.1.0
info:
  title: Markets Gazette API
  description: |
    API for Markets Gazette — a financial news aggregation and analysis platform.

    ## Authentication
    Most endpoints are public. The `/api/agents/*` endpoints support optional
    Bearer token authentication controlled by the `NUXT_API_TOKEN` environment variable.
    When `NUXT_API_TOKEN` is set, requests must include `Authorization: Bearer <token>`.

    Pipeline operations (RSS fetching, article processing, resets) are managed exclusively
    via the admin panel and are not part of the public API.

    ## Rate Limiting
    - **digest**: Sections are cached for 2 hours in the database
  version: 1.0.0
  contact:
    name: Markets Gazette

servers:
  - url: /api
    description: Current server

tags:
  - name: News
    description: News articles feed, filtering, and processing
  - name: Agents
    description: Machine-readable feed for AI agents and portfolio systems
  - name: Pipeline
    description: Batch processing pipeline status

paths:

  # ─── News ────────────────────────────────────────────────────────────────

  /news:
    get:
      tags: [News]
      summary: List processed news articles
      description: |
        Returns processed news items with optional filtering by signal, asset class,
        country, date range, and search term. Results are ordered by date descending.

        The response includes metadata with the total count of matching articles
        (before signal filtering) and per-signal counts computed from the deduplicated
        dataset. When a `signal` filter is active, `items` contains only matching
        articles but `meta.total` and `meta.signals` reflect all articles in the
        date/filter range.
      parameters:
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - name: signal
          in: query
          description: Filter items by market signal (applied in-memory after deduplication; `meta.signals` always reflects all items)
          schema:
            $ref: '#/components/schemas/Signal'
        - name: assetClass
          in: query
          description: Filter by asset class
          schema:
            $ref: '#/components/schemas/AssetClass'
        - name: countryCode
          in: query
          description: Filter by country (ISO 3166-1 alpha-2, case-insensitive)
          schema:
            type: string
            example: US
        - name: search
          in: query
          description: Full-text search across title, asset, symbol, and descriptions (case-insensitive)
          schema:
            type: string
            example: bitcoin
        - name: limit
          in: query
          description: Maximum number of results to return
          schema:
            type: integer
            default: 1000
            minimum: 1
      responses:
        '200':
          description: News articles with metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NewsResponse'

  /news/aggregated:
    get:
      tags: [News]
      summary: List aggregated daily signals
      description: Returns aggregated signals for assets grouped by date. Each entry combines multiple news articles into a single signal with a synthesized effect description.
      parameters:
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - name: signal
          in: query
          description: Filter by signal type
          schema:
            $ref: '#/components/schemas/Signal'
        - name: limit
          in: query
          description: Maximum number of results
          schema:
            type: integer
            default: 500
            minimum: 1
      responses:
        '200':
          description: List of aggregated signals
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AggregatedSignal'

  /news/digest:
    get:
      tags: [News]
      summary: AI-generated market digest
      description: |
        Returns a daily digest with AI-synthesized summaries for each asset class.
        Sections are cached in the database for 2 hours. Each section may be freshly
        generated via Gemini API or served from cache.
      responses:
        '200':
          description: Market digest
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Digest'
        '500':
          description: Gemini API key not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /news/feeds:
    get:
      tags: [News]
      summary: List configured RSS feed URLs
      description: Returns the list of RSS sources configured in the application. Used by the client to trigger per-feed fetch requests.
      responses:
        '200':
          description: Feed URLs
          content:
            application/json:
              schema:
                type: object
                properties:
                  urls:
                    type: array
                    items:
                      type: string
                      format: uri
                    example:
                      - https://feeds.bloomberg.com/markets/news.rss
                required: [urls]
        '400':
          description: No RSS feeds configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /news/pending-count:
    get:
      tags: [News]
      summary: Get unprocessed article count
      description: Returns the count of articles that have been fetched but not yet analysed by the AI pipeline.
      responses:
        '200':
          description: Pending count
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                    description: Number of unprocessed articles
                    example: 7
                required: [count]

  /news/last-update:
    get:
      tags: [News]
      summary: Get latest processing timestamp
      description: Returns the `createdAt` timestamp of the most recently processed article.
      responses:
        '200':
          description: Last update timestamp
          content:
            application/json:
              schema:
                type: object
                properties:
                  lastUpdate:
                    type: string
                    format: date-time
                    nullable: true
                    description: ISO 8601 timestamp of the most recently processed article, or null if none
                    example: '2026-02-20T14:30:00.000Z'
                required: [lastUpdate]

  /news/stream:
    get:
      tags: [News]
      summary: Real-time article processing events (SSE)
      description: |
        Server-Sent Events endpoint for real-time notifications when articles
        are processed or RSS feeds are fetched. Used by the client in autonomous
        mode (VPS) to show the "new articles" banner without polling.

        Connect via `EventSource('/api/news/stream')`. The connection
        auto-reconnects on disconnect (browser built-in).
      responses:
        '200':
          description: SSE event stream
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Events:
                  - `article-processed` → `{"remainingCount": <number>}`
                  - `fetch-complete` → `{"pending": <number>}`

  /news/statistics:
    get:
      tags: [News]
      summary: Sentiment statistics by category across timeframes
      description: |
        Returns per-category positive/negative article counts across three
        timeframes (24h, 7d, 30d). Categories are asset classes (Finance context)
        or AI topics (AI context). Articles are deduplicated before counting.
      parameters:
        - name: search
          in: query
          description: Filter articles by title or asset name (case-insensitive)
          schema:
            type: string
      responses:
        '200':
          description: Statistics by timeframe
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatisticsResponse'

  # ─── Agents ──────────────────────────────────────────────────────────────

  /agents/news:
    get:
      tags: [Agents]
      summary: Machine-readable news feed
      description: |
        A portable, filterable news feed designed for AI agents and portfolio
        management systems. Returns bilingual descriptions and structured metadata.

        **Authentication**: Controlled by the `NUXT_API_TOKEN` environment variable.
        - If the variable is **not set**, the endpoint is publicly accessible.
        - If the variable **is set**, requests must include a valid Bearer token.
      security:
        - BearerAuth: []
        - {}
      parameters:
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - name: signal
          in: query
          description: Filter by market signal
          schema:
            $ref: '#/components/schemas/Signal'
        - name: assetClass
          in: query
          description: Filter by asset class
          schema:
            $ref: '#/components/schemas/AssetClass'
        - name: countryCode
          in: query
          description: Filter by country (ISO 3166-1 alpha-2, case-insensitive)
          schema:
            type: string
            example: US
        - name: search
          in: query
          description: Full-text search across title and descriptions (case-insensitive)
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum results (clamped to 1–500)
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 500
        - name: lang
          in: query
          description: Language for `description` and `effect` fields
          schema:
            type: string
            enum: [en, it]
            default: en
      responses:
        '200':
          description: News feed response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentNewsResponse'
        '401':
          description: Invalid or missing Bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /agents/stream:
    get:
      tags: [Agents]
      summary: Real-time article stream (SSE)
      description: |
        Server-Sent Events endpoint that streams processed articles in real-time
        as Gemini analyzes them. Designed for AI agents, MCP tools, and portfolio
        management systems that need live data.

        Only active in autonomous mode (VPS/Docker) where the server-side event
        bus is running.

        **Authentication**: Same as `/agents/news` — controlled by `NUXT_API_TOKEN`.

        **Events emitted:**
        - `connected` — immediately on connection with context and active filters
        - `article` — full processed article data (filtered by query params)
        - `fetch-complete` — RSS fetch cycle found new pending articles
        - `heartbeat` — keepalive every 30 seconds

        **Example:**
        ```
        curl -N -H "Authorization: Bearer <token>" \
          "https://example.com/api/agents/stream?lang=en&signal=POSITIVE"
        ```
      security:
        - BearerAuth: []
        - {}
      parameters:
        - name: signal
          in: query
          description: Filter by market signal
          schema:
            $ref: '#/components/schemas/Signal'
        - name: assetClass
          in: query
          description: Filter by asset class (Finance context) or AI topic (AI context)
          schema:
            $ref: '#/components/schemas/AssetClass'
        - name: lang
          in: query
          description: Language for `description` and `effect` fields
          schema:
            type: string
            enum: [en, it]
            default: en
      responses:
        '200':
          description: SSE event stream
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Events:
                  - `connected` → `{"appContext":"FINANCE","filters":{"signal":null,"assetClass":null,"lang":"en"},"timestamp":"..."}`
                  - `article` → `{"article":{...AgentNewsItem},"remainingCount":<number>}`
                  - `fetch-complete` → `{"pending":<number>}`
                  - `heartbeat` → `{"timestamp":"..."}`
        '401':
          description: Invalid or missing Bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ─── RSS Feed ─────────────────────────────────────────────────────────────

  /feed.xml:
    get:
      tags: [News]
      summary: RSS 2.0 feed of processed articles
      description: |
        Public RSS feed containing AI-processed news articles with signal analysis,
        market impact summaries, and asset classification. Auto-discoverable via
        `<link rel="alternate" type="application/rss+xml">` in the HTML head.

        The feed is served from a Nitro server route (not under `/api`), accessible
        at the root: `https://example.com/feed.xml`.

        Each item includes the AI-generated signal (POSITIVE/NEGATIVE/NEUTRAL),
        localized descriptions, and categorization by asset class or AI topic.
      parameters:
        - name: lang
          in: query
          description: Language for descriptions and effects
          schema:
            type: string
            enum: [it, en]
            default: it
        - name: signal
          in: query
          description: Filter by market signal
          schema:
            $ref: '#/components/schemas/Signal'
        - name: limit
          in: query
          description: Maximum number of articles (1–100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: RSS 2.0 XML feed
          content:
            application/rss+xml:
              schema:
                type: string
                description: RSS 2.0 compliant XML document

  # ─── Pipeline ─────────────────────────────────────────────────────────────

  /pipeline/status:
    get:
      tags: [Pipeline]
      summary: Get pipeline status
      description: |
        Returns the status of the most recent batch processing run. Automatically
        marks stale runs (no heartbeat for > 15 minutes) as `FAILED`.
      responses:
        '200':
          description: Pipeline status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PipelineStatus'

# ─── Components ─────────────────────────────────────────────────────────────

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        API token set via `NUXT_API_TOKEN` environment variable.
        Only required when the env var is set.

  parameters:
    from:
      name: from
      in: query
      description: ISO 8601 date-time lower bound (inclusive)
      schema:
        type: string
        format: date-time
        example: '2026-02-19T00:00:00.000Z'
    to:
      name: to
      in: query
      description: ISO 8601 date-time upper bound (inclusive)
      schema:
        type: string
        format: date-time
        example: '2026-02-20T23:59:59.999Z'

  schemas:

    Signal:
      type: string
      enum: [POSITIVE, NEGATIVE, NEUTRAL]
      description: Market sentiment signal

    AssetClass:
      type: string
      enum:
        - ETF
        - STOCK
        - BOND
        - COMMODITY
        - CRYPTO
        - INDEX
        - FOREX
        - PRIVATE_FUND
        - REAL_ESTATE
        - OTHER
      description: Financial asset class

    PipelineRunStatus:
      type: string
      enum: [RUNNING, COMPLETED, FAILED]

    Error:
      type: object
      properties:
        statusCode:
          type: integer
          example: 400
        statusMessage:
          type: string
          example: Bad Request
      required: [statusCode, statusMessage]

    NewsArticle:
      type: object
      description: A processed news article with AI-extracted metadata
      properties:
        id:
          type: string
          description: SHA-1 hash of the article URL
          example: a3f5c8e2b1d4e9f0123456789abcdef012345678
        title:
          type: string
          example: BTC surges on ETF inflows
        content:
          type: string
          nullable: true
          description: Full article text (may be null for RSS-only entries)
        date:
          type: string
          format: date-time
          example: '2026-02-20T10:00:00.000Z'
        link:
          type: string
          format: uri
          example: https://example.com/btc-surge
        signal:
          $ref: '#/components/schemas/Signal'
          nullable: true
        asset:
          type: string
          nullable: true
          example: Bitcoin
        symbol:
          type: string
          nullable: true
          example: BTC
        shortDescription:
          type: string
          nullable: true
          description: Italian AI-generated summary (≤2 sentences)
          example: Bitcoin sale grazie ai flussi record negli ETF spot.
        effect:
          type: string
          nullable: true
          description: Italian AI-generated market impact (1 sentence)
          example: Effetto positivo sul sentiment crypto a breve termine.
        shortDescriptionEn:
          type: string
          nullable: true
          description: English AI-generated summary (≤2 sentences)
          example: Bitcoin rises on record ETF inflows.
        shortDescriptionEs:
          type: string
          nullable: true
          description: Spanish AI-generated summary (≤2 sentences)
          example: Bitcoin sube por los flujos récord en ETFs.
        effectEn:
          type: string
          nullable: true
          description: English AI-generated market impact (1 sentence)
          example: Positive effect on short-term crypto sentiment.
        effectEs:
          type: string
          nullable: true
          description: Spanish AI-generated market impact (1 sentence)
          example: Efecto positivo en el sentimiento crypto a corto plazo.
        assetClass:
          $ref: '#/components/schemas/AssetClass'
          nullable: true
        countryCode:
          type: string
          nullable: true
          description: ISO 3166-1 alpha-2 country code
          example: US
        relevanceScore:
          type: integer
          nullable: true
          description: AI-assigned relevance score (1–10)
          example: 7
        processed:
          type: boolean
          description: Whether the article has been analysed by the AI pipeline
          example: true
        aggregatedId:
          type: string
          nullable: true
          description: ID of the parent AggregatedSignal record
        createdAt:
          type: string
          format: date-time
          example: '2026-02-20T10:05:00.000Z'
      required:
        - id
        - title
        - date
        - link
        - processed

    AggregatedSignal:
      type: object
      description: Daily aggregated signal for an asset, combining multiple news articles
      properties:
        id:
          type: string
          example: btc-2026-02-20
        asset:
          type: string
          example: Bitcoin
        symbol:
          type: string
          nullable: true
          example: BTC
        date:
          type: string
          format: date-time
          example: '2026-02-20T00:00:00.000Z'
        signal:
          $ref: '#/components/schemas/Signal'
        combinedEffect:
          type: string
          description: Italian synthesized market effect for the day
          example: Il flusso positivo negli ETF ha spinto BTC al rialzo.
        combinedEffectEn:
          type: string
          nullable: true
          description: English synthesized market effect for the day
          example: Positive ETF flows pushed BTC higher.
        newsCount:
          type: integer
          description: Number of articles contributing to this signal
          example: 4
        news:
          type: array
          items:
            $ref: '#/components/schemas/NewsArticle'
        createdAt:
          type: string
          format: date-time
          example: '2026-02-20T12:00:00.000Z'
      required:
        - id
        - asset
        - date
        - signal
        - combinedEffect
        - newsCount
        - news

    DigestSection:
      type: object
      description: AI-generated digest section for one asset class
      properties:
        assetClass:
          $ref: '#/components/schemas/AssetClass'
        summary:
          type: string
          description: Italian summary (≤2 sentences)
          example: Le azioni tech hanno recuperato dopo i dati sull'inflazione.
        summaryEn:
          type: string
          description: English summary (≤2 sentences)
          example: Tech stocks recovered after inflation data.
        consequence:
          type: string
          description: Italian market impact (1 sentence)
          example: Possibile rally di breve termine nel settore tecnologico.
        consequenceEn:
          type: string
          description: English market impact (1 sentence)
          example: Possible short-term rally in the tech sector.
        newsCount:
          type: integer
          description: Number of articles from the last 24 hours in this class
          example: 12
        fromCache:
          type: boolean
          description: True if served from the 2-hour database cache
          example: false
      required:
        - assetClass
        - summary
        - summaryEn
        - consequence
        - consequenceEn
        - newsCount
        - fromCache

    Digest:
      type: object
      properties:
        sections:
          type: array
          items:
            $ref: '#/components/schemas/DigestSection'
        generatedAt:
          type: string
          format: date-time
          description: Timestamp of the oldest cached section (or now if freshly generated)
          example: '2026-02-20T12:00:00.000Z'
        totalNewsCount:
          type: integer
          description: Total articles across all sections
          example: 87
      required:
        - sections
        - generatedAt
        - totalNewsCount

    AgentNewsItem:
      type: object
      description: A compact news item for AI agent consumption
      properties:
        id:
          type: string
          example: a3f5c8e2b1d4e9f0123456789abcdef012345678
        date:
          type: string
          format: date-time
          example: '2026-02-20T10:00:00.000Z'
        title:
          type: string
          example: BTC surges on ETF inflows
        link:
          type: string
          format: uri
          example: https://example.com/btc-surge
        signal:
          $ref: '#/components/schemas/Signal'
          nullable: true
        asset:
          type: string
          nullable: true
          example: Bitcoin
        symbol:
          type: string
          nullable: true
          example: BTC
        assetClass:
          $ref: '#/components/schemas/AssetClass'
          nullable: true
        countryCode:
          type: string
          nullable: true
          example: US
        description:
          type: string
          nullable: true
          description: AI-generated summary in the requested language
          example: Bitcoin rises on record ETF inflows.
        effect:
          type: string
          nullable: true
          description: AI-generated market impact in the requested language
          example: Positive effect on short-term crypto sentiment.
      required:
        - id
        - date
        - title
        - link

    AgentNewsMeta:
      type: object
      properties:
        generatedAt:
          type: string
          format: date-time
          description: Response generation timestamp
          example: '2026-02-20T14:00:00.000Z'
        from:
          type: string
          format: date-time
          description: Effective lower date bound applied
          example: '2026-02-19T14:00:00.000Z'
        to:
          type: string
          format: date-time
          description: Effective upper date bound applied
          example: '2026-02-20T14:00:00.000Z'
        lang:
          type: string
          enum: [en, it]
          description: Language used for description/effect fields
          example: en
        total:
          type: integer
          description: Number of items returned
          example: 23
      required:
        - generatedAt
        - from
        - to
        - lang
        - total

    AgentNewsResponse:
      type: object
      properties:
        meta:
          $ref: '#/components/schemas/AgentNewsMeta'
        items:
          type: array
          items:
            $ref: '#/components/schemas/AgentNewsItem'
      required:
        - meta
        - items

    SignalCounts:
      type: object
      description: Per-signal article counts computed from the full deduplicated dataset (before signal filtering)
      properties:
        positive:
          type: integer
          description: Number of articles with POSITIVE signal
          example: 12
        negative:
          type: integer
          description: Number of articles with NEGATIVE signal
          example: 5
        neutral:
          type: integer
          description: Number of articles with NEUTRAL signal
          example: 8
      required: [positive, negative, neutral]

    NewsMeta:
      type: object
      description: Metadata about the news query result
      properties:
        total:
          type: integer
          description: Total number of articles matching the query (before signal filtering, after deduplication)
          example: 25
        signals:
          $ref: '#/components/schemas/SignalCounts'
      required: [total, signals]

    NewsResponse:
      type: object
      description: Paginated news response with metadata
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/NewsArticle'
        meta:
          $ref: '#/components/schemas/NewsMeta'
      required: [items, meta]

    CategoryStats:
      type: object
      description: Sentiment counts for a single category
      properties:
        category:
          type: string
          description: Asset class (Finance) or AI topic (AI context)
          example: STOCK
        positive:
          type: integer
          description: Number of articles with POSITIVE signal
          example: 8
        negative:
          type: integer
          description: Number of articles with NEGATIVE signal
          example: 3
      required: [category, positive, negative]

    StatisticsResponse:
      type: object
      description: Sentiment statistics grouped by category across multiple timeframes
      properties:
        timeframes:
          type: object
          properties:
            '24h':
              type: array
              items:
                $ref: '#/components/schemas/CategoryStats'
            '7d':
              type: array
              items:
                $ref: '#/components/schemas/CategoryStats'
            '30d':
              type: array
              items:
                $ref: '#/components/schemas/CategoryStats'
          required: ['24h', '7d', '30d']
      required: [timeframes]

    PipelineRun:
      type: object
      properties:
        id:
          type: string
          example: run_20260220_143000
        status:
          $ref: '#/components/schemas/PipelineRunStatus'
        phase:
          type: string
          nullable: true
          description: Current processing phase description
          example: Analysing article 12 of 30
        message:
          type: string
          nullable: true
          description: Status message or error detail
          example: null
        totalItems:
          type: integer
          description: Total articles in scope for this run
          example: 30
        processedItems:
          type: integer
          description: Successfully processed articles
          example: 12
        errorCount:
          type: integer
          description: Articles that failed processing
          example: 0
        startedAt:
          type: string
          format: date-time
          example: '2026-02-20T14:30:00.000Z'
        updatedAt:
          type: string
          format: date-time
          description: Last heartbeat timestamp
          example: '2026-02-20T14:31:05.000Z'
        completedAt:
          type: string
          format: date-time
          nullable: true
          example: null
        resultSummary:
          type: object
          nullable: true
          description: JSON summary written at completion
      required:
        - id
        - status
        - totalItems
        - processedItems
        - errorCount
        - startedAt
        - updatedAt

    PipelineStatus:
      type: object
      properties:
        running:
          type: boolean
          description: True if the pipeline is currently running
          example: false
        lastRun:
          $ref: '#/components/schemas/PipelineRun'
          nullable: true
          description: Most recent pipeline run, or null if none exists
      required:
        - running
        - lastRun
