Quick solution
If your dapp calls an RPC provider directly from the browser, your API key is already public — view-source, the network tab, or the JavaScript bundle exposes it to anyone who looks. The fix is not hiding the key harder; it is making the key worthless to anyone but your app. In order of effort: turn on your provider's referrer allowlist so the endpoint only answers requests claiming your domain; create separate keys per app and per environment so a leak is contained and revocable; cap the leaked key's blast radius with method restrictions and rate limits; and for anything valuable, move RPC calls behind a thin backend proxy or use short-lived JWTs so the browser never holds a long-lived credential at all.
The rest of this guide explains what each layer actually stops, what it does not, and which combination fits your project.
Why frontend RPC keys are always public
A browser dapp has a structural problem that no amount of cleverness removes: everything the browser needs to make a request, the user — and any attacker — can read. The RPC URL with its embedded key sits in your bundled JavaScript, appears in the DevTools network tab on every call, and gets scraped by bots that do nothing all day but crawl deployed sites and public GitHub repositories for strings shaped like provider URLs. Environment variables do not help here; a VITE_ or NEXT_PUBLIC_ prefixed variable is inlined into the shipped bundle at build time, which is exactly what those prefixes mean. "Obfuscating" the key with string-splitting or encoding only changes how many seconds extraction takes.
This is worth internalizing because it changes the goal. Backend secrets are protected by concealment — the key never leaves your server. Frontend RPC keys cannot be concealed, so they must be protected by constraint: bind the key to your domain, limit what it can do, watch what it spends, and keep a kill switch handy. Every provider security feature in this guide is one of those four constraints wearing a product name.
It also means one habit should end today: never reuse a frontend key anywhere else. The key in your dapp's bundle must not be the key your backend cron jobs use, or the one with access to paid add-on APIs. The moment a key ships in a bundle, treat it as published.
More in Node & RPC Infrastructure
What an attacker actually does with a leaked key

The attack is mundane, which is why it is common: free compute. Someone building their own bot, indexer, or dapp finds your key and points their traffic at your endpoint. Your provider meters that usage against your quota. On a free tier, your app starts throwing 429 errors mid-month for traffic you never sent — the debugging pattern in our rate limit guide applies, except no amount of request-shaping on your side fixes someone else's traffic. On a paid plan with overage billing, the failure mode is a surprise invoice instead of an outage, which is arguably worse because nothing visibly breaks.
Two clarifications keep the threat model honest. First, a leaked RPC key does not endanger user funds — RPC credentials authorize requests, not transactions; only private keys sign transactions. The damage is quota theft, cost, and downtime. Second, the attacker is rarely targeting you. Scrapers harvest thousands of keys indiscriminately, so "my project is too small to attack" is not a defense; being harvestable is the only qualification.
You can verify exposure in two minutes: open your deployed site, open DevTools, and search the network tab for your provider's domain. The full request URL — key included — is right there. Assume everyone else has done the same.
A concrete arithmetic example shows how fast quota theft compounds. Suppose your dapp legitimately makes about 50,000 RPC calls a day and your free tier comfortably covers three times that. One scraper wires your key into a modest indexing script polling every two seconds — that alone is 43,200 calls a day, and scrapers rarely stop at one script or one operator, because harvested keys get shared and resold in bulk lists. Within days, background abuse can exceed your real traffic several times over, and the first symptom you see is your own users hitting 429 errors during your peak hours, since quota exhaustion does not schedule itself politely. The dashboard graph tells the story instantly — a second daily rhythm layered over yours, often in a different timezone — but only if you look, which is the practical argument for checking per-key usage before the month ends rather than after the outage starts.
The five defenses, weakest to strongest

