Time to put Klippy on the internet. But first, think about what deploying means: /ask becomes a public address, and every call to it spends your model budget. Anyone who finds it could call it all day. So before the first deploy, you'll add two locks. Then you'll ship, and bits will call your Worker to check that it's live.
Verified wrangler 4.157.0agents 0.33.0ai 7.0.162
Start from a-04 on GitHub. Starting here, or behind? Get the code at that tag:
git clone https://github.com/ryanyogan/klippy cd klippy git checkout a-04
1. Lock one: only you
/ask will need a header, Authorization: Bearer <token>, where the token is a long random string only you know. Make apps/agent/src/guard.ts:
env.KLIPPY_TOKENis a secret: a value Cloudflare stores for your Worker and hands it onenv, like a binding. It's never in your code.No token set?
isOwnersays no. A missing secret should lock the door, not open it.Why hash both sides and use
timingSafeEqual, instead of===? A plain comparison stops at the first wrong character, so a wrong guess that starts right takes a tiny bit longer. Over many tries, that timing can leak the token. Hashing makes both values the same length, andtimingSafeEqualtakes the same time whatever they hold.
2. Lock two: not too often
Even you shouldn't be able to make a thousand calls a minute by accident. Add this to guard.ts, between isOwner and sha256:
env.LIMITER is Cloudflare's Rate Limiting binding. limit({ key }) counts calls per key and says whether this one is allowed. On /ask the key is the caller's address. Declare the binding, and the secret, in wrangler.jsonc after the ai block:
secrets.requiredtells wrangler the Worker needsKLIPPY_TOKEN:wrangler typestypes it, andwrangler devwarns when it's missing.The limiter allows 10 calls per 60 seconds per key.
namespace_idis any number you choose, unique in your account.
Be honest with yourself about what this is. The limiter's counts are kept per Cloudflare location and are approximate. It's a brake, not a bill cap. The real cap on spending lives at your AI Gateway, which can set rate limits and spend limits per gateway. Course F sets them up.
3. One ask() for every route
Two routes are about to ask the model the same way, so move the call into its own file. Make apps/agent/src/ask.ts (the body is the call you wrote last lesson):
Then /ask in index.ts checks both locks before anything else:
The order matters: the cheap checks (token, limit, body) run before the expensive one (the model). A caller without the token never costs you a model call. Update the imports at the top of index.ts to import { ask, MAX_QUESTION } from './ask'; and import { clientAddress, isOwner, underLimit } from './guard';, and delete the old generateText and model imports.
4. Your token, locally
Locally, secrets live in apps/agent/.dev.vars, which .gitignore keeps out of git. Make a token and put it there:
openssl rand -hex 32Copy the output, and write it in apps/agent/.dev.vars as KLIPPY_TOKEN= followed by your token. (Klippy's repo has a .dev.vars.example with a placeholder.) Then run pnpm --filter agent run cf-typegen, so env.KLIPPY_TOKEN and env.LIMITER are typed.
5. A door for bits
bits checks your deploy by calling one address on it: GET
/klippy/check?nonce=…. It must stay public, since bits has no token. But anyone else can call it too, so it must never cost more than a few model calls, however often it's called. Make apps/agent/src/check.ts:
The nonce is a random word bits sends, so it knows the answer is fresh. Only 8 to 64 letters, digits,
-or_are accepted, so the route never echoes back whatever someone types.The probe asks the model a fixed question. Its answer is the same for everyone, so it's kept for five minutes in
cached: a hundred checks in a minute cost one model call.cachedlives in one copy of your Worker, and Cloudflare may run several. So a fresh probe also needs the limiter, under one shared key,'check': at most 10 fresh probes a minute in total, whoever asks.TAGsays which lesson your code is at. You'll change it at the end of each course.Cache-Control: no-storestops anything in between from keeping an old answer.
Route it in index.ts, after /ask, and add import { check } from './check'; at the top:
6. See it run
Last, turn on logs and traces (a timeline of each request, including the call to AI Gateway), so you can see what your live Worker does. Replace the observability block in wrangler.jsonc:
Traces start counting toward Cloudflare's Observability pricing on 1 December 2026; the free allowance covers a learning project.
Now restart pnpm dev and try every door. Use your own token in place of ours:
The second check took 7 milliseconds in our dev log, against 2.3 seconds for the first: same probe, from the cache, with the new nonce. The answers came from the free fallback model, as in the last lesson.
Now the tests. /ask needs a token now, so last lesson's tests would all get a 401. Give the tests a known token of their own in apps/agent/vitest.config.mts:
Then copy the updated test/ask.spec.ts and the new test/check.spec.ts from the repo at tag a-05 (the simplest way: copy the whole apps/agent/test/ folder). They send that token, test both locks, and prove two checks make one model call. Run pnpm test. Ours: 14 passed.
If every /ask says 401, check that .dev.vars is in apps/agent, not the root, and restart pnpm dev: secrets are read at start.
7. Ship it
Set the same token on Cloudflare (it asks you to paste it), then deploy. From klippy/:
pnpm --filter agent exec wrangler secret put KLIPPY_TOKEN pnpm deploy
pnpm deploy prints your Worker's address, like https://klippy.<your-subdomain>.workers.dev. Open /klippy/check?nonce=hello-from-me on it in your browser and you'll see your own check.
What we recorded, and what we didn't. We haven't deployed the reference Klippy for this lesson, so there's no recorded output of a live deploy here. This is our dry run, which builds everything and stops before uploading:
Two more commands worth knowing. pnpm --filter agent exec wrangler tail streams your live Worker's logs to the terminal while you send it requests. And every deploy is a new version: wrangler deployments list shows them, and wrangler rollback puts the previous one back in seconds if a deploy goes wrong.
8. Let bits check it
Paste your Worker's address below. bits calls /klippy/check on it from its own server and checks the reply.
A pass proves a Worker at that address answers the way this lesson asks. It doesn't prove who wrote it: the check isn't anti-cheat, it's a finish line.
That's Klippy v0: live, locked, logged and checked. Your code matches tag a-05 (changes from a-04). Next course, Klippy gets tools: the weather, the time, notes and links.