apps / sports
sports.dancroak.com is a scoreboard and a news reader. I built it with Instinct (invite code), a personal AI assistant, using text messages.
What it shows
The front page lists games across the NFL, college football, MLB, the NHL, the EPL, the Champions League, the Nations League, and golf. Each row shows the teams, the start time or score, and a win probability.
The win probability is the Kalshi market price. I do not run a prediction model. The market price updates when new information arrives.
A green dot marks a live game. A gray dot and "Final" mark a completed game. The whole row links to the game page for a large tap target.
A game page carries a probability chart, a written summary, and the play timeline. A live summary changes when the play feed shows a key event, such as a goal, a card, or a penalty. It never changes on a timer. A final game gets a short recap.
A final game also shows an official highlight film when the film plays in the US. The channel feeds carry no region data. The server reads the watch page of each film and skips a blocked film.
Team, league, and venue pages carry a short written summary. A league page also lists the games and the standings.

A second page displays sports news. The app ingests RSS feeds into SQLite.
Text messages as the interface
I directed the build over iMessage while the assistant typed. I sent screenshots when a layout looked incorrect. The assistant modified the templates and deployed updates.
I made the product decisions, such as removing clutter and setting tap targets. The assistant made the implementation choices.
I ran operations the same way. I chose the backup architecture over text, and the assistant compared the options and implemented the choice.
A phone prevents micromanagement. I cannot read a code diff on it. I review the visible outcome, not the code.
A second agent writes the content
The assistant that builds the app does not write the summaries. A second agent, which I call the content agent, has that standing job.
The split follows the work. The builder changes code, deploys, and keeps the host healthy. The content agent changes words and sources only.
On a schedule, the content agent scrapes sources that have no feed or that need a real browser. It stages each article as a JSON file that the ingest job reads. It watches the needs lists.
A needs list names each game, team, league, or venue whose source articles changed since the last summary. The app computes the list. The agent writes to it.
The app also decides which articles count. A gate drops women's coverage, pieces about another fixture, and pieces that name neither team. When no article fits, the agent saves an empty summary and the page shows none. I prefer a page with nothing to a page with the wrong text.
The agent saves each summary through the same Go binary on the VM, with one command for each page type: game, team, league, and venue. A venue page carries a short note about the building and its setting, not a recap.
What autonomous operation needs
- Network access. The assistant runs in an ephemeral sandbox. It connects to the VM over SSH. My vault holds the ed25519 private key. After each sandbox reset, the assistant reads the key from the vault and reconnects. I take no action.
- Host permissions. The assistant has passwordless
sudoon the VM. It installs packages and restarts services without my intervention. - Persistent state. The sandbox resets about once a day. Git on the VM preserves the application code, systemd units, Caddyfile, and crontab.
- External revocation. I can revoke access by closing SSH in the cloud firewall, revoking the R2 API token, or terminating the VM. The R2 token accepts requests from the VM addresses only.
Who decides the access design
Each control that removes the assistant lives in a console I hold, not in a file on the host.
An assistant optimizes for the goal I state, and I stated access without me. Containment is the second goal, and it holds only if I state it too. I own the access design for that reason.
The stack
Code runs in three places. The VM serves the site and holds the data. The sandbox of the assistant drives the work and keeps nothing. External services supply the scores, the market lines, and the news.
A scheduler wakes the sandbox on a timer. The scrapers in the sandbox drive a remote Chrome session. A source that needs a real browser also needs cookies, and the vault holds those cookies. The summary worker calls the Go binary on the VM over SSH, and the binary saves the summary. The sandbox never touches the database file.

The application uses one Go binary and hml templates. SQLite stores the articles and team metadata. The application opens SQLite in WAL mode on a single connection:
db, err := sql.Open("sqlite", dbPath+"?_busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL&_foreign_keys=ON")
db.SetMaxOpenConns(1)
The web server and the ingest process write to the same database file. A
single connection prevents internal locks, and _busy_timeout handles write
contention. I explain these options in my sqlite notes.
systemd runs the application as a template unit on two ports, sports@8081
and sports@8082, each with Restart=always. One port serves at a time.
Caddy terminates TLS and proxies traffic to the port that serves:
sports.dancroak.com {
reverse_proxy 127.0.0.1:8081
}
cron runs the ingest command on a timer. Each job pipes its output through
systemd-cat into journald, so the VM keeps no log files.
The VM
The service runs on an Ubuntu 24.04 VM on Ubicloud for $14 per month. A sandbox that resets cannot host a service that must stay up.
The firewall permits ingress on ports 22, 80, and 443. sshd accepts public keys only. It rejects passwords and root logins.
The application uses the pure-Go modernc.org/sqlite driver, so the VM compiles the binary without a C compiler. The driver includes FTS5, which the news search reads.
Sockeye, my code host, holds the source of truth and runs the
checks. A merge there does not deploy. The assistant bundles main and runs
the ship script on the VM, so the VM checkout always equals production.
The VM holds the primary repository, and R2 holds the disaster recovery copies. The application writes gzipped SQLite snapshots on the VM. An hourly cron job and each deploy push the changed snapshots and repository bundles to Cloudflare R2. Tiered prefixes and bucket lifecycle rules expire the old copies, so off-box storage holds steady at about 3 GB. A small Go CLI signs the S3 requests, so the VM needs no AWS SDK.
If the VM fails, I provision a new instance and fetch the most recent bundle and snapshot. Then I clone the bundle and restore the database. A drill in September 2026 restored the service on a fresh Ubuntu 24.04 VM from those copies.
Operations
The board footer reports feed freshness and backup health, so a failed ingest reads as a failure rather than as a quiet news day.
A deploy holds every connection. A blue/green pipeline builds the candidate, starts it on the idle port, checks its health, flips the proxy through Caddy's admin API, and stops the old process. Caddy never restarts. A failed health check leaves the old binary serving. A second Go CLI on the VM runs the pipeline from a TOML config.
The assistant runs the scheduled health checks. I want it to catch the failure I did not think to check for.
- A watchdog script on the VM checks the serving port every minute. Eight failed checks in a row restart the application. Twelve flip the proxy to the other port. Four interventions in fifteen minutes trip a circuit breaker, and the watchdog waits for me.
- A probe on the VM requests the public site every minute and records the result in an uptime database. The database syncs to R2 daily. I read availability as a measurement.
- A rehearsal on the VM restores the newest database snapshot and git bundle from R2 each Wednesday.
- A report reaches my inbox each Sunday: availability, outage minutes, latency percentiles, watchdog actions, and the rehearsal result. The week of September 13 measured 99.70 percent availability across about 1,000 checks, with three minutes of outage and 104 milliseconds at the ninety-fifth percentile.
Value
The site shows the data I want, with no clutter, for the price of a text message and an inexpensive virtual machine.