On July 6, 2026, a founder posting as obr described watching an OpenAI balance drain in 34 minutes. The part that should worry you isn't the speed. It's that the $300 and the $600 threshold alerts landed at the same moment. Two limits, one notification burst, both already blown through by the time anything fired.
He'd set a hard limit. It didn't hold.
TL;DR
Spend limits are billing controls, not request-time gates. Usage gets metered and aggregated after requests complete, so the number your limit is checked against always trails what's actually been spent. A parallelized attack finishes before the accounting catches up. If you're reading this holding a bill: revoke every key in the org (not just the one), reset your password and turn on MFA, export the usage data as evidence before it ages out of the dashboard, then file the dispute.
Why the spend limit didn't hold
Here's the mechanism nobody explains when they tell you to "just set a hard limit."
Your limit isn't a bouncer standing at the door of every API call. Requests get served, then metered, then rolled up into an account-level usage figure. The limit is evaluated against that rolled-up figure. Which means enforcement is always looking at a slightly stale number.
Under normal traffic that lag doesn't matter. Your app makes a few hundred calls an hour and the accounting keeps pace easily. Under abuse it matters enormously, because automated abuse doesn't send requests one at a time. It fans out, hundreds of concurrent calls, often across multiple models. The spend curve goes near-vertical while the aggregation is still catching its breath.
The simultaneous $300 and $600 alerts in obr's thread are the tell. Those aren't two events that happened together. They're two thresholds that had already been crossed by the time the system computed a number and started sending mail.
Treat your spend limit as a reporting feature. It tells you something went wrong. It doesn't stop it going wrong. The only controls that act at request time are ones you build: a server route that holds the key, and a rate limiter that counts per user.
This isn't a bug report. It's how post-paid metered billing works nearly everywhere, and it's the same reason a stolen AWS key can run up a cloud bill overnight. What's changed is who's holding the key. Five years ago these keys lived in infrastructure managed by people who'd thought about blast radius. Now they live in a .env file that Cursor generated for a founder who shipped their first app last month.
Four more founders posted variations of the same story to the OpenAI developer community in three weeks. A June 24 thread reports charges above $1.4 million with no human escalation. A June 23 thread from a user running a production SaaS reports API access blocked while the disputed usage was unresolved, which is its own kind of bad: the fraud takes your money and the response takes your product. A June 15 thread describes a Pro account and a company organization both banned for distillation after a key compromise, with no human reply after a week. The attacker's traffic looked like policy abuse, so the platform's automated enforcement hit the victim.
And the question itself is old. A 2024 thread asking exactly this, why the maximum limit didn't take effect, is still sitting there unanswered.
The first hour
Order matters here. Do these in sequence.
Revoke every key in the organization, not just the one you think leaked.
Go to the provider console and delete the compromised key. Then delete every other key in the same org or project.
This feels like overkill. It isn't. Whatever got the attacker one key (a committed .env, a config file scraped off a machine, a public bundle) almost always exposed the rest of the file too. Rotating one key and leaving four siblings alive means you'll be back here next week.
Your app goes down when you do this. That's fine. Down costs less than the meter running.
Reset the account password and turn on MFA.
If the attacker got in through your dashboard login rather than a leaked key, revoking keys accomplishes nothing. They just mint a new one.
Reset the password to something new (not a variant of the old one), enable multi-factor authentication, and sign out all other sessions. While you're in there, check the members list on the organization for accounts you don't recognize, and check whether any new project or key was created in the last few days.
Export the usage data before it ages out.
Do this before you contact support, not after. Provider dashboards keep fine-grained usage for a limited window and then roll it into coarser buckets. The per-minute detail that proves your case is the first thing to disappear.
Pull all of it:
- Usage broken down as finely as the console allows, ideally per request or per minute, covering the whole attack window plus the day before it
- The billing invoice or the raw usage export in CSV form
- Every threshold alert email, with headers, because the send timestamps are what demonstrate the lag
- Your own application logs for the same window, which show your legitimate traffic volume and make the anomaly obvious by contrast
Screenshot the console too. Exports and screenshots corroborate each other.
File the dispute with a timeline, not a complaint.
Support cases that read as narratives get triaged slowly. Cases that read as evidence move faster. Lead with facts in this order:
- The timestamp you revoked the key
- The timestamp of the first unauthorized request and the last one
- Your normal daily spend, versus the disputed amount
- The leak vector, if you've found it, and what you changed
Say plainly that you're disputing the charges as unauthorized and that the key has been revoked. Attach the exports. Then, separately, contact your card issuer or bank to ask about your options, because platform disputes in these threads have run for weeks. Ask, don't chargeback blind: an unexpected chargeback can get an account suspended, which is not what you want while you're still trying to recover the product.
Don't delete the compromised key's usage history, close the project, or remove the payment method before you've exported everything. Founders in these threads have made recovery harder by tearing the account down in a panic. You need the account intact to prove what happened in it.
How it got out
Four paths cover almost everything we see in scans of AI-built apps. Work through them in order, because they take about two minutes each.
1. The key is in your client bundle behind a public prefix
The single most common one. Your framework has a convention that marks environment variables as browser-safe: NEXT_PUBLIC_ in Next.js, VITE_ in anything Vite-based (Lovable, most Bolt output, plain React and Svelte templates), PUBLIC_ in Astro and SvelteKit. Anything with that prefix is compiled into the JavaScript your visitors download.
AI coding tools add the prefix when the alternative would be a build error. It fixes the error. It also publishes your key.
Check it from your live site, not your source tree:
# Pull your deployed JS and grep it for key patterns
curl -s https://yourapp.com | grep -oE '/[^"]+\.js' | while read -r f; do
curl -s "https://yourapp.com$f"
done | grep -oE '(sk-[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|AIza[A-Za-z0-9_-]{35})'
Or open your app, hit F12, go to Sources, and Ctrl+F for sk-ant-, sk-proj-, and AIza. If anything comes back, that key has been public for as long as the site has been live.
# And in your source
grep -rE "(NEXT_PUBLIC|VITE|PUBLIC)_[A-Z_]*(OPENAI|ANTHROPIC|CLAUDE|GEMINI|GOOGLE)[A-Z_]*KEY" .
2. A .env file went into a public repo
git log --all --full-history -- .env .env.local .env.production
Any output means the file was committed at some point. Deleting it later doesn't help; it's still in history, and public repos are scraped continuously by bots that watch the GitHub events firehose. Keys posted to public repos have been picked up in under a minute in published research. Assume anything ever committed is compromised.
3. The key was pasted into a chat or an agent session
You debugged something at 1am by pasting your .env into a chat window. Or your coding agent read the file into its context to fix a config problem, and that context went to a third-party model, got logged by a proxy, or landed in a session transcript that syncs somewhere.
There's no grep for this one. Think back over the last few weeks: any support ticket, Slack message, screen recording, Loom demo, GitHub issue, or agent session where the key was on screen. A key that appeared in a screenshot in a public issue is a key that leaked.
4. The key is in a Lovable, Bolt, or v0 preview deploy
Preview and share URLs from these tools are unlisted, not private. The build that runs on them carries the same environment variables as your production build, including anything with a public prefix. People post those URLs in Discord servers, in launch threads, in feedback requests.
Open your own preview URL, view source, and search it the same way you searched production.
There's a newer vector worth knowing about. On July 11, 2026, several npm packages published by jscrambler were backdoored with an infostealer that specifically targets AI coding assistant configuration files, which is exactly where founders keep their Anthropic and OpenAI keys. If your keys sit in plaintext in an assistant config and you install packages regularly, a supply chain compromise reaches them without anyone touching your repo or your deploy.
Where to revoke and where the evidence lives, by provider
OpenAI
Revoke at platform.openai.com > API keys. Delete the compromised key, then delete the rest of the keys in the project and check the Organization > Members list for accounts you don't recognize.
Usage evidence lives under Usage in the platform dashboard, filterable by project, model, and API key. The billing section holds invoices and the credit history that shows the drain. Export what you can and screenshot the rest. Threshold alert emails are worth keeping in the original form with headers intact, since the point you're making is about when they arrived relative to the spend.
Disputes go through the support widget in the platform dashboard. Based on the threads above, expect this to take time and to require you to be persistent.
Anthropic / Claude
Revoke at the Claude Console under Settings > API keys. Same rule as everywhere else: delete all keys in the workspace, not just the one you suspect.
One thing that changed recently and is worth acting on: Anthropic added optional expiration dates for API keys on July 8, 2026. The release notes say existing keys are unaffected, which means every key you made before that date never expires, and any new key where you leave the field alone doesn't either. When you create replacement keys, set an expiry. Anything for a prototype, a demo, or a script you'll run twice should be dated to die.
Usage data is under Usage and Cost in the console, broken out by workspace and API key. Pull the cost report for the attack window along with the invoice.
Google Gemini
Gemini keys are Google Cloud API keys, which changes the shape of the response. Revoke at aistudio.google.com if the key came from AI Studio, or in the Cloud Console under APIs & Services > Credentials if it came from a GCP project.
The upside: Google Cloud gives you real controls the other two don't. On the credential itself you can restrict which APIs the key can call and which HTTP referrers or IP addresses may use it, which turns a stolen key into a useless string outside your own domain. Do that on every replacement key.
Evidence lives in Cloud Billing > Reports, filtered to the Generative Language API, and in Cloud Logging if you had request logging on. Billing reports here are more granular than the other two providers give you, which helps a dispute.
The permanent fix
Rotating keys ends this incident. These three changes are what stops the next one.
Put the key behind a server route. This is the whole ballgame. The browser never sees a provider key. Your frontend calls your own endpoint, your endpoint calls the provider:
// app/api/chat/route.ts (Next.js)
// server/api/chat.post.ts (Nuxt)
import OpenAI from "openai";
// No NEXT_PUBLIC_ prefix. This never reaches the browser.
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function POST(req: Request) {
const session = await getSession(req); // require a logged-in user
if (!session) return new Response("Unauthorized", { status: 401 });
const ok = await consumeQuota(session.userId); // see below
if (!ok) return new Response("Rate limited", { status: 429 });
const { prompt } = await req.json();
const completion = await client.chat.completions.create({
model: "gpt-4o-mini",
max_tokens: 500, // cap the blast radius per call
messages: [{ role: "user", content: prompt }],
});
return Response.json({ text: completion.choices[0].message.content });
}
Two details in there that matter more than they look. max_tokens bounds what any single request can cost. And the session check means an attacker who finds your endpoint still has to get past your auth to spend your money.
Rate limit per user, and cap the total. An unauthenticated proxy route is just a slower key leak: people will find it and resell access to it. Count requests per user per hour, and separately track a global daily ceiling that trips a kill switch:
// Pseudocode, back it with Redis, Upstash, or your database
async function consumeQuota(userId: string) {
const perUser = await incr(`u:${userId}:${hourKey()}`, 3600);
if (perUser > 50) return false;
const globalToday = await incr(`global:${dayKey()}`, 86400);
if (globalToday > 5000) {
await notifyOwner("Global AI request ceiling hit; requests paused.");
return false;
}
return true;
}
Set the global ceiling at something like three times your worst normal day. It should never fire. When it does, that's the signal your provider's spend limit was too slow to give you.
Cap the money at the card, not just the platform. Use a virtual card with a hard monthly limit from your bank or from a provider like Privacy or Revolut, and put only that card on your AI accounts. When the platform limit lags, the card declines. It's the one control in this list that doesn't depend on the provider's accounting keeping up, and it's the reason a bad night ends at your cap instead of at whatever the attacker felt like spending.
Prepaid credits work similarly on OpenAI: keep a small balance and turn auto-recharge off. Auto-recharge is what turns a $50 incident into a four-figure one, because it keeps refilling the tank while the fire burns.
After you've rotated, scan your live site rather than your repo. The gap between what's in your source and what's actually in your deployed bundle is where these keys hide, and preview deploys carry the same variables as production. We see keys in client bundles constantly on apps built with Lovable, Bolt, and v0, usually because a public prefix was the fastest way past a build error.
Why didn't my hard limit work?
A spend limit is a billing control, not a rate limiter sitting in front of every request. Usage is metered and aggregated after requests complete, so the number the limit is checked against trails what has actually been spent. In the July 2026 OpenAI community thread from user obr, the $300 and $600 threshold alerts arrived at the same moment, after a key had already been drained over 34 minutes. If an attacker parallelizes requests, spend outruns the accounting.
Will OpenAI refund a stolen API key charge?
There's no published guarantee either way, and the public threads show slow, inconsistent outcomes. One user reported being ignored for three weeks on a $1,893 fraudulent balance. A June 2026 thread reports charges above $1.4 million with no human escalation. Treat a refund as something you argue for with evidence: exact revocation time, exported usage data, and the alert email timestamps.
How fast can a stolen API key be drained?
Faster than most founders assume. The July 6, 2026 thread from user obr describes a balance drained in 34 minutes. Automated abuse doesn't work through your key one request at a time. It fans out across many concurrent requests, which is exactly why post-hoc spend accounting can't keep up with it.
Do Anthropic API keys expire?
Only if you set an expiration. Anthropic added optional expiration dates for API keys on July 8, 2026, and the release notes state existing keys are unaffected. So every key created before that date, and any new one where you skip the field, never expires on its own. Set an expiry on anything you're creating for a prototype or a throwaway script.
Should I delete the key or disable billing first?
Delete the key first, and delete every other key in the same organization while you're there. Removing a payment method can leave an outstanding balance and may cut off the dashboard you need for the usage export. Kill access, pull evidence, then deal with billing.
My key was only in a preview deploy, not production. Am I still exposed?
Yes, if the preview was reachable without a password. Preview URLs from Lovable, Bolt, and v0 are unlisted rather than private, and the same environment variables get baked into that build. Those URLs get posted in Discord servers and launch threads, and they get scraped. Treat a key that ever shipped to a public preview as burned.
Check whether your live site is shipping an OpenAI, Anthropic, or Gemini key in its JavaScript bundle. We scan the deployed app, not the repo, including preview URLs. Free, no signup required.