sports / scoreboard
sports.dancroak.com is a scoreboard and a news reader. I built it with Instinct, a personal AI assistant, using text messages.
I texted instructions from my phone. The assistant implemented the code and deployed the service.
What it shows
The front page lists games across the NFL, college football, MLB, the NHL, EPL, and Champions League. Each row shows the teams, the start time or score, and a win probability. The board hides preseason games in every league.
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 second page displays sports news. The app ingests RSS feeds into SQLite. The assistant writes article summaries every 30 minutes.
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.
The loop covers operations on the same terms. I chose the backup architecture over text: the VM holds the primary repository, and R2 holds the disaster recovery copies. The assistant compared the options and implemented the choice.
A phone interface prevents micromanagement. I cannot inspect code diffs easily on a phone. I inspect the live site and report defects. The assistant identifies the cause and fixes the bug. I review the visible outcome rather than the code change.
What autonomous operation needs
Autonomous operation requires four components:
- Network access. The assistant runs in an ephemeral sandbox. It stores a reusable Tailscale auth key in its vault. It joins my tailnet after each sandbox reset and connects to the VM by hostname.
- Host permissions. The assistant has passwordless
sudoon the VM. It installs packages and restarts services without my intervention. - Persistent state. The sandbox resets every 20 hours. Git on the VM preserves the application code, systemd units, Caddyfile, and crontab. R2 holds off-box copies of the code and the database.
- External revocation. I can revoke access by deleting the Tailscale key, revoking the R2 API token, or terminating the VM. The R2 token accepts requests from the VM addresses only. These controls do not require SSH access to the host.
Who decides the access design
The assistant reaches the VM over Tailscale and authenticates by tailnet identity. No private key sits outside the VM, and no SSH port faces the internet. 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
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 database options in sqlite.
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
}
A deploy flips the upstream between the two ports through Caddy's admin API, so Caddy never restarts.
cron runs the ingest command every 30 minutes. A trigger fires each backup:
the application backs up the database after a clean ingest or a saved
summary, and a git post-commit hook bundles the repositories after each
commit, so a commit reaches R2 in seconds. An hourly cron job runs a full
backup as the floor. 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, so the host is a machine that outlives the assistant that manages it.
The firewall permits ingress on ports 80 and 443 only. SSH access requires Tailscale.
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.
The VM holds the primary git repository, and no GitHub mirror shadows it. Each ingest, each summarization, and each git commit write a gzipped SQLite snapshot and a git bundle of each repository to Cloudflare R2. An hourly cron job stays as the floor. A small Go CLI signs the S3 requests, so the VM carries no AWS SDK. If the VM fails, I provision a new instance, fetch the most recent bundle and snapshot, clone the bundle, and restore the database.
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. journald holds the job output.
A deploy holds every connection. A blue/green pipeline builds the candidate, starts it on the idle port, checks its health, flips the proxy, and stops the old process. 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. The bar I want is for it to catch the failure I did not think to check for. An assistant that writes code provides value. An assistant that maintains production reliability provides more value.
Value
The site is fast and personal. It displays the data that I want without clutter. It costs me a text message and an inexpensive virtual machine.