openapi: 3.1.0
info:
  title: Maelia Observatory API
  version: "1.0"
  description: |
    Proprietary eSIM market data from the Maelia Observatory: monthly frozen
    price-per-GB index (11-reseller panel, FX guard), a best-plan resolver,
    and real-world field-test measurements.

    **Payment**: endpoints are metered via the x402 protocol. An unpaid request
    returns `402 Payment Required` with an `accepts` array describing price,
    network and pay-to address; retry with the `X-PAYMENT` header. Testnet:
    Base Sepolia via the default facilitator. Mainnet: Base via CDP facilitator.

    **Contract**: v1 is additive-only. Fields are never renamed, removed or
    retyped. Nullable fields are always present with `null`. Monthly snapshots
    are immutable: identical (country, month) requests return identical data
    forever.
  contact:
    name: Maelia Observatory
    url: https://driftasia.com/methodology
  x-license-terms: proprietary-agent-use — agent/internal use permitted, dataset republication prohibited

servers:
  - url: https://api.driftasia.com/v1
    description: Production hub (single hub for all language families)

tags:
  - name: price-data
    description: Frozen monthly price index
  - name: resolver
    description: Decision endpoints
  - name: field-data
    description: Real-world measurements

paths:
  /price-index/{country}:
    get:
      tags: [price-data]
      operationId: getPriceIndex
      summary: Monthly frozen price-per-GB index for one country (full panel)
      description: >
        Full panel of tracked plans plus pre-computed aggregates
        (cheapest / median / most expensive price-per-GB). Served from
        immutable monthly snapshots.
      x-x402:
        price: "$0.01"
        asset: USDC
      parameters:
        - name: country
          in: path
          required: true
          schema:
            type: string
            pattern: "^[a-z]{2}$"
          description: ISO 3166-1 alpha-2, lowercase.
          example: th
        - name: month
          in: query
          required: false
          schema:
            type: string
            pattern: "^[0-9]{4}-(0[1-9]|1[0-2])$"
          description: Snapshot month (YYYY-MM). Omit for the latest frozen snapshot.
        - name: profile
          in: query
          required: false
          schema:
            type: string
            pattern: "^[0-9]+gb-[0-9]+d$"
          description: "Optional plan filter: '{gb}gb-{days}d' (e.g. 10gb-15d)."
      responses:
        "200":
          description: Price index for the resolved snapshot.
          content:
            application/json:
              schema:
                $ref: "./schemas/price-index.schema.json"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /best-plan:
    get:
      tags: [resolver]
      operationId: getBestPlan
      summary: Best plan for a stated need, with single-criterion alternatives
      description: >
        Decision endpoint. Returns one `recommended` plan (composite score:
        need coverage x price-per-GB x field-tested speed, weighted by
        `priority`) plus `alternatives` optimizing a single criterion each
        (cheapest / most_data / best_speed). This is where the price index
        and the field-test dataset intersect.
      x-x402:
        price: "$0.005"
        asset: USDC
      parameters:
        - name: country
          in: query
          required: true
          schema:
            type: string
            pattern: "^[a-z]{2}$"
          example: th
        - name: gb
          in: query
          required: false
          schema:
            type: number
            exclusiveMinimum: 0
          description: Data need in GB. Omit to let the resolver optimize overall value.
        - name: days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
          description: Trip duration in days.
        - name: priority
          in: query
          required: false
          schema:
            type: string
            enum: [balanced, cheapest, most_data, best_speed]
            default: balanced
        - name: unlimited_ok
          in: query
          required: false
          schema:
            type: boolean
            default: true
      responses:
        "200":
          description: Recommendation plus alternatives.
          content:
            application/json:
              schema:
                $ref: "./schemas/best-plan.schema.json"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /field-tests/{country}:
    get:
      tags: [field-data]
      operationId: getFieldTests
      summary: Real-world speed measurements (raw or aggregated)
      description: >
        Raw speedtest measurements taken through purchasable eSIMs
        (operator, city, generalized location context, DL/UL/latency,
        network type, reseller traceability), or per-(operator, city)
        statistics with `aggregate=true`. An empty result is a valid 200
        with trust.coverage = "none".
      x-x402:
        price: "$0.03"
        asset: USDC
      parameters:
        - name: country
          in: path
          required: true
          schema:
            type: string
            pattern: "^[a-z]{2}$"
          example: th
        - name: city
          in: query
          required: false
          schema:
            type: string
            pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"
          example: bangkok
        - name: operator
          in: query
          required: false
          schema:
            type: string
            pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"
          example: ais
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Minimum measurement date.
        - name: aggregate
          in: query
          required: false
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Measurements (raw mode) or statistics (aggregate mode).
          content:
            application/json:
              schema:
                $ref: "./schemas/field-tests.schema.json"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  responses:
    PaymentRequired:
      description: >
        x402 payment challenge. Retry the same request with the X-PAYMENT
        header carrying a signed payment payload for one of the accepted
        schemes. See https://x402.org for client libraries.
      content:
        application/json:
          schema:
            type: object
            properties:
              x402Version:
                type: integer
              accepts:
                type: array
                items:
                  type: object
                  description: Payment requirement (scheme, network, amount, payTo, asset, resource).
              error:
                type: string
      headers: {}
    BadRequest:
      description: Malformed parameter.
      content:
        application/json:
          schema:
            $ref: "./schemas/common.schema.json#/$defs/error_response"
    NotFound:
      description: Country outside panel, or requested snapshot does not exist.
      content:
        application/json:
          schema:
            $ref: "./schemas/common.schema.json#/$defs/error_response"
    RateLimited:
      description: Anti-abuse quota exceeded.
      content:
        application/json:
          schema:
            $ref: "./schemas/common.schema.json#/$defs/error_response"
