# HTTP API пула

Справочник по API для скриптов, которые ходят к пулу без клиента `rpc_pool_client` (с клиентом достаточно `docs/INTEGRATION.md`).
Запросы к самому API идут напрямую: это твой собственный сервер. А вот список RPC, который он отдаёт, можно использовать только через прокси:

> **Work traffic to these RPCs MUST go through your proxy. Never call them from the host's main IP.**

Правило и его обоснование: `docs/RULES.md`. Примеры `curl` ниже ходят к API пула. Любой `curl` к URL из ответа должен идти через прокси:
`curl --noproxy '' -x "$PROXY" ...` (подробности про `NO_PROXY` в `docs/RULES.md`).

## Основы

| | |
|---|---|
| Адрес | `https://bomzrpc.online` (публичный пул: nginx с TLS перед сервером; в примерах ниже он лежит в `$RPC_POOL_URL`). Локальный сервер для разработки: `http://127.0.0.1:8787` (`RPC_POOL_HOST`, `RPC_POOL_PORT`), без TLS |
| Префикс | `/v1` |
| Формат | JSON (`application/json; charset=utf-8`). Время везде ISO-8601 UTC: `YYYY-MM-DDTHH:MM:SSZ` |
| Аутентификация | заголовок `X-API-Key: <ключ>` на всех путях `/v1/*`, кроме `/v1/ping` и `/v1/health` |
| Лимит | token bucket на каждый ключ: `RPC_POOL_API_RATE_PER_MIN` (по умолчанию 600) запросов в минуту. Бакет стартует полным, так что всплеск до 600 запросов проходит. Публичные пути лимит не расходуют |
| Политика | заголовок `X-RPC-Pool-Policy: proxy-required` в **каждом** ответе `/v1/*` (в том числе `401`, `404`, `405`, `429`, `500`, `503`), плюс блок `policy` в теле `/rpcs` и `/export` |
| Ключи | ключ выдаёт владелец пула, отдельный на каждый проект (`docs/INTEGRATION.md`). Отозванный ключ перестаёт работать со следующего запроса |
| Тело запроса | сервер принимает до 1 МиБ, иначе `413` (JSON). На публичном адресе nginx режет тело раньше, на 64 КБ, и отвечает своим `413` (HTML, не JSON); для `POST /v1/report` это не мешает: жалоба занимает несколько сотен байт |

Вне `/v1` сервер отдаёт только публичный сайт, без ключа и без заголовка политики: `/` (описание на русском), `/llms.txt`, `/docs/` и `/docs/<NAME>.md`, `/client/`, `/client/<файл>.whl` и `/client/latest.whl` (редирект на последнюю сборку; для `uv add` нужно настоящее имя файла), плюс панель `/ui` с её файлами `/ui/app.js` и `/ui/style.css` (сами файлы открыты без ключа, данные панель берёт из `/v1` с ключом; на все ответы `/ui*` ставятся `Content-Security-Policy` и `X-Content-Type-Options: nosniff`). Что и откуда публикуется, задаёт владелец сервера: `RPC_POOL_PUBLIC_URL`, `RPC_POOL_DOCS_DIR`, `RPC_POOL_PUBLIC_DOCS` (под `/docs/` открыты только документы из этого списка) и `RPC_POOL_CLIENT_DIST`. Всё остальное вне `/v1` это обычный `404` aiohttp.

Ключ на каждый проект свой: жалобы (`POST /v1/report`) считаются по ключам, и общий ключ делит и жалобы, и лимит запросов (`docs/INTEGRATION.md`).

## Эндпоинты

| Метод | Путь | Ключ | Что делает |
|---|---|---|---|
| GET | `/v1/ping` | нет | `{"pong": true}`, для мониторинга |
| GET | `/v1/health` | нет | жив ли сервис, когда начался последний collect, check и cleanup |
| GET | `/v1/chains` | да | список сетей со счётчиками RPC, фильтры `ecosystem`, `testnet`, `has_alive`, `q` |
| GET | `/v1/chains/{chain}` | да | одна сеть: то же и `ref_height`, `block_rate` |
| GET | `/v1/chains/{chain}/rpcs` | да | **главный метод**: RPC сети, лучшие первыми |
| POST | `/v1/report` | да | жалоба: RPC не отвечает через прокси |
| GET | `/v1/unmapped` | да | URL, чью сеть не удалось определить (нет проверяльщика экосистемы или из identity нельзя собрать ключ) |
| GET | `/v1/stats` | да | итоги последнего завершённого прогона каждого вида: collect, check, caps, cleanup; идёт ли новый |
| GET | `/v1/export` | да | всё, что отдаётся, одним JSON |
| GET | `/v1/overview` | да | сводка для панели `/ui`: сети, endpoints, экосистемы, последние прогоны, каталоги, диск |
| GET | `/v1/chains/{chain}/endpoints` | да | **все** endpoints сети со статусом и причиной последней ошибки, для панели `/ui` |