Defense 1 is referrer and origin allowlists. Every major provider offers this: the endpoint only answers HTTP requests whose Referer header (or Origin, for WebSocket connections) matches your registered domain. QuickNode's documentation requires exact-match domains — wildcards are unsupported — and is candid about the limit: headers "can be spoofed or manipulated by attackers using custom HTTP clients." An allowlist stops the lazy attacker — the scraper that pastes your key into its own bot without customization — which in practice is most of them. It does not stop anyone willing to set one header. Turn it on anyway; it is free and takes a minute.
Defense 2 is key separation and rotation. One key per application, per environment. QuickNode supports multiple authentication tokens on a single endpoint precisely so you can "separate environments, isolate applications, and enable individual usage monitoring"; Alchemy's guidance is to scope each key's permissions and rotate keys on a schedule — annually at minimum, immediately on suspected leakage. Separation converts a leak from "rotate everything and break production" into "revoke one token, ship one config change." It is the difference between a fire drill and a line item.
Defense 3 is method restrictions and rate caps. If a frontend only ever calls a dozen read methods and eth_sendRawTransaction, a key that can call anything else is pure downside. Providers on paid tiers let you disable methods per endpoint and cap request rates per token, so a stolen key cannot run archive-depth scans or batch floods on your dime. This is the constraint that caps the size of a bad outcome rather than its probability.
Defense 4 is short-lived JWTs. Instead of a static key, your backend signs a JSON Web Token with a built-in expiry, and the frontend uses that. Alchemy's documentation recommends frontends employ "JWTs with very short expiration periods"; QuickNode positions JWT authentication as embedding "custom permissions, user information, and expiration times directly into the token." A stolen token now expires in minutes. The cost: you need a small backend component to mint tokens, which brings you most of the way to option five anyway.
Defense 5 is a backend proxy. The browser calls your API; your server holds the real key and forwards RPC requests. The credential never ships, full stop — this is the only option that restores actual concealment. You also gain a control point for caching (identical reads collapse into one upstream call), per-user rate limiting, and request logging. The costs are real: you now operate a service in the request path, you add one network hop of latency, and proxying WebSocket subscriptions is meaningfully more work than proxying HTTP — the transport trade-offs in our WebSocket vs HTTP guide all apply to the proxy you are now running.
One design rule keeps a proxy from becoming a new liability: forward an allowlist of methods, not arbitrary JSON. A proxy that blindly relays whatever the browser sends has merely moved the abuse point — anyone can call your public API route with the same expensive requests they would have sent to the provider. A proxy that accepts only the dozen methods your app actually uses, validates parameters, and applies per-IP rate limits is a genuine upgrade: the attacker now faces your rules on your terms instead of a provider's generic ones.
Defenses compared

The derived column estimates what each layer costs a serious attacker to defeat — the practical measure of a control, since a control that costs nothing to bypass only filters the unmotivated.
| Defense | Setup effort | Stops the scraper bot? | Attacker's cost to bypass | Residual risk |
|---|---|---|---|---|
| Referrer/origin allowlist | Minutes | Yes | One spoofed header | Targeted abuse continues |
| Key separation + rotation | Minutes | No (limits damage) | Must re-harvest after each rotation | Leak window between rotations |
| Method + rate restrictions | Minutes (paid tiers) | No (limits damage) | Cannot exceed caps at all | Abuse within caps still bills you |
| Short-lived JWT | Hours (token service) | Yes | Must compromise the minting flow | Minting endpoint becomes the target |
| Backend proxy | Days | Yes | Must abuse your public API instead | You now operate the choke point |
Read it as a stack, not a menu — the layers compose, and the standard combinations fall out of the table. Choose allowlist + key separation if you want the best protection available in under ten minutes; this should be the floor for every deployed dapp. Add method and rate restrictions if you are on a paid plan and a surprise bill would hurt. Choose JWT or a proxy if the key unlocks anything expensive, your quota is business-critical, or you are already running a backend — at which point the proxy usually wins, because it subsumes the JWT's benefit and adds caching.
Which setup fits your situation

