Never Miss a Game

A site for every sport. None of them knew about my week. So I built a small agent that sends me one message. Here is how, for someone who has never built one.

This page is the short version of a 30-minute O'Reilly session, Zero to Agent in 30 Minutes. It skips the code and keeps the shape: what already existed, what had to be built, what one run looks like, and what the process taught. Everything on it is real: the data, the run, the message.

In one minute. Once a week, an agent reads the schedules for every sport I follow and sends my phone one message: the games worth my evening, each with a reason, and nothing when there is nothing. The schedule data and the small tool that reads it already existed. The one thing built for this is a written policy, a job description the agent follows, plus a short list of what it may run. One run takes about a minute. Four commands at the bottom of this page reproduce it on your machine with your teams, typed into one small file or ticked in a form.

The problem

I had built a website for each sport I follow: the World Cup first, then the WNBA, the NFL, the NBA, the Premier League, and more. Each one is good at its own game: every fixture in my time zone, on the channel I actually have. Then I had a new problem. I did not want that many tabs open. A hub that lists them all helped. What none of the sites could do was answer the only question I had on a Wednesday morning:

What, out of everything on this week, is worth my evening? And, when the honest answer is "nothing", to say so instead of padding.

Listing games is a script. Deciding which four matter is judgment. Judgment is what the agent is for.

What already existed

Nothing on this list was made for the agent. The build was the connections between them.

Data

Ten schedule sites

Each commits its full schedule to disk as a data file and refreshes nightly. No API to call, no key, no scraping.

Runtime

Claude Code CLI

One command reads a prompt, calls only the tools it is allowed to call, and exits. There is no server to write.

Clock

launchd

The Mac's own scheduler. One file, one integer for the weekday. If the Mac is asleep, it runs at the next wake.

Phone

ntfy.sh

One web request to a topic lands as a push notification. Opt-in through a single environment variable.

How it fits together

Two halves. The grey half is deterministic: every question with one right answer (parsing, time zones, "is this a real fixture") is answered by a small tool, the same way every week. The amber half is judgment: a written policy the agent follows. Keeping the halves apart is the whole design.

schedule repos committed to disk wnbanfl premier lgnba world cupeuros madness x2copa plus preferences.json: your teams one local tool read-schedules.mjs 0.3 s, nothing to install read-only the agent Claude Code, run unattended may run 2 scripts, read 1 file "if nothing's worth watching, say so, don't pad" THE PART THAT DECIDES written policy concierge.md, a job description launchd Wednesdays, 7:00 am one message notify.sh to phone + archived to a file

Grey is the deterministic half and already existed or is plain scripting. Amber is the only thing that had to be authored with care: a policy, written in prose, that says what counts, what to ignore, and what one message looks like.

What one run looks like

  1. The clock fires. Wednesday, 7:00. launchd starts a shell script; nobody is at the keyboard.
  2. The script starts the agent with the policy as its prompt and a short allowlist: it may run two scripts and read one file. Nothing else.
  3. One tool call. The agent runs the schedule reader. A third of a second later it has the week: every game, every caveat flagged, about 7 KB of text.
  4. Judgment. Sixty to ninety seconds of quiet. Which games involve teams I follow (all of them go in), which one or two others are genuinely worth it, and one "why I care" per line, nine words or fewer.
  5. One delivery call. The notifier prints the message, raises a banner, writes it to disk, and pushes it to the phone.
  6. The check. The script looks for the file on disk. If it is not there, the run fails, whatever the agent said. Verify the effect, not the report.
This week in sports
NFL Week 2 lands on top of a WNBA stretch where all three of your teams play three or four times, so Sunday runs from a 6 AM kickoff to the Fire's late-afternoon tip. Thu 9/17 Mercury at Fire, 7:00 PM, Fox 12: Fire's grit test, first of four this week. Fri 9/18 Liberty at Lynx, 4:30 PM, ION: JJ and Stewie versus Miles' offense, week's best. Sun 9/20 Sunderland at Man City, 6:00 AM: Haaland at home, worth the early alarm. Seahawks at Cardinals, 1:25 PM, FOX: division game between two of yours, neighbor asks. Mon 9/21 Giants at Rams, 5:15 PM, ESPN: Rams close out your division's Week 2. Tue 9/22 Lynx at Fever, 5:00 PM, ESPN: Miles' offense on national TV, midweek. Nothing this week from the Nuggets or Spurs: Spurs return Tuesday, Oct 20 at 6:30 PM local, Nuggets Thursday, Oct 22 at 6:30 PM local.

