Field log · Security · AI
Calling an LLM from Android without shipping your API key
I build a lot of apps that talk to an AI model, and there's a mistake I made early that could have cost me real money: putting the provider's API key inside the app. It felt safe because it was "in the build config." It wasn't safe at all. Here's why, and the pattern I use now.
If your app calls an AI API — a chat companion, a tutor, an image describer — something in that request has to authenticate with the provider. The tempting shortcut is to drop the key into your Gradle config and read it from BuildConfig at runtime. It compiles, it works, and it quietly hands your secret to anyone who downloads the app.
Why "it's in buildConfigField" is not safe
Here's the version that looks responsible — the key isn't even in your source, it's injected at build time:
// build.gradle buildConfigField "String", "AI_API_KEY", "\"sk-....\"" // somewhere in the app val key = BuildConfig.AI_API_KEY callProvider(key, prompt)
The problem is that BuildConfig constants are compiled into the APK as plain values. Anyone can pull your app from the store, run a standard decompiler, and read that string in a couple of minutes. There's no obfuscation setting that fixes this — the app has to have the key in a usable form to use it, so the key is extractable, full stop. Once it's out, someone can burn through your quota, run up a bill in your name, or abuse the provider under your account. This isn't theoretical; scanning APKs for leaked keys is an automated hobby for a lot of people.
The rule I wish I'd started with: a secret that ships inside the app is not a secret. If the client can read it, so can an attacker. The only place an AI provider key belongs is on a server you control — never in the APK, never in BuildConfig, never in a committed file.
The fix: put a thin proxy between the app and the provider
The pattern is simple. Your app never talks to the AI provider directly. Instead it calls your endpoint, and that endpoint — running on a server, holding the real key — forwards the request to the provider and streams back the response. The key lives only in the server's environment.
Android app ──► your endpoint (holds the key) ──► AI provider
◄── response ◄──
I use Firebase Cloud Functions for this because it slots into the Firebase stack these apps already use, but the shape is the same on any serverless platform. The function reads the key from its environment config (not from code), calls the provider, and returns the result:
// Cloud Function (sketch)
export const chat = onCall(async (req) => {
const key = process.env.AI_API_KEY; // server-side only
const reply = await callProvider(key, req.data.prompt);
return { reply };
});
On the device, the app just calls the function. No key anywhere in the client:
// Android (sketch)
val result = functions
.getHttpsCallable("chat")
.call(mapOf("prompt" to userText))
.await()
What the proxy buys you beyond hiding the key
Moving the call server-side isn't just damage control — it unlocks things you can't do from the client:
- Rotate the key without shipping an update. Change it in the server env and every user is instantly on the new key. A key baked into the app can only be changed by publishing a new version and waiting for people to update.
- Rate-limit and authenticate. Require the caller to be a signed-in user, cap requests per user per day, and block obvious abuse — all before you spend a token.
- Swap providers or models centrally. Change which model you call, or move to a different provider, on the server. The app doesn't know or care.
- Keep prompt logic private. Your system prompt and any business logic stay on the server instead of being readable in the decompiled app.
If you've already shipped a key
The uncomfortable but necessary steps, in order: revoke the exposed key immediately in the provider's console — assume it's compromised the moment it shipped. Generate a new one and put it only in your server environment. Move the call behind a proxy as above. Then release an app update that talks to the proxy instead of the provider. Revoking first is the important part; the leaked key can be abused for as long as it stays valid, and there's no way to claw it back out of the APKs already on people's phones.
The lesson cost me nothing in the end only because I caught it before it mattered. The fix is not much more work than the shortcut — a small function and one changed call site — and it converts "a secret every user can read" into "a secret only my server holds." For anything that costs money per request, that's not optional.
Platform APIs and pricing evolve; treat the code here as illustrative sketches, and follow your AI provider's and cloud platform's current security guidance for production use.