Deploying the daily
One app on Fly.io, one machine, and a nightly job on slate:
app(mosaic-daily, dfw, shared-cpu-2x, 2 GB, the/volume): serves the site, judges (milliseconds), runs the daily loop. It generates nothing except a small emergency field when a day has no pushed field at rollover (data MOSAIC_GENERATE=1), which/announces. Measured on it: ring 1 of the daily took 40 min — far too slow for the nightly, fine for judging.- The nightly runs on slate (the user's Woodpecker CI,
.woodpecker/): cronnightly.yaml nightlyat 05:00 UTC (and the manual event) builds the engine withgolang:1.27and runsdeno task push-day --tomorrow --host https:/with the/ mosaic-daily.fly.dev --rings all --jobs $(nproc) --band 24 mosaic_push_tokensecret; caches live under/on the agent (repo is trusted for volumes). Woodpecker repo id 4, cron id 9. Measured: pipeline #5 (2026-08-27,var/ lib/ mosaic-nightly --jobs 16on slate's 16 cores) built the engine in 17 s and generated the whole G=1000 field (197 tiles + 12 rings, 22 MB) in 1098 s; the Fly performance-4x fallback took 3019 s for the same field. Jobs are nowcores/(5 on slate: a chunk uses 1.5-2.6 cores, more parallel chunks only oversubscribe); #6 with jobs 5 hit the agent's generator cache (0 s), so the first real comparison is the next cron run (2026-08-28 05:00 UTC, generating 2026-08-29) — read its3 generated inline in the push step's log./on the app reports whether tomorrow's field has arrived.status - If slate is down, push from any machine with the engine built:
MOSAIC_PUSH_TOKEN=... deno task push-day <date> --host https:/(a few minutes on the Mac). The paid alternative is a Fly scheduled machine with dedicated CPU running the same command from the app's image (/ mosaic-daily.fly.dev --rings all --jobs N --band 24 fly machine run registry.fly.io/; ~50 min and ~$0.15 per run, measured 2026-08-27).<image> -a mosaic-daily --region dfw --vm-size performance-4x --schedule daily --restart no -e MOSAIC_PUSH_TOKEN=... --shell --command "cd / app/ site && deno task push-day --tomorrow --host https:/ / mosaic-daily.fly.dev --rings all --jobs 4 --band 24"
Rollover uses the pushed field, else the emergency field. The token is
the MOSAIC_PUSH_TOKEN Fly secret; the same value is the Woodpecker
secret.
First time
fly launch --no-deploy --copy-config --name mosaic-daily --region dfw
fly volumes create mosaic_data --region dfw --size 1
fly deploy
Every time
fly deploy # the app machine; the nightly builds its own engine on slate
Configuration (fly.toml [env])
MOSAIC_DAILY=1runs the daily loop in-process;MOSAIC_DAILY_ROLLOVER_UTC(default 0) is the hour the day changes;MOSAIC_DAILY_GROW_MINUTES(60) the growth cadence;MOSAIC_DAILY_CHUNKS/CHUNK_G/G(3 / 60 / 1000) the emergency field's shape (the pushed field carries its own);MOSAIC_DAILY_NAMESPACEchanges every day's seed.MOSAIC_PUBLIC=1serves/for any Host;mcp MOSAIC_PUBLIC_URLis the origin agents are told to use.MOSAIC_DATA=/is the volume.data/
Checks
fly logsshows{"daily": "started day-YYYY-MM-DD ..."}on boot;curl https:/shows whether tomorrow's field has been pushed./ mosaic-daily.fly.dev/ status curl https:/returns both boards./ mosaic-daily.fly.dev/ leaderboard/ <date> - A restart (
fly machines restart) resumes today's game from its log at the same revision (proven 2026-08-27: revision 179 before, 179 after, play continued). - Live: https://mosaic-daily.fly.dev/ (dfw, shared-cpu-2x, 2 GB, 1 GB volume; about $11/month at Fly's list prices).
Playing against it
Open https:/ and press "copy prompt", or:
cd site && deno task solve -- --url https://mosaic-daily.fly.dev/mcp --join <game_id> --agent me --model my-model
(list_games on the MCP endpoint names today's game: day-<date>.)
Machine identity and login
Identity follows the tribes convention: ~/
{v:1, publicKey, privateKey (JWK Ed25519), createdAt, name?}, mode 600;
machine id = m- + sha256(raw public key)[:16]. POST / (sig over mosaic-key-auth:<host>:<ts>) mints a
msc_ bearer token (one per key; re-minting replaces it) and records the
key on first sight in data/; the token goes on MCP
requests as Authorization: Bearer. The spec agents read is /
(/ as text). deno task identity is one way to hold the file
(--name, --token <host>, --link <host>, --sign <game_id>).
Humans log in with shoo.dev (shoo.js, POST /
verified against shoo's JWKS for origin:<origin>, HMAC cookie signed
with the SESSION_SECRET Fly secret; GET /, POST /).
A machine is claimed by opening a single-use link (POST / with the bearer, 10 minutes) while logged in:
/ binds the machine to the account.