`{chain}` это ключ сети (`eip155:8453`), chain id (`8453`) или любой её алиас (`base`). Порядок разрешения: `docs/CHAINS.md`.
**В скриптах передавай chain id или ключ, не алиас:** алиас это удобство для рук, а когда каталоги меняются, он может стать неоднозначным и начать отвечать `404 unknown_chain`.
Неизвестная сеть даёт `404 unknown_chain` с подсказками (раздел «Ошибки»).

---

### GET /v1/ping

Без ключа. Ответ `200`: `{"pong": true}`. Проверяет, что процесс жив и слушает порт.

```bash
curl -si "$RPC_POOL_URL/v1/ping"     # 200, заголовок X-RPC-Pool-Policy: proxy-required
```

### GET /v1/health

Без ключа. Ответ `200`:

```json
{
  "status": "ok",
  "last_collect_at": "2026-10-18T18:46:12Z",
  "last_check_at": "2026-10-18T18:48:12Z",
  "last_cleanup_at": null
}
```

`last_*_at` это время **начала** последнего прогона соответствующего вида или `null`, если такого прогона ещё не было. Идущий прогон считается.
Если `last_collect_at` заметно старше интервала сбора (по умолчанию 60 минут), планировщик не работает: сообщи владельцу сервера (причина видна в его логах).

### GET /v1/chains

Сети из базы, отсортированы по ключу. Параметры (все необязательные, сочетаются через AND):

| Параметр | Значения | Смысл |
|---|---|---|
| `ecosystem` | `evm`, `cosmos`, `solana`, `sui`, `aptos`, `starknet`, `substrate`, `near`, `tron`, `stellar`, `xrpl`, `algorand`, `bitcoin`, `unknown` (регистр не важен) | только сети этой экосистемы. Другое значение: `400` |
| `testnet` | `true` / `false` | только тестнеты или только основные сети. Сети с неизвестным флагом (`is_testnet: null`) не попадают **ни в то, ни в другое** |
| `has_alive` | `true` / `false` | `true`: хотя бы один endpoint в статусе `alive`; `false`: ни одного |
| `q` | строка | подстрока (без учёта регистра) в ключе, названии или любом алиасе |

Ответ `200` это объект с массивом `chains`:

```json
{
  "chains": [
    {
      "key": "eip155:8453",
      "name": "Base",
      "ecosystem": "evm",
      "evm_chain_id": 8453,
      "is_testnet": false,
      "alive": 3,
      "total": 3,
      "aliases": ["base"]
    }
  ]
}
```

- `evm_chain_id` есть только у EVM-сетей, у остальных `null`.
- `is_testnet`: `true`, `false` или `null` (ни один каталог не сказал).
- `total` это все endpoints сети (endpoint это пара сеть + URL; любой транспорт, любой статус), `alive` это endpoints в статусе `alive`. Выдача `/rpcs` может быть меньше `alive`:
  там дополнительно отсекаются незащищённые адреса, устаревшие проверки и исключённые жалобами (раздел «Какие RPC отдаёт пул»).
- `aliases` это однозначные алиасы сети, по алфавиту. Алиас, который претендует на две сети, не регистрируется ни за одной.

```bash
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains?q=base&testnet=false" | jq '.chains[] | {key, alive, total}'
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains?ecosystem=solana"
```

### GET /v1/chains/{chain}

Одна сеть. Ответ `200` это плоский объект: все поля элемента `/v1/chains` плюс две оценки, на которые опирается проверка отставания:

```json
{
  "key": "eip155:8453",
  "name": "Base",
  "ecosystem": "evm",
  "evm_chain_id": 8453,
  "is_testnet": false,
  "alive": 3,
  "total": 3,
  "aliases": ["base"],
  "ref_height": 21876545,
  "block_rate": 0.5
}
```

- `ref_height` это эталонная высота сети по последнему прогону check: если корректных (правильная identity) ответов три и больше, берётся вторая по величине высота, иначе максимальная. `null`, пока сеть не проверялась.
- `block_rate` это блоков в секунду, оценка по двум последним эталонным высотам (окно от минуты). `null`, пока оценить нельзя; тогда отставание считается в блоках (порог `RPC_POOL_LAG_FALLBACK_BLOCKS`).

```bash
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/8453"
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/eip155:8453"     # то же самое
```

### GET /v1/chains/{chain}/rpcs

**Главный метод**: RPC сети, которым пул доверяет, лучшие первыми. Параметры:

| Параметр | Значения | По умолчанию | Смысл |
|---|---|---|---|
| `transport` | `http` / `ws` | `http` | `http` это `https://` (и `http://` с `insecure=true`), `ws` это `wss://` (и `ws://`). Другое значение: `400` |
| `limit` | целое от 1 до 1000 | 200 | сколько RPC вернуть. Применяется после сортировки |
| `insecure` | `true` / `false` | `false` | `true` добавляет адреса без TLS (`http://`, `ws://`). Трафик к ним идёт через сторонние прокси открытым текстом: прокси читает и может подменять ответы |
| `tracking` | `none` | не задан | только RPC, которым каталог (Chainlist) поставил метку `tracking: none`. RPC без метки (из других каталогов) не подходят. Другое значение: `400` |
| `require` | список через запятую: `send_tx`, `logs`, `archive`, `archive_full` | не задан | только RPC с нужными возможностями (все перечисленные сразу). Пустое значение и незнакомое имя: `400` |