Situation 1: A static-hosted dapp with no backend at all
Your site deploys to a CDN, there is no server, and adding one feels like defeat. Do the free trio now: enable the referrer allowlist, use a dedicated key that exists nowhere but this deployment, and set whatever method and rate caps your tier allows. Accept the residual risk consciously — a determined abuser can still spoof your domain — and mitigate with monitoring: check the provider dashboard's per-key usage weekly, and treat an unexplained volume jump as a rotation trigger. For a read-mostly dapp on a free tier, this is a defensible steady state, not a shortcut.
Situation 2: A dapp with a backend already in place
You have API routes or serverless functions serving other features, so the marginal cost of a proxy is one route and an environment variable. Take the strong option: move RPC calls server-side, keep the provider key out of the client bundle entirely, and add a small cache for the reads your users repeat (token metadata, balances on a short TTL). Keep a separate, allowlisted, low-privilege frontend key only if you need browser-side WebSocket subscriptions that are not worth proxying yet. Most teams in this situation are one afternoon away from the best available posture and simply have not scheduled the afternoon.
Situation 3: You just found your key in a public repo or a deployed bundle
Treat it as an incident with an order of operations. Rotate or revoke the key at the provider first — that kills live abuse immediately. Check usage graphs for the exposure window so you know whether anyone actually found it, and whether a bill is coming. Then fix the leak's cause: if the key was committed to git, note that rotation is the remedy — deleting the file in a new commit leaves the key in history, and scrapers read history. Finally, upgrade your posture one rung — separated keys and an allowlist at minimum — because keys that leak once usually leaked through a workflow, and workflows repeat until changed.
FAQ
Is it ever acceptable to ship an RPC API key in the frontend? Yes — it is the standard pattern for dapps without backends, and providers design for it. Acceptable means constrained: a dedicated key, a referrer allowlist, the tightest method and rate caps available, and usage monitoring. What is not acceptable is shipping an unconstrained key that also authorizes your backend or paid add-ons.
Does hiding the key in an environment variable protect it? No. Frontend build tools inline public-prefixed environment variables into the shipped JavaScript. Environment variables keep secrets out of source code; only a server keeps them out of the shipped product. The distinction between those two is the entire problem.
Can someone steal user funds with my leaked RPC key? No. RPC keys authorize requests to a node, not transactions on chain — transactions require users' private keys, which your RPC provider never sees. The realistic damage is stolen quota, degraded service for your users, and unexpected charges.
Do referrer allowlists work for WebSocket connections too? Yes, via the Origin header rather than Referer — QuickNode's implementation checks Origin on WebSocket upgrades. The same caveat applies: a custom client can forge Origin, so treat the allowlist as a filter for opportunistic abuse, not an authentication mechanism.
Won't a backend proxy slow my dapp down? It adds one hop, typically single-digit to low tens of milliseconds when the proxy runs near your users. Against Solana-style subsecond expectations that can matter; against typical EVM read latency it is noise — and proxy-side caching frequently makes repeated reads faster than direct provider calls. Measure before assuming the hop is the bottleneck; the fundamentals in our RPC infrastructure primer cover where RPC latency actually comes from.
Should mobile apps be treated like browser frontends? Yes, and slightly worse. A native app's binary can be unpacked and its embedded strings extracted, but referrer allowlists do not protect it — mobile HTTP clients send no browser-style Referer header, so that entire layer drops out. For mobile, key separation plus method and rate caps are the floor, and a backend proxy or short-lived token flow is the realistic recommendation earlier than it would be on the web.
Do I need all five layers? No — you need the layers proportionate to what the key can cost you. A weekend project on a free tier is well served by an allowlist and a dedicated key: worst case, you rotate and move on. A production dapp whose provider bill has a credit card behind it should add method and rate caps and seriously price out the proxy. Security spending should track the size of the bad outcome, not a checklist.
How do I know if my key is already being abused? Compare the provider dashboard's usage graph against your own analytics. Abuse looks like volume your traffic cannot explain: requests at hours your users are asleep, methods your code never calls, or a baseline that keeps climbing while your user count does not. Per-key usage monitoring is the main reason to separate keys even before any attacker shows up.
Sources
[Alchemy documentation
[QuickNode documentation
[RFC 8725




