Every AI feature starts with the same shortcut.
The model is an HTTP API. The browser can speak HTTP. There is a button on a page, and the thing it needs is one request away — so why bounce it through your own server first? One fewer hop, one fewer thing to build, and it works immediately.
It does work immediately. That is the problem with it.
What You Hand Over
When the browser calls the model directly, four things leave your control at once, and only the first is obvious.
The credential. Whatever key authorises that call is now in the client. Not hidden — in it. Minified, base64'd, behind a build step, it does not matter: anyone who opens the network tab has it, and they can use it against the provider directly, for anything, until you notice.
The spend. Every cap from the last chapter assumed a server making a decision. A browser calling a provider makes those decisions itself, or not at all. Your ceiling, your lanes, your per-tenant budget: all bypassed.
The prompt. If the browser assembles the request, the browser assembles the instructions. Every rule from Chapter 3 — the scope clause, the refusal shape, the output contract — becomes a suggestion that any user can replace with their own.
Identity. The provider knows the call happened. It has no idea who made it. You cannot attribute it, rate-limit it, bill it, or investigate it, because the only identity in play is the key everyone is sharing.
The last two are the ones people miss, and they are why "just keep the key on a proxy" is not the whole answer.
The Failure That Taught Us
Here is the actual bug, because it is more interesting than the textbook version.
The feature is inline editing: a merchant is on their own live site, clicks a field, and asks for a better sentence. That needed a credential, and it had one — a short-lived, per-site editing token, minted when the editing session opened.
The token was sent in the URL. The browser called the AI service directly, token in the query string, and the AI service used it to decide whether to proceed.
Except it could not. That service had no way to validate that token, because it did not own it. The table mapping tokens to sites lived in a different service. The AI service received an opaque string, had nothing to check it against, and did the only thing it could: it proceeded.
So the credential was decorative. The endpoint was, in practice, open — to anyone who could construct the request, with a prompt of their choosing, billed to us and rate-limited by nothing.
The lesson generalises well beyond this codebase:
Authentication has to happen where the facts live. A service that does not own the table cannot enforce the rule. Handing it a token only makes it look like it can.
If you take one idea from this chapter, take that one. It is the reason the fix is a new service boundary rather than a stricter check in the old place.