openapi: "3.1.0"
info:
  title: "Roundhouse read API"
  version: "0"
  summary: "The x402 settlement index and agent identity record, as JSON."
  description: |-
    Public, read-only, and free to start — no key required. Roundhouse indexes x402
    settlements and ERC-8004 identities into one queryable record.
    
    Two conventions worth knowing before you build against it:
    
    - **An empty array is an answer; a failure is not.** A refused query returns 503
      `upstream_unavailable` naming the resource, never 200 with an empty list. On a
      settlement index "no rows" would read as "nothing ever happened".
    - **Aggregates are per-entity.** Every figure is one wallet's or one pair's own. Site-wide
      totals are deliberately confined to the /stats page and are not served here.
    
    Cursor-paginated lists carry `next_before`; pass it back as `?before=`. The leaderboard is
    the exception and pages by `?offset=`.
  license:
    name: "Roundhouse terms"
    url: "https://www.roundhouse.studio/terms"
  contact:
    name: Roundhouse
    url: "https://www.roundhouse.studio/docs/api"
servers:
  - url: "https://www.roundhouse.studio/api/v0"
    description: "The live base URL. `api.roundhouse.studio` is not bound yet, so use this origin."
tags:
  - name: Settlements
    description: "The settlement ledger: the feed, and one wallet's slice of it."
  - name: Entities
    description: "Wallets resolved to identities, their own aggregates, and reputation."
  - name: Discovery
    description: "The service catalog, the relayers, and the payment graph."
  - name: Capabilities
    description: "Endpoints grouped by the job they do, so providers of one capability compare on a shared request, response and price."
  - name: SQL
    description: "Read-only SQL for questions the fixed endpoints do not answer."
  - name: KYA
    description: "Signed agent attestations and the payment memos that commit to them."
  - name: x402
    description: "Paying for a Roundhouse resource over x402."
  - name: Telemetry
    description: "Self-reported call telemetry from x402 clients. Kept apart from the settlement index, which is chain-verified."
