01 — Selected work
Bolão Copa 2026
Live — archived after the final
A prediction league for the 2026 World Cup. Friends and family called the
score of all 104 matches; a scoring engine settled the arguments.
Built as a single Cloudflare Worker: the React SPA ships as static assets,
a Hono API handles auth, predictions and rankings, D1 holds the state and
KV absorbs the traffic. During the tournament, scheduled jobs synced live
results and news — retired now that the cup is over, the site stays up as
a freeze-frame of the final table.
System — one worker serves the SPA, the API and the sync pipelines
104 matches scored
1 worker · 0 servers
React 18 · Hono · TypeScript
D1 + KV
$0/month infra
Murdoku
Live — new cases published regularly
A competitive murder-mystery logic game, adapted from the puzzle book
genre: read the clues, place every suspect on the floor plan, accuse the
killer — faster than everyone else.
The heart of it is a constraint solver that proves every published case
has exactly one solution before players ever see it. An admin builder
composes rooms, furniture and structured clues that render as natural
Portuguese sentences; players race a server-side clock with a single
accusation each, solo or in teams, over an F1-style season leaderboard.
Same calm architecture as the bolão: one Cloudflare Worker, D1, KV.
System — the solver gates publishing; the API guards the clock and the secret
13 clue types
1 accusation per case
React 18 · Hono · TypeScript
D1 + KV
$0/month infra
Totoliga
Live — season 2026/27
A Totobola-style prediction game for the Portuguese league. Call 1X2 on
every matchday; correct calls earn points and half a star — stars buy
doubles and triples, just like the real coupon.
Matchdays in the Liga sprawl unpredictably across Friday-to-Monday, so
there is no single deadline: every match locks server-side at its own
kickoff. Hourly jobs rebuild the calendar from football-data.org —
new rounds appear on their own, rescheduled games move themselves —
and a 15-minute results pass only spends API calls when matches are
actually hot. Same calm architecture: one Cloudflare Worker, D1, and
an idempotent scoring engine an admin can always override by hand.
System — every match locks at its own kickoff; the sync loop keeps the calendar honest
34 matchdays · 1X2
3 pts + ½★ per hit
doubles 1★ · triples 2★
React 18 · Hono · D1
$0/month infra
Kabum
Live — open a room, bring three friends
A four-player arena game in the spirit of the bomb-laying classics:
blast crates, grab power-ups, trap your friends. Room codes, online
multiplayer, bots that hold their own.
The whole simulation is one deterministic Rust crate — integer-only
math, seeded RNG — compiled to WebAssembly and run twice. In the
browser it powers offline practice; on Cloudflare it lives inside a
Durable Object, one per room, ticking 20 times a second as the sole
authority on every match: the sim itself validates each move, so a
modified client can't cheat physics. State streams to players as
compact binary snapshots over WebSockets; the canvas just
interpolates and draws the neon.
System — one Rust sim, two runtimes: practice in the browser, authority in a Durable Object
2–4 players · room codes
Rust → wasm · one sim, two runtimes
20 ticks/s server authority
1 Durable Object per match
$0/month infra
Letra a Letra
Live — one word, ten gallows
Hangman for a crowd: rooms of up to ten, one secret word, and a
personal gallows per guesser. Wrong letters are yours alone — the
word belongs to everyone.
After Kabum's Rust-and-wasm adventure, this one is deliberately boring
tech doing a social job. The entire rulebook — turns, per-player
gallows, the word-setter role the room's owner can assign and the
setter can pass on, hint reveals, full-word risks, sit-outs, the
persistent room scoreboard — is a single pure TypeScript state
machine, shared verbatim by client and server and pinned down by
thirty unit tests. A Durable Object per room wraps it with WebSockets
and a 45-second turn clock, broadcasting personalized views: only the
word's creator ever sees the secret, and accented letters match their
plain keys, so CORAÇÃO yields to a C.
System — one pure state machine, wrapped by a Durable Object per room, one personalized view per player
2–10 players · room codes
a gallows each · one shared word
45 s turns · hints · word risks
1 Durable Object per room
$0/month infra
Striker
Live — Division Four is waiting
Striker (Cult Games, 1989) gave the ZX Spectrum a whole football
career — Division Four at 18, retirement at 34 — and resolved every
chance with one sweeping bar and the S key. This rebuild keeps the
1989 career and gives it a stage.
It started faithful — green paper, bitmap ZX font, wireframe
goalmouth — and one playtest later the verdict came back: keep the
career, lose the skin. The shipped game is a floodlit rebuild in
this site's own machine-room language: dark DOM menus carrying the
original's key letters, and a canvas match stage with the camera
behind the striker — perspective pitch, a keeper who sets and
dives, a net that ripples when you score. Every chance — one-on-one,
header, volley, penalty, spilled rebound — is a two-phase skill
shot: a reticle you steer along the goal frame, then a power bar
with a sweet zone, judged by a keeper that reads centrality and
power quality and dives the right way only if he reads it. Around
the goalmouth sits the full career: thirty-week seasons with cup
rounds and internationals threaded between league weeks, a weekly
three-chance shooting drill that sharpens whatever regime you
picked, summer transfers to Italy, France, West Germany or Spain,
promotion and relegation, honours. Still no build step and no
server — static files on a Worker, fonts self-hosted, three save
slots plus autosave in localStorage, and saves from the first build
load unchanged. Before each ship, a headless harness played a full
seventeen-season career to retirement.
System — one loop, all in the browser: the match feed offers a chance, two phases of input, one keeper model, and every outcome writes the career
ZX Spectrum source · 1989
age 18 → 34 · 17 seasons
5 chance types · 1 keeper model
no build step · assets-only worker
$0/month infra
King
Live — four seats, ten hands
King is the card game every family plays slightly differently: ten
hands, six that punish you for winning tricks and four that pay you
for it, and a scoresheet nobody ever explains to the newcomer at the
table. This build deals the rules alongside the cards.
A table needs four, and the rules take a night to learn — so the room
fills itself. Any empty seat takes a bot at one of three levels: naive,
heuristic, or one that counts what has already fallen. The only hard
rule is that at least one human sits down. The teaching layer isn't a
tutorial you skip on the way in: every hand opens with its own brief —
what it costs, what to avoid, one piece of advice — the score table is
live and opens row by row to show the arithmetic (units × value, and
the exact cards that scored), and between hands a recap replays what
the trick pile actually cost you. Six negative hands drain to −160 on
the King of Hearts alone; four positive hands pay +25 a trick with the
dealer naming trumps; the whole match sums to exactly zero, which is
the fact that finally makes the scoresheet legible. Underneath is the
same spine as the other room games — a pure state machine with zero
I/O, one Durable Object per room, the room evaporating when the last
player leaves. The constraint worth the trouble: the engine has no
words. It emits codes, never sentences, so all six languages live in
the client and four players can read the same table in four different
languages. The 45-second clock runs only while a connected human holds
the turn — a disconnect pauses it, a rejoin restarts it — bots think
for two thirds of a second so a human eye can follow the table, and if
someone walks out mid-match a medium bot takes their seat so the other
three still get their ten hands.
System — one room, one engine that has no words: every seat gets a code-only view, and each client speaks it in its own language
10 hands · 6 negative + 4 positive
the match sums to zero
4 seats · bots at 3 levels
6 languages · engine speaks codes
$0/month infra
Lume
Live — three turns a day, then the light costs a coin
Interactive gamebooks in the 1980s solo-adventure tradition: read a
passage, choose, roll two dice against SKILL or LUCK, fight in rounds,
carry a poison until you find the cure. Lume is the first of these
machines built to be paid for — and the money is why the architecture
looks the way it does.
A story is pure data: a manifest, an item and status dictionary, and a
graph of passages with choices, requirements, effects and encounters.
It lives in the worker and never reaches the browser whole — a passage
is revealed only when a turn is spent on it, and the response carries
just that page, the visible choices, and the dice log. The engine is a
pure state machine over that data: no I/O, replayable from a seed,
exercised by unit tests and by a Monte Carlo reader that walks each
story hundreds of times before deploy to prove every ending is
reachable and that death is neither certain nor impossible. The dice
are xorshift over a seed that advances with every roll and is stored
with the run, so nobody — not the client, not the author — can nudge
them. The economy is one place where correctness is money: three free
turns refill at midnight and bank up to nine, paid turns never expire,
and a subscription simply makes every turn cost zero. Spending a turn
and advancing the story are one D1 batch with cross-conditional
updates — the run advances only if the wallet can pay, the wallet is
charged only if the run advanced, the log is appended only if both
happened — so a double-click or a second tab can never burn a turn
without a page, or reveal a page without a turn. Stripe is spoken
without an SDK: hosted Checkout with inline prices, a WebCrypto check
of the webhook signature, and an event table that makes every delivery
idempotent. The authoring studio the original blueprint imagined was
deferred on purpose: git is the studio, the lint is the reviewer, and
the simulator is the playtester.
System — a story the browser never sees whole, a pure engine, and one atomic batch where the page and the coin change hands together
2 original stories · 70 passages · 10 endings
pure engine · seeded dice · Monte Carlo gate
turn + page + log in one atomic batch
Stripe packs + Unlimited · signed webhooks
$0/month infra
02 — Next
006
Shipped as Terrace Manager — live now. Case study to follow.
007
Shipped as Vrum — live now. Case study to follow.
008
Shipped as Quem é Quem? — live now. Case study to follow.