API Key Security
Why your key cannot ship in the build
Verified against a working setup on 2026-08-10
This chapter is short because the answer is short. It exists because the mistake is common, expensive, and completely avoidable.
Every place people hide it, and how it is found
| Hiding place | How it is extracted | Effort |
|---|---|---|
A string in a script | strings on the binary | Seconds. |
A SerializeField | It is in the scene or prefab asset | Seconds. |
A ScriptableObject | Asset bundles are readable | Minutes. |
Split across variables | It is reassembled at runtime | Minutes. |
XOR / base64 "encryption" | The key to decrypt ships too | Minutes. |
Behind IL2CPP | Decompilers exist for this | An afternoon. |
A WebGL build | It is plain text in the browser | Open DevTools. |
The pattern is not that these are bad hiding places. It is that there is no good one. Anything the game can read at runtime, a player can read too, because the game runs on their machine.
The fix: a proxy you control
Move the key to a machine the player does not own. The game talks to your server; your server talks to the API.
- 1 The game sends the player line to your endpoint
No key, no model name, no system prompt. Just the input and a session token.
- 2 Your server adds the key and the prompt
The key lives in an environment variable on the server. The system prompt lives there too — players cannot read or edit it.
- 3 Your server calls the API and relays the reply
This is the natural place for rate limits, abuse filtering, and cost logging.
app.post("/chat", async (req, res) => {
// 1. Authenticate the player. Without this, your proxy is an
// open, free API for the entire internet.
const player = await authenticate(req);
if (!player) return res.status(401).end();
// 2. Rate limit per player, not globally.
if (await overQuota(player.id)) return res.status(429).end();
// 3. Your key. Server-side environment variable, never in git.
const upstream = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-haiku-4-5",
max_tokens: 300,
system: SYSTEM_PROMPT, // yours, not the client's
messages: [{ role: "user", content: String(req.body.line).slice(0, 500) }],
}),
});
const data = await upstream.json();
await recordUsage(player.id, data.usage); // cost tracking
// 4. Return only what the game needs.
res.json({ text: data.content?.find(b => b.type === "text")?.text ?? "" });
});What the proxy buys you beyond safety
- Change the model without shipping a patch. Swap Haiku for Sonnet server-side.
- Change the prompt without shipping a patch. Tune your NPC's voice live.
- Per-player rate limits, so one user cannot drain your budget.
- Real cost data, because you see every request.
- A kill switch. If costs spike, you can turn it off.
That second point is worth dwelling on. Prompts need iteration, and a prompt baked into a build is frozen until your next release. Keeping it server-side turns tuning into a config change.
If you only remember one checklist
- The key lives in a server environment variable. Never in the project.
- Add the key file pattern to
.gitignorebefore writing any code. - The proxy authenticates every request.
- The proxy rate limits per player.
- The client sends input only — never the model, prompt, or key.
- Set a spend alert on your API account today, not after launch.
What to take away
- Anything shipped to a player is public, including your key.
- No client-side obfuscation works. Do not spend time on it.
- A proxy is the only real fix, and it is not much code.
- An unauthenticated proxy is a different way to lose money.
- Server-side prompts and models mean you can tune without a patch.