Decrypt the Narrative

A hidden narrative has been shattered into single words. Collect the fragments from the API and put the sentence back together. The catch is that the API misbehaves on purpose.

This is a chaos-engineering challenge, originally built for a university hackathon and now free for anyone to play or host. It takes a few minutes to understand and a few hours to beat, and the difficulty is not in the puzzle — it is in the transport.

The real question is not can you call an HTTP endpoint. It is: can you write a client that keeps working when the server is actively lying to you?

The loop

Three endpoints. Register, collect, submit.

# 1. Register your team and get a token
curl -X POST https://dcrypt.run/auth -H "team: your-team"
# {"token":"3f2b9c14-...","team":"your-team","remaining":20}

# 2. Collect one word at a time. This is the part that fights back.
curl https://dcrypt.run/fragment \
  -H "team: your-team" -H "token: 3f2b9c14-..."
# {"word":"lorem","position":4}

# 3. Submit once you have every word
curl -X POST https://dcrypt.run/validate \
  -H "team: your-team" -H "token: 3f2b9c14-..." \
  -H "Content-Type: application/json" \
  -d '{"submission":"lorem ipsum dolor sit amet ..."}'

Every fragment carries its position, so reassembly is a sorting problem rather than a guessing game. No signup, no API keys, no rate-limit paperwork — pick a team name and start.

The API lies to you

Roughly 55% of fragment requests misbehave, across twelve failure modes. All of them are things that happen to real distributed systems, just compressed into a single afternoon.

EventWhat your client seesWord usable?
delayThe real fragment, 0.5–5s lateYes
slow_burstThe real fragment, after several stacked pausesYes
token_drainThe real fragment, and 1–3 extra quota silently goneYes
out_of_orderA genuine fragment, but always from the tail of the textYes
duplicate_fragment{"fragments":[x,x]} — a different shape entirelyYes
malformed_json200 OK, labelled JSON, truncated bodyNo
broken_json200 OK, a body that was never JSONNo
html_injection200 OK, an HTML error page labelled as JSONNo
empty_response200 OK with {}No
error_code418, 429, 500 or 504No
reverse_textA fragment whose word is reversedCorrupt
unicode_garbleA fragment whose word has junk appendedCorrupt

Delays scale with how long your team has been running, up to double, so the system gets worse under sustained load. Which is, of course, the point.

What each one is really teaching you

reverse_text and unicode_garble corrupt a word without telling you. There is no flag, no error, no clue in the response. The only defence is to sample each position several times and take the value you saw most often — so a strong client has to reason about confidence, not just parsing.

The quota economy

A token is worth 20 requests, and every call spends one whether or not it returns anything useful. A 500, an empty body and a clean word all cost exactly the same. This is deliberate: it means "retry until it works" runs out of road, and you have to think about what a request is worth.

When your quota runs out, call /auth again for a fresh token. Your progress is preserved across re-authentication, and the scoreboard counts how many times you have had to do it.

What counts as progress

Your score is distinct words received intact — not requests made. The server tracks this itself, so it cannot be talked into accepting a guess. You will not be allowed to submit until every word has genuinely reached you, and until then /validate tells you exactly where you are:

{"detail":"You've seen 41 of 50 words. Keep exploring fragments before submitting."}

Corrupted deliveries do not count. Final comparison ignores case, punctuation and extra whitespace, so you are marked on recovering the words and their order — not on reproducing the punctuation.

For calibration

Against the live narrative, a working client needed 880 requests and 46 re-authentications. It reached full word coverage at request 480 and still submitted the wrong answer twice, because its majority vote was losing to a reversed word. Budget accordingly.

The scoreboard

Every team appears on the live scoreboard as soon as it authenticates: words recovered, quota remaining, chaos absorbed, time elapsed, and whether it is actively decoding, idle, or finished. It refreshes every second and a half and is built to be projected on a wall.

Run your own

The whole thing is open source under MIT — one Python module and a self-contained HTML scoreboard, with no build step.

The narrative is supplied through a NARRATIVE_TEXT environment variable, which means the answer is not in the repository. You can read every line of the source without spoiling the hosted puzzle, and set your own text for your own event. Chaos rate, quota size and timeouts are all environment variables too, and the chaos rate can be turned down mid-event if the room gets stuck.

export NARRATIVE_TEXT="Whatever sentence your teams should reassemble."
uvicorn decrypt_api:app

If you run a hackathon, a graduate onboarding week, or a resilience workshop, help yourself.

← Back to the live scoreboard API documentation Source on GitHub