Значения `require`:

| Значение | Подходят RPC, у которых |
|---|---|
| `send_tx` | `caps.send_tx` равно `true` (принимают транзакции) |
| `logs` | `caps.logs` не меньше 1000 (`eth_getLogs` на диапазон от 1000 блоков) |
| `archive` | `caps.archive` равно `recent` или `full` |
| `archive_full` | `caps.archive` равно `full` |

RPC с неизвестной возможностью (`null`) под фильтр `require` не попадают. Возможности измеряются только у EVM-RPC (раз в сутки для всех живых, а новые RPC сразу после первой проверки).

Ответ `200` для `/v1/chains/8453/rpcs?limit=3&require=send_tx` (третий RPC без измеренных возможностей отфильтрован):

```json
{
  "chain": {
    "key": "eip155:8453",
    "name": "Base",
    "ecosystem": "evm",
    "evm_chain_id": 8453,
    "is_testnet": false
  },
  "policy": {
    "proxy_required": true,
    "rule": "Work traffic to these RPCs MUST go through your proxy. Never call them from the host's main IP.",
    "docs": "docs/RULES.md"
  },
  "generated_at": "2026-10-18T19:00:12Z",
  "next_check_at": "2026-10-18T19:46:12Z",
  "rpcs": [
    {
      "url": "https://base-rpc.publicnode.com",
      "transport": "http",
      "score": 0.9982,
      "uptime_24h": 1.0,
      "streak": 24,
      "latency_ms": 180,
      "height": 21876543,
      "lag_s": 0.0,
      "tracking": "none",
      "caps": {"send_tx": true, "logs": 10000, "archive": "recent"},
      "sources": ["chainlist", "layerzero", "publicnode"],
      "last_ok_at": "2026-10-18T18:57:12Z"
    },
    {
      "url": "https://mainnet.base.org",
      "transport": "http",
      "score": 0.9699,
      "uptime_24h": 0.9583,
      "streak": 24,
      "latency_ms": 95,
      "height": 21876543,
      "lag_s": 0.0,
      "tracking": "limited",
      "caps": {"send_tx": true, "logs": 1000, "archive": "none"},
      "sources": ["chainlist", "superchain"],
      "last_ok_at": "2026-10-18T18:57:12Z"
    }
  ]
}
```

Поля ответа:

| Поле | Смысл |
|---|---|
| `chain` | `key`, `name`, `ecosystem`, `evm_chain_id`, `is_testnet` (как в `/v1/chains`) |
| `policy` | правило №1 как контракт: `proxy_required: true`, текст правила, ссылка на `docs/RULES.md`. Клиент на любом языке обязан его прочитать (`docs/RULES.md`) |
| `generated_at` | момент формирования ответа |
| `next_check_at` | начало последнего collect плюс `RPC_POOL_COLLECT_INTERVAL_MIN`: когда ждать следующий плановый цикл. `null`, пока не было ни одного collect. Это оценка: планировщик смотрит на часы раз в минуту, и цикл может начаться чуть позже |
| `rpcs[].url` | адрес RPC, нормализованный (схема и хост в нижнем регистре, без стандартного порта и завершающего `/`) |
| `rpcs[].transport` | `http` или `ws` |
| `rpcs[].score` | надёжность от 0 до 1, чем больше, тем лучше (формула ниже) |
| `rpcs[].uptime_24h` | доля успешных проверок за 24 часа среди проверок с определённым итогом (ответы `429` не считаются). `null`, если таких проверок за сутки не было. Округлено до 4 знаков |
| `rpcs[].streak` | сколько проверок подряд прошло успешно |
| `rpcs[].latency_ms` | время ответа на первый (identity) запрос последней измеренной проверки; `null`, пока не измерено |
| `rpcs[].height` | высота (номер блока, слот, чекпойнт, ledger) в последней измеренной проверке |
| `rpcs[].lag_s` | отставание от эталонной высоты сети в секундах; `null`, пока скорость блоков сети неизвестна |
| `rpcs[].tracking` | метка приватности из Chainlist: `none`, `limited`, `yes`; `null`, если каталог её не даёт |
| `rpcs[].caps` | возможности EVM-RPC: `send_tx` (`true`, `false`, `null`), `logs` (самый широкий прошедший диапазон `eth_getLogs`: `0`, `1000`, `10000`, `null`), `archive` (`none`, `recent`, `full`, `null`). `null` значит «не измерено» |
| `rpcs[].sources` | каталоги, которые когда-либо упоминали этот URL (по алфавиту); не показывает, где RPC числится сейчас |
| `rpcs[].last_ok_at` | время последней успешной проверки |

