Rotate ENS lookups across free RPCs, fall back to paid on 429 - #10
Closed
frolic wants to merge 2 commits into
Closed
Rotate ENS lookups across free RPCs, fall back to paid on 429#10frolic wants to merge 2 commits into
frolic wants to merge 2 commits into
Conversation
Wrap the resolver's viem client in a `fallback` transport over a randomly-shuffled pool of free public RPCs, with the paid endpoint (ETHEREUM_RPC_URL) pinned last. viem advances to the next transport on any non-user error (429s included) and retries each zero times, so the paid RPC is only hit when every free RPC is failing — cutting paid RPC cost. Per-request shuffle spreads load across the pool (a stateless Worker can't do true round-robin). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Replace the guessed RPC list with 7 endpoints actually verified to resolve ENS forward+reverse in <400ms; several well-known public RPCs (llamarpc, cloudflare-eth, 1rpc, ankr) were down, rate-limited, or reverted on the universal-resolver call. Adds scripts/verify-rpcs.ts (pnpm --filter ./api run verify:rpcs) to re-check the pool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 9, 2026
Owner
Author
|
Superseded by #21, which brings this forward onto the v2 monorepo API worker ( |
frolic
added a commit
that referenced
this pull request
Jul 15, 2026
Brings **#10** (RPC rotation) and **#11** (chainlist-backed healthy list) forward onto the v2 monorepo API worker, rebuilt around a cron + KV. ## How it works - **`ethereumTransport`** — viem `fallback` across the free RPCs in random order, paid endpoint (`ETHEREUM_RPC_URL`) last. viem retries each transport zero times and advances on any non-user error, so a **429 rolls over transparently** — keeping paid usage, and cost, minimal. Shuffling spreads load (a stateless Worker can't round-robin). - **Cron (`0 * * * *`)** — health-checks the chainlist candidates and writes the survivors to **KV**, once for the whole fleet. - **Request path** — one KV read: ```ts const healthy = (await env.RPCS.get<string[]>(RPCS_KEY, "json")) ?? []; transport: ethereumTransport(healthy, env.ETHEREUM_RPC_URL) ``` A cold/empty list just means straight to the paid RPC — the transport already handles that. ## Why cron + KV rather than refreshing on the request path `caches.default` is **per-colo**. Refreshing lazily meant *every* data center re-ran the full 41-endpoint health-check pass hourly — roughly **2k check requests/hour** against the free RPCs the rotation exists to lean on, which is how you get rate-limited by them. The cron does **~41/hour**, once. And because a cron runs in a single colo, the list has to live somewhere global — hence KV, not the Cache API. ## What that deleted Falling back to the paid RPC on a cold KV removed the seed list, and with it most of the machinery: - `rpcUrls.ts` (7-endpoint seed list), `getHealthyRpcs.ts` (cache + refresh + staleness), `scripts/verify-rpcs.ts`, and a dead `alchemy.run` esbuild stub in the tests. - Gone as concepts: `waitUntil`, the `Age` check, the `refreshing` dedupe flag, and the two competing TTLs. ## Verified on a preview stage - Cron registered: `['0 * * * *']`; per-stage KV namespace created. - A triggered refresh wrote the health-checked list to KV (nodereal, publicnode, mevblocker, regional blxrbdn, …). - **Cold KV** → resolves via the paid RPC. **Warm** → resolves via the KV list. - Typecheck clean; **15/15 tests**, including the cold-KV→paid path in both the transport unit test and the miniflare integration test (which now runs with an empty KV). **Note:** the deploy token needed `Workers KV Storage: Edit` added. Supersedes #10 and #11. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #9. Cuts paid RPC cost by rotating ENS lookups across free public RPCs, only using the paid endpoint (
ETHEREUM_RPC_URL, Alchemy) when the free ones are all failing/rate-limited.Approach
viem's
fallbacktransport already does the hard part — I confirmed in its source that it advances to the next transport on any non-user error (429s roll over) and retries each transport zero times, only throwing once the last one fails. So the whole feature is:api/src/rpcUrls.ts— the free RPC pool (easy to tune).api/src/ethereumTransport.ts—fallback([...shuffle(freeRpcs), paid]).router.ts— use it instead ofhttp(env.ETHEREUM_RPC_URL).Per-request shuffle spreads load across the pool — without it
fallbackalways tries the first URL first and would hammer it into rate limits. A stateless Worker can't do true round-robin (no shared counter), so randomization is the pragmatic equivalent.Not rpc-racer's racing
Took the idea from rpc-racer, not its approach: it fans each request out to N RPCs in parallel and races them. That multiplies request volume to the free tiers (more 429s, more usage) — counterproductive when the goal is cost. Sequential fallback hits exactly one endpoint per call in the happy path.
Tests
api/src/ethereumTransport.test.ts(2 new, 8 total passing):Paid usage now only occurs on cache misses where the free pool is exhausted (the worker still edge-caches results 24h).
🤖 Generated with Claude Code