Fix guides

OpenAI or Anthropic key exposed, or a surprise AI bill

Your AI usage spiked overnight or a scanner flagged your key. How to lock down the AI endpoint in a vibe-coded app: rotate the key, move calls server-side, and add auth, rate limits, and spend caps.

What you are seeing

Why it happens

  1. The key has a public prefix, like VITE_OPENAI_API_KEY or NEXT_PUBLIC_OPENAI_API_KEY, so it ships in the JavaScript bundle (see the env vars guide).
  2. The AI added dangerouslyAllowBrowser: true to the SDK to make an error go away, which puts the key in the browser.
  3. A .env file was committed at some point. Deleting it later does not remove it from git history.
  4. The server route has no auth check, so anyone can use it as a free proxy to your account.
  5. There is no per-user rate limit and no cap on output tokens, so one script or a retry loop can run up the bill.
  6. The provider account has no budget limit or alert.

How to fix it

1. Rotate the key first

Create a new key in your provider's dashboard, add it to your host's environment variables with no public prefix, redeploy, and only then delete the old key. Deleting first takes your app down; not deleting leaves the leak open.

2. Find every place the key is exposed

grep -rnE "sk-[A-Za-z0-9_-]{20,}|dangerouslyAllowBrowser|(VITE|NEXT_PUBLIC)_(OPENAI|ANTHROPIC)" src app lib 2>/dev/null
git log --all --oneline -S "sk-"

3. Call the model from a server route that checks the user (Next.js + Supabase)

// app/api/chat/route.ts
import OpenAI from 'openai';
import { createClient } from '@/lib/supabase/server';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function POST(req) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response('Unauthorized', { status: 401 });

  const { message } = await req.json();
  if (typeof message !== 'string' || message.length > 4000) {
    return new Response('Bad request', { status: 400 });
  }
  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini', // or the model you use
    max_completion_tokens: 500,
    messages: [{ role: 'user', content: message }],
  });
  return Response.json({ reply: completion.choices[0].message.content });
}

4. Add a per-user rate limit (Upstash example)

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(20, '1 h'),
});

// inside the route, after the auth check
const { success } = await ratelimit.limit(user.id);
if (!success) return new Response('Too many requests', { status: 429 });

5. Cap the damage at the provider

In your provider's billing settings, set a monthly budget or usage limit with an email alert well below it. Use a separate key for each app so you can see and revoke usage per key.

How to know it is fixed

Calling the route while signed out returns 401, searching the built JS for sk- finds nothing, the 21st request in an hour from one user returns 429, and the usage page shows traffic only on the new key.