```bash
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/8453/rpcs?limit=20" | jq '.rpcs[] | {url, score, caps}'
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/1/rpcs?require=send_tx,logs&tracking=none&limit=5"
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/137/rpcs?transport=ws"
```

#### Какие RPC отдаёт пул

RPC попадает в `rpcs` и в `/v1/export`, только если выполнено **всё**:

1. Его статус `alive`: последняя проверка прошла (identity верна, высота получена, отставания нет; единичный ответ `429` статус не меняет).
2. Его не исключили жалобы (`POST /v1/report` ниже).
3. Последняя успешная проверка не старше **трёх интервалов сбора** (`3 × RPC_POOL_COLLECT_INTERVAL_MIN`, по умолчанию 180 минут). Это страховка на случай, если планировщик встал: пул не ручается за RPC, которого давно никто не проверял.
   Следствие: если сервис не работал больше трёх часов, свежих `ok` нет ни у кого. Поэтому пул вообще не отдаёт списков, пока ни один check не завершился за эти три интервала:
   `/rpcs` и `/v1/export` отвечают `503 pool_stale` (раздел «Ошибки»), а не пустым `rpcs`. Клиент `rpc_pool_client` на `503` берёт свой кеш (`docs/FAILOVER.md`, раздел 6), скрипт без клиента должен делать так же.
4. Адрес защищённый (`https://`, `wss://`), если не задано `insecure=true`.
5. Транспорт совпадает с `transport`.
6. Выполнены фильтры `tracking` и `require`.

Все остальные RPC (`new`, `sick`, лагающие, чужая сеть) в выдачу не попадают, но хранятся и проверяются дальше; тот, что долго не проходит проверку, со временем удаляется.

#### Порядок

1. RPC, на которые есть свежая жалоба (отодвинутые на час), стоят **в конце**.
2. Остальные по убыванию `score`.
3. При равном `score` по возрастанию `latency_ms`; RPC без измеренной задержки последними.
4. Если и это равно, порядок стабильный (по времени появления в базе).

`score` считается так:

```
score = 0.70 * uptime + 0.25 * min(streak, 24) / 24 + 0.05 * (1 - min(latency_ms, 5000) / 5000)
```

- `uptime` это `uptime_24h`, но пока за сутки меньше 3 проверок с определённым итогом, берётся нейтральные 0.5. Поэтому новичок с `uptime_24h: 1.0` по двум проверкам стоит ниже проверенного: при `streak` 2 и задержке 410 мс его `score` равен 0.4167. Число проверок за сутки в ответ не выводится, оно нужно только для этого порога.
- Неизвестная задержка считается худшей (5000 мс).
- Скорость почти не влияет: вес задержки всего 0.05. Но это не строгий тай-брейк по задержке, а слагаемое. Надёжность важнее быстроты (решение владельца).

### POST /v1/report

Скрипт сообщает, что RPC не отвечает **через его прокси**. Зачем: RPC может быть жив для основного IP сервера и банить IP прокси, ежечасная проверка этого не видит.
Клиент `rpc_pool_client` отправляет жалобы сам (`docs/FAILOVER.md`); свой код может слать их так:

```bash
curl -sS -X POST -H "X-API-Key: $RPC_POOL_API_KEY" -H 'content-type: application/json' \
  --data '{"chain": "8453", "url": "https://mainnet.base.org", "error": "http_429", "detail": "429 on three requests in a row"}' \
  "$RPC_POOL_URL/v1/report"
```

Тело запроса (JSON-объект):

| Поле | Обязательное | Смысл |
|---|---|---|
| `chain` | да | chain id или ключ сети (алиас тоже принимается, но в скриптах его не используют: `/v1/chains/{chain}`) |
| `url` | да | адрес RPC так, как его отдал пул. Он нормализуется так же, как при сборе. Ответ и логи этот URL не повторяют: он может нести ключ |
| `error` | да | одно из `timeout`, `http_403`, `http_429`, `conn`, `rpc_error` |
| `detail` | нет | пояснение строкой, обрезается до 500 символов. Без секретов и без URL с ключами |

Ответы:

| Код | Тело | Когда |
|---|---|---|
| `202` | `{"accepted": true}` | жалоба принята |
| `202` | `{"accepted": false, "reason": "duplicate"}` | этот ключ уже жаловался на этот RPC за последний час; ничего не изменилось |
| `400` | `bad_request` | тело не JSON-объект, нет обязательного поля, `error` вне списка, `detail` не строка |
| `404` | `unknown_chain` | сети нет (с подсказками) |
| `404` | `unknown_rpc` | в этой сети нет такого RPC (в том числе URL не прошёл нормализацию) |

Что жалоба меняет (политика, она фиксированная):