paths:
  "/flows":
    get:
      operationId: getFlows
      summary: "Live flow of funds"
      description: "The newest settlements across every indexed chain, payer to payee. This is the same feed the `/flows` page renders."
      tags:
        - Settlements
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  flows:
                    description: "Settlement rows, newest first, decorated with resolved display names."
                  next_before:
                    description: "Cursor for the next page, or `null` at the end of the list."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: before
          in: query
          required: false
          description: "Opaque cursor from a previous response's `next_before`. It encodes the whole sort key, tiebreaker included, so a page boundary inside a group of rows sharing a timestamp does not skip the rest. Do not parse it or build one by hand."
          schema:
            type: string
  "/transactions":
    get:
      operationId: getTransactions
      summary: "Global settlement feed"
      description: "Every indexed settlement, newest first, filterable by how confidently it is known to be an x402 payment."
      tags:
        - Settlements
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  transactions:
                    description: "Settlement rows, newest first."
                  next_before:
                    description: "Cursor for the next page, or `null`."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: before
          in: query
          required: false
          description: "Opaque cursor from a previous response's `next_before`. It encodes the whole sort key, tiebreaker included, so a page boundary inside a group of rows sharing a timestamp does not skip the rest. Do not parse it or build one by hand."
          schema:
            type: string
        - name: min_probability
          in: query
          required: false
          description: "Keep only rows at or above this x402 confidence. `1.0` is proven by an EIP-3009 `AuthorizationUsed` marker; `0.05` is a bare transfer to a catalogued address."
          schema:
            type: number
          example: 0.5
  "/entities/{wallet}/settlements":
    get:
      operationId: getEntitySettlements
      summary: "Settlements for one wallet"
      description: "Raw settlements where this wallet is either the payer or the payee. Capped at 100 per page: the settlements indexes are keyed `(payer, chain_id)` and `(payee, chain_id)` without `block_time`, so a wallet's history cannot yet be walked deeply in time order."
      tags:
        - Settlements
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  wallet:
                    description: "The wallet queried, lowercased."
                  settlements:
                    description: "Settlement rows."
                  next_before:
                    description: "Cursor for the next page, or `null`."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: wallet
          in: path
          required: true
          description: "EVM address. Case-insensitive — it is lowercased before lookup."
          schema:
            type: string
          example: "0x6157a191c961290420c68c1aa73c84444d769055"
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: before
          in: query
          required: false
          description: "Opaque cursor from a previous response's `next_before`. It encodes the whole sort key, tiebreaker included, so a page boundary inside a group of rows sharing a timestamp does not skip the rest. Do not parse it or build one by hand."
          schema:
            type: string
  "/agents/{wallet}":
    get:
      operationId: getAgent
      summary: "Per-agent stats and identity"
      description: "One wallet in its paying role: its resolved identity and its own settlement aggregates. Every figure is that wallet's own — there are no site-wide totals on this endpoint."
      tags:
        - Entities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  entity:
                    description: "Resolved identity: display name, ENS, ERC-8004 id, role, first and last seen."
                  stats:
                    description: "That wallet's own settlement aggregates."
                  settlements:
                    description: "A recent slice of its activity."
        "404":
          description: "No indexed entity for this wallet in this role."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: wallet
          in: path
          required: true
          description: "EVM address. Case-insensitive — it is lowercased before lookup."
          schema:
            type: string
          example: "0x6157a191c961290420c68c1aa73c84444d769055"
  "/merchants/{wallet}":
    get:
      operationId: getMerchant
      summary: "Per-merchant stats and identity"
      description: "The same shape as `/agents/{wallet}`, for a wallet in its receiving role."
      tags:
        - Entities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  entity:
                    description: "Resolved identity."
                  stats:
                    description: "That wallet's own settlement aggregates."
                  settlements:
                    description: "A recent slice of its activity."
        "404":
          description: "No indexed entity for this wallet in this role."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: wallet
          in: path
          required: true
          description: "EVM address. Case-insensitive — it is lowercased before lookup."
          schema:
            type: string
          example: "0x6157a191c961290420c68c1aa73c84444d769055"
  "/agents":
    get:
      operationId: listAgents
      summary: "Indexed ERC-8004 agents"
      description: "Agents read from the on-chain IdentityRegistry, with their registration card and payment wallet."
      tags:
        - Entities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  agents:
                    description: "Agent rows with identity, wallet and trust score."
                  next_before:
                    description: "Cursor for the next page, or `null`."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: before
          in: query
          required: false
          description: "Opaque cursor from a previous response's `next_before`. It encodes the whole sort key, tiebreaker included, so a page boundary inside a group of rows sharing a timestamp does not skip the rest. Do not parse it or build one by hand."
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: "Free-text search over name and description."
          schema:
            type: string
        - name: x402
          in: query
          required: false
          description: "Set to `true` to keep only agents with observed on-chain x402 payments."
          schema:
            type: string
            enum:
              - "true"
  "/wallets":
    get:
      operationId: listWallets
      summary: "Every address the index has seen"
      description: "All settling wallets, filterable by the role they were seen in."
      tags:
        - Entities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  wallets:
                    description: "Wallet rows with role and last seen."
                  next_before:
                    description: "Cursor for the next page, or `null`."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: before
          in: query
          required: false
          description: "Opaque cursor from a previous response's `next_before`. It encodes the whole sort key, tiebreaker included, so a page boundary inside a group of rows sharing a timestamp does not skip the rest. Do not parse it or build one by hand."
          schema:
            type: string
        - name: role
          in: query
          required: false
          description: "Restrict to one observed role."
          schema:
            type: string
            enum:
              - agent
              - merchant
              - both
              - unknown
  "/leaderboard":
    get:
      operationId: getLeaderboard
      summary: "Agents ranked by trust"
      description: "A ranked board. It pages by `offset` rather than a cursor, because a rank is a position and `score` is nullable — a keyset has nothing stable to compare against across the scored/unscored boundary."
      tags:
        - Entities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  leaderboard:
                    description: "Ranked agent rows."
                  next_offset:
                    description: "Offset for the next page, or `null`."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: offset
          in: query
          required: false
          description: "Rows to skip. Use the returned `next_offset`."
          schema:
            type: integer
            default: 0
        - name: include_spam
          in: query
          required: false
          description: "Set to `1` to include agents with no feedback, which are hidden by default."
          schema:
            type: string
            enum:
              - "1"
  "/agents/{agentId}/feedback":
    get:
      operationId: getAgentFeedback
      summary: "ERC-8004 feedback for one agent"
      description: "Raw feedback entries from the ReputationRegistry for a single agent id."
      tags:
        - Entities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  agent_id:
                    description: "The id queried."
                  chain:
                    description: "The chain the feedback was read from."
                  feedback:
                    description: "Feedback entries."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: agentId
          in: path
          required: true
          description: "ERC-8004 agent id."
          schema:
            type: string
          example: "1"
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: chain
          in: query
          required: false
          description: "CAIP-2 chain id. An agent id is not unique across chains."
          schema:
            type: string
          example: "eip155:8453"
  "/endpoints":
    get:
      operationId: listEndpoints
      summary: "Indexed x402 service directory"
      description: "The x402 service catalog, ordered by observed 30-day call volume. A listing is a claim; the observed columns are evidence — prefer `is_live` with non-zero `l30_unique_payers`. Note the path stays `/v0/endpoints` although the human page moved to `/services`; renaming it would break published integrations."
      tags:
        - Discovery
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  endpoints:
                    description: "Catalog rows with price, network, payTo, liveness and observed demand."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 200; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 200
        - name: q
          in: query
          required: false
          description: "Free-text search over service name, description and resource URL."
          schema:
            type: string
  "/unified":
    get:
      operationId: listCapabilities
      summary: "The capability catalogue"
      description: "Every job the catalog is grouped by, with the canonical request and response each provider of that job is mapped onto — which is what lets an agent swap one endpoint for another without changing its code. Static: no market figures here, because each one costs its own catalog walk. Ask `/unified/{slug}` for a capability's offers and going rate."
      tags:
        - Capabilities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  capabilities:
                    description: "Slug, name, group, summary, priced unit, and the canonical input/output fields."
                  count:
                    description: "How many capabilities the registry defines."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/unified/{slug}":
    get:
      operationId: getCapability
      summary: "One capability: who offers it, and what it costs"
      description: "Every indexed endpoint that does this job, with the market's going rate. `price.spot` is the call-WEIGHTED price, not the median — a median counts one wallet's forty near-identical listings as forty votes for its price. Check `price.spot_basis`: `median` means no offer reported any calls and the figure is a fallback, which does not mean the same thing as a market rate. `truncated: true` means the offer list is a floor."
      tags:
        - Capabilities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  capability:
                    description: "The registry entry: the job, its priced unit, and its canonical request/response."
                  price:
                    description: "Going rate (`spot`), its basis, and the min / p25 / median / p75 / max spread."
                  stats:
                    description: "How many offers, distinct providers, distinct hosts, and how many were answering when last probed."
                  models:
                    description: "For inference capabilities: which model families are sold here, and by how many providers."
                  offers:
                    description: "Each endpoint, its listed price, its index against spot (100 = the going rate), and its reported 30-day demand."
                  truncated:
                    description: "True when the candidate read hit its cap, so the offer list is a floor rather than the whole market."
        "404":
          description: "No capability with that slug — see /v0/unified for the catalogue."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: slug
          in: path
          required: true
          description: "Capability slug, from `/v0/unified`."
          schema:
            type: string
          example: web-search
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 200; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 200
        - name: probable
          in: query
          required: false
          description: "Include endpoints the matcher was not confident about. Off by default: grouping two endpoints that do different jobs invents a cheaper competitor that does not exist."
          schema:
            type: boolean
            default: false
  "/unified/{slug}/recommend":
    get:
      operationId: recommendCapabilityOffers
      summary: "Shortlist endpoints for a capability"
      description: "A ranked shortlist of endpoints that do this job, under criteria you choose and the response echoes back. Every term is something the endpoint publishes about itself — its listed price against the going rate, and its own reported demand. Roundhouse does not endorse any provider, and there is no cross-capability ranking. Only confident matches are ever recommended."
      tags:
        - Capabilities
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  capability:
                    description: "The registry entry for the job being shortlisted."
                  criteria:
                    description: "Your criteria, echoed — a recommendation whose basis is invisible is an opinion."
                  price:
                    description: "The capability's going rate, so each candidate's index has context."
                  candidates:
                    description: "Ranked endpoints, each with `reasons` for where it placed."
                  considered:
                    description: "How many confident offers were ranked before filters."
                  excluded:
                    description: "How many were dropped, by which filter."
                  truncated:
                    description: "True when the candidate read hit its cap."
        "404":
          description: "No capability with that slug — see /v0/unified for the catalogue."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: slug
          in: path
          required: true
          description: "Capability slug, from `/v0/unified`."
          schema:
            type: string
          example: web-search
        - name: prefer
          in: query
          required: false
          description: "What to weight: cheapest, most-used, or an even split."
          schema:
            type: string
            enum:
              - balanced
              - price
              - demand
            default: balanced
        - name: live
          in: query
          required: false
          description: "Only endpoints answering when last probed."
          schema:
            type: boolean
            default: false
        - name: max_price
          in: query
          required: false
          description: "Hard ceiling in USDC per call. Endpoints with no published price are excluded when set."
          schema:
            type: number
        - name: model
          in: query
          required: false
          description: "Require the endpoint to name this model family (inference capabilities only)."
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 25; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 5
            minimum: 1
            maximum: 25
  "/facilitators":
    get:
      operationId: listFacilitators
      summary: "Relayers, per chain"
      description: "The facilitators seen relaying settlements, with their known relayer wallets."
      tags:
        - Discovery
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  chain:
                    description: "The chain queried."
                  facilitators:
                    description: "Facilitator rows."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: chain
          in: query
          required: false
          description: "CAIP-2 chain id."
          schema:
            type: string
          example: "eip155:8453"
  "/graph":
    get:
      operationId: getGraph
      summary: "Payment graph: nodes and payer→payee edges"
      description: "Entities as nodes and settlements aggregated into edges, over a bounded window. Two window forms: `window=N` is a row count running to the live tip (briefly cacheable); `days=N` covers **whole UTC days only**, excluding today, so an answer is stable for the day. Use `days` for anything an agent will cache."
      tags:
        - Discovery
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  nodes:
                    description: "One entry per wallet: role, display name, ENS."
                  edges:
                    description: "Aggregated payer→payee pairs with volume and count."
                  series:
                    description: "Per-bucket totals across the window."
                  facilitators:
                    description: "Relayers seen in the window."
                  meta:
                    description: "Window bounds and how it was read."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: window
          in: query
          required: false
          description: "Row count back from the live tip."
          schema:
            type: integer
            default: 500
        - name: days
          in: query
          required: false
          description: "Whole UTC days, excluding today. Mutually exclusive with `window`."
          schema:
            type: integer
        - name: wallet
          in: query
          required: false
          description: "Restrict to edges touching this wallet."
          schema:
            type: string
        - name: chain
          in: query
          required: false
          description: "CAIP-2 chain id."
          schema:
            type: string
          example: "eip155:8453"
        - name: via
          in: query
          required: false
          description: "Restrict to settlements relayed by this facilitator key or wallet."
          schema:
            type: string
        - name: min_usd
          in: query
          required: false
          description: "Drop settlements below this USD value."
          schema:
            type: number
        - name: min_probability
          in: query
          required: false
          description: "Minimum x402 confidence."
          schema:
            type: number
        - name: edges
          in: query
          required: false
          description: "Cap on edges returned."
          schema:
            type: integer
  "/telemetry":
    post:
      operationId: ingestTelemetry
      summary: "Submit client-reported x402 call telemetry"
      description: "The sink endpoint for the `@roundhouseai/x402` client's remote telemetry (`telemetry.sink`). Post `{ records: [...] }` — one ledger record per call the client made, paid or not. What lands is self-reported: it is stored apart from the settlement index, is never promoted into it, and is never counted in any aggregate. Request and response bodies are stripped before anything is written, whatever payload level the client sent. Ingest is idempotent on each record's own id, so replaying a batch is safe."
      tags:
        - Telemetry
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  ok:
                    description: "Always true on a stored batch."
                  accepted:
                    description: "Records written. A record already held counts here too."
                  invalid:
                    description: "Records the schema rejected. Reported, never fatal to the batch."
                  truncated:
                    description: "Records dropped for exceeding the per-request cap."
        "400":
          description: "`invalid_json` malformed body, or `invalid_batch` — not `{ records: [...] }`."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "401":
          description: "`invalid_api_key` — not found, revoked, or expired."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "`rate_limited` — 6 batches/minute anonymous, 120 with a key."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        required: true
        description: "`records` is the client ledger batch. A batch above 2,000 records is truncated rather than refused, because refusing one wedges the client outbox replaying it."
        content:
          "application/json":
            schema:
              type: object
            example:
              records:
                - id: "01J8ZK3M9Q"
                  ts: "2026-08-26T13:43:02.114Z"
                  url: "https://provider.example/v1/lei"
                  method: GET
                  payment:
                    paid: true
                    amount: "0.002"
                    asset: usdc
                    chain: "eip155:8453"
                  response:
                    status: 200
                    latencyMs: 412
                  exitCode: 0
      security:
        - {}
        - bearerAuth: []
        - apiKeyHeader: []
  "/sql":
    post:
      operationId: runSql
      summary: "Read-only SQL over the public dataset"
      description: "One `SELECT` (or `WITH`) over the public data-layer tables. Enforced in the database, not the worker: a read-only transaction as a role that can see only those tables, an 8-second statement timeout and a hard 300-row cap. No semicolons, no comments, no multi-statement."
      tags:
        - SQL
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  rows:
                    description: "Result rows."
                  row_count:
                    description: "Number of rows returned."
                  duration_ms:
                    description: "Server-side execution time."
                  authenticated:
                    description: "Whether a key was accepted."
                  tier:
                    description: "`anonymous`, `trial`, or an organization tier."
        "400":
          description: "`invalid_query` failed the sandbox checks, `invalid_json` malformed body, or `query_failed` ran and errored."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "401":
          description: "`invalid_api_key` — not found, revoked, or expired."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "402":
          description: "`insufficient_qu` — the organization is out of Query Units."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "`rate_limited` — 30 queries/minute anonymous, 240 with a trial key."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        required: true
        description: "`sql` is the statement. `limit` caps rows and is itself clamped to 300."
        content:
          "application/json":
            schema:
              type: object
            example:
              sql: "select wallet, inbound_usd, inbound_count from mv_entity_rollups order by inbound_usd desc limit 10"
              limit: 100
      security:
        - {}
        - bearerAuth: []
        - apiKeyHeader: []
  "/kya/{digest}":
    get:
      operationId: getAttestation
      summary: "Read one KYA attestation"
      description: "A published, verified attestation by its memo digest. The read re-derives the digest from the stored document, so a record that resolves is proof rather than a lookup."
      tags:
        - KYA
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  attestation:
                    description: "The canonical document."
                  signature:
                    description: "The signature over it, and which method recovered."
                  settlement:
                    description: "The settlement whose `authorization.nonce` committed to this digest, if one is indexed."
        "404":
          description: "No published, verified attestation for this digest."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: digest
          in: path
          required: true
          description: "sha256 of the canonical attestation, hex with `0x`."
          schema:
            type: string
  "/kya/agents/{wallet}":
    get:
      operationId: listAgentAttestations
      summary: "Attestations by one signer"
      description: "Published attestations signed by this wallet."
      tags:
        - KYA
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  wallet:
                    description: "The signer queried."
                  attestations:
                    description: "Published attestations, newest first."
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      parameters:
        - name: wallet
          in: path
          required: true
          description: "EVM address. Case-insensitive — it is lowercased before lookup."
          schema:
            type: string
          example: "0x6157a191c961290420c68c1aa73c84444d769055"
        - name: limit
          in: query
          required: false
          description: "Rows to return. Clamped to 100; values outside the range are clamped, not rejected."
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
  "/kya/verify":
    post:
      operationId: verifyAttestation
      summary: "Verify an attestation without storing it"
      description: "Stateless. Recomputes the digest and recovers the signature, and tells you which signing method matched. Nothing is written and nothing is published."
      tags:
        - KYA
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  valid:
                    description: "Whether the signature recovered to `attestation.agent`."
                  digest:
                    description: "The memo — sha256 of the canonical document."
                  method:
                    description: "`eip712` or `personal_sign`, whichever recovered."
        "400":
          description: "The document is malformed, expired, or outside the accepted `issuedAt` window."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        required: true
        description: "The attestation document and its signature."
        content:
          "application/json":
            schema:
              type: object
            example:
              attestation:
                kya: 1
                agent: "0x0000000000000000000000000000000000000000"
                chainId: 8453
                subject: ""
                statement: "Replace this with the statement you signed."
                dataHash: "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
                issuedAt: "2026-08-24T10:00:00.000Z"
                expiresAt: ""
                nonce: "0x1111111111111111111111111111111111111111111111111111111111111111"
              signature: "0x"
  "/test/x402":
    get:
      operationId: testX402
      summary: "x402 test drive — pay $0.01, get an API key"
      description: "A real paywalled resource. Called without payment it answers `402` with standard x402 requirements, which is the cheapest way to see a live challenge. Pay it and it mints a trial API key raising the SQL limit to 240 queries/minute for 30 days. It also advertises KYA and accepts a memo in `X-PAYMENT-MEMO`."
      tags:
        - x402
      responses:
        "200":
          description: Success.
          content:
            "application/json":
              schema:
                type: object
                properties:
                  paid:
                    description: "`true` once settled."
                  api_key:
                    description: "The key, returned exactly once — only its hash is stored."
                  tier:
                    description: "The key's tier."
                  expires_at:
                    description: "When the key stops working."
                  limits:
                    description: "Per-minute query limits for this key and for anonymous callers."
                  payment:
                    description: "Network, payer, payTo, asset, amount, transaction hash and facilitator."
        "402":
          description: "No payment attached (the normal first response), or the facilitator rejected it."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "409":
          description: "`key_already_issued` — that settlement already bought a key."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "`upstream_unavailable` — the underlying query was refused. An empty array is a real answer; this is not one."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "An `rh_live_…` key as a bearer token. Optional — raises rate limits."
    apiKeyHeader:
      type: apiKey
      in: header
      name: x-api-key
      description: "The same key, as a header. Equivalent to the bearer form."
  schemas:
    Error:
      type: object
      description: "Errors never echo the query or PostgREST's `details`/`hint`, both of which would leak the statement and its filter values."
      properties:
        error:
          type: string
          description: "Stable machine-readable code."
        detail:
          type: string
          description: "Human-readable explanation. May be absent."
      required:
        - error
