PulseChain RPC Failover Open Standard
Draft v0.20 · published 2026-08-26 · open to any implementer, no permission needed
Twice in two months, PulseChain worked perfectly while most people could not use it. In July 2026 the official gateway died for fifteen hours; in August the community gateway everyone had fled to died for two days. Both times the fix was the same and arrived too late: switch to an endpoint that was up all along. This page turns that fix into a standard — written rules plus a shared registry — so the switching happens in software, before the panic, for everyone at once.
The Principle
The failure unit is one hostname — whoever owns it. July killed the official gateway, August killed the community favorite, and every g4mm4 service fell together in identical windows, which is what provider-level failure looks like. No single endpoint, however good its record, is an architecture. Resilience lives in the client: every app and wallet holding several doors and walking to the next one without being asked.
Everything below is tokenless, permissionless, and buildable today. It stands on two open Ethereum standards — wallet_addEthereumChain (EIP-3085) and wallet_switchEthereumChain (EIP-3326) — and on measurement this site already publishes daily. Prior art exists and is welcome company: ethers.js ships a FallbackProvider, and ERC-5139 drafted a provider-list format — this standard is a chain-specific practice and etiquette layer that sits on top of such tools, not a replacement for them; a developer already using FallbackProvider conforms by feeding it this registry and these rules. What the wider EVM world never produced is exactly that layer: named real endpoints, measured continuously, with the etiquette that keeps volunteer gateways alive. That is the gap this page fills for PulseChain.
1 · The Shared Registry
A machine-readable list of PulseChain's public gateways, shaped so a wallet can consume it directly for EIP-3085 (chain ID, currency, explorer included), lives at /data/rpc-registry.json. Beside it, the living measurement: /data/rpc-health.json, refreshed every six hours by an unretried probe of every listed gateway — latency timed, staleness judged by cross-gateway block-height comparison. The daily record renders on RPC.
- Listing is measurement, not endorsement — a gateway earns its row by being probed, and its health record tells the truth about it either way. A failing gateway stays listed with a failing record; delisting happens only when a URL is retired, reassigned, or turns hostile.
- Consumers rotate — an app that reads the registry and hardcodes its favorite has rebuilt the herd. Hold several; walk the list.
- Operators can join, and here is the door — run a public PulseChain gateway and send its URL, your operator identity, and a status page link through the Suggest button on any page of this site; the seat joins the probe pool and starts earning its record with the next cycle. The registry is how storm-time volunteer capacity becomes standing capacity instead of a lost announcement. Healthy operator practice earns lasting rows: a stated identity, a status page, and honest rate limits outlast anonymous capacity that vanishes under load.
- The probe, exactly — so the health feed itself can be audited and recreated: one unretried
eth_blockNumberPOST per gateway per cycle (retries would mask the truth being measured), a nine-second timeout, four cycles a day (00:22, 06:43, 12:43, 18:43 UTC). A gateway reads slow above 2.5 seconds, and stale when its height sits three or more blocks behind the tallest answer in the same cycle — staleness is judged against the other gateways, never self-reported. The published summary carries each day's worst state per gateway plus the freshest reading; every raw probe is committed to the public history file. - The registry must not become the new single point — one hostname publishing the endpoint list would repeat the disease this standard treats. Mirror it freely: copy it, pin it to IPFS, republish it — the license below permits all of it — and any conforming implementation may use any health-checked list that follows the same schema and discipline. This site is the primary publisher, not the sole source of truth.
2 · Rotation — Rules for an App's Own Traffic
Every number in this standard — the timeout budget, the probe cadence, the scan-chunk sizes — is a recommended default, not a law: each one came from measurement on the live gateways (the measurement basis is published with the record on RPC), and implementers should re-validate and tune for their own workload.
- Hold at least three gateways from independent operators, ordered by preference — a local or private node first, if the user has one, then trusted public seats. Two is a coin flip during a storm; three survived both of 2026's.
- One timed try, then advance — give each gateway a single attempt with a hard timeout budget (about nine seconds measured well in practice), then move to the next. Never retry a dead gateway in a loop.
- Sticky cursor, with a cooldown — a gateway that answered keeps first turn, so a dead primary costs its timeout once per session, not once per request. The cursor's natural scope is the session (per tab); persisting the last-good seat per origin across sessions is fine and saves the next visit a timeout, so long as it expires on the next failure rather than becoming a new hardcoded favorite. A gateway that failed sits out a cooldown before it is tried again — 30 to 60 seconds as the default, doubling while failures continue, capped around ten minutes; a flapping endpoint hammered on every request is how one outage becomes two.
- Reads rotate freely; writes rebroadcast, never re-sign — a read served by any honest gateway is the same read. A transaction is different: if a broadcast times out, the transaction may have landed anyway, so failover means rebroadcasting the identical signed transaction to the next gateway (safe — same hash, the network deduplicates) and checking for the receipt before anything else. Never auto-re-sign, and never let a spinner talk the user into signing twice — the July storm's 304M-PLS fee receipt is what panic re-signing costs.
- The write path, step by step — after any broadcast, poll for the receipt on a healthy gateway for at least two to three block times (PulseChain blocks land about every ten seconds; ~30 seconds is a sane default) before concluding anything. "No receipt yet" has two very different meanings: pending (the transaction is known to the network and will land — wait) versus unknown (no gateway has seen the hash — rebroadcast the same signed bytes). With several transactions in flight, rebroadcast in nonce order — a later nonce cannot land before an earlier one, and re-sending only the newest strands them all. The transaction hash is a function of the signed bytes — derive it locally when you can; a gateway that never returns the hash is not a reason to lose the handle, and an "already known" reply must never orphan a landed transaction. One reply is overloaded: "nonce too low" can also mean a different transaction already consumed that nonce — if this hash never gets a receipt, stop waiting, and do not invite a same-nonce re-sign. And tell the user what is happening in words that prevent panic: "sent, waiting for confirmation — do not sign again" is UI, but it is also safety equipment.
- Judge staleness, not just liveness — probe with
eth_blockNumberand compare heights across gateways: a fast answer three blocks behind is still a wrong answer. No single gateway can confess to being stale; only the comparison can catch it. The comparison has edge cases: gateways can briefly disagree at the chain tip harmlessly, and in a bad enough storm a majority of public seats can lag together, making the laggard look like the truth — so a pure majority vote among public gateways is not the last word. For decisions where being wrong costs money, prefer a private node first, then the longest-record seats, over any majority of strangers. - Fail honestly — when every door is dead, show the last good data with its timestamp, or say plainly that reads are failing. A silent blank teaches users the chain is broken when only a gateway is; both storms filled timelines with exactly that misdiagnosis.
3 · The Guided Switch — Reaching the Visitor's Wallet
An app's rotation fixes the app's eyes; the visitor's wallet is still pointed wherever it's pointed, and a transaction signed against a dead wallet gateway hangs at broadcast. An app cannot change a wallet's RPC — and should not want to. It can offer.
- Detect — a confirm step that produces no wallet response inside the timeout budget is a wallet-side gateway failure, distinguishable from the app's own reads working fine.
- Offer — call
wallet_addEthereumChainwith chain 369 and a currently-healthy gateway from the registry. The wallet shows the visitor an approval naming the URL; nothing changes without their click. Never switch silently, and never offer an endpoint that isn't on a published, health-checked list. - Credit where seen — the aggregator Piteas shipped this behavior within hours of the August storm starting, the fullest version yet seen on PulseChain (their own account of it). The implementation is theirs; the pattern is two open EIPs and belongs to everyone.
4 · Query Etiquette — Why Free Gateways Die
Gateway operators and our own measurements agree on what actually kills public endpoints: not user transactions but inefficient reads — above all, unbounded eth_getLogs scans from block zero, retried in loops by apps that never index. A standard that only lists gateways feeds volunteers to that grinder. Conforming apps:
- Walk ranges — log scans go in bounded chunks (measured on the default gateway: a multi-address filtered scan survives about five days of blocks per call; a single-address scan about thirty), never from block zero with a topic filter.
- Cache what cannot change — confirmed history is immutable; refetching it is pure load. Batch small calls; keep batches modest (free seats measurably refuse large ones).
- Degrade politely — on a rate-limit answer, back off and rotate; hammering a limiter is how one app's bug becomes everyone's outage.
5 · The Deepest Fix — Wallet-Level Automatic Failover
Everything above softens outages; one change would nearly end them. The deepest fix is wallet-level automatic failover — the wallet itself holding several RPC URLs per network and advancing on failure, the way apps already do. The wallet is where balances render and transactions broadcast; when its one configured gateway dies, every dapp on earth looks broken at once. As re-verified against primary sources on 2026-08-26, no wallet documents checkable automatic failover on PulseChain — two vendor claims exist without documentation, and the capability matrix on RPC records them as claims. The column is earned by published documentation, and it is still empty. A conforming wallet:
- stores several RPC URLs per network (MetaMask already does) and advances automatically on timeout, telling the user which gateway it moved to;
- seeds its list from a published registry rather than one shipped default;
- lets the user pin a preferred gateway — automatic failover is a floor, never a leash.
The first wallet to ship this owns the matrix column — and the next storm's headlines.
Security Notes — What a Healthy Gateway Still Isn't
A gateway that answers fast at the right height is available — not honest. Any RPC can still lie about what you see, censor transactions, or log your addresses beside your IP; health checks measure none of that. So: prefer gateways with named operators and a track record; treat a private or local node as the gold standard the ordering already prefers; for reads where being wrong costs money, compare the answer across two independent gateways before acting on it; and the guided switch's rule bears repeating as a security rule, not just a courtesy — no endpoint enters a user's wallet without the wallet's own approval window naming it, ever. The fuller threat model lives on RPC.
The Conformance Checklist
The sections above, folded into a list an implementer can walk in one sitting. Every box is checkable from the outside — in the app's network tab, its UI text, or its published docs.
An App Conforms (Sections 2 + 4) When It:
- ☐ holds at least three independent gateways, a local or private node first when the user has one;
- ☐ gives each gateway one timed attempt (~9 s default) then advances — never a retry loop on a dead seat;
- ☐ keeps a sticky cursor, and cools a failed seat down (30–60 s, doubling, capped ~10 min);
- ☐ never auto-re-signs — failover on writes means rebroadcasting the identical signed bytes only;
- ☐ derives or accepts the transaction hash from the signed bytes, so no gateway reply can orphan the handle;
- ☐ fails immediately on permanent rejections (insufficient funds, gas too low) instead of spinning the receipt window;
- ☐ polls for the receipt ~30 s (two to three blocks) before treating a broadcast as unknown, and distinguishes pending from unknown;
- ☐ rebroadcasts multiple in-flight transactions in nonce order;
- ☐ shows the anti-panic line: "sent, waiting for confirmation — do not sign again";
- ☐ judges staleness by cross-gateway height comparison, not liveness alone;
- ☐ on total failure shows last-good data with its timestamp, or an honest "reads are failing" — never a silent blank;
- ☐ walks log scans in bounded ranges — never unbounded eth_getLogs from block zero;
- ☐ caches immutable history and keeps batches modest;
- ☐ backs off and rotates on a rate-limit answer.
Full Conformance Adds (Section 3):
- ☐ detects wallet-side gateway failure (the confirm step times out while the app's own reads work);
- ☐ offers a currently-healthy registry endpoint via wallet_addEthereumChain — never a silent switch;
- ☐ offers only endpoints from a published, health-checked list.
A Wallet Conforms (Section 5) When It:
- ☐ stores multiple RPC URLs per network;
- ☐ advances automatically on timeout or failure, telling the user which gateway it moved to;
- ☐ seeds its list from a published registry, or lets the user do so;
- ☐ lets the user pin a preferred gateway — failover is a floor, never a leash;
- ☐ documents the behavior publicly — publication is what earns the matrix column.
A Minimal Rotator — a Sketch, Not a Library
Roughly sixty lines carry the whole pattern. Three details in it matter more than they look: an RPC-level error (a revert, bad params) is a valid answer from a healthy gateway — it follows you to every seat, so it must never trigger rotation or cooldown; when every seat is cooling down at once, every seat is walked soonest-free first — a storm must degrade service, never disable the app; and the write path only ever re-sends the same signed bytes, returning a known hash after the receipt window so the caller owns the longer-lived poller and the anti-panic message. The cooldown doubles on consecutive failures (45 s base, 10-minute cap, full reset on success), and the landed() test is a heuristic, not a complete catalogue of client error strings — production code broadens it, and adds per-origin persistence, staleness comparison, logging, and the honest-failure UI strings.
// Minimal conforming rotation helper (TypeScript) — a starting point.
// Gateways arrive ordered: local/private node first, then preference.
const TIMEOUT_MS = 9000;
const BASE_COOLDOWN_MS = 45000; // 45 s, doubling on consecutive failures
const MAX_COOLDOWN_MS = 600000; // 10 min cap
const RECEIPT_WAIT_MS = 30000; // ~3 PulseChain blocks
class RpcRotator {
private lastGood?: string;
private coolUntil = new Map<string, number>();
private consecutiveFailures = new Map<string, number>();
constructor(private gateways: { url: string }[]) {}
private seats(): { url: string }[] {
const now = Date.now();
const bySoonest = [...this.gateways].sort((a, b) =>
(this.coolUntil.get(a.url) ?? 0) - (this.coolUntil.get(b.url) ?? 0));
const open = bySoonest.filter(g => (this.coolUntil.get(g.url) ?? 0) <= now);
if (!open.length) return bySoonest; // storm: walk ALL seats soonest-first, no sticky
// sticky applies to OPEN seats only: the one that answered keeps first turn
return open.sort((a, b) =>
(a.url === this.lastGood ? -1 : 0) - (b.url === this.lastGood ? -1 : 0));
}
private markFailure(url: string) {
const count = (this.consecutiveFailures.get(url) ?? 0) + 1;
this.consecutiveFailures.set(url, count);
this.coolUntil.set(url, Date.now() +
Math.min(BASE_COOLDOWN_MS * Math.pow(2, count - 1), MAX_COOLDOWN_MS));
}
private markSuccess(url: string) {
this.consecutiveFailures.set(url, 0);
this.coolUntil.delete(url);
this.lastGood = url;
}
async request(method: string, params: unknown[] = []): Promise<any> {
let lastErr: unknown;
for (const gw of this.seats()) {
try {
const res = await fetch(gw.url, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
signal: AbortSignal.timeout(TIMEOUT_MS),
});
if (res.status === 429) throw new Error('rate-limited'); // etiquette: back off + rotate
if (!res.ok) throw new Error('HTTP ' + res.status);
const json = await res.json();
if (json.error) // a VALID answer from a healthy seat — never rotate on it
throw Object.assign(new Error(json.error.message || 'RPC error'), { rpcError: true });
this.markSuccess(gw.url);
return json.result;
} catch (e: any) {
if (e.rpcError) throw e; // reverts follow you to every seat
this.markFailure(gw.url);
lastErr = e;
}
}
throw lastErr ?? new Error('all gateways failed'); // caller shows honest failure text
}
// Write path: rebroadcast identical bytes only — NEVER re-sign.
// Returns a hash when known, so the CALLER owns the longer-lived poller and
// the anti-panic line: "sent, waiting for confirmation — do not sign again".
// Production: pass keccak256(signedTx) as knownHash so the handle never
// depends on any gateway's reply — an "already known" answer then cannot
// orphan a landed transaction.
async sendSigned(signedTx: string, knownHash?: string): Promise<string | undefined> {
let hash: string | undefined = knownHash;
const landed = (e: any) => // HEURISTIC, not a complete catalogue — broaden in production
/already known|already exists|nonce too low|replacement transaction underpriced/i
.test(String(e?.message ?? e));
// NOTE: "nonce too low" is overloaded — it can also mean a DIFFERENT
// transaction consumed the nonce. If this hash never gets a receipt,
// stop waiting; never invite a same-nonce re-sign.
const permanent = (e: any) => e?.rpcError && !landed(e); // insufficient funds, gas too low…
const broadcast = async () => {
try {
const h = await this.request('eth_sendRawTransaction', [signedTx]);
if (h) hash = h;
} catch (e: any) {
if (permanent(e)) throw e; // a permanent rejection follows you everywhere — fail NOW
/* landed(e) → pending/landed somewhere; transport already handled in request() */
}
};
await broadcast();
const deadline = Date.now() + RECEIPT_WAIT_MS;
while (Date.now() < deadline) {
if (hash) {
const r = await this.request('eth_getTransactionReceipt', [hash]).catch(() => null);
if (r) return hash; // landed and mined
} else {
await broadcast(); // no hash yet: same bytes, next healthy seat
}
await new Promise(r => setTimeout(r, 4000));
}
// After the window: prefer the known hash — the caller keeps polling and
// keeps the user from re-signing. One last broadcast only if still empty.
if (hash) return hash;
await broadcast();
return hash; // may still be undefined; caller keeps the anti-panic poller
}
}Seen in the Wild
Evidence, not spec — the spec sections above stand as v0.20 left them; this list records implementations of the pattern as they appear, each verified from the software itself.
- The official bridge, 2026-08-26 — pulsechain-bridge-server v1.1.5 (bridge.pulsechain.com, commit
ba083c1don the project’s own GitLab) ships a configured backup list of seven public Ethereum RPC endpoints — absent from the prior release’s bundle — alongside its existing per-chain custom-RPC option. That is this standard’s direction on the bridge’s Ethereum side: several seats, and the visitor’s own seat above them all. Verified from the release’s shipped bundle, not from the announcement; conformance in the section-2 sense (timed tries, cooldowns, honest failure text) is not claimed — the endpoint list and the custom-RPC door are what the release proves. The PulseChain side of the same bundle still names a single gateway, which is the gap this standard exists to close.
Conformance, License, Status
- Conformance — an app conforms by doing sections 2 and 4; it conforms fully by adding section 3. A wallet conforms by doing section 5. Reference implementations exist; no badge, no gatekeeper — the claim is checkable in any app's network tab, which is the only audit that matters.
- Getting started — the minimal conforming app is small: fetch the registry (or ship a copy), hold three seats with a local node first if the user has one, one timed try then advance with a cooldown, honest failure text, ranged log scans. A live minimal example of the guided switch runs on this site's own RPC page — one button, one EIP-3085 call, endpoint chosen from the health file. To test failover, point your primary seat at an unroutable address and watch your app: it should degrade to the next seat in one timeout with no user-visible drama — if the page blanks or hammers the dead seat, it doesn't conform yet.
- License — this standard and the registry are free to implement, copy, and republish with or without credit. The health data is open at its stated URLs.
- Status — draft v0.21, first published 2026-08-26, written from the measured record of two storms and hardened through three outside review rounds. Through v0.20 the versions alternated pens — even numbers this site's edits, odd numbers an outside reviewer's rounds; from v0.21 the numbers simply count rounds. v0.12 folded in the first review (registry decentralization, the security notes, the write-path walk, tunable defaults); v0.14 answered the v0.13 round (probe transparency, cooldown numbers, the operator door) and added the conformance checklist plus the minimal rotator sketch — corrected here first, because the reviewer's own draft rotated on RPC-level errors, polled receipts for a null hash, and locked out entirely under universal cooldown; v0.16 folded in the v0.15 patch round — doubling cooldown (45 s base, ×2 per consecutive failure, 10-minute cap, reset on success) and a write path that returns the known hash after the receipt window so the caller owns the longer poller and the anti-panic message; v0.18 folded in the v0.17 round — the transaction hash derived from the signed bytes so an "already known" reply cannot orphan a landed transaction, permanent rejections (insufficient funds, gas too low) fail immediately instead of spinning the receipt window, and the all-cooling fallback walks every seat soonest-first instead of one; v0.20 folded in the v0.19 polish — two new checklist boxes (hash from bytes; permanent-rejection fail-fast), the overloaded-"nonce too low" warning (it can mean a different transaction consumed the nonce — never invite a same-nonce re-sign), sticky restricted to open seats, and section 2 retitled for its write rules.; v0.21 (2026-08-27) added the non-normative Seen in the Wild appendix — a citation-only round, this site's pen, spec sections untouched from v0.20. The v0.19 review's own words: publishable — stop unless implementers file real bugs. Corrections and additions are welcome through the Suggest button on any page; gateway operators who want a row in the probe pool, same door.
