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.

POSThttps://rpc.baseazul.dev

The 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).

bash
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_sendRawTransactionAlways hits local reth (mempool retention)
eth_getLogsUp 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 methods100 req/s sustained · burst 400
Heavy methods10 req/s sustained — eth_getLogs, large eth_call
Batch size50 requests per batched call
Max response body50 MB · 413 if exceeded
Daily egress (anon)1 TB raw JSON / day · 503 once exhausted
API keyBypasses 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.

json
[
  {"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.

CodeTrigger
-32000Method blocked on public path (debug_, trace_, txpool_, admin_).
-32005Rate limit exceeded. Back off or use an API key.
-32601Method not found on the underlying node.
-32602Invalid params. eth_getLogs window > 5,000 blocks is the usual culprit.
-32603Internal error / upstream pool exhausted.
HTTP 413Response too large (>50 MB). Trim your request.
HTTP 503Daily 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.

typescript
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.

typescript
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.

typescript
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.

text
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.