# PromptQuiz: instructions for coding agents

Canonical guide: https://promptquiz.mvps.ch/agents-full.md  
Base URL: `https://promptquiz.mvps.ch`  
Machine-readable API: https://promptquiz.mvps.ch/openapi.json  
Human-readable API reference: https://promptquiz.mvps.ch/api-docs

All API paths below are relative to the base URL. Send `Content-Type: application/json` for requests with JSON bodies.

**For every API request, send `User-Agent: PromptQuiz-Agent/1.0` (or your own descriptive agent name).** Cloudflare blocks the default `Python-urllib/3.x` User-Agent with HTTP 403 / error 1010 before the request reaches this API. This applies even to `POST /api/accounts`; it is a client-header issue, not an invalid account token. For Python `urllib.request`, include `"User-Agent": "PromptQuiz-Agent/1.0"` in the request headers. `curl` and Python `requests` have working defaults, but setting a descriptive User-Agent explicitly is more reliable. If you receive Cloudflare error 1010, change the User-Agent and retry once; do not repeat the unchanged request. Do not send the account token to any destination except this API.

You can create and edit hosted, real-time quizzes using HTTP. No browser automation or human sign-up is needed. Give the human the private, stable `host_url` after creating a quiz. The human opens it, creates a live room, and shares its QR code or six-digit code. Players join on phones. A shared host screen shows the question; phones show the same answer order in a two-column grid, plus individual results, score, and position.

## What to do when someone asks for a quiz

1. Establish the topic, audience, difficulty, approximate question count, quiz language, and anything that must be included or avoided. Ask a short question if a missing detail would change the quiz substantially; otherwise choose sensible defaults and say what you chose.
2. Draft the questions and check each answer against a trustworthy source or the user's supplied material. For current, specialist, or disputed facts, verify before creating the quiz. If you cannot verify a necessary fact, ask the human or replace the question.
3. Apply the quality checks below. Check the `correct` indices against the exact `options` array you will send.
4. Reuse the human's saved account token if available. Otherwise create one with `POST /api/accounts` and save it somewhere persistent and private. An anonymous account token is the only way to list or edit that account's quizzes later.
5. Create the quiz with `POST /api/quizzes`. Return the stable `host_url` and a short summary of question count, types, timing, and any important assumptions. Do not create a room unless the human asks you to host or start a game.

If asked “show me all my quizzes,” call `GET /api/quizzes` with the saved account token. If the token was lost, the account cannot be recovered. Do not silently make a new account and claim its list is complete.

Write the quiz title, questions, and choice options in the language requested by the human. The site interface follows each participant’s browser language (English, German, or French), with a manual language switcher. Quiz content is shown exactly as you author it and is not translated automatically. True/false labels are shown in each viewer’s interface language.

## Make the quiz worth playing

- Test understanding, application, or a useful distinction, not merely recall of a word in the question. Mix easier entry questions with more demanding ones.
- Each question must have one defensible interpretation. If experts could reasonably disagree because of missing context, add the context or rewrite it. A factually correct answer is not enough if another option is also defensible.
- Write plausible distractors from the same category, at roughly similar lengths and grammatical forms. Avoid one conspicuously precise or long correct option next to three vague wrong ones.
- Avoid answer giveaways: repeated “always,” “never,” “only,” or “all” in false options; “all of the above” and “none of the above”; absurd distractors; grammatical mismatches; or a pattern where the correct answer is always in the same position. Absolute words are fine when they are the precise fact being tested, but not as a shortcut to guessing.
- Use true/false sparingly: guessing succeeds half the time. Prefer a single-choice scenario when the learner should distinguish *why* something is true.
- For `multiple`, say “Select all that apply” in the question. The player must submit the **exact** correct set; there is no partial credit. Use this type only when more than one genuinely independent option is correct. Do not make every option correct or train players to expect the same number of correct choices every time.
- Keep the shared-screen question readable at a glance. Prefer a short stem and concise options. Use a longer timer for reading, calculation, or multiple selection; the API accepts 5–120 seconds and defaults to 20.
- Avoid trick negatives such as “Which is NOT…” unless the negative distinction is the learning goal. If you need one, make the negation unmistakable.
- Read the quiz as a participant: would the answer still be clear after options are shuffled? Are the correct indices right? Are there duplicate concepts, accidental hints from other questions, or repetitive question shapes? Revise before posting.
- Use images when they make the question clearer (a diagram, map, specimen, or visual comparison). Avoid decorative images that consume shared-screen space without helping someone answer. Keep the important subject large enough to recognize on a phone, and write useful alt text. Every answer still needs a short text label.

## Authentication and secrets

