Before you publish your Cursor app: a bill-shock and security checklist
NextPlay Solutions · October 18, 2026
One key in the browser is enough. A stranger does not need an account on your Cursor app. If they can copy the key, the invoice goes to the project that issued it, which is you or your client. On September 23, 2025, a student posted in r/googlecloud that a Gemini key left on GitHub became a $55,444.78 bill. That write-up is what the poster said happened. A bill that size can wreck a student, a freelancer, or the client who owns the account. The charge works the same way when a coding agent puts the key in the client so the demo runs.
Where the key ends up
Cursor is the editor. The public link is whatever host you deployed to, often Vercel when the app is Next.js. The leak is in that deployed JavaScript, not in Cursor's account settings.
Next.js keeps environment variables on the server unless the name starts with NEXT_PUBLIC_. That prefix tells Next.js to inline the value into the JavaScript sent to the browser, at build time. NEXT_PUBLIC_OPENAI_API_KEY is a public key, whatever the rest of the name says. The same thing happens when a client component, a file with "use client", contains the key as a string, or when a server component passes process.env.OPENAI_API_KEY into a client component as a prop. The browser receives that prop. Next.js environment variables.
The usual way it gets there: you paste the key into the Cursor chat and ask the agent to make the demo work. The agent then writes it into the client. A Cloud Agent secret is a different store. Cursor says those secrets are encrypted at rest and in transit and injected into the agent environment. An Environment Variable is visible to the agent. A Runtime Secret is redacted from the transcript, tool output, and commits, and is still visible in the agent terminal. A Build Secret is only for the Docker build. None of those is the environment of the published site. A key you type into chat is visible to the model even when the Secrets tab is set up correctly. Cloud Agent secrets.
Search the live bundle by hand
Open the published URL in Chrome. Open DevTools, then Sources. Press Ctrl+Shift+F on Windows or Linux, or Cmd+Shift+F on a Mac, and search every loaded file. Run these searches one at a time:
- sk- and sk-ant- for OpenAI and Anthropic
- AIza for a Google or Gemini key
- sk_live_ and rk_live_ for a Stripe secret or restricted key
- service_role for a legacy Supabase key
- sb_secret_ for a current Supabase secret key
A hit in the HTML, a JavaScript file, or a source map means that value shipped to the browser. The Supabase publishable key (sb_publishable_) and the legacy anon key are supposed to be there. A provider key, a service_role JWT, or an sb_secret_ key is not. Then rotate every key you found. Deleting it from the project does not un-leak a bundle people can already download.
Move the call to a route handler or server action
Put the provider call in a route handler, a route.ts file under app/, or in a server action in a file that starts with "use server". Read process.env.OPENAI_API_KEY only there. Do not return the key, and do not pass it to a client component.
Next.js says a server action stays on the server, and that the action is still a POST anyone can send if they know how to call it. Checking the user only on the page that renders the form does not protect the action. Inside the action, confirm the caller is allowed to use it, validate the input, cap tokens, and rate-limit the route. Otherwise the key is hidden and the meter is still public. Server Actions.
Set the private key on the host, with no NEXT_PUBLIC_ prefix. On Vercel, environment variables are encrypted at rest. Choose Secret for the key. Secret values are write-only after you save them. A change applies to the next deployment, not to one that is already live, so redeploy after you add it. Keep the local copy in .env.local, and do not commit env files. Next.js says the default template gitignores them because you almost never want those files in the repository. The Reddit bill started as a key in git. Vercel environment variables.
Supabase keys and row level security
Supabase is deprecating the long-lived anon and service_role JWTs by the end of 2026. The replacements are a publishable key (sb_publishable_) and a secret key (sb_secret_). Both systems work at the same time until you disable the legacy keys. API keys.
The publishable key, and the legacy anon key, are supposed to be in the browser. They only reach what Row Level Security allows. A secret key and a service_role key bypass those policies. Supabase says a new secret key does not work in a browser: the API returns 401 based on the User-Agent. That block does not save you. Supabase also says an attacker can still use the key from other tools, and the legacy service_role JWT does not have that browser check. If either value is in the bundle, rotate it after the client code is fixed. Row Level Security.
In the Supabase dashboard, enable Row Level Security on every table in the public schema. Write policies for the anon and authenticated roles. A policy of using (true) on private data lets every visitor read the row. The publishable key may stay in the client as NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. The secret key stays in a server-only variable.
Check /.env and /.git
In the address bar, open https://your-domain/.env and https://your-domain/.git/config, using the host you are about to share. A real env file is lines of NAME=value. A git config contains [core] and a remote section. A 404, a login wall, or the app's HTML page is what you want. If either file comes back, treat every value in it as public and rotate those credentials.
Set a spend cap that stops the meter
The cap lives on the provider account, not in the coding tool. Set it before you share the link. In the Reddit thread, nothing says a budget alert stopped the usage. A notification you never see is not a lock on the key. These are the controls each provider documents. Enforcement is not always instant, so a limit can still be passed by a little.
- OpenAI: open Organization limits, edit the monthly spend limit, and turn on Enforce a hard limit. A spend alert only sends a notification. Traffic continues. OpenAI says a hard limit makes later requests fail, and that recorded spend can slightly exceed the number while the limit propagates. Spend limits.
- Anthropic: in the Claude Console, open Settings, then Billing, then Spend limits, and choose Adjust limit or Set limit. The limit you set cannot exceed the tier cap. Rate limits.
- Gemini API: AI Studio has a project monthly spend cap under Spend, and a separate cap for the billing-account tier. Google marks the project cap experimental. Billing data can lag by about ten minutes, long-running tasks can go past the cap, and invoiced accounts are excluded. Gemini API billing.
- Google Cloud: an alerts-only budget does not set a hard cap. Google says so. A spend-cap budget can pause one eligible service in one project, and Google says that enforcement is not instant and in-flight requests can still be billed. Budget alerts · Spend cap budgets.
- Supabase, on the Pro plan: the Spend Cap disallows further use of a covered item after its quota, until the next billing cycle. It does not cover Compute, branching, read replicas, custom domains, extra disk performance, IPv4, log drains, phone MFA, or point-in-time recovery. It is not a dollar amount you type in. Cost control.
Stripe is different. Stripe documents billing thresholds and usage alerts for what you charge your own customers. Those controls do not stop a leaked sk_live_ key from creating charges on your Stripe account. If that key is in the bundle, roll it in the Stripe dashboard. The publishable key, pk_, is the one that belongs in the browser.
Paste this into the agent
Paste the prompt below into the project chat. Let the agent move the code, then rotate any key that was already public. The prompt tells the agent not to print the key.
You are fixing a Next.js app in Cursor before it is published. A provider key in the browser can be copied by anyone who loads the site. OpenAI, Anthropic, Gemini, and Stripe bill the account that issued the key. Do this: 1. Search the repo for sk-, sk-ant-, AIza, sk_live_, rk_live_, service_role, and sb_secret_. Remove those values from client components, from any NEXT_PUBLIC_ variable, from files under public/, and from any committed .env file. 2. Next.js inlines NEXT_PUBLIC_ into the browser bundle at build time. Only a public value, such as the Supabase URL and the publishable or anon key, may use that prefix. Name private keys OPENAI_API_KEY, ANTHROPIC_API_KEY, or SUPABASE_SECRET_KEY with no NEXT_PUBLIC_ prefix. 3. Move the provider call into a route handler (app/api/.../route.ts) or a server action marked with "use server". Read process.env only in that server code. Do not pass the key as a prop to a client component, and do not return it. 4. Treat the route or action as public. Check that the caller is allowed to use it, validate the input, cap tokens, and rate-limit it. Rendering the form only for logged-in users does not protect the action. 5. Enable Row Level Security on every Supabase table in the public schema. Do not use a policy of using (true) for private data. Never ship a service_role JWT or an sb_secret_ key. 6. Tell me the variable names to set on the host, without printing the values. On Vercel, add each private key as a Secret environment variable and redeploy. A Cursor Cloud Agent secret is for the agent machine. It is not the published site's environment. 7. Rotate every key that was already in the client or in git. Do not print any secret value.
What a Burnproof scan actually checks
The free scan reads the public HTML and up to six same-origin scripts. Scripts past that cap are unread. In that text it looks for a fixed set of shapes: OpenAI keys starting with sk-, Anthropic sk-ant-, Google AIza (a Firebase client config is reported separately and is meant to be public), Stripe sk_live_, rk_live_, test secrets, and whsec_, AWS access key ids, GitHub, Slack, and SendGrid tokens, a PEM private key, and a Supabase JWT whose payload role is service_role or anon.
It does not search for sb_secret_. That is why the DevTools search above is still worth doing. If it finds a Supabase project URL and a legacy anon key, it asks up to five tables whether they answer with no logged-in user, and it keeps a row count when the server sends one. It does not download rows. It does not send a service_role key anywhere. It requests /.env and /.git/config and only reports whether the body looks like those files. It also notes a few missing security headers. It does not call OpenAI, Anthropic, Gemini, or Stripe, and it does not read Firebase Security Rules. A quiet report is not a promise the app is safe.
Scan your app free
Paste the published URL. The free scan takes about a minute and masks anything that looks like a key. It reads the public page. It does not call the provider with a key it finds.
Scan your app free