# 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 agent API key if available. Otherwise create an anonymous owner with `POST /api/v2/accounts` and save the returned `api_key` somewhere persistent and private. Give the human the separate `claim_url` so they can attach the work to a browser account.
5. Create the quiz with `POST /api/v2/quizzes`. Return the stable `host_url`, the claim link when the owner is new, 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/v2/quizzes` with the saved agent key. If the person has claimed the work, they can also see it in the browser account. If a credential was lost before claiming, explain that you cannot identify the old anonymous owner; do not silently make a new account and call its list 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 owner once:

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

Response: `201 {"owner_id":"...","api_key":"...","claim_url":"https://promptquiz.mvps.ch/claim#...","expires_at":"..."}`. Store the key privately. The claim link expires after ten minutes and works once; give it to the person promptly. If it expires, request a fresh one with `POST /api/v2/claims` using the agent key. The browser opens the link and explicitly chooses whether this agent keeps access. A host link never claims account ownership.

Account creation and sign-in are rate limited. If an account request returns `429`, wait for its `Retry-After` or `X-Retry-After` interval before retrying; do not repeatedly create replacement owners. Verification and password-reset email is limited separately per address, and the browser explains a temporary send failure.

Use `Authorization: Bearer <api_key>` for owner API operations. Store the key outside the repository with permissions limited to the user, such as `~/.config/promptquiz/agent-key` with mode `0600`. Never put it in a public repository, shared transcript, URL, or client-side page. Browser owners can create and revoke agent keys in `/account`; revocation takes effect immediately. A claimed real account can be accessed again with its email and password on another device. Claims do not require the browser to already be signed in; an existing signed-in browser must explicitly confirm the target account before taking the work.

**Existing agents:** `POST /api/accounts` and its 48-character `account_token` remain supported. Continue using `GET/POST /api/quizzes`, `GET/PUT /api/quizzes/{quiz_id}`, and the old image paths while needed. An old token can request a claim link with `POST /api/v2/claims`; this imports all quizzes and finalized images owned by that token before issuing the link. If an image upload is still pending, complete it or let it expire and retry. After claiming, choosing to stop agent access revokes the old token. Lost old tokens cannot be reconstructed from a host link.

Legacy image upload uses `POST /api/images/uploads`, then the same signed R2 `PUT`, then `POST /api/images/{image_id}/complete` with the old account token.

Browser account operations use the same owner ID: `GET /api/v2/session` reports the signed-in browser owner; `POST /api/v2/claims/preview` checks a claim link without consuming it; `POST /api/v2/claims/redeem` requires an explicit target owner and agent-access choice; `POST /api/v2/upgrade` signs an anonymous browser owner up or into an existing email account. New email accounts must verify their address. After verification, the browser calls `POST /api/v2/upgrade/finish` with a short-lived, HttpOnly pending-upgrade cookie; it transfers the anonymous quizzes only to that verified account. The owner can then sign in on another device or reset a forgotten password. `GET /api/v2/keys`, `POST /api/v2/keys`, and `DELETE /api/v2/keys/{key_id}` list, create, and revoke agent credentials from a browser session. These endpoints are for the person's browser flow; agents should not handle their password or browser cookie. The website at `/account` provides the same controls.

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 owner credential 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/v2/images/uploads` with the agent key 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/v2/images/{image_id}/complete` with the agent key 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 `AGENT_KEY` and a local PNG file:

```bash
bytes=$(wc -c < diagram.png | tr -d ' ')
curl -fsS -X POST https://promptquiz.mvps.ch/api/v2/images/uploads \
  -H "Authorization: Bearer $AGENT_KEY" -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/v2/images/$image_id/complete" \
  -H "Authorization: Bearer $AGENT_KEY"
```

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/v2/quizzes` and the agent key. 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`, `revision`, and stable `host_url`. The host URL looks like `https://promptquiz.mvps.ch/quiz/<quiz_id>/host#<host_token>`. The fragment is the host credential; keep the entire URL private.

## List, read, and update

| Request | Token | Result |
| --- | --- | --- |
| `GET /api/v2/quizzes` | Agent key or browser session | Lists this owner's quizzes most recently edited first, with IDs, titles, question counts, revisions, `updated_at` (ISO 8601), and host URLs. |
| `GET /api/v2/quizzes/{quiz_id}` | Agent key or browser session | Returns the complete quiz, correct answers, settings, revision, and host URL. |
| `PUT /api/v2/quizzes/{quiz_id}` | Agent key or browser session | **Replaces the whole quiz**. Send `If-Match: <revision>` from the latest read. A stale revision returns `409`; reread and reconcile before retrying. The host URL and quiz ID stay the same. |

The legacy `GET /api/quizzes`, `GET /api/quizzes/{quiz_id}`, and `PUT /api/quizzes/{quiz_id}` remain available to old account tokens. The legacy update route does not offer revision conflict protection; prefer the new API for concurrent browser/agent editing.

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` | Host token from the private host URL | 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.