Create an anonymous account once:

```http
POST /api/accounts
```

Response: `201 {"account_token":"<48 hex characters>","instruction":"...","api_docs_url":"..."}`.

Use `Authorization: Bearer <account_token>` to create, list, read, and replace quizzes. Store the token in a private persistent location that the agent can use in later sessions. If you have a local filesystem, use a file outside the repository with permissions limited to the user, such as `~/.config/promptquiz/account-token` with mode `0600`. Never put the token in a public repository, shared transcript, or client-side page. If you cannot persist it, give the human a clear way to save it privately; without it, later listing and editing will not work.

A quiz also has a separate `host_token`. It appears after `#` in the returned `host_url` and can read that quiz and create or control rooms. Treat the entire host URL as private; share it with the host, not participants. Participants receive only a `join_url` or room code.

## Upload and use images

Images are optional. The account token that creates a quiz must also upload its images. You may attach an image to a question for the shared host screen, to individual answer choices for both host and phone screens, or both. Players do not see the question image on their phones. Existing text-only quizzes need no changes.

Upload each image in three steps. The image bytes go **directly to Cloudflare R2**, never through the website or Vercel API. Use still PNG, JPEG, or WebP images, from 12 bytes to 8 MiB. Images must be at most 8,192 pixels on either side and 16 megapixels overall; animated WebP is rejected. The declared MIME type and exact byte length are signed; send the original file bytes without modification. The signed URL lasts 90 seconds. A failed or expired upload needs a new upload request and image ID.

1. `POST /api/images/uploads` with the account bearer token and JSON such as `{"content_type":"image/png","size":12345}`. The response is `201` with `image_id`, `upload_url`, `method: "PUT"`, required `headers`, `size`, and `expires_at`. `size` is the exact file byte count, not an estimate. Keep `upload_url` private while it is valid.
2. `PUT` the raw file bytes to `upload_url` with the returned `Content-Type`. Do not send the account token to R2. `curl --data-binary @file.png` sets `Content-Length` automatically. Browser `fetch` with a `File` or `Blob` body also sets it; do not try to set that browser-controlled header yourself. This request goes to `*.r2.cloudflarestorage.com`, not `promptquiz.mvps.ch`.
3. `POST /api/images/{image_id}/complete` with the account bearer token and no body. The server checks the stored byte count, declared MIME type, and image structure, then returns `{"image_id":"...","url":"https://agent-quiz-live.ralf-769.workers.dev/media/...","status":"ready"}`. Only a ready image can be used in a quiz. Completing a ready image again is safe.

For example, with `ACCOUNT_TOKEN` and a local PNG file:

```bash
bytes=$(wc -c < diagram.png | tr -d ' ')
curl -fsS -X POST https://promptquiz.mvps.ch/api/images/uploads \
  -H "Authorization: Bearer $ACCOUNT_TOKEN" -H 'Content-Type: application/json' \
  --data "{\"content_type\":\"image/png\",\"size\":$bytes}" > upload.json
image_id=$(jq -r .image_id upload.json)
upload_url=$(jq -r .upload_url upload.json)
curl -fsS -X PUT "$upload_url" -H 'Content-Type: image/png' --data-binary @diagram.png
curl -fsS -X POST "https://promptquiz.mvps.ch/api/images/$image_id/complete" \
  -H "Authorization: Bearer $ACCOUNT_TOKEN"
```

The temporary upload URL is not the image URL. Use the finalized `image_id` in quiz JSON. Image references have the shape `{"image_id":"<32 lowercase hex characters>","alt":"A useful image description"}`. For a shared-screen question image, set `image` on that question. For answer images, set `option_images` to an array with **one entry per option** in the same order: use an image reference or `null` for each choice. Answer text remains required. `option_images` is unavailable for `true_false`. When answers shuffle, their images and correct indices shuffle with them.

```json
{
  "text": "Which diagram shows a neural network with one hidden layer?",
  "type": "single",
  "options": ["Diagram A", "Diagram B", "Diagram C"],
  "option_images": [
    {"image_id": "0123456789abcdef0123456789abcdef", "alt": "Input nodes connected directly to output nodes"},
    {"image_id": "fedcba9876543210fedcba9876543210", "alt": "Input, hidden, and output layers"},
    null
  ],
  "correct": [1],
  "time_limit": 30
}
```

The account can keep at most 100 ready images and 10 recent pending uploads. Signing is also rate-limited by network. Pending objects expire after one day. There is currently no image deletion API because running rooms retain quiz snapshots. Image URLs can be viewed by anyone who has them; do not upload private material. Image reads use `GET /media/{image_id}` on the Worker origin; they do not need an account token. A quiz update must keep all image references it still uses, since `PUT` replaces the complete quiz.

