Docs
The endpoint
One HTTPS URL serves all standard JSON-RPC methods. Public traffic does not require an API key. Privileged keys exist for higher limits and access to admin-only namespaces.
https://rpc.baseazul.devThe endpoint is standard JSON-RPC 2.0. Content-Type must be application/json. Body is a single request object or an array (batch). Responses are gzip/zstd compressed when the client advertises support.
First request
Sanity-check the endpoint with a chainId call. You should see 0x2105 (decimal 8453).
curl -X POST https://rpc.baseazul.dev \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'
# → {"jsonrpc":"2.0","result":"0x2105","id":1}CORS
The endpoint sets Access-Control-Allow-Origin: *. Browser fetch works from any origin. Preflight (OPTIONS) responds with 204 and a 24-hour max-age.
Method support
All standard eth_*, net_*, and web3_* methods are available on the public path. Heavy/admin namespaces are blocked unless you authenticate with an API key.
eth_* | All standard reads, writes, filters |
net_* | — |
web3_* | — |
eth_sendRawTransaction | Always hits local reth (mempool retention) |
eth_getLogs | Up to 5,000-block window per call |
debug_* | Blocked on public path. Available with API key. |
trace_* | Parity-style traces not exposed. |
txpool_* | Blocked on public path. |
admin_* | Never exposed. |
Need debug_* or trace_* for production analytics? Reach out for an API key.
Rate limits
Per-IP limits on anonymous traffic. API-key holders bypass all of these.
Standard methods | 100 req/s sustained · burst 400 |
Heavy methods | 10 req/s sustained — eth_getLogs, large eth_call |
Batch size | 50 requests per batched call |
Max response body | 50 MB · 413 if exceeded |
Daily egress (anon) | 1 TB raw JSON / day · 503 once exhausted |
API key | Bypasses every cap above |
Hitting the cap returns -32005 with a Retry-After header. Back off with jitter; don’t spin.
Batching
Batch up to 50 requests in a single POST. Useful for fan-out reads (multicall warmup, ENS resolution, parallel eth_call). One TLS handshake, lower tail latency.
[
{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1},
{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xabc…","latest"],"id":2},
{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":3}
]eth_getLogs window
Max range is 5,000 blocks per call. Wider ranges return -32602. Page through history by chunking the range and parallelizing requests.
Error codes
JSON-RPC errors follow standard codes. HTTP-level errors come back as raw responses.
| Code | Trigger |
|---|---|
-32000 | Method blocked on public path (debug_, trace_, txpool_, admin_). |
-32005 | Rate limit exceeded. Back off or use an API key. |
-32601 | Method not found on the underlying node. |
-32602 | Invalid params. eth_getLogs window > 5,000 blocks is the usual culprit. |
-32603 | Internal error / upstream pool exhausted. |
HTTP 413 | Response too large (>50 MB). Trim your request. |
HTTP 503 | Daily egress quota exhausted (anonymous only). Use a key. |
Add a fallback
Azul is single-region. For production, pair it with a paid provider. viem has a first-class fallback transport: first URL wins; second takes over on error or timeout.
import { createPublicClient, http, fallback } from 'viem';
import { base } from 'viem/chains';
export const client = createPublicClient({
chain: base,
transport: fallback([
http('https://rpc.baseazul.dev'), // Azul — primary, fast, no logging
http('https://mainnet.base.org'), // Coinbase default — last resort
], { retryCount: 1, retryDelay: 100 }),
});Subscribe to new blocks
Don’t poll eth_blockNumber in a tight loop. WebSocket subscriptions are available with an API key — request one if your workload depends on live tip-follow.
import { createPublicClient, webSocket } from 'viem';
import { base } from 'viem/chains';
const client = createPublicClient({
chain: base,
transport: webSocket('wss://rpc.baseazul.dev/ws?api_key=YOUR_KEY'),
});
const unwatch = client.watchBlockNumber({
onBlockNumber: (n) => console.log('new tip', n),
});Page through eth_getLogs
Chunk wide ranges into 5,000-block windows and parallelize. The proxy handles concurrent batches up to your per-IP cap.
async function getLogsRange(client, address, fromBlock, toBlock) {
const PAGE = 5000;
const tasks = [];
for (let start = fromBlock; start <= toBlock; start += PAGE) {
const end = Math.min(start + PAGE - 1, toBlock);
tasks.push(client.getLogs({ address, fromBlock: start, toBlock: end }));
}
return (await Promise.all(tasks)).flat();
}Read/write split
The proxy treats reads and writes very differently. Reads load-balance across a pool of 16 Base RPCs and never touch the local node. Writes always hit the local reth and the top-2 upstreams in parallel.
This is deliberate: reads stay off the local node so your traffic pattern never leaks into the box that holds the bot. Writes go to local first so the proxy retains the transaction in its mempool for downstream observers.
reads (eth_call, eth_getBalance, eth_getLogs, …)
→ upstream pool — 16 Base RPCs, top-3 latency-weighted
writes (eth_sendRawTransaction, eth_sendTransaction)
→ local reth ALWAYS (mempool retention)
+ top-2 upstreams in parallel (fast inclusion)Sequencer propagation
For writes, the parallel fan-out is the key edge. The proxy submits your raw tx to local reth and to the two fastest healthy upstreams at the same time, so it reaches the Base sequencer over whichever path is fastest right now. Compared to a single-upstream submission, this trims tail-latency for inclusion and removes a class of dropped-tx failure modes.
Read-side network routing follows the same principle: every 15s health-check picks the top-3 fastest healthy upstreams and serves reads from a random one. A pool member that slows down or starts erroring drops out automatically.
Upstream pool
The read pool is 16 known public Base RPCs. Latency-weighted top-3 random selection. Health checks every 15s. Failed upstreams auto-degrade and auto-recover after 60s. If the entire pool is exhausted, requests return -32603 with no local fallback — by design, so reads never accidentally hit the local node.
What we don't log
The RPC path retains nothing per-request:
- No IP addresses
- No request bodies (so: no wallet addresses, no contract addresses)
- No response bodies
- No cookies, sessions, or cross-request identifiers
We do count anonymous aggregate bytes/day in memory for the egress cap. That counter resets daily and isn’t attributed to any IP or request. Full posture statement: /privacy.