how to build

Project · ★★★ · Advanced

Quiz Maker

Paste your class notes, or type a topic, and get a ten-question quiz you can send to friends with a link. They play it, see why each answer is right, and fight for the top of the leaderboard. You build the part that talks to a language model, and you build it the way real apps do: the model is called from your server, with no key anywhere in the browser for someone to copy.

You need

What you'll learn

What this shows the agent can do

The brief

Make a new folder for the project. Copy the brief below into a file called BRIEF.md in that folder, or download BRIEF.md. Open the agent in the folder and say:

Read BRIEF.md and start with milestone 1. Stop after each milestone so I can check it.
# Quiz Maker — turn any text into a quiz you can share (★★★)

Build a quiz maker. Paste a page of notes, or type a topic like "the water cycle", and a minute later there is a ten-question multiple-choice quiz. A Cloudflare Worker sends the text to a language model on Workers AI, checks that the answer is valid quiz JSON, and saves the quiz in KV under a short code. Send the link to friends: they play it, see their score and an explanation for each answer, and get on that quiz's top-10 leaderboard. The real lesson is where the model call lives: in server code, behind a binding or a secret, and never in the browser with a key anyone can copy.

## Done looks like

- The maker screen: a big text box (up to 8,000 characters) or a topic box (up to 100), a difficulty choice (easy, medium, hard), a maker password box and "Make quiz".
- While it works, the page says what is happening. Within about a minute it shows the new quiz's link, like `/q/H7KM2P`.
- Every quiz has exactly 10 questions, each with 4 choices, one right answer and a one-sentence explanation.
- The play screen shows one question at a time. After each answer it shows right or wrong and the explanation, then "Next". The end screen shows the score out of 10.
- Answers are checked by the Worker. The browser never receives the answer key before the player answers, so reading the page source does not help.
- After the last question, a player can enter a name (up to 12 characters) and, if the score makes the top 10, it appears on the quiz's leaderboard.
- Every quiz page says "Written by a model. It can be wrong."
- A wrong maker password gets "Wrong password" and no model call. Playing needs only the link.
- Live at the address `npx wrangler deploy` prints. No console errors.

## Constraints

- One Cloudflare Worker with static assets, a Workers AI binding (`AI`) and a KV binding (`QUIZZES`). Use wrangler. Free plan only.
- The model is called only from the Worker, through the `AI` binding. No API key appears in the front end or in the repo. If you later switch to an outside model API, its key goes in with `npx wrangler secret put` and stays on the server.
- Pick a text model from the Workers AI model list (developers.cloudflare.com/workers-ai/models). Prefer one that supports JSON output. Ask for JSON only, then parse it defensively.
- Validate on the server: exactly 10 questions; each has `q` (up to 200 characters), `choices` (exactly 4 different strings, each up to 80), `answer` (0 to 3) and `explain` (up to 200). Trim and clean the text. If it fails, retry the model call once, then give a clear error.
- Workers AI has a free daily allowance. Protect it: making a quiz needs `MAKER_PASSWORD`, a Worker secret set with `npx wrangler secret put MAKER_PASSWORD`. Cap the input length before calling the model.
- KV keys: `quiz:<code>` holds `{title, difficulty, created, questions}`, written once. Do not store the pasted text, only the questions. `board:<code>` holds the top 10 as `[{name, score, ms}]`.
- KV allows 1,000 writes a day on the free plan and is eventually consistent, so write the board only when a score makes the top 10. Note in a comment that two scores at the same moment can overwrite each other, and that a Durable Object or D1 would fix it.
- Codes: 6 characters from an alphabet with no lookalikes.
- Routes: `POST /api/quiz` (make), `GET /api/quiz/<code>` (questions without answers), `POST /api/quiz/<code>/check` (`{i, choice}` returns right or wrong, the right answer and the explanation), `POST /api/quiz/<code>/finish` (all choices; the Worker counts the score), `GET /api/quiz/<code>/board`.
- Tell the model in the prompt: questions only about the given text or topic, no personal details about real private people.
- Front end: plain HTML, CSS and JavaScript, no frameworks, no build step. No accounts, no tracking. Phone works.

## Milestones

1. **A player with a fixed quiz.** The play screen working from a hard-coded quiz object in the front end. No server yet.
   Check: you can play all 10 questions, see an explanation after each, and get a score at the end.
2. **A Worker.** Set up the Worker with static assets serving the player, and `GET /api/health` returning `ok`. Run it locally.
   Check: the quiz plays from the local wrangler address and `/api/health` shows `ok`.
