Repository navigation
[Community] Arc Testnet survival kit — RPC fallbacks, USDC decimals, EIP-7825 gas caps #305
Description
Activity
Ran the kit's claims against live Arc Testnet (2026-09-02) and the v0.8.0 source before answering the two asks. Disclosure: I reviewed #299, which this kit cites.
Ask 2 — verification of the USDC-decimal / gas-cap semantics
Everything I could test independently checks out:
Claim Result Chain id 5042002✅ eth_chainId→0x4cef52on all endpoints tested todayUSDC at 0x3600…0000, 6 decimals✅ live eth_call:decimals()= 6,symbol()=USDCNative 18-dec / ERC-20 6-dec, shared pool, 10¹² scale ✅ live account 0x570f…1e78:eth_getBalance=771915579006064000,balanceOf=771915— exact÷10¹²truncation match, so the max-send dust trap is realBlock gas limit 30,000,000 ✅ live eth_getBlockByNumber--rpc.gascapdefault 30M✅ in source: crates/node/src/args.rspins--rpc.gascap=30000000(flips Reth's 50M default; covered bytest_rpc_gascap_default_is_thirty_millionand the launched-node test incrates/execution-e2e/tests/e2e/static_rpc_gas_cap.rs)16,777,216 = 2²⁴ is protocol-level (EIP-7825, Osaka), not endpoint policy ✅ consistent with crates/evm/src/evm.rs, whose tests explicitly disable the "EIP-7825 tx gas limit cap" to run bigger-gas fixtures — i.e. the cap is enforced in the EVM layer, so "switching RPC providers will not help" is the correct adviceAll 9 public endpoints in constants.ts✅ all answered eth_chainIdwith0x4cef52todayBonus datapoint from the endpoint sweep:
rpc.quicknode.testnet.arc.ioreturned-32011("request limit reached") to a singleeth_chainId— the public budget was already saturated by other users' traffic at that moment. That's real-world justification for both the fallback list andwaitForReceiptWithRetry; a-32011can hit you on your very first call of the day.receipt.tswalking thecausechain (rather than checkingerror.codeat the top level) is the right shape —-32011surfaces at different wrapping depths depending on which viem call path throws, which is exactly why unwrappedwaitForTransactionReceiptaborts while most read paths advance past it.Two small tightening suggestions, not corrections:
rpc.ts— spread the first hit. Plainfallback()always starts aturls[0], so every copy-paste user of the kit hammersrpc.testnet.arc.networkfirst while eight endpoints idle — and with per-endpoint budgets this tight, that ordering matters. Considerfallback(transports, { rank: true })or rotating the list to a random start index at client creation.gas.ts— one-comment caveat.allowance (N)is a single message shape shared by balance-derived and cap-derived budgets; the kit's value-match against16_777_216/30_000_000is a reasonable heuristic, but a balance that coincidentally derives to exactly one of those values would misclassify. Worth a comment so nobody treats the returnedcauseas ground truth in funds-related error handling.
Ask 1 — should it live upstream?
External is the right home, and not because of quality. This repo's docs are deliberately node-operator scoped — application-developer guidance has consistently been routed to developers.circle.com rather than into
arc-node, so a viem-patterns kit wouldn't land here regardless. The durable upstream fix for the shared failure modes stays #299 (endpoint table + cap semantics in docs the right audience will find); the kit works well as the copy-paste companion to it. Keeping the "not official Circle/Arc material" framing prominent in the README is the right call.Live companion tool — Arc RPC Pulse probes all 9 public testnet endpoints (chainId, latency, rate limits) and refreshes every 10 minutes:\n\nhttps://kutluhaneth46.github.io/arc-rpc-pulse/\n\nSource: https://github.com/kutluhaneth46/arc-rpc-pulse\n\nPairs with the survival kit; endpoint list matches #299 / constants.ts.
Went through Arc RPC Pulse end to end — site,
data.json, probe code, and workflow — and re-probed independently. It works, and its current snapshot independently reproduced the QuickNode-32011I reported above, which is a good sign. One finding below is worth acting on, because it changes both this dashboard's math and the survival kit's fallback strategy.Verified
- Page serves 200;
docs/data.jsonwas fresh at fetch time (checkedAtmatched the last workflow run),expectedChainId0x4cef52correct, and all nineblockNumbervalues clustered within one block of each other — so no endpoint is silently serving a stale head right now. - The nine URLs in
lib/endpoints.mjsmatch the docs: add public testnet RPC guide for gas caps and rate limits #299 table exactly — same hosts, same order, no drift. ✔️
The finding: the nine endpoints are not nine independent budgets
The two QuickNode hosts are the same backend behind one rate-limit budget:
rpc.quicknode.testnet.arc.network -> 64.130.45.56 rpc.quicknode.testnet.arc.io -> 64.130.45.56 (identical)And empirically, not just by DNS — I sent five rapid
eth_chainIdcalls to.network(call 1 succeeded, calls 2–5 returned-32011), then immediately hit.io, which also returned-32011. Exhausting one host exhausts the other.Two consequences:
- For the dashboard: "9 total / 8 healthy" overstates redundancy. The meaningful denominator is distinct providers, not hostnames. Grouping the
.network/.iopairs (or showing a provider-level row with the hostname variants nested) would report real availability. Worth noting your own snapshot already shows the artifact of this —quicknode-networkred whilequicknode-iogreen, at the same instant, on the same backend. - For the survival kit: this is the more important one. A
fallback()list that rotates…quicknode.testnet.arc.network→…quicknode.testnet.arc.ioafter a-32011is retrying into the same exhausted budget. Fallback ordering should hop providers, not hostnames — which strengthens therank: true/ shuffle suggestion from my earlier comment, but with the extra constraint that the pairs should be treated as one unit.
Three smaller notes
- Rate-limited ≠ unhealthy.
probeEndpointsetsok: falseon a-32011, which drops that endpoint fromavgLatencyMsandfastest, and counts it inunhealthy. But a-32011is routine and transient — as your own kit'swaitForReceiptWithRetryassumes. A distinctdegraded/limitedstate, plus one retry with backoff before classifying, would avoid a red square for a node that's up and answering a second later. - Latency is single-vantage and self-contended.
pulseAllfires all nine probes viaPromise.allfrom one GitHub Actions runner, so the numbers include the tool's own concurrency and one network position. Sampled serially from a different vantage just now: primary99ms(pulse: 380),rpc.testnet.arc.io145ms(pulse: 176),arc-testnet.drpc.org147ms(pulse: 575). Thefastestbadge in particular isn't stable across vantage points — worth labelling it "as measured from a GitHub Actions runner" so nobody picks a production endpoint off it. - Silent-staleness risk.
app.jsrendersLast check · <local time>with no age check, and*/10 * * * *on GitHub Actions is best-effort — scheduled runs get delayed under load, and scheduled workflows on public repos are subject to the 60-day inactivity auto-disable (don't assume the bot's own commits reset that clock). If the cron stops, the page keeps showing a plausible-looking timestamp. A rendered "updated N minutes ago" plus a stale badge past ~30 minutes would make a stalled feed obvious.
Also worth weighing against the above: probing 9 endpoints × 2 calls every 10 minutes is ~2.6k calls/day into budgets the tool itself reports as saturated. Given how quickly the QuickNode budget emptied in my five-call test, folding the shared-backend pairs into a single probe would cut that noticeably while improving accuracy.
- Page serves 200;
Addressed the RPC Pulse findings in kutluhaneth46/arc-rpc-pulse@master:\n\n- Provider budgets: 5 provider rows, 9 hostnames nested; QuickNode tagged \shared budget\n- Limited ≠ down: -32011\ → \limited\ status after 1s retry; summary counts providers OK separately\n- Serial probes from one runner (no Promise.all fan-out)\n- Stale badge when snapshot >30m old; age shown as \Nm ago\n- Survival kit: \ARC_TESTNET_RPC_PROVIDER_URLS\ (5 URLs) + \allback(..., { rank: true })\ — no .network→.io hop on same provider\n\nLive: https://kutluhaneth46.github.io/arc-rpc-pulse/
Checked
ad67eae(pulse) andb291e7c(kit) against the live snapshot — all four items are implemented as described:- Provider grouping —
PROVIDERSwith 5 rows / 9 hosts nested,sharedBudget: trueon QuickNode, and the live JSON now reportsprovidersOk: 5/5alongsidehostsOk. ✔️ limited≠ down —probeHostretries once after 1s on-32011and returnsstatus: "limited", kept out ofhostsError. ✔️- Serial probes —
Promise.allfan-out gone; nestedforloops. ✔️ - Stale badge / vantage —
STALEbadge past 30m withNm ago, and the vantage line renders. ✔️ - Kit —
ARC_TESTNET_RPC_PROVIDER_URLSis exactly one URL per provider budget, andarcTestnetTransportdefaults to it withrank: true. ✔️
I ran more probes against the assumptions underneath these changes, and three results are worth having:
1. The QuickNode budget is ≈1 request per second — your 1s retry is correctly calibrated. Tested pairs of
eth_chainIdat increasing gaps:Gap between calls 2nd call 0s (back-to-back) -320111s ok 2s ok 5s ok So
RETRY_DELAY_MS = 1_000sits right at the refill boundary — worth a comment in the code so nobody "optimizes" it to 250ms later.2. The other three pairs show no shared-budget behavior. I re-ran my QuickNode method (8 rapid calls to host A, then immediately host B) against Arc, dRPC, and Blockdaemon: all 8 succeeded on every host A, and every host B answered fine straight after. So
sharedBudget: falseon those three is consistent with observation. One caveat worth encoding as "unverified" rather than "false": absence of limiting at this volume isn't proof of separate buckets — only that their budgets are far looser than QuickNode's, which limits at the second request.3. Residual shared-budget risk inside
probeHostitself. On success it fireseth_chainIdand theneth_blockNumberback-to-back with no delay. For QuickNode those two land inside the same ~1s window, and theeth_blockNumberfailure is swallowed by the barecatch {}— so the host would reportstatus: "ok"withblockNumber: null, i.e. silently missing data rather than a visible error. In practice the current snapshot has block numbers on all nine (natural per-call latency of 200–450ms is apparently enough), and I reproduced the sequence manually against a rested budget with both calls succeeding — so it's marginal, not broken. A small inter-call delay whensharedBudgetis set would make it deterministic instead of latency-dependent.On
rank: true— completing my own earlier advice, since it has a background cost. viem's ranker pings every transport everyinterval, andintervaldefaults toclient.pollingInterval(4s), usingnet_listeningas the ping.- Good news, and not a given: I verified all five provider endpoints implement
net_listeningand returntrue, so the default ping works on Arc — no false "unstable" deranking. - But at the 4s default that's ~0.25 req/s of background traffic per provider. Against QuickNode's measured ~1 req/s ceiling, ranking alone consumes roughly a quarter of the budget before the app issues a single call. For these endpoints I'd pass viem's own documented example config instead of bare
true:rank: { interval: 60_000, sampleCount: 5 }.
Two trivia to close:
providerStatushas a redundant branch —every(h => limited)is fully subsumed by thesome(h => limited)line right after it, so the middle line is dead. AnddRPC.orgcame back at 50ms in this snapshot versus 575ms in the previous one — a 10× swing on one host between runs, which is a good argument for the vantage note you added and against readingfastestas a recommendation.- Provider grouping —
@huklaa I've previously posted related work on this community kit / RPC pulse thread. If you're planning a PR, please check existing open PRs first so we don't duplicate — happy to coordinate.
Summary
Community reference repo with copy-paste Arc Testnet helpers — RPC fallbacks, USDC decimal traps, EIP-7825 gas-cap parsing, and receipt retry on
-32011.Repo: https://github.com/kutluhaneth46/arc-dev-survival-kit
Motivation
Several open issues/PRs describe the same failure modes this kit encodes as portable TypeScript:
rpc.ts—fallback()across public endpointsgas.ts— EIP-7825 vs balance allowance errorsusdc.ts— native 18-dec vs ERC-20 6-dec USDCreceipt.ts— retrywaitForTransactionReceipton concurrency limitsContent aligns with the EIP-7825 corrections in #299 (reviewed by @osr21). The survival kit is linked from the PR thread, not from upstream docs — per maintainer preference for third-party links.
What it is / isn't
Ask
Related contributor PRs: arc-node #299, #297, #295, arc-commerce #58.
Additional context
Unofficial community resource — feedback welcome.