# §spec/event-stream — the /ws contract, machine-readable (the WS twin of
# openapi.yaml; served at /asyncapi.yaml). Every frame is one JSON text message
# { type, at, data } — `type` is the append-only vocabulary (retired names are
# never reused, like error_key), `at` is the server's ISO-8601 publish time.
# Account-scoped events also carry data.balances (the account's tiers at publish
# time, same shape as the REST read) so consumers never need a follow-up fetch —
# absent only when the daemon is unreachable. The intg lane validates emitted
# frames against these schemas (the WS drift wall, mirroring the HTTP one).
asyncapi: 3.0.0
info:
  title: plurnk.web /ws event stream
  version: 1.0.0
  description: >
    Public socket: every connection receives `hello`, then `status` and `rates`
    snapshots (re-broadcast on change). An AUTHENTICATED upgrade additionally
    receives that account's money events: an initial `balance` snapshot, then
    `deposit_credited`, `deposit_tier`, and `inference_debit` as they happen.
    Authentication is either a same-origin `pk.sid` browser session cookie or
    an `Authorization: Bearer pct_...` OAuth control token bound to the exact
    `/api/v1` resource with `plurnk:control`. Query-string credentials, `pk_`
    inference/search usage keys, and `/mcp`-bound control tokens always remain
    on the anonymous public stream.

channels:
  ws:
    address: /ws
    messages:
      hello:
        $ref: "#/components/messages/hello"
      status:
        $ref: "#/components/messages/status"
      rates:
        $ref: "#/components/messages/rates"
      balance:
        $ref: "#/components/messages/balance"
      deposit_credited:
        $ref: "#/components/messages/deposit_credited"
      deposit_tier:
        $ref: "#/components/messages/deposit_tier"
      inference_debit:
        $ref: "#/components/messages/inference_debit"

operations:
  receiveEvents:
    action: receive
    channel:
      $ref: "#/channels/ws"

components:
  schemas:
    piconero:
      type: string
      description: Exact piconero as a BigInt-safe decimal string.
      pattern: "^-?[0-9]+$"
    balances:
      type: object
      description: The account's tiers at publish time; absent when the daemon
        is unreachable (fail-closed, never fabricated).
      required: [spendable_piconero]
      properties:
        spendable_piconero: { $ref: "#/components/schemas/piconero" }
    envelope:
      type: object
      required: [type, at]
      properties:
        type: { type: string }
        at: { type: string, description: Server publish time, ISO-8601. }

  messages:
    hello:
      summary: Connection acknowledgment — sent to every socket on open.
      payload:
        type: object
        required: [type, at]
        properties:
          type: { const: hello }
          at: { type: string }

    status:
      summary: Public subsystem health — a banner reveals ONLY on an offline
        signal (fail-closed lives client-side).
      payload:
        type: object
        required: [type, at, data]
        properties:
          type: { const: status }
          at: { type: string }
          data:
            type: object
            required: [model_offline, oracle_offline, wallet_offline]
            properties:
              model_offline: { type: boolean }
              oracle_offline: { type: boolean }
              wallet_offline: { type: boolean }

    rates:
      summary: The live pricing snapshot (the endpoint owns the numbers;
        display-only, never the money path).
      payload:
        type: object
        required: [type, at, data]
        properties:
          type: { const: rates }
          at: { type: string }
          data:
            type: object
            required: [xmr_usd, services, fetched_at]
            properties:
              xmr_usd: {}
              fetched_at: { type: string }
              # One entry per SERVICE (endpoint configuration); the token-
              # category prices are its columns. The marketplace metrics
              # (context_tokens, tq, conformance_pct, latency_p50/p95_ms) are the
              # endpoint's measurements — present only when real; the prices are
              # required exact piconero (decimal strings).
              services:
                type: array
                items:
                  type: object
                  required: [name, input_piconero, cached_piconero, reasoning_piconero, output_piconero]
                  properties:
                    name: { type: string }
                    context_tokens: { type: integer }
                    tq: {}
                    conformance_pct: {}
                    latency_p50_ms: {}
                    latency_p95_ms: {}
                    input_piconero: { $ref: "#/components/schemas/piconero" }
                    cached_piconero: { $ref: "#/components/schemas/piconero" }
                    reasoning_piconero: { $ref: "#/components/schemas/piconero" }
                    output_piconero: { $ref: "#/components/schemas/piconero" }

    balance:
      summary: The authed connection's initial balance snapshot.
      payload:
        type: object
        required: [type, at, data]
        properties:
          type: { const: balance }
          at: { type: string }
          data:
            type: object
            required: [balances]
            properties:
              balances: { $ref: "#/components/schemas/balances" }

    deposit_credited:
      summary: A chain deposit reached the credited tier (spendable).
      payload:
        type: object
        required: [type, at, data]
        properties:
          type: { const: deposit_credited }
          at: { type: string }
          data:
            type: object
            required: [amount_piconero]
            properties:
              amount_piconero: { $ref: "#/components/schemas/piconero" }
              source_ref: { type: string }
              target_kind: { type: string }
              target_id: {}
              balances: { $ref: "#/components/schemas/balances" }

    deposit_tier:
      summary: A deposit crossed a confirmation tier (credited | settled).
      payload:
        type: object
        required: [type, at, data]
        properties:
          type: { const: deposit_tier }
          at: { type: string }
          data:
            type: object
            required: [tier, amount_piconero]
            properties:
              tier: { enum: [credited, settled] }
              amount_piconero: { $ref: "#/components/schemas/piconero" }
              target_kind: { type: string }
              target_id: {}
              balances: { $ref: "#/components/schemas/balances" }

    inference_debit:
      summary: A metered inference charge landed (reserve or settle).
      payload:
        type: object
        required: [type, at, data]
        properties:
          type: { const: inference_debit }
          at: { type: string }
          data:
            type: object
            required: [amount_piconero, source_ref]
            properties:
              amount_piconero: { $ref: "#/components/schemas/piconero" }
              source_ref: { type: string }
              balances: { $ref: "#/components/schemas/balances" }