3. **First model call.** Add the `AI` binding. `POST /api/quiz` sends a topic to the model and returns the raw text. Show it on a test page.
   Check: the topic "volcanoes" returns text that looks like quiz questions about volcanoes.
4. **Valid JSON or a clear error.** Ask for JSON only, parse it, validate every field, retry once, and turn failures into a short message.
   Check: ten topics in a row each give exactly 10 valid questions or a clear error, and the page never shows a broken quiz.
5. **Password and go live.** Add `MAKER_PASSWORD`. Deploy. Make a quiz from your phone by pasting class notes.
   Check: a wrong password is refused without a model call (nothing new in the Workers AI usage), and the right one makes a quiz from your phone that you can play there and then.
> A quiz now lives only in the tab that made it. The next two keep it under a short code so anyone with the link can play, and keep the answers on the server.
6. **Save and share.** Store the quiz in KV under a code. Serve `/q/<code>`. Move answer checking to the server with the `check` and `finish` routes, and remove answers from what the browser gets. Deploy again.
   Check: open the link in another browser and play it; search the page source and network responses for the answers before answering: they are not there.
7. **Leaderboard.** Name entry at the end, top 10 per quiz in KV, shown on the end screen.
   Check: three players with different scores appear in the right order, and an 11th lower score does not write to KV.

## Stretch

- "Report a wrong answer" on each question, stored as a count in KV, shown to the quiz maker.
- A timer mode where faster right answers score more, scored on the server.
- Quiz from a web page address: the Worker fetches the page, strips it to text and caps the length.
- Picture questions using the photo-wall approach (R2 plus signed addresses).
- Move leaderboards to a Durable Object so simultaneous scores never collide.

## Leave out

- Accounts, classes, teacher dashboards.
- Any model call or API key in the browser.
- Storing the text people paste.
- Tracking, ads.

Start by reading WRITING.md and the guide chapter on data if you need storage.

Milestones

The same milestones as the brief. Tick each one off when its check passes. Do one per sitting if you like. The dot in the list marks the point where you have something that works on its own. Everything after it makes it remember things or work for more than one person. Stop at the dot and use what you built before you go on.

  1. A player with a fixed quiz. The play screen working from a hard-coded quiz object in the front end. No server yet.

    Check: you can play all 10 questions, see an explanation after each, and get a score at the end.

  2. A Worker. Set up the Worker with static assets serving the player, and GET /api/health returning ok. Run it locally.

    Check: the quiz plays from the local wrangler address and /api/health shows ok.

  3. First model call. Add the AI binding. POST /api/quiz sends a topic to the model and returns the raw text. Show it on a test page.

    Check: the topic "volcanoes" returns text that looks like quiz questions about volcanoes.

  4. Valid JSON or a clear error. Ask for JSON only, parse it, validate every field, retry once, and turn failures into a short message.

    Check: ten topics in a row each give exactly 10 valid questions or a clear error, and the page never shows a broken quiz.

  5. Password and go live. Add MAKER_PASSWORD. Deploy. Make a quiz from your phone by pasting class notes.

    Check: a wrong password is refused without a model call (nothing new in the Workers AI usage), and the right one makes a quiz from your phone that you can play there and then.

  6. A quiz now lives only in the tab that made it. The next two keep it under a short code so anyone with the link can play, and keep the answers on the server.
  7. Save and share. Store the quiz in KV under a code. Serve /q/<code>. Move answer checking to the server with the check and finish routes, and remove answers from what the browser gets. Deploy again.

    Check: open the link in another browser and play it; search the page source and network responses for the answers before answering: they are not there.

  8. Leaderboard. Name entry at the end, top 10 per quiz in KV, shown on the end screen.

    Check: three players with different scores appear in the right order, and an 11th lower score does not write to KV.

Make it yours

Once it works, pick one of these and ask for it as a new milestone.

If it goes wrong

The model sometimes returns text around the JSON, and the quiz breaks.

Models add friendly sentences or code fences unless told and checked. Say this to the agent:

Sometimes the quiz breaks because the model adds text around the JSON. Ask for JSON only, strip anything outside the outer braces, validate every field, and retry once before showing an error.

You can see the answers in the browser's network panel.

The question route is sending the whole quiz object. Say this to the agent:

The answers are visible in the network panel. Remove answer and explain from GET /api/quiz/<code> and only send them back from the check route after the player answers.

"AI binding not found" or similar when running.

The binding is missing from the wrangler config, or the name does not match env.AI in the code. Say this to the agent:

The Worker says the AI binding is missing. Show me the wrangler config and the line that calls the model, and make the binding names match.