openapi: 3.1.0
info:
  title: Solstera Market Intelligence Public API
  version: 1.0.0
  description: >-
    Broker-neutral, read-only access to persisted Solstera intelligence. The API
    exposes research artifacts and never accepts brokerage credentials or orders.
    Webhook consumers must compute HMAC-SHA256 over the exact
    `solstera-timestamp.raw_body` bytes, compare `solstera-signature` in constant
    time, reject timestamps older than five minutes, and deduplicate the stable
    `solstera-delivery` identifier before applying an event.
  x-solstera-sdks:
    typescript: /developers/sdk/solstera-market-intelligence-0.2.0.tgz
    python: /developers/sdk/solstera_market_intelligence-0.2.0-py3-none-any.whl
servers:
  - url: https://hermes-mi.solsteralabs.com/api/v1
    description: Compatibility production hostname
security:
  - bearerAuth: []
tags:
  - name: Intelligence
  - name: Evidence
  - name: Integration
paths:
  /:
    get:
      tags: [Integration]
      summary: Discover available capabilities for the current key
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /intelligence/latest:
    get:
      tags: [Intelligence]
      summary: Latest persisted intelligence summary
      x-solstera-scope: intelligence:read
      responses:
        '200':
          description: Signed latest intelligence summary with source provenance
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          data_provenance: { $ref: '#/components/schemas/DataProvenance' }
        '404': { $ref: '#/components/responses/NotFound' }
  /signing-key:
    get:
      tags: [Integration]
      summary: Ed25519 public key for response verification
      responses:
        '200':
          description: Public verification key
  /forecasts:
    get:
      tags: [Intelligence]
      summary: Multi-asset, multi-horizon forecasts
      x-solstera-scope: forecasts:read
      parameters:
        - $ref: '#/components/parameters/Asset'
        - $ref: '#/components/parameters/Horizon'
        - $ref: '#/components/parameters/AsOf'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /opportunities:
    get:
      tags: [Intelligence]
      summary: Ranked opportunities and transparent 0–100 ratings
      x-solstera-scope: opportunities:read
      parameters:
        - $ref: '#/components/parameters/Asset'
        - $ref: '#/components/parameters/Horizon'
        - $ref: '#/components/parameters/AsOf'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /analogs:
    get:
      tags: [Evidence]
      summary: Weighted historical analogs and their later outcomes
      x-solstera-scope: analogs:read
      parameters:
        - $ref: '#/components/parameters/Asset'
        - $ref: '#/components/parameters/Horizon'
        - $ref: '#/components/parameters/AsOf'
        - $ref: '#/components/parameters/AnalogLimit'
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /days-like-today:
    get:
      tags: [Evidence]
      summary: Historical days most like the selected market day, with explanations and realized outcomes
      description: >-
        Returns the exact persisted analog set used by the signed daily report. Similarity is
        explanatory context, not a causal claim or a guarantee that outcomes will repeat.
      x-solstera-scope: analogs:read
      parameters:
        - $ref: '#/components/parameters/Asset'
        - $ref: '#/components/parameters/Horizon'
        - $ref: '#/components/parameters/AsOf'
        - $ref: '#/components/parameters/AnalogLimit'
      responses:
        '200':
          description: Signed days-like-today object
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DaysLikeTodayEnvelope' }
        '400':
          description: Invalid horizon
        '404': { $ref: '#/components/responses/NotFound' }
  /recommendations/history:
    get:
      tags: [Evidence]
      summary: Immutable recommendations joined to later realized outcomes
      x-solstera-scope: performance:read
      parameters:
        - $ref: '#/components/parameters/Asset'
        - $ref: '#/components/parameters/Horizon'
        - name: status
          in: query
          schema: { type: string, enum: [all, open, resolved], default: all }
        - $ref: '#/components/parameters/RecommendationLimit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /model-health:
    get:
      tags: [Evidence]
      summary: Current and historical model-health evidence
      x-solstera-scope: performance:read
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /research/ask:
    post:
      tags: [Intelligence]
      summary: Ask a bounded research question about the signed daily report
      description: >-
        The copilot is grounded only in the selected persisted report. It may explain
        analogs, forecasts, ratings, outcomes, and limitations, but it does not browse
        outside the report and never places or recommends a brokerage order.
      x-solstera-scope: research:ask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [question]
              properties:
                question: { type: string, minLength: 1, maxLength: 1200 }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '400':
          description: Missing or oversized question
        '404': { $ref: '#/components/responses/NotFound' }
        '503':
          description: Research copilot is not configured
  /assets:
    get:
      tags: [Intelligence]
      summary: Supported asset and horizon registry
      x-solstera-scope: intelligence:read
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /portfolio/{account}:
    get:
      tags: [Evidence]
      summary: Read-only portfolio ledger; never an order interface
      x-solstera-scope: portfolio:read
      parameters:
        - name: account
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/NotFound' }
  /portfolio:
    get:
      tags: [Evidence]
      summary: Read-only portfolio ledger for all accessible accounts
      x-solstera-scope: portfolio:read
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /usage:
    get:
      tags: [Integration]
      summary: Current key's 30-day request and model-usage summary
      description: >-
        Returns request counts plus any report-grounded copilot token usage attributed
        to this key. Estimated model cost is informational and is not an invoice.
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /webhooks:
    get:
      tags: [Integration]
      summary: List the tenant's webhook subscriptions
      x-solstera-scope: webhooks:manage
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      tags: [Integration]
      summary: Create a signed webhook subscription
      description: >-
        The signing secret is returned once. Deliveries include solstera-event,
        solstera-delivery, solstera-timestamp, and solstera-signature headers.
        Verify the exact raw body before parsing, allow no more than five minutes
        of clock skew, and retain delivery identifiers to reject replays.
      x-solstera-scope: webhooks:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, pattern: '^https://' }
                description: { type: string, maxLength: 200 }
                events:
                  type: array
                  minItems: 1
                  items: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '201': { $ref: '#/components/responses/Success' }
  /webhooks/{id}:
    delete:
      tags: [Integration]
      summary: Revoke a webhook subscription
      x-solstera-scope: webhooks:manage
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/NotFound' }
  /webhooks/{id}/test:
    post:
      tags: [Integration]
      summary: Send a signed test delivery
      x-solstera-scope: webhooks:manage
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200': { $ref: '#/components/responses/Success' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: smi_test_… or smi_live_…
  parameters:
    Asset:
      name: asset
      in: query
      schema: { type: string }
    Horizon:
      name: horizon
      in: query
      schema: { $ref: '#/components/schemas/Horizon' }
    AsOf:
      name: as_of
      in: query
      description: Optional persisted artifact date
      schema: { type: string, format: date }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
    RecommendationLimit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 250, default: 100 }
    AnalogLimit:
      name: limit
      in: query
      description: The signed daily report persists at most six report-level matches
      schema: { type: integer, minimum: 1, maximum: 6, default: 6 }
    Offset:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, default: 0 }
    WebhookId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
  schemas:
    Horizon:
      type: string
      enum: [1w, 1m, 3m, 6m, 9m, 12m, 24m]
    WebhookEvent:
      type: string
      enum: [intelligence.report.published, opportunity.rating.changed, regime.changed, model_health.changed]
    DataProvenance:
      type: object
      required: [mode, market_provider, sources, fred_series_clearance_count, disclaimer]
      properties:
        mode: { type: string, enum: [synthetic, live] }
        market_provider: { type: string }
        fred_series_clearance_count: { type: integer, minimum: 0 }
        disclaimer: { type: string }
        sources:
          type: array
          items:
            type: object
            required: [id, name, enabled, rights_status]
            properties:
              id: { type: string }
              name: { type: string }
              role: { type: [string, 'null'] }
              enabled: { type: boolean }
              rights_status: { type: string }
              official_terms: { type: [string, 'null'], format: uri }
    FeatureEvidence:
      type: object
      required: [feature, current_value, historical_value, standardized_gap]
      properties:
        feature: { type: string }
        current_value: { type: number }
        historical_value: { type: number }
        standardized_gap: { type: number, minimum: 0 }
    SimilarityEvidence:
      type: object
      required: [source, score, distance, comparability, event_similarity, structural_penalty]
      properties:
        source: { type: string, enum: [persisted, unavailable] }
        score: { type: [number, 'null'], minimum: 0, maximum: 1 }
        distance: { type: [number, 'null'], minimum: 0 }
        comparability: { type: [number, 'null'], minimum: 0, maximum: 1 }
        event_comparability: { type: [number, 'null'], minimum: 0, maximum: 1 }
        event_similarity: { type: [number, 'null'], minimum: -1, maximum: 1 }
        structural_penalty: { type: [number, 'null'] }
    RealizedOutcome:
      type: object
      required: [return, target_date, observed, observable_as_of, reason]
      properties:
        return: { type: [number, 'null'] }
        target_date: { type: [string, 'null'], format: date }
        observed: { type: boolean }
        observable_as_of: { type: [string, 'null'], format: date }
        reason: { type: [string, 'null'] }
    DaysLikeTodayMatch:
      type: object
      required: [rank, historical_date, weight, similarity, key_similarities, key_differences, realized_outcomes]
      properties:
        rank: { type: integer, minimum: 1 }
        historical_date: { type: string, format: date }
        weight: { type: [number, 'null'], minimum: 0, maximum: 1 }
        similarity: { $ref: '#/components/schemas/SimilarityEvidence' }
        comparable_feature_count: { type: [integer, 'null'], minimum: 0 }
        key_similarities:
          type: array
          items: { $ref: '#/components/schemas/FeatureEvidence' }
        key_differences:
          type: array
          items: { $ref: '#/components/schemas/FeatureEvidence' }
        realized_outcomes:
          type: object
          additionalProperties:
            type: object
            additionalProperties: { $ref: '#/components/schemas/RealizedOutcome' }
    DaysLikeToday:
      type: object
      required: [object, schema_version, as_of, model_version, source_signature, horizons, methodology, current_day, matches, outcome_summary, limitations]
      properties:
        object: { const: days_like_today }
        schema_version: { const: '1.0' }
        as_of: { type: string, format: date }
        model_version: { type: string }
        source_signature: { type: string }
        horizons:
          type: array
          items: { $ref: '#/components/schemas/Horizon' }
        methodology: { type: object }
        current_day: { type: object }
        matches:
          type: array
          items: { $ref: '#/components/schemas/DaysLikeTodayMatch' }
        outcome_summary: { type: object }
        limitations:
          type: array
          items:
            type: object
            required: [code, explanation]
            properties:
              code: { type: string }
              explanation: { type: string }
    Signature:
      type: object
      required: [algorithm, value]
      properties:
        algorithm: { type: string, enum: [ed25519] }
        key_id: { type: string }
        value: { type: string }
    Envelope:
      type: object
      required: [meta, data, signature]
      properties:
        meta:
          type: object
          required: [api_version, environment, generated_at, request_id, request_method, request_path, disclaimer]
          properties:
            api_version: { const: v1 }
            environment: { type: string, enum: [test, live] }
            generated_at: { type: string, format: date-time }
            request_id: { type: string, format: uuid }
            request_method: { type: string, enum: [GET, HEAD, POST, DELETE] }
            request_path:
              type: string
              description: Exact API path and query string bound into this response signature
            source_artifact: { type: [string, 'null'] }
            disclaimer: { type: string }
        data: {}
        signature: { $ref: '#/components/schemas/Signature' }
    DaysLikeTodayEnvelope:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/DaysLikeToday' }
    Error:
      type: object
      properties:
        error: { type: string }
        request_id: { type: string }
  responses:
    Success:
      description: Signed response envelope
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
    NotFound:
      description: Persisted resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
