openapi: 3.0.3
info:
  title: Prophecy Market Ratings API
  version: 0.1.0
  description: |
    The Market Ratings API provides:
    - rating bands
    - scoring criteria and weights
    - rating results for market definitions

    Ratings are based on market-definition quality only. Scores are normalized
    to a 0-100 scale and combined using a weighted sum.

    Each criterion is either:
    - static: rule-based or procedural evaluation
    - llm: language-model-based semantic evaluation

    Criteria can be:
    - gate: must pass for rating to proceed
    - score: contributes to the weighted final score

tags:
  - name: Ratings
    description: Rating metadata and market ratings
  - name: Badges
    description: SVG badge images for ratings

paths:
  /v0/ratings/bands:
    get:
      tags: [Ratings]
      operationId: listRatingBands
      summary: List rating bands
      description: Returns the mapping from score ranges to rating labels and definitions.
      responses:
        '200':
          description: Rating bands
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RatingBandsResponse'
              example:
                bands:
                  - rating: Aaa
                    min_score: 90
                    max_score: 100
                    definition: Exceptional definition quality; highly reliable and unambiguous.
                  - rating: Aa
                    min_score: 80
                    max_score: 89
                    definition: Very strong; minor weaknesses.
                  - rating: A
                    min_score: 70
                    max_score: 79
                    definition: Strong; some limitations.
                  - rating: Baa
                    min_score: 60
                    max_score: 69
                    definition: Adequate; moderate ambiguity or risk.
                  - rating: Ba
                    min_score: 50
                    max_score: 59
                    definition: Speculative; notable weaknesses.
                  - rating: B
                    min_score: 35
                    max_score: 49
                    definition: Weak; high risk of poor resolution.
                  - rating: Caa
                    min_score: 20
                    max_score: 34
                    definition: Very weak; high likelihood of problematic resolution.
                  - rating: Ca
                    min_score: 10
                    max_score: 19
                    definition: Highly unreliable; severe structural issues.
                  - rating: C
                    min_score: 0
                    max_score: 9
                    definition: Structurally broken or guaranteed to fail resolution.

  /v0/ratings/criteria:
    get:
      tags: [Ratings]
      operationId: listRatingCriteria
      summary: List rating criteria
      description: |
        Returns the criteria used to score market-definition quality.

        Each criterion:
        - is either a gate (must pass) or a score (weighted contribution)
        - is evaluated using either static rules or an LLM
      responses:
        '200':
          description: Criteria metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CriteriaResponse'
              example:
                scoring_scale:
                  min: 0
                  max: 100
                  unit: normalized_score
                final_score_formula: weighted_sum
                criteria:
                  - key: source_reachability
                    name: Source Reachability
                    description: Are the provided source URLs reachable by an agent?
                    criterion_type: gate
                    evaluation: static
                  - key: source_blocklisted
                    name: Source Blocklisted
                    description: Are any of the sources on a blocklist?
                    criterion_type: gate
                    evaluation: static
                  - key: source_count
                    name: Source Count
                    description: Are there enough sources for the market to be resolved fairly?
                    weight: 0.2
                    criterion_type: score
                    evaluation: static
                  - key: source_agreement
                    name: Source Agreement
                    description: Is the percentage of sources that agree high enough?
                    weight: 0.2
                    criterion_type: score
                    evaluation: static
                  - key: source_history
                    name: Source History
                    description: Has the source been used in previous markets that successfully resolved?
                    weight: 0.1
                    criterion_type: score
                    evaluation: static
                  - key: prompt_subjectivity
                    name: Prompt Subjectivity
                    description: How precise, specific and unambiguous is the evaluation prompt?
                    weight: 0.1
                    criterion_type: score
                    evaluation: llm
                  - key: temporal_soundness
                    name: Temporal Soundness
                    description: For a time-based market, does the source encode the time window being evaluated?
                    weight: 0.1
                    criterion_type: score
                    evaluation: llm
                  - key: source_relevancy
                    name: Source Relevancy
                    description: Are the sources likely to contain the information described in the prompt?
                    weight: 0.2
                    criterion_type: score
                    evaluation: llm

  /v0/ratings/markets/{id}:
    get:
      tags: [Ratings]
      operationId: getMarketRating
      summary: Get a market rating by ID
      description: Returns a previously stored rating for a market definition identified by its ID (first 16 bytes of the SHA-256 hash, hex-encoded).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Market rating result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketDefinitionRatingResponse'
        '404':
          description: No rating found for this ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v0/ratings/markets/latest:
    get:
      tags: [Ratings]
      operationId: listLatestRatings
      summary: List the most recent market ratings
      description: Returns the most recently created market ratings, ordered by creation time descending. Uses cursor-based pagination.
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - name: limit
          in: query
          required: false
          description: Maximum number of ratings to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: List of recent market ratings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LatestRatingsResponse'

  /v0/ratings/markets:
    post:
      tags: [Ratings]
      operationId: rateMarketDefinition
      summary: Rate a market definition
      description: |
        Returns the rating result for a provided market definition.

        The response includes:
        - final weighted score
        - assigned rating band
        - per-criterion scoring
        - warnings
        - optional supporting evidence
      parameters:
        - name: include_evidence
          in: query
          required: false
          description: Whether to include supporting evidence and criterion-level reports.
          schema:
            type: boolean
            default: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarketDefinition'
            example:
              prompt: "Who won the 2026 Australian Grand Prix? Return the exact driver name."
              answer_type: string
              expected_results:
                - "Max Verstappen"
              comparison_operator: "=="
              source_urls:
                - "https://www.formula1.com/en/results/2026/races/1279/australia/race-result"
                - "https://racingnews365.com/2026-f1-australian-grand-prix-results"
              min_agreement: 2
              resolution_start: "2026-03-23T00:00:00Z"
              resolution_end: "2026-03-24T00:00:00Z"
      responses:
        '200':
          description: Market rating result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketDefinitionRatingResponse'
              example:
                rated_at: "2026-03-23T16:20:00Z"
                final_score: 86.4
                rating_band:
                  rating: Aa
                  min_score: 80
                  max_score: 89
                  definition: Very strong; minor weaknesses.
                criteria:
                  - key: source_reachability
                    name: Source Reachability
                    criterion_type: gate
                    evaluation: static
                    status: pass
                    summary: All source URLs are reachable.
                    evidence:
                      checks:
                        - code: URL_REACHABLE
                          target: "https://www.formula1.com/en/results/2026/races/1279/australia/race-result"
                          status: pass
                        - code: URL_REACHABLE
                          target: "https://racingnews365.com/2026-f1-australian-grand-prix-results"
                          status: pass
                  - key: source_count
                    name: Source Count
                    weight: 0.2
                    score: 90
                    weighted_score: 18
                    criterion_type: score
                    evaluation: static
                    status: pass
                    summary: Sufficient sources provided.
                    evidence:
                      checks:
                        - code: MIN_SOURCES
                          status: pass
                          detail: "2 sources provided, minimum is 1"
                  - key: source_relevancy
                    name: Source Relevancy
                    weight: 0.2
                    score: 85
                    weighted_score: 17
                    criterion_type: score
                    evaluation: llm
                    status: pass
                    summary: Sources are highly relevant to the prompt.
                    evidence:
                      reasoning: |
                        The prompt asks for the winner of the 2026 Australian Grand Prix.
                        Source 1 (formula1.com) is the official F1 website's race results page for exactly this race.
                        Source 2 (racingnews365.com) is a dedicated F1 news site with results for the same race.
                        Both sources are highly likely to contain the exact driver name requested.
                      score_breakdown:
                        - url: "https://www.formula1.com/en/results/2026/races/1279/australia/race-result"
                          score: 95
                        - url: "https://racingnews365.com/2026-f1-australian-grand-prix-results"
                          score: 75
                warnings:
                  - code: CONFIG_MISMATCH
                    message: num_sources (1) is less than urls provided (2).
        '400':
          description: Invalid market definition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Rating could not be produced (e.g., gate criterion failed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v0/ratings/bands/{bandId}/badge.svg:
    get:
      tags: [Badges]
      operationId: getBandBadge
      summary: Get a rating band badge as SVG
      description: Returns an SVG badge image for the specified rating band. Returns an error badge SVG if the band is not found.
      parameters:
        - name: bandId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/Rating'
        - $ref: '#/components/parameters/BadgeSize'
        - $ref: '#/components/parameters/BadgeTheme'
        - $ref: '#/components/parameters/BadgeWidth'
        - $ref: '#/components/parameters/BadgeHeight'
      responses:
        '200':
          description: SVG badge image
          content:
            image/svg+xml:
              schema:
                type: string

  /v0/ratings/markets/{id}/badge.svg:
    get:
      tags: [Badges]
      operationId: getMarketBadge
      summary: Get a market rating badge as SVG
      description: Returns an SVG badge image for the specified market's rating. Returns an error badge SVG if the market is not found.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/BadgeSize'
        - $ref: '#/components/parameters/BadgeTheme'
        - $ref: '#/components/parameters/BadgeWidth'
        - $ref: '#/components/parameters/BadgeHeight'
      responses:
        '200':
          description: SVG badge image
          content:
            image/svg+xml:
              schema:
                type: string

components:
  parameters:
    Cursor:
      name: cursor
      in: query
      description: Pagination cursor from previous response
      schema:
        $ref: '#/components/schemas/Cursor'
    BadgeSize:
      name: size
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/BadgeSize'
    BadgeTheme:
      name: theme
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/BadgeTheme'
    BadgeWidth:
      name: width
      in: query
      required: false
      description: Custom width in pixels. Aspect ratio is preserved — providing width computes height automatically. Overrides the size preset.
      schema:
        type: integer
        minimum: 100
        maximum: 400
    BadgeHeight:
      name: height
      in: query
      required: false
      description: Custom height in pixels. Aspect ratio is preserved — providing height computes width automatically. Overrides the size preset.
      schema:
        type: integer
        minimum: 20
        maximum: 90

  schemas:
    Cursor:
      type: string
      description: Opaque pagination cursor from a previous response.

    BadgeSize:
      type: string
      enum: [small, medium, large]
      default: medium

    BadgeTheme:
      type: string
      enum: [dark, light]
      default: dark

    PaginatedList:
      type: object
      required: [has_more]
      properties:
        has_more:
          type: boolean
        next_cursor:
          $ref: '#/components/schemas/Cursor'

    AnswerType:
      type: string
      enum: [string, number]

    ComparisonOperator:
      type: string
      enum: ['==', '!=', '>', '>=', '<', '<=']

    MarketDefinition:
      type: object
      required: [prompt, answer_type, expected_results, source_urls, min_agreement, resolution_start, resolution_end]
      properties:
        prompt:
          type: string
          description: The question or prompt that defines what the market is resolving.
        answer_type:
          $ref: '#/components/schemas/AnswerType'
        expected_results:
          oneOf:
            - type: array
              items:
                type: string
              minItems: 1
            - type: array
              items:
                type: number
              minItems: 1
          description: Expected result values. All must be the same type, matching answer_type.
        comparison_operator:
          $ref: '#/components/schemas/ComparisonOperator'
        source_urls:
          type: array
          items:
            type: string
            format: uri
          description: URLs to use as resolution sources.
        min_agreement:
          type: integer
          minimum: 1
          description: Minimum number of sources that must agree for resolution.
        resolution_start:
          type: string
          format: date-time
          description: When resolution period begins.
        resolution_end:
          type: string
          format: date-time
          description: When resolution period ends.

    Rating:
      type: string
      enum: [Aaa, Aa, A, Baa, Ba, B, Caa, Ca, C]

    CriterionType:
      type: string
      enum: [gate, score]
      description: |
        - gate: must pass for rating to proceed
        - score: contributes to the weighted final score

    Evaluation:
      type: string
      enum: [static, llm]
      description: |
        - static: rule-based or procedural evaluation
        - llm: language-model-based semantic evaluation

    CheckStatus:
      type: string
      enum: [pass, fail, warning, not_run]

    RatingBand:
      type: object
      required: [rating, min_score, max_score, definition]
      properties:
        rating:
          $ref: '#/components/schemas/Rating'
        min_score:
          type: number
          minimum: 0
          maximum: 100
        max_score:
          type: number
          minimum: 0
          maximum: 100
        definition:
          type: string

    RatingBandsResponse:
      type: object
      required: [bands]
      properties:
        bands:
          type: array
          items:
            $ref: '#/components/schemas/RatingBand'

    ScoringScale:
      type: object
      required: [min, max, unit]
      properties:
        min:
          type: number
          enum: [0]
        max:
          type: number
          enum: [100]
        unit:
          type: string
          enum: [normalized_score]

    CriterionDefinition:
      type: object
      required: [key, name, description, criterion_type, evaluation]
      properties:
        key:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
        name:
          type: string
        description:
          type: string
        criterion_type:
          $ref: '#/components/schemas/CriterionType'
        evaluation:
          $ref: '#/components/schemas/Evaluation'
        weight:
          type: number
          minimum: 0
          maximum: 1
          description: Required for score criteria, not applicable for gate criteria.

    CriteriaResponse:
      type: object
      required: [scoring_scale, final_score_formula, criteria]
      properties:
        scoring_scale:
          $ref: '#/components/schemas/ScoringScale'
        final_score_formula:
          type: string
          enum: [weighted_sum]
        criteria:
          type: array
          items:
            $ref: '#/components/schemas/CriterionDefinition'

    StaticCheck:
      type: object
      required: [code, status]
      properties:
        code:
          type: string
        target:
          type: string
          description: The target of the check (e.g., URL, field name).
        status:
          $ref: '#/components/schemas/CheckStatus'
        detail:
          type: string

    CriterionEvidenceStatic:
      type: object
      required: [checks]
      properties:
        checks:
          type: array
          items:
            $ref: '#/components/schemas/StaticCheck'

    CriterionEvidenceLlm:
      type: object
      required: [reasoning]
      properties:
        reasoning:
          type: string
          description: Detailed thought process from the LLM explaining how it arrived at the score.
        score_breakdown:
          type: array
          description: Breakdown of scores for sub-components (e.g., individual URLs, options, etc).
          items:
            type: object
            additionalProperties: true
            description: Flexible object containing sub-component details and scores.

    CriterionEvidence:
      oneOf:
        - $ref: '#/components/schemas/CriterionEvidenceStatic'
        - $ref: '#/components/schemas/CriterionEvidenceLlm'

    CriterionRating:
      type: object
      required:
        - key
        - name
        - criterion_type
        - evaluation
        - status
      properties:
        key:
          type: string
        name:
          type: string
        criterion_type:
          $ref: '#/components/schemas/CriterionType'
        evaluation:
          $ref: '#/components/schemas/Evaluation'
        weight:
          type: number
          minimum: 0
          maximum: 1
          description: Present only for score criteria.
        score:
          type: number
          minimum: 0
          maximum: 100
          description: Present only for score criteria.
        weighted_score:
          type: number
          minimum: 0
          maximum: 100
          description: Present only for score criteria.
        status:
          type: string
          enum: [pass, warning, fail, not_run]
        summary:
          type: string
        evidence:
          $ref: '#/components/schemas/CriterionEvidence'

    Warning:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string

    MarketDefinitionRatingResponse:
      type: object
      required:
        - id
        - market_hash
        - rating_version
        - rated_at
        - final_score
        - rating_band
        - criteria
      properties:
        id:
          type: string
          description: Short identifier (first 16 bytes of the SHA-256 hash, hex-encoded).
        market_hash:
          type: string
          description: SHA-256 hash of the canonical market definition.
        rating_version:
          type: string
          description: Version of the rating engine that produced this rating.
        rated_at:
          type: string
          format: date-time
        final_score:
          type: number
          minimum: 0
          maximum: 100
        rating_band:
          $ref: '#/components/schemas/RatingBand'
        criteria:
          type: array
          items:
            $ref: '#/components/schemas/CriterionRating'
        market_definition:
          $ref: '#/components/schemas/MarketDefinition'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/Warning'

    LatestRatingEntry:
      type: object
      required: [id, market_hash, prompt, rating, final_score, rated_at, rating_version]
      properties:
        id:
          type: string
          description: Short identifier (first 16 bytes of the SHA-256 hash, hex-encoded).
        market_hash:
          type: string
        prompt:
          type: string
          description: The market prompt extracted from the stored definition.
        rating:
          $ref: '#/components/schemas/Rating'
        final_score:
          type: number
          minimum: 0
          maximum: 100
        rated_at:
          type: string
          format: date-time
        rating_version:
          type: string

    LatestRatingsResponse:
      allOf:
        - $ref: '#/components/schemas/PaginatedList'
        - type: object
          required: [ratings]
          properties:
            ratings:
              type: array
              items:
                $ref: '#/components/schemas/LatestRatingEntry'

    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          allOf:
            - $ref: '#/components/schemas/Error'
            - type: object
              properties:
                details:
                  type: array
                  items:
                    type: string
