{
  "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\nsettlements and ERC-8004 identities into one queryable record.\n\nTwo conventions worth knowing before you build against it:\n\n- **An empty array is an answer; a failure is not.** A refused query returns 503\n  `upstream_unavailable` naming the resource, never 200 with an empty list. On a\n  settlement index \"no rows\" would read as \"nothing ever happened\".\n- **Aggregates are per-entity.** Every figure is one wallet's or one pair's own. Site-wide\n  totals are deliberately confined to the /stats page and are not served here.\n\nCursor-paginated lists carry `next_before`; pass it back as `?before=`. The leaderboard is\nthe 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"
        ]
      }
    }
  }
}
