
01 the problem
A competitive game backed by an LLM the player is actively trying to game — running on a paid API, in front of minors — is a stack of production hazards. The model can be prompt-injected. It can be flattered into a high score. A bad actor can burn through your API bill. And the model can say things a family audience shouldn't hear.
02 the solution
Built the game so the model *can't* be gamed — structurally, not with hope. The Gemini call returns taste only (five bounded axes plus one in-voice line). The favor delta is derived server-side by a pure function the model can't see or reach. Prompt injection, sycophancy-gaming, and denial-of-wallet are handled at the architecture layer. A deterministic red-line pre-filter runs before any model call, so flagged input costs zero.
03decisions & tradeoffs
Model returns taste, server derives the score
Making the favor delta a field the LLM can output makes it a field the LLM can be talked into inflating. Removing the field entirely is stronger than any prompt hardening.
Structured, low-temperature output only
Predictable schema across every turn, cheaper to reason about, easier to write invariant tests against. Voice comes from prompt, not temperature.
Deterministic pre-filter before model call
Red-line content should never spend a token. Rules-based short-circuit is faster, cheaper, and auditable in a way an LLM classifier isn't.
Cached degrade mode over hard failure under load
Concurrency damper serves an in-voice cached reaction with zero model calls when spend or load spikes. Still routes a neutral score through the same derive-favor function. Meter never breaks, character never breaks.
04 tech stack
Google Gemini
Server-side proxy, low-temperature structured taste-only output
React + TypeScript
Web app, share-card generator, favor meter UI
Vercel serverless
API routes with atomic-slot concurrency damper (TOCTOU-safe)
Supabase
Auth, standing and rivalry persistence across days
05 results
- The model never emits the score — favor delta is derived server-side from clamped, weighted taste axes
- Denial-of-wallet bounded five ways: 3-turn daily cap, per-IP + per-user rate limits, atomic concurrency slot, cached degrade mode, Vercel spend hard-stop
- Content pre-filter short-circuits red-line input before any model call — zero cost on flagged requests
- Prompt injection handled structurally (player reply rides as data being judged, never in the system instruction)