- Жалобы **дедуплицируются по паре (ключ API, RPC) за час**: зациклившийся скрипт не может утопить RPC в одиночку, повтор от него в течение часа даёт `duplicate`.
- **1–2 разных ключа** пожаловались за последний час: RPC уходит в конец списка на 1 час (`report_penalty_until = now + 1 ч`, каждая новая принятая жалоба отсчитывает час заново).
- **3 и больше разных ключей** за последний час: RPC исключается из выдачи на 1 час (`excluded_until = now + 1 ч`, продлевается очередной принятой жалобой).
- Сам статус RPC жалоба не меняет, проверки идут как обычно.
- **Исключение действует ровно час.** Вариант из дизайна «до первой успешной проверки после истечения часа» **не реализован**: через час RPC возвращается в выдачу сам, если он по-прежнему `alive` и проверен недавно. Если он опять не работает через прокси, три ключа пожалуются снова.
- Жалобы старше 7 дней удаляет ежедневная чистка.

### GET /v1/unmapped

URL, чью сеть определить не удалось: для экосистемы нет проверяльщика, либо RPC ответил, кто он, но из его identity нельзя собрать ключ сети (в ней нет ни одного символа `[a-z0-9]`). Сети, чьей identity нет в таблице `src/rpc_pool/data/chains_nonevm.yaml`, пул заводит сам под ключом `<экосистема>:x-<slug>` (`docs/CHAINS.md`, раздел 9), в unmapped они не попадают. В выдачу `/rpcs` URL из unmapped не попадают.
Строки сгруппированы по тройке (`ecosystem`, `identity`, `reason`), самая большая группа первой. Ответ `200`:

```json
{
  "unmapped": [
    {
      "ecosystem": "near",
      "identity": "---",
      "reason": "no_table_entry",
      "names": ["Odd Net"],
      "sources": ["lifi", "rpcinfo"],
      "sample_urls": ["https://odd.example.org", "https://odd-2.example.org", "https://odd-3.example.org"],
      "count": 4
    }
  ]
}
```

Пример условный: сеть с обычной identity сюда не попадает, её пул заводит как автосеть.

| Поле | Смысл |
|---|---|
| `ecosystem` | экосистема, чьим проверяльщиком отвечали |
| `identity` | что RPC сказал о себе (genesis hash, имя сети, passphrase...); `null` для `no_checker` |
| `reason` | `no_table_entry`: из identity нельзя собрать ключ автосети (в ней нет `[a-z0-9]`). `no_checker`: для экосистемы нет проверяльщика |
| `names` | названия сети из каталогов (разные, по алфавиту) |
| `sources` | каталоги, которые принесли эти URL |
| `sample_urls` | до трёх URL группы, по алфавиту. Ключи API в адресах (сегменты пути вида ключа и `user:pass@`) заменены на `***`, как в логах и на панели; настоящие адреса из ответа не получить |
| `count` | сколько разных URL в группе (считаются настоящие адреса, поэтому два адреса, различающиеся только ключом, дают `count` 2 и один `sample_urls`) |

Как читать и что с этим делать: `docs/CHAINS.md`, раздел «Как читать /v1/unmapped».

### GET /v1/stats

Итог **последнего завершённого** прогона каждого вида и флаг, идёт ли сейчас новый. Ключ верхнего уровня это вид прогона (`collect`, `check`, `caps`, `cleanup`); вид, который ни разу не запускался, в ответе отсутствует.

```json
{
  "collect": {
    "started_at": "2026-10-18T18:46:12Z",
    "finished_at": "2026-10-18T18:48:12Z",
    "stats": {
      "chainlist": {"fetched": 7601, "kept": 7587, "dropped_url": 14, "resolved": 7587, "pending": 0, "error": null},
      "blockpi": {"fetched": 0, "kept": 0, "dropped_url": 0, "resolved": 0, "pending": 0, "error": "SourceError: blockpi: no network-item blocks in the page"}
    },
    "aborted": null,
    "running": false
  },
  "check": {
    "started_at": "2026-10-18T18:48:12Z",
    "finished_at": "2026-10-18T18:58:12Z",
    "stats": {
      "probed": 8240, "ok": 4310, "lagging": 41, "wrong_chain": 122, "fail": 3702, "inconclusive": 65,
      "rechecked": 3750, "recovered": 48, "pending_resolved": 15, "pending_unmapped": 9,
      "skipped_no_checker": 1, "errors": {}, "chains": null
    },
    "aborted": null,
    "running": true
  },
  "caps": {
    "started_at": "2026-10-18T18:58:12Z",
    "finished_at": "2026-10-18T18:59:12Z",
    "stats": {"only_unchecked": true, "probed": 37, "failed": 0, "unknown": 2, "error": null},
    "aborted": null,
    "running": false
  }
}
```

- `started_at`, `finished_at`, `stats`, `aborted` описывают последний **завершённый** прогон: идущий прогон его не скрывает. `running: true`, пока идёт прогон, начатый последним (в примере выше идёт новый `check`, а показан итог прошлого).
  Если первый прогон вида ещё не завершился, завершённого нет: `started_at` и `finished_at` равны `null`, `stats` пустой объект `{}`, `running: true`.
