They got it from your website. Anything prefixed VITE_ is baked into the JavaScript bundle that every visitor downloads. Open your site, DevTools > Sources, search sk- and you'll find it. Bots crawl for exactly this.
Replit Secrets doesn't help when the frontend reads it, because the build copies the value into public code.
Now:
- The old key is deleted, good. Check the OpenAI dashboard for other keys/projects you don't recognize.
- Put the new key only on a server. Smallest possible change: a tiny backend endpoint (Express on the same Repl, or a serverless function) that calls OpenAI, and your React app calls that.
Note the model and limits are decided server side. Otherwise the endpoint itself becomes a free proxy.
app.post("/api/chat", requireUser, rateLimit, async (req, res) => { const r = await openai.chat.completions.create({ model: "gpt-4o-mini", // fixed on the server, not from the client messages: req.body.messages.slice(-10), max_tokens: 500, }); res.json(r.choices[0].message); }); - Put auth + a per-user rate limit on that endpoint.
- In OpenAI: use a separate project for this app, set budget alerts, and keep auto-recharge off. With prepaid credits, the balance is effectively your hard cap.
Refund: open a support ticket, explain the key was exposed and show the model mismatch. Not guaranteed but people do sometimes get it back.