Automation· NYRR 9+1 Watcher · GitHub

Marathon Race & Volunteer Alerts

The Problem

To qualify for the TCS NY Marathon through NYRR’s 9+1 program, you need nine qualifying races plus one volunteer shift. Open spots are scarce and fill within hours, and the listing page itself is unreliable: its “available only” filter often shows slots that are already gone or gated behind a Leadership/Medical modal. Refreshing by hand doesn’t scale, and trusting the filter produces false alarms.

What I Did

I built a watcher that renders the listing page, follows every event link, and re-checks true status on each event’s detail page. Because those pages are client-rendered and hydrate a few seconds after load, a plain HTTP request returns no assignment data at all. Getting an accurate read meant rendering headlessly with Playwright and waiting out that hydration window rather than trusting the raw response.

The harder problem was precision, not plumbing. NEAR CAPACITY spots looked open but often opened a Leadership/Medical-only modal on click, so I excluded them entirely and alert only on clean AVAILABLE status. Every qualifying rule (required tag, excluded roles, gated assignments, races the user is already running) lives in a config file rather than code, so the tool stays generic instead of hardcoded to my own race list. A dedupe store means a spot alerts once when it flips open, not on every poll.

Alert-only by design: it finds a qualifying spot and Discord-pings a direct registration link. No login, no credentials, no auto-booking. You still click register yourself. Deployed on a schedule via systemd, with a GitHub Actions cron path as a fallback.

NYRR Watcher Discord alert: a 9+1 spot opening for the TCS New York City Marathon Start, with event, assignment, status, and tag fields
Live Discord alert, fired the moment a qualifying 9+1 spot flips to AVAILABLE
Design Decisions
Alert-only, no booking
No login, no credentials, no action on your behalf. Safe to run and safe to share.
NEAR CAPACITY ≠ bookable
An early version alerted on it and produced false alarms from the Leadership/Medical modal, so it’s excluded entirely.
Rules live in config, not code
Excluded races and qualifying logic are data anyone can point at their own situation, not hardcoded to mine.
Stack
Playwright
Headless Chromium rendering for the client-hydrated NYRR/haku event pages, the only reliable way to read true assignment status.
Python (pure parser + matcher)
Rendered text parsed into structured assignment records, then filtered against qualifying rules; both are pure functions, fully unit-tested against fixtures.
YAML config
Qualifying rules, excluded races, and polling settings as data rather than code.
Discord webhooks
Rich embed alerts carrying the event, assignment, status, and a direct registration link.
JSON Lines log
Append-only record of every detection, independent of the dedupe state.
systemd timer / GitHub Actions
Two interchangeable deployment paths for a single-shot check-and-exit script.
← All work