- `aborted` равно `null` или имени исключения (`CancelledError`, `RuntimeError`), которое оборвало прогон. Статистика такого прогона неполная.
- Поля `stats` каждого вида читает владелец сервера при эксплуатации (руководство владельца сервера, на сайте не публикуется).

```bash
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/stats" | jq '.collect.stats | to_entries[] | select(.value.error != null) | {source: .key, error: .value.error}'
```

### GET /v1/export

Весь отдаваемый пул одним JSON, для кеша в скриптах и для бэкапа списка. Параметров нет. Берутся те же RPC и в том же порядке, что в `/rpcs` (определение выше) без `insecure`-адресов и без фильтров `tracking` и `require`.

```json
{
  "policy": {
    "proxy_required": true,
    "rule": "Work traffic to these RPCs MUST go through your proxy. Never call them from the host's main IP.",
    "docs": "docs/RULES.md"
  },
  "generated_at": "2026-10-18T19:00:12Z",
  "chains": [
    {
      "chain": "eip155:1",
      "http": [
        {
          "url": "https://ethereum-rpc.publicnode.com",
          "transport": "http",
          "score": 0.9985,
          "uptime_24h": 1.0,
          "streak": 24,
          "latency_ms": 150,
          "height": 24000000,
          "lag_s": 0.0,
          "tracking": "none",
          "caps": {"send_tx": true, "logs": 10000, "archive": "full"},
          "sources": ["chainlist", "publicnode"],
          "last_ok_at": "2026-10-18T18:57:12Z"
        }
      ],
      "ws": []
    }
  ]
}
```

- `chain` это **строка с ключом сети** (`eip155:1`), а не объект, как в `/rpcs`. Остальные поля RPC такие же, как в `/rpcs`.
- В списке только сети, у которых есть хотя бы один отдаваемый RPC; сети по ключу в алфавитном порядке. У каждой сети оба списка (`http`, `ws`), один может быть пустым.
- **Не больше 200 RPC на сеть и транспорт**: у сети с большим числом RPC в export попадут лучшие 200. Полный список (до 1000) даёт `/v1/chains/{chain}/rpcs?limit=1000`.

### GET /v1/overview

Сводка для панели `/ui`: одним запросом всё, что она показывает на главной. Параметров нет. В отличие от `/rpcs` и `/export`, отвечает `200` и когда пул устарел: об этом говорит `pool_stale`.

```json
{
  "generated_at": "2026-10-18T19:00:12Z",
  "pool_stale": false,
  "chains": {"total": 2310, "with_alive": 412, "by_kind": {"mainnet": 301, "testnet": 98, "unknown": 13}},
  "endpoints": {"total": 8240, "alive": 4310, "alive_ws": 620, "sick": 3702, "new": 228},
  "by_ecosystem": [
    {"ecosystem": "evm", "chains": 2100, "chains_alive": 380, "endpoints": 7600, "endpoints_alive": 4000},
    {"ecosystem": "cosmos", "chains": 90, "chains_alive": 22, "endpoints": 300, "endpoints_alive": 180}
  ],
  "runs": {
    "collect": {"started_at": "2026-10-18T18:46:12Z", "finished_at": "2026-10-18T18:48:12Z", "aborted": null},
    "check": {"started_at": "2026-10-18T18:48:12Z", "finished_at": "2026-10-18T18:58:12Z", "aborted": null},
    "caps": {"started_at": "2026-10-18T18:58:12Z", "finished_at": "2026-10-18T18:59:12Z", "aborted": null},
    "cleanup": {"started_at": null, "finished_at": null, "aborted": null}
  },
  "sources": [
    {"name": "chainlist", "fetched": 7601, "kept": 7587, "error": null},
    {"name": "blockpi", "fetched": 0, "kept": 0, "error": "SourceError: blockpi: no network-item blocks in the page"}
  ],
  "pending": 15,
  "unmapped_groups": 9,
  "new_chains": [
    {"key": "sui:x-0f1e2d3c4b5a6978", "name": "Some Sui Fork", "ecosystem": "sui", "is_testnet": null, "created_at": "2026-10-18T18:51:40Z", "alive": 2},
    {"key": "eip155:4202", "name": "Lisk Sepolia", "ecosystem": "evm", "is_testnet": true, "created_at": "2026-10-16T09:12:03Z", "alive": 3}
  ],
  "disk": {"free_bytes": 52613349376, "low": false}
}
```

