There's a chat button on the site now. Bottom-right corner, a small horned face, on every page with a header. Tap it and something answers.
Before the engineering, the size of the decision: I've built a front desk for a studio with one shipped game and roughly the foot traffic you'd expect from that. On the record early, because the rest of this is going to sound like I know what I'm doing. What pushed it over was the site quietly reaching 48 readable pages — and the question everybody actually arrives with, which of these can I play right now, being answered across about four of them and prominently on none.
Its name is Oni, which is how you say 0n1 out loud. It's a doorman with horns, and the only surface on this site that could plausibly lie to you. That's where the design starts.
The one thing it isn't allowed to do
Everything else here is fact-checked against the source code of the thing it describes. A chat answering from a model's memory would be the one place that stopped being true, and it would fail convincingly: wrong platform, wrong price, a feature I never built, delivered warmly.
So Oni can't recall. It can only read.
At build time the site renders itself into two committed artifacts: an index of what
pages exist and what's in each one (about 30 KB — the build fails outright if it grows past
45), and a map of path → the clean text of every page. The model then gets exactly one
tool, read_page, with no URL parameter and no network access whatsoever. The map is the
allowlist: a path that isn't a key in that object doesn't exist, and there's nothing behind
it to reach for even if it did. Both files are committed, so the diff is precisely what the
greeter is newly allowed to say.
On top sits the one file no script regenerates — the greeter's conscience, distilled from each game's fact sheet. Calling it hand-written would flatter me: a model drafted it and I argued with it until it was right, which is how everything here gets made. What matters is that nothing overwrites it, and that it outranks every page it reads. LineBy is iPhone only. LineBy does have an in-app purchase, so "no in-app purchases" is false. Duels ships opt-out analytics, so a blanket "no analytics" is a lie. Never a release date, not even a hedged one. And never I'll pass that on to the developer — it can't, and implying otherwise is how a front desk becomes a liar about the one thing that mattered.
The evening I made it worse to save money
Thinking shares the token budget with the answer, so my first configuration switched thinking off and capped a whole turn at 400 tokens. Extremely thrifty. Then I asked it, on the live site, what I could play today.
It told me about LineBy. Accurately, warmly, and without mentioning that Worldwright has a playable demo running in the browser, which is currently the most interesting thing on the entire site.
It hadn't read anything. Thinking-off had made the model tool-shy — it answered from the index, which was cheap and wrong by omission, and I'd have marked it a pass if I hadn't known the right answer. The fix was adaptive thinking back on and the cap raised to 1,600 tokens: spending more money in order to be correct.
The one useful thing: your tool definitions are part of the cache prefix
Prompt caching is what makes this affordable — the index rides in every single conversation, and reading it from cache costs a tenth of what those tokens cost uncached. But the cached prefix starts at the beginning of the prompt, and tool definitions render before the system prompt.
So if you build your tool definition at runtime — a generated list of valid paths, a page count, today's date, anything — you change the first bytes of the prefix on every request, and every cache read becomes a cache write. Nothing breaks. The answers are exactly as good. You find out roughly 10× later, on the bill.
// Frozen literal, never built at runtime: anything dynamic in here moves the
// cache prefix on EVERY request, because tools render before `system`.
export const READ_PAGE_TOOL = { name: 'read_page', description: '…', input_schema: { /* … */ } }
// The index carries the only breakpoint. Everything after it varies freely —
// so this per-visitor note costs its own few tokens and never turns a read into a write.
const system = [{ type: 'text', text: INDEX, cache_control: { type: 'ephemeral' } }]
if (pageNote) system.push({ type: 'text', text: pageNote })
Sort your prompt by how often each part changes, most-frozen first, and put the breakpoint at the seam. Everything downstream of it is free to vary. Everything upstream is a promise.
What it doesn't do
It isn't support, it isn't me, and it says so before you type a word. It can't send anything, file anything, refund anything or pass a message on. Conversations are kept 90 days so I can check its answers, and anything real goes to a human at [email protected].
It's capped in five places, which took one bad sum to arrive at: consent, a human-check, five sessions per IP per day, two spend counters, and finally Anthropic's own limits. Each looked sensible alone — but a single IP's whole daily allowance is sessions × per-session ceiling, and the first time I did that properly it came to $9 against a $3 daily budget. One determined visitor could drink the day and rest the greeter for everybody else. Caps only mean anything as a set.
And from yesterday: the button now asks whether the greeter can actually serve before it renders anything. Missing key, spent budget, Anthropic unreachable — no button appears, and the contact page's ordinary cards do the job they did perfectly well before any of this existed. A visitor arriving during an outage used to walk the whole ceremony — consent screen, human-check, one of their five daily slots — in order to reach I couldn't finish that one.
Resting isn't an error state. The most useful thing a front desk can do when nobody's home is not be there — which is a strange principle to land on nine days into building the front desk, and the one I'd keep if I had to throw the rest away.
Go ask it something. More soon — RSS if you'd like to know what else I've put on the internet with my name attached to it.
