{
  "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=`.\n\nEvery endpoint here is also a Model Context Protocol tool at `POST /v0/mcp`, gated by the\nsame API key. See https://roundhouseai.io/docs/api/mcp.",
    "license": {
      "name": "Roundhouse terms",
      "url": "https://roundhouseai.io/terms"
    },
    "contact": {
      "name": "Roundhouse",
      "url": "https://roundhouseai.io/docs/api"
    }
  },
  "servers": [
    {
      "url": "https://api.roundhouseai.io/v0",
      "description": "Canonical base URL. The API worker on its own host, isolated from the dashboard."
    },
    {
      "url": "https://roundhouseai.io/api/v0",
      "description": "The same API through the dashboard origin. Same responses, one more hop; needs no CORS from a browser on that origin."
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "What the credential in front of a request is entitled to, what it has left, and what each route costs on each of the two paid lanes."
    },
    {
      "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": "MCP",
      "description": "The same endpoints as Model Context Protocol tools, gated by the same API key. See `./mcp-tools` for the tool registry."
    },
    {
      "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": {
    "/account": {
      "get": {
        "operationId": "getAccount",
        "summary": "What this caller is entitled to, and what it would be charged",
        "description": "Who the credential in front of this request resolves to, which of the two paid lanes it would take, what it has left to spend, and the price of every route on both lanes. Unmetered and answerable WITHOUT a key: checking your balance must never cost a unit and must never be the thing that fails. A caller with no key gets a real answer (`tier: anonymous`, `lane: x402`, the full price list), not a 401, so both prices can be compared before committing to either. An unrecognised key also answers 200, with `key_recognised: false` — this is the endpoint you reach for when your key is the thing that is broken, so it must work then.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authenticated": {
                      "description": "Whether a credential was accepted."
                    },
                    "key_present": {
                      "description": "Whether a credential was sent at all."
                    },
                    "key_recognised": {
                      "description": "Whether it resolved, or NULL when none was sent — three-valued, because \"no key\" and \"a key that did not resolve\" need different fixes and one `false` for both would conflate them."
                    },
                    "tier": {
                      "description": "`anonymous`, `trial`, `oauth`, or the organization key tier."
                    },
                    "plan": {
                      "description": "The organization's plan key, or null when there is no organization."
                    },
                    "lane": {
                      "description": "Which lane this caller would take on a metered route, so it can be PREDICTED rather than discovered by being refused: `credit` draws down Query Units, `x402` pays per call, `invalid_key` resolves to nothing, `no_account` is a caller with no organisation behind it on the MCP surface, where there is no x402 door, and `upgrade_required` is that same surface reached by a caller that HAS an organisation whose plan excludes the route. The last two were one value until a signed-in connection with an organisation and a balance was told it had no account behind it."
                    },
                    "organisation": {
                      "description": "Id and display name, or null. Named so an operator with several accounts knows which one to top up."
                    },
                    "balance": {
                      "description": "Units available and the low-balance threshold, or NULL with a `balance_note` saying why there is no balance to report — an organizationless caller has none, which is not the same as having zero. `low_balance` is already computed here so a client does not have to know the threshold to apply it."
                    },
                    "metered_routes": {
                      "description": "Every route with what one call costs on each lane: `qu` on the credit lane, `x402_usd` on the payment lane. Routes that cost nothing are listed AT ZERO rather than omitted, because a caller cannot tell \"free\" from \"not thought about\" by absence. `runSql`'s `qu` is its MINIMUM — a query is priced by the time it takes, and `sql_tiers` gives the ceilings."
                    },
                    "balance_note": {
                      "description": "Present only when `balance` is null, saying WHICH kind of nothing it is: no credential, one that did not resolve, a trial key that holds no balance by design, or an organization row that could not be read. A null balance is never a zero."
                    },
                    "sql_tiers": {
                      "description": "What one `run_sql` call may cost, which is no longer one number: a query is priced by the time it takes, at one Query Unit per started five seconds with a floor of one. Each tier gives its `timeout_seconds`, its `max_qu` (what is HELD for the call and the most it can cost), its own rate limit, and two separate booleans — `allowed` is whether this caller's plan may buy it, `available` is whether this deployment can serve it at all. They are separate because \"upgrade your plan\" and \"report a misconfiguration\" are different things to go and do, and `note` says which applies."
                    },
                    "top_up_url": {
                      "description": "Org-scoped deep link to the top-up page, or the plans page for a caller with no organization."
                    },
                    "as_of": {
                      "description": "When this answer was assembled."
                    },
                    "summary": {
                      "description": "One sentence saying what this lane means, for a human reading it in an agent transcript."
                    }
                  }
                }
              }
            }
          },
          "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"
                }
              }
            }
          }
        },
        "security": [
          {},
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ]
      }
    },
    "/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. Each row also carries `is_self_payment` (payer = payee, annotated rather than only excluded — a third of the feed can be self-payment) and `evidence`: `{ verified, method, probability, tier }`. `verified` is THREE-VALUED — `true` a named facilitator relayed it, `false` examined with no EIP-3009 marker, and `null` gasless and authorized but NOT ESTABLISHED as x402, which is not the same as disproven. `tier` is `proven` | `probable` | `unproven` | `not_x402` | `ungraded`."
                    },
                    "next_before": {
                      "description": "Cursor for the next page, or `null` at the end of the list."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On a non-empty page `meta` also carries `page_self_payments`, `page_self_payment_usd`, `page_gross_usd` and `page_organic_usd`. All four are THIS PAGE only, as the names say: they are not site-wide figures and summing them across pages is not a total. On an EMPTY list it instead carries `dataset_last_written_at`, `dataset_row_count` and any `warnings`."
                    }
                  }
                }
              }
            }
          },
          "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. Each row also carries `is_self_payment` (payer = payee, annotated rather than only excluded — a third of the feed can be self-payment) and `evidence`: `{ verified, method, probability, tier }`. `verified` is THREE-VALUED — `true` a named facilitator relayed it, `false` examined with no EIP-3009 marker, and `null` gasless and authorized but NOT ESTABLISHED as x402, which is not the same as disproven. `tier` is `proven` | `probable` | `unproven` | `not_x402` | `ungraded`."
                    },
                    "next_before": {
                      "description": "Cursor for the next page, or `null`. Minted from the rows READ, not the rows shown, so `exclude_self_payments` cannot end the walk early."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On a non-empty page `meta` also carries `page_self_payments`, `page_self_payment_usd`, `page_gross_usd` and `page_organic_usd`. All four are THIS PAGE only, as the names say: they are not site-wide figures and summing them across pages is not a total. `self_payments_excluded` is set when the page was filtered."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` — `min_probability` outside `[0,1]`, or `exclude_self_payments` not one of `true`/`false`/`1`/`0`.",
            "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": "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 a named facilitator having relayed it; `0.05` is a bare transfer to a catalogued address. Read `evidence` on the row rather than thresholding this number blind — the ladder has five rungs and the gap that matters is `verified: true` against `verified: null`, not a decimal.",
            "schema": {
              "type": "number"
            },
            "example": 0.5
          },
          {
            "name": "exclude_self_payments",
            "in": "query",
            "required": false,
            "description": "Set to `true` to drop rows where payer = payee. Off by default: a feed is a record of what happened and a wallet paying itself did happen, so the rows are ANNOTATED (`is_self_payment`) rather than hidden. Applied after the page is read, so a filtered page can be short of `limit` while more rows remain — keep following `next_before`.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "1",
                "0"
              ]
            }
          }
        ]
      }
    },
    "/entities/{wallet}/settlements": {
      "get": {
        "operationId": "getEntitySettlements",
        "summary": "Settlements for one wallet",
        "description": "Settlements where this wallet is either the payer or the payee, newest first. Walkable deeply in time order: `(payer, block_time desc)` and `(payee, block_time desc)` cover it, so page N costs what page 1 does and a wallet's whole history can be paged for velocity or trend.",
        "tags": [
          "Settlements"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "wallet": {
                      "description": "The wallet queried, lowercased."
                    },
                    "settlements": {
                      "description": "Settlement rows, newest first, decorated with resolved display names. Each row also carries `is_self_payment` (payer = payee, annotated rather than only excluded — a third of the feed can be self-payment) and `evidence`: `{ verified, method, probability, tier }`. `verified` is THREE-VALUED — `true` a named facilitator relayed it, `false` examined with no EIP-3009 marker, and `null` gasless and authorized but NOT ESTABLISHED as x402, which is not the same as disproven. `tier` is `proven` | `probable` | `unproven` | `not_x402` | `ungraded`."
                    },
                    "next_before": {
                      "description": "Cursor for the next page, or `null`."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On a non-empty page `meta` also carries `page_self_payments`, `page_self_payment_usd`, `page_gross_usd` and `page_organic_usd`. All four are THIS PAGE only, as the names say: they are not site-wide figures and summing them across pages is not a total."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` — `wallet` is not a well-formed EVM address.",
            "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"
          },
          {
            "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": {
                    "wallet": {
                      "description": "The wallet queried, lowercased."
                    },
                    "role": {
                      "description": "The role it was queried in: `agent` or `merchant`."
                    },
                    "coverage": {
                      "description": "**Read this first.** `known` false means the index has never observed this address, so every figure below is a zero meaning \"not observed\" — not \"observed to be nothing\". Also `first_seen`, `last_seen`, `settlements_scanned`, `scan_truncated` and `scan_cap`."
                    },
                    "entities": {
                      "description": "Resolved identity rows: display name, ENS, ERC-8004 card, role, first and last seen."
                    },
                    "erc8004_agents": {
                      "description": "ERC-8004 registrations whose on-chain agentWallet is this address."
                    },
                    "aggregates": {
                      "description": "That wallet's own totals: inbound/outbound USD and counts, distinct counterparties, chain split. `basis` says what they were computed over — `index_rollup` is the whole history, `scanned_settlements` is the capped scan and is a floor when `coverage.scan_truncated` is set. `distinct_counterparties` excludes the wallet itself on both bases. `chain_split` is the exception and carries its own `chain_split_basis`: the rollup is keyed by wallet and holds no chain dimension, so that split is ALWAYS the scan and is capped with it, whatever the object-level `basis` says."
                    },
                    "data_quality": {
                      "description": "The risk annotation. **Route on `verdict`, not on `note`.** `verdict` is one of `unobserved` (never seen — nothing assessed), `patterns_detected`, `no_patterns_detected`, or `insufficient_evidence` — the last meaning the address WAS observed, nothing fired, and not enough of its history was examined for that to rule anything out. `checks` carries every check with its `threshold`, what was `observed`, its own `basis`, and `conclusive`: a check that FIRED is always conclusive (a pattern seen in a sample happened), a check that did not fire on a truncated scan is not. `note` says the same thing in a sentence and is derived from the two."
                    },
                    "recent": {
                      "description": "A slice of its activity, `recent_limit` rows (default 5). Each row also carries `is_self_payment` (payer = payee, annotated rather than only excluded — a third of the feed can be self-payment) and `evidence`: `{ verified, method, probability, tier }`. `verified` is THREE-VALUED — `true` a named facilitator relayed it, `false` examined with no EIP-3009 marker, and `null` gasless and authorized but NOT ESTABLISHED as x402, which is not the same as disproven. `tier` is `proven` | `probable` | `unproven` | `not_x402` | `ungraded`."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` — `wallet` is not a well-formed EVM address.",
            "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"
          },
          {
            "name": "recent_limit",
            "in": "query",
            "required": false,
            "description": "Settlement rows to embed in the profile. 0 omits them. Default 5 — this was an unbounded 25, which is most of the payload on a surface where a response is billed as context tokens.",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 0,
              "maximum": 50
            }
          }
        ]
      }
    },
    "/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": {
                    "wallet": {
                      "description": "The wallet queried, lowercased."
                    },
                    "role": {
                      "description": "The role it was queried in: `agent` or `merchant`."
                    },
                    "coverage": {
                      "description": "**Read this first.** `known` false means the index has never observed this address, so every figure below is a zero meaning \"not observed\" — not \"observed to be nothing\". Also `first_seen`, `last_seen`, `settlements_scanned`, `scan_truncated` and `scan_cap`."
                    },
                    "entities": {
                      "description": "Resolved identity rows: display name, ENS, ERC-8004 card, role, first and last seen."
                    },
                    "erc8004_agents": {
                      "description": "ERC-8004 registrations whose on-chain agentWallet is this address."
                    },
                    "aggregates": {
                      "description": "That wallet's own totals: inbound/outbound USD and counts, distinct counterparties, chain split. `basis` says what they were computed over — `index_rollup` is the whole history, `scanned_settlements` is the capped scan and is a floor when `coverage.scan_truncated` is set. `distinct_counterparties` excludes the wallet itself on both bases. `chain_split` is the exception and carries its own `chain_split_basis`: the rollup is keyed by wallet and holds no chain dimension, so that split is ALWAYS the scan and is capped with it, whatever the object-level `basis` says."
                    },
                    "data_quality": {
                      "description": "The risk annotation. **Route on `verdict`, not on `note`.** `verdict` is one of `unobserved` (never seen — nothing assessed), `patterns_detected`, `no_patterns_detected`, or `insufficient_evidence` — the last meaning the address WAS observed, nothing fired, and not enough of its history was examined for that to rule anything out. `checks` carries every check with its `threshold`, what was `observed`, its own `basis`, and `conclusive`: a check that FIRED is always conclusive (a pattern seen in a sample happened), a check that did not fire on a truncated scan is not. `note` says the same thing in a sentence and is derived from the two."
                    },
                    "recent": {
                      "description": "A slice of its activity, `recent_limit` rows (default 5). Each row also carries `is_self_payment` (payer = payee, annotated rather than only excluded — a third of the feed can be self-payment) and `evidence`: `{ verified, method, probability, tier }`. `verified` is THREE-VALUED — `true` a named facilitator relayed it, `false` examined with no EIP-3009 marker, and `null` gasless and authorized but NOT ESTABLISHED as x402, which is not the same as disproven. `tier` is `proven` | `probable` | `unproven` | `not_x402` | `ungraded`."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` — `wallet` is not a well-formed EVM address.",
            "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"
          },
          {
            "name": "recent_limit",
            "in": "query",
            "required": false,
            "description": "Settlement rows to embed in the profile. 0 omits them. Default 5 — this was an unbounded 25, which is most of the payload on a surface where a response is billed as context tokens.",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 0,
              "maximum": 50
            }
          }
        ]
      }
    },
    "/agents": {
      "get": {
        "operationId": "listAgents",
        "summary": "Indexed ERC-8004 agents",
        "description": "Agents read from the on-chain IdentityRegistry, with their registration card and payment wallet. `score` is the ERC-8004 reputation score derived from on-chain feedback — it is the REGISTRY's number, not a Roundhouse rating, and it is null for every agent no counterparty has left feedback for. Roundhouse publishes no trust score of its own.",
        "tags": [
          "Entities"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agents": {
                      "description": "Agent rows with identity, wallet and the ERC-8004 `score` (null where the registry holds no feedback). `x402_support` is the registration card's own declaration; `last_activity_at` and `activity_status` are the observed halves, derived from settlements and feedback."
                    },
                    "next_before": {
                      "description": "Cursor for the next page, or `null`."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On an EMPTY list it also carries `dataset_last_written_at`, `dataset_row_count` and any `warnings` (e.g. `endpoints_catalog_stale`) — so a zero can be read as a quiet market or a stalled ingest rather than guessed at."
                    }
                  }
                }
              }
            }
          },
          "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_declared",
            "in": "query",
            "required": false,
            "description": "Set to `true` to keep only agents whose own ERC-8004 registration card declares x402 support (`x402Support: true`). This is the agent's CLAIM ABOUT ITSELF, copied out of a document its owner wrote, and it is not evidence of anything: four of five agents on a page of this filter had `last_activity_at: null`, `activity_status: \"unknown\"` and no feedback — never observed doing anything. It was named `x402` and documented as \"observed on-chain x402 payments\", which is the one confusion this index exists to prevent. The old name still works and means the same thing. There is no observed equivalent yet: `agents` carries no per-wallet settlement column, and the weekly activity scan that would populate one covers five agents a run against tens of thousands, so a filter built on it would answer \"not observed\" about agents nobody has looked at. To screen on evidence today, take the `agent_wallet` and call `get_agent` / `list_entity_settlements` with it.",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          },
          {
            "name": "x402",
            "in": "query",
            "required": false,
            "description": "The earlier name for `x402_declared`, kept because it is published and a caller sending it wants the same rows. Prefer `x402_declared`, whose name says what it reads. Sending both uses `x402_declared`.",
            "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`."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On an EMPTY list it also carries `dataset_last_written_at`, `dataset_row_count` and any `warnings` (e.g. `endpoints_catalog_stale`) — so a zero can be read as a quiet market or a stalled ingest rather than guessed at."
                    }
                  }
                }
              }
            }
          },
          "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": "ERC-8004 agents by registry feedback score",
        "description": "Agents ordered by the ERC-8004 reputation score, which is the on-chain registry's number and not a Roundhouse rating. Unscored agents sort last rather than dropping out, so a board where nothing has been scored still comes back looking ordered — check `meta.page_scored_agents`, which is how many rows on the page carry a score at all. Zero means the order is the `agent_id` tie-break and nothing more. 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": "Agent rows in score order, unscored last."
                    },
                    "next_offset": {
                      "description": "Offset for the next page, or `null`."
                    },
                    "meta": {
                      "description": "The chain served, `as_of`, and `page_scored_agents` — rows on this page with a non-null score. A `no_agent_on_this_page_has_a_score` warning when that is zero."
                    }
                  }
                }
              }
            }
          },
          "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."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On an EMPTY list it also carries `dataset_last_written_at`, `dataset_row_count` and any `warnings` (e.g. `endpoints_catalog_stale`) — so a zero can be read as a quiet market or a stalled ingest rather than guessed at."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` — `agentId` is not a registry id, or `chain` is not an indexed chain.",
            "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": "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."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On an EMPTY list it also carries `dataset_last_written_at`, `dataset_row_count` and any `warnings` (e.g. `endpoints_catalog_stale`) — so a zero can be read as a quiet market or a stalled ingest rather than guessed at."
                    }
                  }
                }
              }
            }
          },
          "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. Send `fields=compact` when you are only choosing a capability: the full response carries both IO schemas for all of them.",
        "tags": [
          "Capabilities"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "capabilities": {
                      "description": "Slug, name, group, priced unit and — on `fields=full` — summary and the canonical input/output fields."
                    },
                    "count": {
                      "description": "How many capabilities the registry defines."
                    },
                    "fields": {
                      "description": "Which shape was served: `full` or `compact`."
                    },
                    "note": {
                      "description": "Where the market figures for one capability live."
                    }
                  }
                }
              }
            }
          },
          "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": "fields",
            "in": "query",
            "required": false,
            "description": "`compact` returns slug, name, group and unit only — enough to pick a capability, without 34 pairs of IO schemas in the payload.",
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "compact"
              ],
              "default": "full"
            }
          }
        ]
      }
    },
    "/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."
                    },
                    "note": {
                      "description": "What the ranking is and is not — Roundhouse endorses no provider."
                    }
                  }
                }
              }
            }
          },
          "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. Paged: this used to return up to 200 rows unpaged, most of their columns null. Null-valued columns are omitted from each row.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chain": {
                      "description": "The chain served — this deployment's default when none was named."
                    },
                    "facilitators": {
                      "description": "Facilitator rows, busiest first. Null-valued fields are omitted."
                    },
                    "next_offset": {
                      "description": "Offset for the next page, or `null`."
                    },
                    "meta": {
                      "description": "The chain served and `as_of`. On an EMPTY list it also carries `dataset_last_written_at`, `dataset_row_count` and any `warnings` (e.g. `endpoints_catalog_stale`) — so a zero can be read as a quiet market or a stalled ingest rather than guessed at."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` — `chain` is not an indexed chain.",
            "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": "chain",
            "in": "query",
            "required": false,
            "description": "CAIP-2 chain id. Defaults to the chain this deployment indexes (`eip155:8453` in production, `eip155:84532` on staging) — the same default every endpoint on this API uses. A chain the index does not carry is refused with 400 listing the supported set, never answered with an empty list.",
            "schema": {
              "type": "string"
            },
            "example": "eip155:8453"
          },
          {
            "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": 25,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip. Use the returned `next_offset`.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "enriched_only",
            "in": "query",
            "required": false,
            "description": "Keep only relayers whose nonce and gas balance have been read. A row without `enriched_at` has nothing populated but its address.",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ]
      }
    },
    "/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, and exactly one may be sent: `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. Self-payments are excluded from BOTH edges and node totals and counted in `meta.excluded`, so `sum(node.window_usd)` is exactly twice `sum(edge.usd)`.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "applied": {
                      "description": "Every parameter as APPLIED, not as requested — both windows, the edge cap, the facilitator caps, the chain, the filters. Read this rather than assuming the request was honoured verbatim. `days` and `window` can BOTH be non-null: a wallet-focused day window is read row by row, so a row cap applies inside the declared range and `complete` says whether it bit."
                    },
                    "complete": {
                      "description": "Whether the declared window was read in full. False means a row cap stopped the read inside the range — the response then covers the newest `applied.window` settlements in it, not the whole of it, and `meta.warnings` says so. Only `?days=` with `?wallet=` can be false."
                    },
                    "days": {
                      "description": "The whole-UTC-day window applied, or `null` on the `window=N` path."
                    },
                    "since": {
                      "description": "Inclusive start of the day window, or `null` on the `window=N` path."
                    },
                    "until": {
                      "description": "Exclusive end of the day window (midnight today), or `null` on the `window=N` path."
                    },
                    "since_day": {
                      "description": "The `since` bound as a UTC date, or `null`."
                    },
                    "until_day": {
                      "description": "The `until` bound as a UTC date, or `null`."
                    },
                    "mode": {
                      "description": "`daily` when the answer came from the per-day rollup, which covers the window exactly; `raw` when it was folded from settlement rows."
                    },
                    "bucket": {
                      "description": "What one source row represented: `day` under `mode: daily`, `settlement` under `raw`."
                    },
                    "truncated": {
                      "description": "True when the edge cap dropped edges the window contained."
                    },
                    "nodes": {
                      "description": "One entry per wallet: role, display name, ENS, and its window totals."
                    },
                    "edges": {
                      "description": "Aggregated payer→payee pairs with volume and count."
                    },
                    "series": {
                      "description": "Per-day bucket totals across the window. `null` on the `window=N` path, which is a row count with no day grain to bucket into."
                    },
                    "facilitators": {
                      "description": "Relayers seen in the window, largest first and capped to `applied.facilitators`. `usd`/`count` are the relayer's own window totals; `links` name only wallets present in `nodes`, capped to `applied.facilitator_links`, with `links_total` and `links_truncated` beside them."
                    },
                    "window": {
                      "description": "How the window was read: rows requested, fetched and used."
                    },
                    "meta": {
                      "description": "The chain served, `as_of`, and `excluded` — the self-payments, low-confidence rows, over-cap edges and over-cap facilitators the window held but the graph does not show, so node and edge totals can be reconciled. `below_min_usd` is `null` when `min_usd` was sent: that filter runs at read time, so its exclusions never reach this response and cannot be counted."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` — `days` and `window` both sent, `days` out of `[1,180]`, `min_probability` out of `[0,1]`, a malformed `wallet`, or a `chain` the index does not carry.",
            "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": "window",
            "in": "query",
            "required": false,
            "description": "Row count back from the live tip. Mutually exclusive with `days` — sending both is refused with 400, not silently resolved in favour of one.",
            "schema": {
              "type": "integer",
              "default": 500,
              "minimum": 50,
              "maximum": 1000
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Whole UTC days, excluding today, honoured exactly: `days=1` is one day. Mutually exclusive with `window`. Out of `[1,180]` is refused rather than clamped.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 180
            }
          },
          {
            "name": "wallet",
            "in": "query",
            "required": false,
            "description": "Restrict to edges touching this wallet. Must be a well-formed EVM address.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chain",
            "in": "query",
            "required": false,
            "description": "CAIP-2 chain id. Defaults to the chain this deployment indexes (`eip155:8453` in production, `eip155:84532` on staging) — the same default every endpoint on this API uses. A chain the index does not carry is refused with 400 listing the supported set, never answered with an empty list.",
            "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, in `[0,1]`. Default 0.5.",
            "schema": {
              "type": "number",
              "default": 0.5,
              "minimum": 0,
              "maximum": 1
            }
          },
          {
            "name": "edges",
            "in": "query",
            "required": false,
            "description": "Hard cap on edges returned, highest volume first. Honoured exactly; `truncated` says whether it bit.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4000
            }
          }
        ]
      }
    },
    "/health/coverage": {
      "get": {
        "operationId": "getCoverage",
        "summary": "Per-dataset freshness — is a zero real?",
        "description": "What each dataset behind this API last had written to it, and how far behind its own cadence that is. This is how a caller tells a quiet market from a stalled ingest: six datasets feed this surface and they fail independently, so an empty `/endpoints` while `/flows` runs live is a stopped catalog crawl, not an empty catalog. No key required — a caller deciding whether to believe an empty list must be able to ask without one, and nothing here is anybody's data.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "as_of": {
                      "description": "When the report was taken."
                    },
                    "chain": {
                      "description": "The chain this deployment indexes."
                    },
                    "datasets": {
                      "description": "One entry per dataset: `last_written_at`, `row_count`, `indexer_lag_seconds`, the `stale_after_seconds` threshold and the `stale` verdict. A null `row_count` means the probe failed and is NOT zero."
                    },
                    "warnings": {
                      "description": "The datasets currently over their staleness threshold, by name."
                    }
                  }
                }
              }
            }
          },
          "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"
                }
              }
            }
          }
        }
      }
    },
    "/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, a statement timeout set by the TIER, and a hard 300-row cap. No semicolons, no comments, no multi-statement.\n\nPriced by the time it takes: one Query Unit per started 5 seconds, floor of 1 — so a query that answers quickly costs 1, which is what every query cost before this was metered. `tier` buys the ceiling (`standard` 15s, `extended` 60s and a paid plan); the tier maximum is HELD when the call starts and the unused part released the moment the duration is known, so `usage.units_charged` on the response is the settled figure. Call `get_account` for the tier table, or send `explain_only` to be quoted without running or paying for anything.",
        "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. This is what the call is priced on."
                    },
                    "authenticated": {
                      "description": "Whether a key was accepted."
                    },
                    "tier": {
                      "description": "The CREDENTIAL: `anonymous`, `trial`, or an organization tier."
                    },
                    "sql_tier": {
                      "description": "The QUERY: which ceiling this run was given and priced at. A separate field from `tier`, which has always meant the credential."
                    },
                    "quote": {
                      "description": "What the query was quoted at before it ran: the tier's ceiling and price bounds, plus the planner's own `planner_cost` and `plan_rows`. The planner figures are ADVISORY — arbitrary units, uncalibrated against this schema — and never charge anything; settlement is from measured duration, which needs no calibration to be right."
                    },
                    "usage": {
                      "description": "What this call cost and what is left. `units_charged` is SETTLED, not the hold: the tier maximum is taken up front and the unused part released once the duration is known."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_query` failed the sandbox checks, `invalid_json` malformed body, `invalid_parameter` an unknown `tier` or a bad `max_qu`, `over_max_qu` the tier could cost more than the caller allowed (nothing held, nothing ran), or `query_failed` ran and errored. A `query_failed` still carries `usage`: a query that ran consumed time, and it is settled by duration like a successful one — a syntax error costs the 1 QU floor, a statement timeout costs the whole tier it bought.",
            "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_balance` — the organization cannot cover the tier MAXIMUM, which is what the affordability check is against. Carries `units_required`, `units_available`, `organisation` and an org-scoped `top_up_url`, and deliberately carries NO x402 `accepts`: a customer who has run out must be told to top up, never quietly charged their wallet instead. `payment_required` is the keyless x402 challenge, which does carry `accepts`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`tier_not_in_plan` — the organization may run SQL at `standard` and pay for it from its balance, but the tier it asked for needs a paid plan. Nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — per organization and per tier, since a long query holds a connection for as long as it runs.",
            "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. `tier` picks the ceiling (default `standard`; an unknown name is a 400, never a silent default). `max_qu` refuses the call before anything is held if the tier could cost more than that. `explain_only` returns the quote and charges nothing.",
          "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": []
          }
        ]
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "summary": "Model Context Protocol endpoint",
        "description": "Every endpoint in this document, as MCP tools, over stateless Streamable HTTP. POST one JSON-RPC 2.0 message per request: `initialize`, `ping` and `tools/list` need no key; a `tools/call` is forwarded to the endpoint the tool wraps and answered by that endpoint's own gate, so the key requirement, the rate limit and the Query Unit charge are identical to calling it directly. Versioned with the rest of this surface: MCP negotiates the wire protocol, not whether a tool still takes the arguments it takes today. There is no server-initiated SSE stream and no session id, so `GET` and `DELETE` answer 405.",
        "tags": [
          "MCP"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "description": "Always `2.0`."
                    },
                    "id": {
                      "description": "The request's own id, echoed."
                    },
                    "result": {
                      "description": "On a `tools/call`: `content` (one text part carrying the endpoint's JSON response verbatim) and `isError`. A failed read sets `isError` and keeps the reason — it is never an empty result."
                    },
                    "error": {
                      "description": "Present instead of `result` when the caller cannot proceed: `-32001` no or bad API key, `-32002` a paid plan or an x402 payment is needed (the requirements are in `data.body`), `-32003` rate limited (`data.retry_after` seconds), `-32601` unknown method, `-32602` unknown tool or argument."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not a JSON-RPC 2.0 object, or is a batch (batching was removed in MCP 2025-06-18).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "GET or DELETE — this endpoint is stateless and offers no stream to resume.",
            "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": "One JSON-RPC 2.0 message. `tools/list` names every tool and its arguments; a tool takes exactly the parameters its endpoint takes, and an unrecognised argument is refused rather than ignored.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                  "name": "list_flows",
                  "arguments": {
                    "limit": 5
                  }
                }
              }
            }
          }
        },
        "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"
              }
            }
          }
        }
      }
    },
    "/queries/{slug}/results": {
      "get": {
        "operationId": "runSavedQuery",
        "summary": "Run a saved query — pay per call, or use an API key",
        "description": "Executes a published saved query and returns its rows. Two ways to pay for one URL: attach an API key and the run is metered in Query Units against your organization, or attach nothing and it answers `402` with standard x402 requirements — settle that and the rows come back, with no account and no key. Only queries whose author has enabled agent calls are payable; the rest need a key. Declared parameters are passed in the query string and are validated BEFORE any payment is taken, so a bad value costs a `400` rather than a settlement.",
        "tags": [
          "x402"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "description": "The slug and name of what ran."
                    },
                    "parameters": {
                      "description": "The values it ran at, after defaults were applied."
                    },
                    "columns": {
                      "description": "Column names, in order."
                    },
                    "rows": {
                      "description": "The result rows."
                    },
                    "row_count": {
                      "description": "How many rows came back."
                    },
                    "row_cap": {
                      "description": "The server's ceiling, so a full page is distinguishable from a truncated one."
                    },
                    "truncated": {
                      "description": "`true` when the answer hit `row_cap`."
                    },
                    "paid_with": {
                      "description": "`x402`, `api_key` or `trial_key`."
                    },
                    "settlement": {
                      "description": "The transaction hash, when the call was paid over x402."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A parameter value does not match its declared type, or the query itself failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is unknown, or the query is not published for x402 calls.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No payment attached (the normal first response), or the facilitator rejected it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such query, or it is private and not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "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": "The query's slug, from its page at `/queries/<slug>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {},
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ]
      }
    },
    "/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"
                }
              }
            }
          }
        }
      }
    },
    "/topup": {
      "get": {
        "operationId": "topupQueryUnits",
        "summary": "Top up Query Units over x402",
        "description": "Self-serve Query Unit purchase for organization API keys. Send your org key and no payment and the answer is a `402` pricing the requested `?usd=`; resend with `X-PAYMENT` and the settled USDC is credited to the key's organization at 200,000 QU per $1. One settlement credits exactly once — a replayed payment answers `409`. Trial keys are refused: they hold no Query Unit balance. Card top-ups live in the dashboard at /dashboard/billing.",
        "tags": [
          "x402"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "paid": {
                      "description": "`true` once settled and credited."
                    },
                    "credited_qu": {
                      "description": "Query Units credited by this settlement."
                    },
                    "new_balance": {
                      "description": "The organization's balance after the credit."
                    },
                    "rate_qu_per_usd": {
                      "description": "The published conversion rate."
                    },
                    "payment": {
                      "description": "Network, payer, payTo, asset, amount, transaction hash and facilitator."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`key_not_org_scoped` — a trial key has no organization to credit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`api_key_required` or `invalid_api_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No payment attached (the normal first response), or the facilitator rejected it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`topup_already_credited` — that settlement already credited Query Units.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`topup_credit_failed` — settled on-chain but not credited; quote the transaction hash to support.",
            "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": "usd",
            "in": "query",
            "required": false,
            "description": "Amount in USD, up to two decimals. Min $1, max $1,000, default $10; anything unreadable falls back to the default rather than guessing upward.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {},
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ]
      }
    }
  },
  "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"
        ]
      }
    }
  }
}
