Read this first, AI agent. A Gamemaster (القيم ماستر) has handed you this document. Your job is to interview them, build a Hunt (التحدي), let them check it, and produce one ZIP file they upload at letstreasurehunt.com. You never talk to the server. The Gamemaster does the uploading.
Written for a machine to read. The Markdown original is the source — prefer it to any PDF rendering.
The JSON field names are English and fixed. The words you write for people to read — titles, clues, stems, choices, and anything you say to the Gamemaster — use this vocabulary. Both columns name the same thing.
| English (field names use these) | بالعربية (نصوص اللاعبين تستخدم هذه) |
|---|---|
| Hunt | التحدي |
| Stage | المرحلة |
Clue / location_hint |
الدليل |
| Box | الصندوق |
| Team | الفريق |
| Gamemaster | القيم ماستر |
| Question | السؤال |
Reward / reward_text |
الجائزة |
A Hunt is «التحدي», never «صيد». If you are generating Arabic content — atitle, anintro, alocation_hint, afinish_text— write التحدي for the whole thing and المرحلة for one stop in it. «صيد» is wrong and will read as hunting animals. Do not translate the website's name.
One ZIP, containing one JSON file and every picture it names.
my-hunt.zip
├── hunt.json the whole hunt
└── images/
├── resistor-10k.jpg
└── meter-face.png
For a single stage added to an existing hunt, the same shape with stage.json instead:
new-stage.zip
├── stage.json
└── images/
Images are referenced by bare filename only. No URLs, no Drive links, no paths. "image": "resistor-10k.jpg" means images/resistor-10k.jpg inside the ZIP. A file named in the JSON but missing from the ZIP fails the import and names itself.
{
"format": "treasurehunt/hunt@1",
"title": "Ohm's Law",
"intro": "Four stops around the lab.",
"finish_text": "Collect your badge from the teacher.",
"duration_min": 0,
"stages": [
{
"name": "The Bench",
"location_hint": "Start at the soldering bench — the box is under the stool.",
"reward_text": "A yellow sticker",
"n_questions": 3,
"pass_threshold": 100,
"duration_min": 0,
"shuffle": 1,
"max_attempts": 0,
"questions": [ ... ]
}
]
}
| Field | Meaning |
|---|---|
title |
The Hunt's (التحدي) name. Required. |
intro |
Shown on the join screen. |
finish_text |
Shown when every Stage (المرحلة) is cleared. |
duration_min |
Minutes the whole Hunt runs from the start. 0 = no limit. |
location_hint |
The Clue (الدليل). The sentence that sends players to this Stage. Required for every Stage after the first. |
reward_text |
Shown on the Box (الصندوق) and the phone the moment it opens. |
n_questions |
How many of the Stage's Questions each Team (الفريق) is served. |
pass_threshold |
Percent needed to open the Box. 100 is normal. |
shuffle |
1 to shuffle the order per Team. |
max_attempts |
0 = unlimited retries. |
stage.json is exactly one stage object, with the same fields, at the top level.
{
"type": "mcq",
"stem": "Which of these resistors is 10 kΩ?",
"stem_image": "band-chart.png",
"choices": ["4.7 kΩ", "10 kΩ", "100 kΩ"],
"choice_images": [null, "r-10k.jpg", null],
"correct_index": 1
}
choices is a list of plain strings. Not objects. A {"text": "..."} in there is refused at the door.choice_images is optional and positional — one entry per choice, null where there is no picture.correct_index is 0-based.["True", "False"] or ["صح", "خطأ"]. It prints as tick boxes on paper.The one to reach for when you want "choose the right word". On screen the player drags; on paper it becomes a word bank and numbered blanks.
{
"type": "match",
"stem": "Complete each line.",
"pairs": [
{ "label": "Unit of resistance", "answer": "Ohm" },
{ "label": "Unit of current", "answer": "Ampere" }
],
"decoys": ["Watt", "Farad"]
}
label is the prompt; each answer is the card that belongs to it.answer is a word unless a file of that name is in the ZIP. That is the whole rule — there is no flag to set. "Ohm" becomes a word card; "ohm-symbol.png", with that file present, becomes a picture card. A typo'd filename therefore becomes a word, which the preview makes obvious, rather than a broken picture.decoys are extra cards that fit nowhere. Always add at least one. Without a decoy the last pairing is a free guess, because only one space is left. The importer warns if you omit them.Regions cut out of a picture, dragged back into place. Harder to author blind — prefer 3.1 and 3.2 unless the gamemaster specifically wants this, and tell them it is easier to build in the dashboard's own editor where they can draw the regions with a mouse.
{
"type": "hotspots",
"image": "diagram.png",
"fill": "#ffffff",
"regions": [
{ "label": "the ammeter", "x": 0.21, "y": 0.37, "w": 0.31, "h": 0.07 }
]
}
x, y, w, h are fractions of the picture, 0 to 1, from the top-left. The importer cuts each region out, paints the hole in fill, and the cut piece becomes the card to drag back.
These are checked on import, all of them before anything is written. A bundle either imports whole or not at all — there is no half-created hunt to clean up, and the importer reports every problem at once rather than one per attempt.
title must be present and non-empty.choices needs 2 to 6 entries, all plain strings. An object in that list is refused by name.correct_index must point at one of them, 0-based.pairs needs at least 2.Entry names inside the ZIP are never used as paths. Every file is stored under a name the server mints, so a folder structure, an absolute path or a ../../ traversal in the archive is harmless — and ignored.
Do these in order. Do not skip to generating.
images/.Ask one question at a time and wait. A gamemaster who is handed twelve questions at once answers three of them.
Produce a single self-contained HTML file that renders every question the way a player would see it: the stem, the choices, the pictures, the word banks. Dark background, one question per block, the correct answer marked in a collapsed "answer key" section at the end.
This is how the gamemaster checks your work without uploading anything. Typos, a wrong correct_index, a question pitched at the wrong level — all of it is visible here and expensive later.
If you cannot produce a file, print the whole hunt as readable text instead. Do not skip this step.
Tell them, in these words:
images.hunt.json beside that folder — not inside it.If the report lists problems, fix them and give them a new ZIP. Each message names the stage and question it came from.
choices as objects. Plain strings.
{
"format": "treasurehunt/hunt@1",
"title": "Units and Symbols",
"intro": "Three stops. Bring a pen.",
"stages": [
{
"name": "The Bench",
"location_hint": "Start at the soldering bench — look under the stool.",
"n_questions": 2,
"pass_threshold": 100,
"questions": [
{
"type": "mcq",
"stem": "What is the unit of resistance?",
"choices": ["Ohm", "Ampere", "Volt"],
"correct_index": 0
},
{
"type": "match",
"stem": "Match each quantity to its unit.",
"pairs": [
{ "label": "Current", "answer": "Ampere" },
{ "label": "Voltage", "answer": "Volt" }
],
"decoys": ["Watt"]
}
]
}
]
}
Everything above describes the file. If you are writing software that talks to the server directly instead, the per-endpoint reference lives in AUTHORING-API.md: authentication, hunt.create, stage.create, question.create, the media endpoint, and the rules an authoring tool has to respect.
The file route exists because it needs no credentials and no network access from the agent. Prefer it.