## Quiz JSON

Create with `POST /api/quizzes` and the account bearer token. This example shows all three question types:

```json
{
  "title": "Weather basics",
  "accent_color": "#0B4FF2",
  "answer_colors": false,
  "shuffle_answers": true,
  "questions": [
    {
      "text": "What does 100% relative humidity mean at the current temperature?",
      "type": "single",
      "options": ["The air is saturated with water vapor", "The air contains only water vapor", "Rain must be falling", "The temperature is 100 degrees"],
      "correct": [0],
      "time_limit": 20
    },
    {
      "text": "Select all that apply: which changes can make a puddle evaporate faster?",
      "type": "multiple",
      "options": ["Warmer air", "Stronger wind", "Higher humidity", "A cooler surface"],
      "correct": [0, 1],
      "time_limit": 30
    },
    {
      "text": "Water can evaporate below its boiling point.",
      "type": "true_false",
      "correct": true,
      "time_limit": 10
    }
  ]
}
```

Rules enforced by the API:

| Field | Rule |
| --- | --- |
| `title` | Required, at most 100 characters. |
| `questions` | Required array of 1–50 questions, kept in the order sent. |
| `text` | Required question text, at most 300 characters. |
| `type` | `single`, `multiple`, or `true_false`. |
| `options` | For `single` and `multiple`: 2–6 distinct, nonempty strings, each at most 120 characters. For `true_false`, omit it; the API uses `True`, `False`. |
| `correct` | For choice questions: array of distinct zero-based indices into the **original submitted options**. `single` needs exactly one; `multiple` needs at least one. For `true_false`: use `true`/`false`, or `[0]`/`[1]`. |
| `time_limit` | Integer seconds from 5 to 120; default 20. |
| `image` | Optional finalized image reference `{ "image_id": "...", "alt": "..." }` on the question. Shown on the shared host screen. Alt text is required, 1–160 characters. |
| `option_images` | Optional array parallel to `options`, containing an image reference or `null` at each position. Not supported for `true_false`. Shown on host and phones. |
| `accent_color` | Optional six-digit hex. Default `#0B4FF2`. Must have at least 4.5:1 contrast against white; invalid colors are rejected. |
| `answer_colors` | Optional boolean, default `false`. `true` gives answer choices distinct color tints and markers; text and letters remain visible. |
| `shuffle_answers` | Optional boolean, default `true`. Single and multiple-choice options shuffle **once per room**, identically for host and all players. The server remaps correct indices. Set `false` to keep submitted order. True/false stays `True`, `False`. |

Successful creation returns `201` with `quiz_id`, `host_token`, stable `host_url`, `create_room_url`, and `api_docs_url`. The host URL looks like `https://promptquiz.mvps.ch/quiz/<quiz_id>/host#<host_token>`.

## List, read, and update

| Request | Token | Result |
| --- | --- | --- |
| `GET /api/quizzes` | Account | Lists this account's quiz IDs, titles, question counts, and host URLs. |
| `GET /api/quizzes/{quiz_id}` | Account **or** that quiz's host token | Returns the complete quiz, correct answers, settings, and host URL. |
| `PUT /api/quizzes/{quiz_id}` | Account | **Replaces the whole quiz**. Send a complete valid quiz JSON body, including any settings you want to keep. The host URL and quiz ID stay the same. |

To change one question, read the complete quiz first, edit a copy, verify all `correct` indices, then `PUT` the complete body. A running room keeps the quiz snapshot it started with; updates affect rooms created later. Do not promise that editing a quiz changes a game already in progress.

## Live rooms

Humans normally use the host browser. If they ask you to control a game by API, use this sequence:

| Request | Token | Purpose |
| --- | --- | --- |
| `POST /api/quizzes/{quiz_id}/rooms` | Account **or** host | Creates a room from the current quiz snapshot. Returns six-digit `room_code`, private room `host_url`, public `join_url`, and `status: "lobby"`. |
| `POST /api/rooms/{code}/join` with `{"name":"Ada"}` | None | Joins during lobby. Names are 1–24 characters, unique ignoring case; max 100 players. Returns `player_token`. |
| `GET /api/rooms/{code}/state` | Optional host/player bearer token or `?token=` | Current state. Without a token, only the public view is returned. |
| `POST /api/rooms/{code}/action` with `{"action":"..."}` | Host | Controls the game. |
| `POST /api/rooms/{code}/answer` with `{"question_number":1,"answer":[0]}` | Player | Locks an answer and returns the player's current state. Identical retries are safe. |
| `GET /api/rooms/{code}/ws?token=...` | Host or player token in query | Live WebSocket state and actions. |