A real message from a real run on September 14, 2026, abridged to fit. The full one is in the repo.

The policy, in seven sections

The policy is a Markdown file, about 1,100 words, written like a job description for a person at a desk who already knows what I like. Its headings run in the order the agent has to decide, and that order is the first best practice: facts before judgment, judgment before output, and the security boundary last.

Job descriptionWho you are, how often you run, and your ONE output. Two paragraphs that set the voice.
Your sources, in orderExactly one tool call. Its output is the truth. Never open the raw files. If a source is missing, say so and move on.
What counts as worth watchingEvery game with a followed team goes in, no exceptions. Up to two extra picks, only with real stakes. Zero is a fine answer.
Sports that are not really thereThe traps in the data, named: bracket placeholders, finished tournaments with no scores, an offseason league, a badge that disagrees with the schedule.
The output contractOne message, one clock, one line per game with a reason, a length cap with a cut order, permission to say "nothing this week", and a snark dial from 1 to 5 that changes the tone and never the facts.
DeliveryOne call to the notifier, then repeat the message in the final reply, because a tool's output never reaches the log.
Tools you may useTwo scripts and Read. No writing, no editing, no git. If the job cannot be done, report that; never invent a schedule to cover it.

What the process taught

Every rule in the policy was paid for by something going wrong first. These are the ones worth carrying to your own agent.

Give the agent one door to the data

Read literally, the World Cup file is 104 unplayed matches, a third of them between teams that do not exist. A small deterministic tool absorbs that once. The agent reads a summary and decides.

Write the policy as prose, not settings

A config can say "max 20 lines". It cannot say "zero picks is better than two weak ones" or define real stakes. The reader reasons like a person; write to it like one.

Name the traps anyway

The tool already handles the placeholders. The policy names them too, with examples, so the rule survives a change to the tool.

Verify the effect, not the report

A run once exited cleanly saying "sent" and left no message anywhere, because a tool's output never reaches the log. Now the notifier writes the message to disk and the runner checks for the file.

A UTC timestamp names the wrong day

A 5:20 PM Phoenix kickoff is stamped 00:20Z the next day. An early run reported the wrong evening. The tool now hands over the local date as a sentence, and the policy forbids reading dates off raw timestamps.

An honest empty week is a good week

The title promises never missing a game you care about. That is exactly why the agent must be willing to send two sentences and stop. Padding is the worst thing this job can do.

Keep the allowlist exact and read-only

Two scripts by name, plus Read. Not "any Node command", which would allow arbitrary code. The schedule sites stay untouchable, which is what makes the job safe to schedule.

Two runs will differ, and that is fine

On identical data, one run took an editor's pick and one did not. Both are inside the contract. A run that differs because the prompt drifted is not, so the policy is frozen and tested against the real thing.

Try it yourself

You need a Mac or Linux machine with Node, git, and the Claude Code CLI signed in. There is nothing else to install; the schedule data is fetched from the public repos behind the sports sites. Four commands, and the fourth takes about 90 seconds.

# get the kit and the schedule data
git clone https://github.com/ismayc/never-miss-a-game.git
cd never-miss-a-game && ./clone-viewers.sh

# the data layer on its own, then the whole agent with the finished policy
node read-schedules.mjs --days 7 --prefs preferences.json --all
git checkout complete && ./run-concierge.sh

Then edit preferences.json: your teams, and a line on why you care about each. That file is the only thing the agent knows about you. If you would rather not edit JSON by hand, use the form: open your preferences.json in it, tick teams, and it writes the same file back (Chrome or Edge save it in place; any browser can download it). To get the message on your phone, install the free ntfy app, subscribe to a long random topic name, and export that name as NTFY_TOPIC before the run; the technical notes have the three steps and a one-line test.