| Поле | Смысл |
|---|---|
| `pool_stale` | то же правило, что у `503 pool_stale` (раздел «Ошибки»): ещё нет завершённого `check` или последний завершился больше `3 × RPC_POOL_COLLECT_INTERVAL_MIN` минут назад |
| `chains` | `total` это все сети пула, `with_alive` сети хотя бы с одним `alive` endpoint. `by_kind` раскладывает именно эти сети по `is_testnet`: `mainnet` (`false`), `testnet` (`true`), `unknown` (`null`); сумма равна `with_alive` |
| `endpoints` | сколько endpoints в каждом статусе: `alive`, `sick`, `new`; `total` их сумма. `alive_ws` это `alive` среди WebSocket (они уже входят в `alive`) |
| `by_ecosystem` | по экосистеме: сколько сетей (`chains`), из них с `alive` endpoint (`chains_alive`), сколько endpoints (`endpoints`) и `alive` среди них (`endpoints_alive`). Порядок: больше `chains_alive` первым, при равенстве больше `chains`, затем по имени. Экосистема без сетей в списке не появляется |
| `runs` | последний **завершённый** прогон каждого вида, как в `/v1/stats`: идущий прогон его не скрывает. Все четыре вида есть всегда; у вида, который ещё не завершился ни разу, три поля равны `null`. `aborted` это имя исключения, оборвавшего прогон, или `null` |
| `sources` | итоги каталогов по последнему завершённому `collect`: `fetched` получено, `kept` оставлено, `error` текст ошибки (URL в нём замаскированы) или `null`. Порядок каталогов как при сборе (`docs/SOURCES.md`), отключённых в списке нет. `[]`, пока `collect` не завершался |
| `pending` | сколько записей ждут определения сети (`docs/CHAINS.md`) |
| `unmapped_groups` | сколько групп в `/v1/unmapped` |
| `new_chains` | сети, чья строка создана за последние 7 дней: самые новые первыми (при равном времени по ключу), не больше 50. `created_at` время создания строки сети, `alive` сколько у неё endpoints со статусом `alive` (как в `/v1/chains`), `is_testnet` `true`, `false` или `null`. Ключи вида `<экосистема>:x-<slug>` это сети, которые пул завёл сам (`docs/CHAINS.md`, раздел 9). На только что созданной базе все сети новые, и список показывает 50 самых свежих |
| `disk` | свободное место там, где лежит файл БД (`RPC_POOL_DB_PATH`): `free_bytes` и `low`, то есть меньше `RPC_POOL_DISK_LOW_BYTES` (по умолчанию 1 ГиБ). Полный диск останавливает запись в БД, а с ней весь пул |

**`alive` здесь это статус endpoint `alive`**, а не «отдаётся»: условие отдачи строже (см. «Какие RPC отдаёт пул» выше: проверка не старше трёх интервалов, нет исключения по жалобам, TLS). Поэтому `endpoints.alive` может быть больше, чем число RPC, которые реально вернёт `/rpcs`. Что отдаётся прямо сейчас, показывает флаг `served` в `/v1/chains/{chain}/endpoints`.

```bash
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/overview" | jq '{pool_stale, endpoints, disk}'
```

### GET /v1/chains/{chain}/endpoints

**Все** endpoints сети, в том числе `sick` и `new`, с подробностями, которых нет в `/rpcs`: статус, отдаётся ли endpoint сейчас, текст последней ошибки. Это данные для панели `/ui`, а не список для работы: для работы бери `/v1/chains/{chain}/rpcs`.

| Параметр | Значения | По умолчанию | Смысл |
|---|---|---|---|
| `status` | `all`, `alive`, `sick`, `new` | `all` | только endpoints в этом статусе |
| `transport` | `all`, `http`, `ws` | `all` | только этого транспорта |

Значения без учёта регистра. Другое значение (в том числе пустое): `400 bad_request`. Параметры проверяются раньше сети. Лимита нет: у сети не больше нескольких сотен endpoints.

```json
{
  "chain": {
    "key": "eip155:8453", "name": "Base", "ecosystem": "evm", "evm_chain_id": 8453, "is_testnet": false,
    "alive": 3, "total": 4, "aliases": ["base"], "ref_height": 21876545, "block_rate": 0.5
  },
  "policy": {"proxy_required": true, "rule": "Work traffic to these RPCs MUST go through your proxy. Never call them from the host's main IP.", "docs": "docs/RULES.md"},
  "generated_at": "2026-10-18T19:00:12Z",
  "endpoints": [
    {
      "url": "https://mainnet.base.org",
      "transport": "http",
      "tls": true,
      "embedded_key": false,
      "status": "alive",
      "served": true,
      "last_outcome": "ok",
      "last_error": null,
      "score": 0.9985,
      "uptime_24h": 1.0,
      "checks_24h": 24,
      "streak": 24,
      "fail_streak": 0,
      "latency_ms": 150,
      "height": 21876543,
      "lag_s": 0.0,
      "caps": {"send_tx": true, "logs": 10000, "archive": "recent"},
      "sources": ["chainlist", "publicnode"],
      "first_seen": "2026-10-10T08:00:01Z",
      "last_check_at": "2026-10-18T18:57:12Z",
      "last_ok_at": "2026-10-18T18:57:12Z",
      "penalized": false,
      "excluded": false
    }
  ]
}
```