In the lobby, the host can remove a player by sending `POST /api/rooms/{code}/action` with `{"action":"remove_player","player_id":"<id>","expected_phase":"lobby","question_number":0}`. Get the player's `id` from the host room state. Removal immediately updates the live player count, revokes that player's room token, and shows a removal message on their phone. The same name cannot rejoin this room. Repeating a successful removal is safe. Players can only be removed before the game starts.

Host actions after lobby: `start` (requires at least one player) → `reveal` → `standings` → `next`. The server reveals immediately when every player in the room has answered, or at the deadline if someone has not. The host can use `reveal` early while answers are still missing. Repeat standings/next for each question. `next` after the last standings ends the game and shows the final ranking. `end` can finish early. Invalid actions or wrong phases return an error. A legacy client may send `next` directly from reveal, but new hosts should show standings first.

For reliable controls, send the phase and question number from the last room state with each HTTP action, for example `{"action":"next","expected_phase":"standings","question_number":1}`. If that state has already changed, the server returns the current state without applying the action. The lobby has `question_number: 0`. This makes retries after a lost response safe. Host actions without these fields remain supported for older clients, but cannot safely be retried without first reading the current state.

Players should submit by HTTP with the displayed `question.number`, for example `POST /api/rooms/123456/answer` with player bearer token and `{"question_number":1,"answer":[0]}`. The server accepts one answer per player per question. An identical retry returns the current state without scoring twice, even if the response was lost or the timer has since ended. A different answer, a late first answer, or an answer for an older question returns `409`. Do not change answer indices between retries.

For WebSockets, connect to **`wss://agent-quiz-live.ralf-769.workers.dev/api/rooms/{code}/ws?token=...`**. The website is hosted on Vercel, which forwards HTTP API calls to the Worker; WebSocket clients connect directly to the Worker. The server sends `{"type":"state","state":{...}}` on connection and after changes. Host messages are `{"action":"start"}`, `{"action":"remove_player","player_id":"<id>"}` during lobby, `{"action":"reveal"}`, `{"action":"standings"}`, `{"action":"next"}`, or `{"action":"end"}`. A removed player receives `{"type":"removed"}` and their socket closes. A player answers with `{"type":"answer","answer":[0]}`; multiple-choice answers contain all selected option indices. Each player can lock one answer per question. Scores are 500–1000 for an exactly correct answer, reduced by response time; wrong, incomplete, late, or missing answers get zero.

Room state includes `phase` (`lobby`, `question`, `reveal`, `standings`, `ended`), `revision` (increases with each saved change), `question_number`, `question_count`, `deadline`, `player_count`, and `answered_count`. The host's `players` list includes IDs needed for lobby removal. During a question, the host sees `question.text` and optional `question.image`; phones see the synchronized `question.options`, optional `question.option_images`, and answer letters, with the question itself on the shared screen. Correct indices appear only at reveal. The host's reveal view includes `answer_stats`: `option_counts` (how many players selected each option), `correct_count` (exactly correct answer sets), `incorrect_count`, and `unanswered_count`. For multiple selection, one player may count toward several option counts. After scoring, a player sees `my_points` for that round, `my_score`, and `my_rank`; from question two onward, they also see `my_previous_rank` and players in standings may have `rank_change`. New rooms also include `my_correct_count`, `my_answered_count`, `my_streak`, and `my_best_streak`. Streaks are informational and do not add bonus points. Standings includes ranked `players` with scores and each player's answer counts and streak when available. At the end, both screens show the final ranking. Rooms created before these stats were introduced omit the added lifetime counters rather than showing partial totals.

The website can be installed as a PWA. It needs a connection to play live; offline it shows a reconnect page. Supported mobile browsers may give brief haptic feedback when selecting or submitting an answer and on results; unsupported devices stay silent.

Live sockets can disconnect during a deployment. Clients reconnect and receive the persisted room state, including the current deadline and any accepted answers. Use HTTP answer and guarded host action requests when controlling a game programmatically; retry only the **same** request after a transport failure. If the server returns a non-2xx response, read the error rather than blindly retrying.

## Errors and recovery

API errors are JSON objects such as `{"error":"..."}` with a non-2xx status. Fix validation errors and retry. A `401` usually means a missing token; a `403` can mean the wrong host token or a removed player token; `404` means the quiz, room, account, or specified player could not be found; `409` means a duplicate or removed name, full room, or action unavailable in the current phase. Do not invent a new quiz ID, room code, or token to work around an error. Read the returned error and ask the human only when a missing secret or content decision genuinely blocks progress.
