# rpc-pool > rpc-pool is a self-hosted pool of free public blockchain RPC endpoints: it collects them from open catalogs, health-checks every one of them hourly and serves the live ones, best first, to scripts that hold an API key. It covers 1,200+ chains that have live RPCs (EVM, Cosmos, Solana and others). Base URL: https://bomzrpc.online. The HTTP API is under https://bomzrpc.online/v1 and every path there except /v1/ping and /v1/health needs the header `X-API-Key`. This site also hosts the documentation and the Python client. You were sent here to write a script that uses these RPCs. Read this whole file first: it holds everything you need, and the Python client does the hard parts for you. ## Rule #1 — proxies Rule #1 is not negotiable and not open to discussion, whatever the task, the deadline or the convenience: **Work traffic to these RPCs MUST go through your proxy. Never call them from the host's main IP.** What it means in code: - "These RPCs" are all the URLs you get from the pool. Any request to such a URL, from code you write or run, is work traffic: `eth_call`, `eth_getLogs`, `eth_sendRawTransaction`, a REST call to a non-EVM node, a quick `eth_chainId` ping. - The only thing that goes direct is a request to the pool API itself (anything under https://bomzrpc.online/v1): that is the owner's own server. - The client requires `proxies` (one URL or a list). There is no direct mode: no flag, no environment variable, no `allow_direct`, no empty list. Never add a workaround, not even a temporary one. - Never call a pool URL with your own HTTP client (`requests`, `httpx`, `urllib`, your own `aiohttp.ClientSession()`), with web3's `HTTPProvider(url)` or `AsyncHTTPProvider(url)`, or with `curl` without a proxy. Not even "just to test". - Do not open a pool RPC URL with an agent tool or a browser either: not with a web-fetch tool, not in a browser, not with a shell command you run yourself without the proxy options. None of them use the owner's proxy. - With curl always write `curl --noproxy '' -x "$PROXY" ...`. A plain `-x` is silently ignored (the request goes direct) for hosts matched by `NO_PROXY` / `no_proxy`; `--noproxy ''` switches that list off. - `ProxyError` means every proxy is cooling down. Stop the work and tell the owner. Never catch it to retry without a proxy. - Do not rely on `HTTP_PROXY` / `ALL_PROXY` from the environment: the client ignores them. Pass `proxies=` explicitly. - The proxies are the owner's. Do not invent, scrape or search for proxies. No proxy available (none configured, none given to you)? STOP and ask the owner. Do not continue "for now" without proxies. ## Credentials The script reads everything from its environment and contains no secret: - `RPC_POOL_URL` = https://bomzrpc.online - `RPC_POOL_API_KEY` = the API key of this project. The owner creates it (one key per project). The client reads both variables when you do not pass `api_url` and `api_key`. - The proxies: the owner's proxy URLs, from the project's own config or environment variable. The examples below use `WORK_PROXIES` (comma-separated). That is the project's variable, not the client's: the client never reads proxies from the environment. If the project already has a proxy setting, use it instead of adding a second one. Proxy URL format: `scheme://USER:PASSWORD@HOST:PORT`, where scheme is one of http, https, socks4, socks5, socks5h. SOCKS needs an explicit port. `socks5h` resolves DNS on the proxy side. Rules: - Never invent, guess or hardcode an API key. Never write one into code, tests, docs, commits, logs or chat. If `RPC_POOL_API_KEY` is not set, stop and ask the owner for a key. - Never print the key, or proxy URLs that contain credentials. Keep `.env` out of git (`.gitignore`). The client does not read `.env`; load it your own way, for example `uv run --env-file .env python main.py`. - Send the key only in the `X-API-Key` header, only to https://bomzrpc.online. ## Python — install The client is the package `rpc-pool-client` (Python 3.12+), served by this site as a wheel. Install it with uv. uv needs the real file name of the wheel in the URL, so do not shorten the URL or swap in a "latest" alias; the file name changes with each release, and this text always shows the newest build. The client: ```bash uv add "rpc-pool-client @ https://bomzrpc.online/client/rpc_pool_client-0.1.0-py3-none-any.whl" ``` The client with web3.py support, INSTEAD of the command above, only if the script uses web3.py: ```bash uv add "rpc-pool-client[web3] @ https://bomzrpc.online/client/rpc_pool_client-0.1.0-py3-none-any.whl" ``` ONLY if the project does not use uv (add `[web3]` the same way if needed): ```bash pip install "rpc-pool-client @ https://bomzrpc.online/client/rpc_pool_client-0.1.0-py3-none-any.whl" ``` All builds: https://bomzrpc.online/client/. Install with `uv add` and never edit the dependency list by hand. The client brings aiohttp, aiohttp-socks and pycryptodome; web3 only comes with the `[web3]` form. ## Python — usage One `RpcPool` per process: create it inside `main()` with `async with`, never at module level. `pool.chain(...)` gives a cheap handle for one chain. The example is complete and runs as it is, with `RPC_POOL_URL`, `RPC_POOL_API_KEY` and `WORK_PROXIES` set: ```python import asyncio import os from rpc_pool_client import NoHealthyRpcError, RpcError, RpcPool WETH_BASE = "0x4200000000000000000000000000000000000006" # the WETH contract on Base async def main() -> None: # WORK_PROXIES is this project's own variable: the owner's proxy URLs, comma-separated. raw = os.environ.get("WORK_PROXIES", "") proxies = [p.strip() for p in raw.split(",") if p.strip()] if not proxies: raise SystemExit("WORK_PROXIES is empty: no proxies, no work (rule #1). Ask the owner.") # api_url and api_key default to RPC_POOL_URL and RPC_POOL_API_KEY. async with RpcPool(proxies=proxies) as pool: base = pool.chain(8453) # a chain id (or the key "eip155:8453"), never an alias like "base" try: head = int(str(await base.call("eth_blockNumber", [])), 16) logs = await base.get_logs_chunked( {"address": WETH_BASE, "fromBlock": head - 100, "toBlock": head}, chunk=1000 ) except RpcError as exc: # The node's own answer (a revert, bad params...): usually the same on every RPC. print("the node says no:", exc.code, exc.message) return except NoHealthyRpcError as exc: # Every RPC of the chain failed in every round. URLs and proxies are masked here. for attempt in exc.attempts: print(attempt.url, attempt.verdict) raise print(head, len(logs)) # ProxyError and PoolApiError are not caught on purpose: let them stop the script. if __name__ == "__main__": asyncio.run(main()) ``` Notes: - `await chain.call(method, params)` returns the JSON-RPC `result` (hex strings stay hex strings). It works for any chain that speaks JSON-RPC over HTTP: EVM, Solana, Starknet, Sui and others (`pool.chain("solana:mainnet").call("getSlot", [])`). - `await chain.get_logs_chunked(filter, chunk=1000)` runs `eth_getLogs` over a block range in pieces and halves a piece the RPCs refuse. `fromBlock` and `toBlock` are block numbers, `"earliest"` or `"latest"`. Use it instead of one big `eth_getLogs` call. - `await chain.call("eth_sendRawTransaction", [raw_tx])` goes only to RPCs that take transactions; resending the same signed transaction is safe. - Reads of old state: `pool.chain(1, archive=True)` offers archive RPCs only (`archive="full"` full-archive only). A state read at a block more than 128 blocks below the head switches to archive RPCs by itself. - `RpcPool(mode="round_robin")` spreads independent requests over the RPCs; the default `"sticky"` stays on one RPC while it works, which is right for reads that must agree with each other. web3.py (install the `[web3]` form). Every request web3 makes becomes a `chain.call(...)` with failover, routing and proxies; a revert arrives as web3's `ContractLogicError`: ```python from web3 import AsyncWeb3 from rpc_pool_client import RpcPool async def with_web3(pool: RpcPool, address: str) -> None: w3 = AsyncWeb3(pool.chain(8453).web3_provider()) print(await w3.eth.block_number) print(await w3.eth.get_balance(address)) # address must be a checksum address ``` For chains that are not JSON-RPC over POST (Aptos REST, Cosmos CometBFT `/status`, ...) use `chain.run(fn)`. The engine calls `fn(url, session)` on one RPC after another; `session` is already bound to the current proxy, so use only that `session`, never your own client: ```python import aiohttp from rpc_pool_client import RpcHttpError, RpcPool async def ledger_info(url: str, session: aiohttp.ClientSession) -> object: base_url = url.rstrip("/") async with session.get(base_url if base_url.endswith("/v1") else f"{base_url}/v1") as response: if not 200 <= response.status < 300: # Raise this before reading the body: the status tells the engine whose fault it is. raise RpcHttpError(response.status, response.reason or "") return await response.json() async def aptos_ledger(pool: RpcPool) -> object: return await pool.chain("aptos:mainnet").run(ledger_info) ``` `fn` may be called several times, on different RPCs, so it must be repeatable. ## What the client does for you - Failover around a ring: the pool's list for the chain (best first) is a ring with a cursor. When an RPC fails, it cools down for 60 s (doubling with each failure in a row, at most 900 s), the cursor moves on and the same request is repeated on the next RPC. When a whole round finds nothing, the list is fetched again (not more often than every 5 s) and a second round starts, then `NoHealthyRpcError`. - Proxy rotation: the client sticks to a proxy while it works. A proxy that fails is put on cooldown and the same RPC is tried through the next proxy. A 401, 403 or 429 from an RPC is tried once more through another proxy; only if that is refused too, the RPC counts as failed. When 3 different RPCs fail to connect through one proxy, the proxy itself is checked (`eth_chainId` through it); if the check fails, the proxy is dead and the failures it caused are not held against the RPCs. All proxies down: `ProxyError`, never a direct connection. - Errors that are yours are returned at once, without failover: a revert, "nonce too low", "insufficient funds" (`RpcError`) and HTTP 400, 413, 422 (`RpcHttpError`). `eth_sendRawTransaction` answered "already known" counts as success. - Capability routing: `eth_sendRawTransaction` only goes to RPCs that take transactions, `eth_getLogs` only to RPCs that served a 1000-block range (for wider spans those that served 10000 come first), archive reads only to archive RPCs. If no RPC of the chain can do it: `NoCapableRpcError`. There is no silent fallback to an unsuitable RPC. - Method marks: an RPC that answers "method not found" is skipped for that method for 600 s (no cooldown, no report). - Reports: the client tells the pool about RPCs that failed for you (`POST /v1/report`), automatically and in the background. Do not build your own reporting. - List refresh: the list of a chain is kept in memory and fetched again at the next call once it is older than `refresh_s`. - Disk cache: every list that arrives is saved under `cache_dir` (`~/.cache/rpc_pool_client`). If the pool is down, overloaded, answers 429 or `503 pool_stale` (it has not finished a health check recently), the client works from the saved list. A refusal (400, 401, 404) is never hidden by the cache. With no saved list and no pool: `PoolApiError`. - Defaults you rarely change: `timeout_s=15` per attempt, `max_rounds=2`, `refresh_s=600`, `mode="sticky"`. So, in Python: do NOT write retry loops around single RPC URLs, do NOT cache or store RPC URLs yourself (the client keeps the list and its copy on disk), and do NOT sort, filter or probe the list yourself. Catch only the final exceptions in the Errors section. ## Choosing chains Name a chain by its chain id or its key, never by an alias. The same reference must be used everywhere in a project. | Chain | chain id | key | |---|---|---| | Ethereum | `1` | `eip155:1` | | Base | `8453` | `eip155:8453` | | Solana | none | `solana:mainnet` | | Cosmos Hub | none | `cosmos:cosmoshub-4` | | Aptos | none | `aptos:mainnet` | - EVM chains: `eip155:`. Cosmos: `cosmos:`. Other ecosystems: `:` (`sui:mainnet`). - A network that is in no curated table is registered by the pool itself as `:x-` (`x-` for automatic, for example `sui:x-0f1e2d3c4b5a6978`). Such a key is stable and works like any other key; its name comes from the catalogs and `is_testnet` is null (unknown). If the owner later gives the network a curated key, the `x-` key stays valid as an alias of it: the answer then names the curated key in `chain.key`. Prefer the key that `GET https://bomzrpc.online/v1/chains?q=` lists. An `x-` chain whose RPCs have all been dead for a day is dropped; if its node reappears, the chain returns under the same key. - Aliases (`base`, `eth`, `sol`) are shortcuts for humans. When the catalogs change, an alias can become ambiguous and the pool then answers `404 unknown_chain` for a script that worked yesterday. A chain id or key is unambiguous forever. - Find the key or chain id: `curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "https://bomzrpc.online/v1/chains?q="` (it needs the API key; it is a request to the pool, so it goes direct). Each item has `key`, `name`, `ecosystem`, `evm_chain_id` (EVM only), `is_testnet`, `alive` (live RPCs) and `aliases`. Use `evm_chain_id` or `key`. Prefer a main network (`is_testnet` false) with `alive` above 0. Filters: `ecosystem=`, `testnet=`, `has_alive=`. - WebSocket: the Python client has no WebSocket support (the pool lists `transport=ws` endpoints, but nothing in the client uses them). Ask the owner before you use a WebSocket RPC at all. If the owner agrees, the connection is work traffic like any other: only through a proxy. ## Not Python? Any language works if it follows the same two rules: take the list from the HTTP API, and send every request to an RPC through a proxy. (Python: skip this section.) Set the proxy explicitly, in the code that makes each request to an RPC (a proxy option you pass to the HTTP client yourself, and a client object that has no way to connect directly). Never rely on `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` from the environment, and make sure `NO_PROXY` / `no_proxy` has no effect on your client: Go, Node, Rust, curl and others skip the proxy for hosts that match `NO_PROXY`, or ignore some of the variables for some schemes, and then the request leaves from the host's own IP. With curl that is `--noproxy ''`; in other stacks do not use environment-based proxy settings at all. The list, a direct request to the pool: `GET https://bomzrpc.online/v1/chains/{chain}/rpcs` with the header `X-API-Key`. `{chain}` is a chain id or a key. Query parameters, all optional: - `transport`: `http` (default) or `ws`. - `require`: a comma-separated list of `send_tx`, `logs`, `archive`, `archive_full`; only RPCs that have all of them. - `limit`: 1 to 1000, default 200. Applied after sorting. - `insecure=true` adds RPCs without TLS: leave it off. `tracking=none` keeps only RPCs marked as not tracking. The answer is JSON: `chain` (`key`, `name`, `ecosystem`, `evm_chain_id`, `is_testnet`), `policy`, `generated_at`, `next_check_at` and `rpcs`, best first. Each RPC has `url`, `transport`, `score` (0 to 1), `uptime_24h`, `streak`, `latency_ms`, `height`, `lag_s`, `tracking`, `caps`, `sources` and `last_ok_at`. `caps.send_tx` is true, false or null; `caps.logs` is the widest `eth_getLogs` block range that worked (0, 1000, 10000 or null); `caps.archive` is `none`, `recent`, `full` or null. Null means not measured, and an RPC with a null capability does not satisfy a `require` filter. Read `policy`: `proxy_required` is true, and `rule` is rule #1. Every `/v1` response also carries the header `X-RPC-Pool-Policy: proxy-required`. Both mean: no connection to any `rpcs[].url` without a proxy. The failover ring in pseudocode (this is what the Python client does): ```text state per chain: rpcs[] best first, cursor, cooldown{rpc.url}, fails{rpc.url}, unsupported{(rpc.url, method)}; proxies[] = the owner's call(method, params): for round in 1..2: # after a round with no answer: refetch the list (at most every 5 s) for rpc in rpcs from cursor, wrapping, skipping cooldown{rpc.url} > now and unsupported{(rpc.url, method)} > now: proxy = first proxy not cooling down; none left -> STOP, never go direct answer = POST rpc.url via proxy, timeout 15 s proxy failed (cannot reach it, 407): cool the proxy down, repeat the same rpc via the next proxy 401, 403, 429: repeat the same rpc ONCE via another proxy (if there is one); refused again -> failure, see "otherwise" result, or the node's own error (revert, nonce too low, HTTP 400): fails{rpc.url} = 0, return it as it is "method not found" and the like: unsupported{(rpc.url, method)} = now + 600 s, next rpc; no cooldown, NO report otherwise (timeout, 5xx, not JSON, -32005, or 401/403/429 refused again after the retry): fails{rpc.url} += 1 cooldown{rpc.url} = now + min(60 * 2^(fails{rpc.url} - 1), 900) s cursor = next rpc; POST /v1/report {chain, url, error} # fire and forget; http_403 / http_429 only after the retry try the next rpc with the same request raise "no healthy RPC" list: GET it when older than 600 s and save it to disk; 5xx, 429 or timeout -> use the last copy saved on disk; 400, 401, 404 are your mistake -> stop ``` Report a bad RPC with `POST https://bomzrpc.online/v1/report`, a JSON body `{"chain": "8453", "url": "", "error": "", "detail": ""}`; the answer is 202 (`{"accepted": false, "reason": "duplicate"}` means this key already reported that RPC within the hour: nothing to do). Report only what failed through your proxy, and a 401, 403 or 429 only after the retry through another proxy. Do not retry reports. Capabilities to route by: `eth_sendRawTransaction` only to RPCs with `caps.send_tx` true; `eth_getLogs` only to `caps.logs` of 1000 or more; old-state reads only to `caps.archive` of `recent` or `full`. curl, with the rule applied (the list goes direct, the RPC call only through the proxy): ```bash curl -fsS -H "X-API-Key: $RPC_POOL_API_KEY" "https://bomzrpc.online/v1/chains/8453/rpcs?limit=5" curl -sS --max-time 15 --noproxy '' -x "$PROXY" -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' "$RPC_URL" ``` The full algorithm, with the verdict table and bash and curl examples: https://bomzrpc.online/docs/FAILOVER.md (in Russian). ## Errors What the pool API answers (the Python client raises `PoolApiError` for 400, 401 and 404, with the status in the message): | Answer | Meaning | What the script does | |---|---|---| | 401 `unauthorized` | The API key is missing, unknown or revoked. | Stop and tell the owner. Do not retry and do not try other keys. | | 404 `unknown_chain` | The chain reference is unknown, or an alias became ambiguous. | Use a chain id or key. The error body has `suggestions`; `GET https://bomzrpc.online/v1/chains?q=` finds the right one. | | 400 `bad_request` | A parameter is wrong. | Fix the code. | | 404 `unknown_rpc` | `POST /v1/report` only: the pool has no such RPC in that chain. | Send the URL exactly as the pool gave it. Drop the report; do not retry it. | | 429 `rate_limited` | More than 600 requests per minute for this key (the default limit). | Slow down. The Python client keeps working from its list in memory or on disk. Other languages: wait and use the last copy. | | 503 `pool_stale` | The pool has not finished a health check recently and does not vouch for its lists. | Use the cached list. The Python client does this by itself; other languages keep the last saved copy and ask again later. | | 5xx, timeout | The pool is down. | Same as 503: use the cached list. | What the Python client raises (everything it raises on purpose is an `RpcPoolError`): | Exception | Meaning | What the script does | |---|---|---| | `ProxyError` | All proxies are cooling down. | STOP. Tell the owner. Never retry without a proxy. | | `PoolApiError` | The pool refused (400, 401, 404), or it is unreachable and there is no saved list. | Check `RPC_POOL_URL`, `RPC_POOL_API_KEY` and the chain id or key. | | `NoHealthyRpcError` | Every RPC of the chain failed in every round. `.attempts` says why. | Log `.attempts` (URLs and proxies are masked). Exit non-zero; the owner re-runs the script later. No retry or sleep wrapper. | | `NoCapableRpcError` | No RPC of the chain can do this (send a transaction, serve logs, archive state). | Change the chain or the method, or wait for the pool's next check. Do not work around it. | | `LogsRangeTooLargeError` | Several RPCs refused the `eth_getLogs` block range. | Use `get_logs_chunked` or a smaller range. | | `RpcError` | The node answered with an error (a revert, a bad nonce, insufficient funds). `.code`, `.message`, `.data`. | Handle it as the node's answer: a revert or a bad nonce repeats on every RPC. A provider's limit error can also arrive this way; read the message, and do not loop on it. | | `RpcHttpError` | An RPC answered an HTTP status that is about your request (400, 413, 422). `.status`. | Fix the request. | | `ValueError` from `RpcPool(...)` | No proxies, no `RPC_POOL_API_KEY` or `RPC_POOL_URL`, or a bad proxy URL. | Fix the configuration. There is no direct mode to fall back to. | ## Checklist before handing the script to the owner - [ ] No RPC URL is written anywhere (code, config, comments, tests). RPCs come only from the pool. - [ ] `proxies` is required and read from the project's config; the script refuses to start without proxies. No direct fallback exists anywhere, tests and helper scripts included. - [ ] It uses `rpc_pool_client` (outside Python: the documented algorithm). No own HTTP client, no `HTTPProvider(url)`, no `curl` without `--noproxy '' -x` for a pool URL; `run(fn)` uses only the session it is given. - [ ] Chains are named by chain id or key, the same way everywhere. No aliases. - [ ] No retry loops around single URLs, no own caching of URLs (in Python the client caches; outside Python keep only the last list the pool gave, on disk, as the algorithm says). Only the final exceptions are caught, and `ProxyError` only to stop. - [ ] One `RpcPool` per process, created in `main()` with `async with`, not at module level. - [ ] `RPC_POOL_URL`, `RPC_POOL_API_KEY` and the proxies come from the environment or config. `.env` is in `.gitignore`. No key, and no proxy credentials, in code, logs or output. - [ ] The client was installed with `uv add` and the wheel URL of the newest build listed at https://bomzrpc.online/client/; no dependency list was edited by hand. - [ ] You told the owner which variables the script needs: `RPC_POOL_URL`, `RPC_POOL_API_KEY` and the proxy variable. ## More The documents are written in Russian; the rules are the same as here. - Index of the documents: https://bomzrpc.online/docs/ - Rule #1 and how it is enforced: https://bomzrpc.online/docs/RULES.md - Failover in detail, error table, pseudocode, curl and bash examples: https://bomzrpc.online/docs/FAILOVER.md - Wiring a project, and a ready block for the project's CLAUDE.md: https://bomzrpc.online/docs/INTEGRATION.md - The HTTP API reference: https://bomzrpc.online/docs/API.md - Chain keys, aliases and how chains are identified: https://bomzrpc.online/docs/CHAINS.md - Client builds: https://bomzrpc.online/client/ When something here is unclear, ask the owner. Do not guess.