Antonio Queiroz — AI Solutions Architect · Lisbon

Complex systems that just work.

I design and ship AI-powered products end to end — model, data, infrastructure and interface — with a bias for boring technology, measured costs, and details people never have to think about.

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

BROWSER React 18 SPA predictions · rankings CLOUDFLARE WORKER STATIC ASSETS · SPA HONO API — /api/* sessions · scoring · rate limits 104 matches settled D1 — SQLITE users · matches · predictions KV cache · rate limiting SCHEDULED SYNC retired — july 2026 EXTERNAL FEEDS football-data.org · news RSS 01 app shell 02 /api json 03 sql 04 cache
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

BROWSER solving grid · builder one accusation, ever CLOUDFLARE WORKER STATIC ASSETS · SPA HONO API — /api/* server-side clock · one-shot accuse the killer never leaves the server D1 — SQLITE puzzles · plays · teams KV rate limiting CONSTRAINT SOLVER unique-solution proof at publish 01 app shell 02 gameplay 03 sql 04 limits 05 publish only if exactly one solution
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

BROWSER 1X2 coupon · standings doubles/triples · stars CLOUDFLARE WORKER STATIC ASSETS · SPA HONO API — /api/* per-match kickoff locks star ledger · idempotent scoring D1 — SQLITE players · rounds · matches predictions · stars no KV — friends-scale on purpose SCHEDULED SYNC hourly calendar · 15-min results FOOTBALL-DATA.ORG Primeira Liga feed — free tier 01 app shell 02 1X2 picks 03 sql 04 rebuild calendar · settle rounds 05 only when hot
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

04 input — [0x01, bits], only on change BROWSER canvas 2d · 60 fps lerp between snapshots input bitmask · bomb latch PRACTICE — offline local wasm sim vs bots CLOUDFLARE WORKER STATIC ASSETS · CLIENT MATCHMAKING — /api/* room code = durable object id DURABLE OBJECT KabumMatch — one per room authoritative wasm · 20 Hz validates every input · bots lobby · rematch · reconnection in-memory only — empty rooms simply evaporate KABUM-CORE — RUST → WASM deterministic · fixed-point · seeded RNG pure C ABI, no bindgen — 39 KB 01 app + wasm 02 room code 03 ws upgrade 05 snapshot — [0x02, ~500 B] · 20 Hz + lobby json one binary — practice mode and the server share the same physics
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

04 actions — letter · word risk · hint · roles, json BROWSERS ×10 vanilla ts · no framework svg gallows · touch keys reconnect by player id one view per player — the setter sees the word CLOUDFLARE WORKER STATIC ASSETS · CLIENT ROOMS — /api/* room code = durable object id DURABLE OBJECT LetraRoom — one per room turn machine · 45 s clock roles: owner · word-setter a gallows each · scoreboard in-memory only — empty rooms simply evaporate SHARED CORE — PURE TS the whole rulebook · 30 unit tests accent-aware — Ç falls to C 01 app shell 02 room code 03 ws upgrade 05 state — personalized json, per player, on every move one module — the client and the room authority share the same rules, typed end to end
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

THE CHANCE one-on-one · header · volley penalty · spilled rebound minute-by-minute match text floodlit canvas stage — camera behind the striker PLAYER INPUT 01 ANGLE — SWEEPING DIAL 02 POWER — SWEET ZONE volley runs hot · header capped KEEPER MODEL reads centrality + power quality read → dives to the ball beaten → dives the wrong way a save can spill a rebound penalties skip the power bar — placement only CAREER ENGINE — PURE JS 30-week season · cup · internationals transfers · honours · 3 saves + auto 01 the chance 02 the strike 03 outcome — goal · save · rebound 04 next fixture no build step, no server — static files on a worker, a whole career in localStorage
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

FOUR SEATS humans + bots · min 1 human spectators watch, never deal 45 s clock — humans only walk out mid-match — a medium bot takes the seat DURABLE OBJECT — ONE PER ROOM KingRoom sockets · turn clock · bot scheduler RoomGame — PURE 10 hands · 6 negative + 4 positive zero I/O · 40 tests gate deploys the match sums to zero CODE-ONLY VIEW one per seat — your hand only events as codes, not sentences no text ever leaves the engine so the room can be read in four languages at once CLIENT — 6 LANGUAGES PT · EN · DE · FR · ES · EL hand brief · live score table · recap 01 a card, by code 02 a view per seat 03 rendered in your language 04 the table explains itself one engine, no words — the rules arrive with the cards, in your 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

READER one passage at a time visible choices only dice log · character sheet 402 → the wall, with a countdown WORKER — HONO step(pack, run, action) pure engine · seeded 2d6 · no I/O ONE D1 BATCH run advances iff wallet can pay wallet charged iff run advanced log appended iff both all or nothing STORY PACKS pure data · bundled server-side lint + Monte Carlo gate deploys every ending proven reachable STRIPE hosted Checkout · inline prices webhook signed · WebCrypto event id table → idempotent WALLET — D1 3 free a day · bank up to 9 paid never expire · unlimited = cost 0 01 a choice id 02 the graph 03 credited 04 charged in the batch 05 only the revealed page the page and the coin change hands in the same transaction — never one without the other
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.

011

In research.

03 — About

Good architecture is invisible — it's the part nobody has to think about.

I work across the stack: LLM systems and agents, APIs, data models and serverless infrastructure. I favor small, legible designs over fashionable complexity — pick pieces that won't wake anyone at 3 a.m., measure what they cost, and make the whole thing feel effortless. Currently exploring what production-grade AI products look like when they're built calm.

04 — Contact

Let's build something that just works.