If your app calls the model straight from the browser or from a shipped mobile binary, the key that pays for those calls is inside the client. Anyone who opens the network tab, or unpacks the app, can read it. The fix is not a cleverer key. It is to move the call behind a route you control, so the key lives on the server and the client never holds it.

Why the client cannot keep a secret

A browser and a mobile app are untrusted environments. Anything you ship to them can be read by the person running them, and a Bearer key is just a string in a header. There is no amount of obfuscation that turns a shipped secret into a safe one. So the model call moves to the server, and the client talks to your server instead.

The proxy shape

The client sends its request to your own endpoint. Your endpoint adds the key, calls the model, and returns the result. The key is read from the server environment, never from the client.

// server route: POST /api/chat
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://webwright.ai/v1",
  apiKey: process.env.WEBWRIGHT_API_KEY, // www_v1_... on the server only
});

export async function POST(req) {
  const { messages } = await req.json();
  const completion = await client.chat.completions.create({
    model: "wren-r1",
    messages,
    max_tokens: 512,
  });
  return Response.json({ reply: completion.choices[0].message.content });
}
// client: the browser never sees the key
const res = await fetch("/api/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ messages: [{ role: "user", content: "Hello" }] }),
});
const { reply } = await res.json();

One request in, one response out. The Authorization header is set on the server, so a 401 for a missing or invalid key can only happen on the server side, where you can log it and where the key actually lives.

The proxy is also your control point

Once every call goes through one route, the cross-cutting work has a single home:

  • Spend ceiling. Count usage.total_tokens from each response and stop a user who passes a budget. The usage object is on every response, so the tally is arithmetic, not an estimate.
  • Rate limit. Enforce a per-user limit in front of the provider limit. The published provider tiers are per API key (Hobby 10 RPM and 2 concurrent, Pro 500 RPM and 20 concurrent), and a 429 carries Retry-After, so your proxy can smooth bursts before they reach that ceiling.
  • One log line per call. Record the user, the model, and the token counts in one place. That is the same log you need later to see cost per successful outcome.
  • A single place to change providers. The base URL and key are read from configuration on the server, so switching or adding a provider is a config change, not a client release.

A short checklist

  • The key is read from the server environment, never bundled into client code.
  • The browser or app calls your route, and your route calls the model.
  • Your route enforces a per-user rate limit and a spend ceiling in front of the provider.
  • Every call logs user, model and usage in one place.
  • Provider base URL and key are configuration, not constants scattered through the client.

A shipped key is a public key. The server proxy is the smallest change that stops that, and it pays for itself the first time you need one place to cap spend.

Want a second set of eyes on where your key and your cost controls sit? Book a short consult and we will walk the request path with you.

The WebWright Team