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.
| Event | What your client sees | Word usable? |
|---|---|---|
delay | The real fragment, 0.5–5s late | Yes |
slow_burst | The real fragment, after several stacked pauses | Yes |
token_drain | The real fragment, and 1–3 extra quota silently gone | Yes |
out_of_order | A genuine fragment, but always from the tail of the text | Yes |
duplicate_fragment | {"fragments":[x,x]} — a different shape entirely | Yes |
malformed_json | 200 OK, labelled JSON, truncated body | No |
broken_json | 200 OK, a body that was never JSON | No |
html_injection | 200 OK, an HTML error page labelled as JSON | No |
empty_response | 200 OK with {} | No |
error_code | 418, 429, 500 or 504 | No |
reverse_text | A fragment whose word is reversed | Corrupt |
unicode_garble | A fragment whose word has junk appended | Corrupt |
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
- Do not trust the status code.
- Do not trust the
Content-Type. - Do not assume the response shape is stable.
- Do not assume a successful HTTP call means you received data.
- And the hardest one: do not assume data that parsed is correct.
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.