- `chain` это тот же объект, что отдаёт `GET /v1/chains/{chain}`. `policy` и заголовок политики стоят, как везде.
- `url` показывается **замаскированным** (`mask_url`): ключ в пути заменён на `***`, `embedded_key: true` говорит, что он там был. Из этого поля подключиться нельзя, и это по замыслу: ключи не должны попадать в панель. Любые URL внутри `last_error` замаскированы так же.
- `status` это `new` (ещё не проверялся), `alive` или `sick`; `last_outcome` итог последней проверки (`ok`, `fail`, `lagging`, `wrong_chain`, `inconclusive`), `null` у непроверенных. `last_error` это ошибка последней записи проверки этого endpoint (не длиннее 200 символов), `null`, если проверка прошла или записей нет (проверки старше `RPC_POOL_CHECKS_RETENTION_D` дней удаляются).
- **`served`** `true`, если `GET /v1/chains/{chain}/rpcs` отдал бы этот endpoint прямо сейчас, без `insecure`, `tracking` и `require`: статус `alive`, нет исключения по жалобам, последний `ok` не старше трёх интервалов `collect`, есть TLS. Условие то же самое (одно определение в коде), а не его копия. Предел `limit` в `served` не учитывается, как и `pool_stale`: когда пул устарел, `/rpcs` отвечает `503` независимо от флага, а панель показывает это через `/v1/overview`.
- `score`, `uptime_24h`, `streak`, `latency_ms`, `height`, `lag_s`, `caps`, `sources` считаются как в `/rpcs`. `checks_24h` это число проверок с определённым итогом за 24 часа (на них держится `uptime_24h`; ответы `429` не считаются). `fail_streak` сколько проверок подряд провалено.
- `penalized`: жалобы временно опустили endpoint в конец выдачи (штраф ещё действует). `excluded`: три разных ключа пожаловались, endpoint исключён, пока исключение не истечёт.
- Порядок: сначала `served`, затем больший `score`, затем меньшая `latency_ms` (неизвестная последней), затем меньший внутренний id (порядок добавления). Штраф за жалобы на порядок не влияет, в отличие от `/rpcs`: его видно по флагу `penalized`.

```bash
curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/8453/endpoints?status=sick" | jq '.endpoints[] | {url, last_outcome, last_error}'
```

---

## Ошибки

Единый формат тела, для любого статуса `4xx` и `5xx`:

```json
{"error": {"code": "unknown_chain", "message": "unknown chain 'bse'", "suggestions": ["base", "basesep"]}}
```

`suggestions` есть только у `unknown_chain`: до пяти ближайших ключей и алиасов (может быть пустым). Заголовок `X-RPC-Pool-Policy: proxy-required` стоит и на ошибках.

| HTTP | `code` | Когда |
|---|---|---|
| 400 | `bad_request` | плохое значение параметра запроса или неправильное тело (`limit must be an integer from 1 to 1000`, `transport must be http or ws`, `ecosystem must be one of: ...`, `require must be a comma-separated list of: ...`, `status must be one of: all, alive, new, sick`, `body must be a JSON object`...). Текст в `message` называет параметр |
| 401 | `unauthorized` | нет заголовка `X-API-Key`, ключ неизвестен или отозван |
| 404 | `unknown_chain` | сеть не найдена; в `suggestions` ближайшие имена. Для `POST /v1/report` тоже |
| 404 | `unknown_rpc` | только `POST /v1/report`: в сети нет такого RPC |
| 404 | `not_found` | нет такого маршрута под `/v1` |
| 405 | `method_not_allowed` | маршрут есть, метод не тот (`GET /v1/report`); заголовок `Allow` перечисляет допустимые методы |
| 413 | `http_error` | тело запроса больше 1 МиБ. Так же называется любая другая ошибка HTTP, которую поднял сам aiohttp |
| 429 | `rate_limited` | ключ исчерпал лимит; подожди (бакет пополняется непрерывно, `Retry-After` нет) |
| 500 | `internal_error` | ошибка сервера; подробности в логе сервера. Тело её не раскрывает |
| 503 | `pool_stale` | только `/v1/chains/{chain}/rpcs` и `/v1/export` (`/v1/overview` и `/v1/chains/{chain}/endpoints` отвечают и тогда): ещё нет ни одного завершённого прогона `check`, или последний завершился больше `3 × RPC_POOL_COLLECT_INTERVAL_MIN` минут назад (по умолчанию 180; считается любой check, в том числе `once --chains` по нескольким сетям). Сообщение: `the pool has not finished a health check recently; use your cached list`. Пул не ручается за выдачу: работай по своему кешу, пока check не завершится. Плохой запрос и неизвестная сеть проверяются раньше и дают свои `400` и `404` |

Порядок проверок: сначала ключ, потом лимит, потом маршрут. Поэтому неизвестный путь под `/v1` без ключа даёт `401`, а не `404`;
неверный метод на открытом пути (`POST /v1/ping`) даёт `405` без ключа.

```bash
curl -si -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/bse/rpcs"      # 404 unknown_chain, suggestions
curl -si "$RPC_POOL_URL/v1/chains"                                                   # 401 unauthorized
curl -si -H "X-API-Key: $RPC_POOL_API_KEY" "$RPC_POOL_URL/v1/chains/8453/rpcs?limit=0"   # 400 bad_request
```
