Theo Ferguson

Featured Projects

babylog

A baby tracker for the two people actually using it at 3am — feeds, nappies, pumping — built around the one part every other app gets wrong: a nursing timer that belongs to the household rather than to a phone. Expo and React Native shipped to iOS through TestFlight, the same codebase exported as the web app, Django REST Framework and SQLite on a Fly volume behind it. The running timer lives on the server as an in-progress event, so one parent can start it and the other can stop it, and clients send intents rather than computed totals because two phones each PATCHing their own view of the elapsed seconds would clobber each other. A feed lasts as long as the timer actually ran, not start to end, since a feed can be paused and picked up again — a rule that took three rounds to state as an invariant instead of once per reported symptom. Offline nursing runs the same accumulator arithmetic on the device and replays it as queued intents carrying their own timestamps. The newest layer is voice logging, and it is deliberately not an agent: one sentence, one model call constrained to a strict JSON schema, no tools and no loop. What makes it trustworthy is the half that already existed — the model's rows are validated server-side by the same per-type validator the CSV importer uses, then staged on a review screen where every row is editable and nothing is written until a person ticks it. The generation schema is an allowlist rather than a mirror of the payload fields, because a model must never be able to author server-owned timer state. The web app is the same codebase as the phone one, minus the microphone, though accounts are invite-only so the live link is a sign-in screen: https://babylog-app.fly.dev . Github: https://github.com/theoferguson/babylog

babylog

NYC Daily

A daily browser game in the Wordle family, for New York. Five locations are named — a landmark, a park, a deli — and the player finds each one on unlabelled satellite imagery, scored on how close the tap lands. One puzzle a day, the same five for everyone, shareable as a grid of squares. React 19 and TypeScript with MapLibre GL over New York City's own aerial surveys, Vercel serverless functions, and Postgres. Scoring is a stretched exponential tuned so that missing by a neighbourhood still earns something while tapping the middle of Manhattan earns nothing, which is the constraint the whole curve is squeezed against. The content pipeline is the harder half: fifty days of puzzles built from Wikipedia and geocoded against Nominatim, then served one day at a time behind a server-side date gate so tomorrow's answers cannot be read today. Telemetry is opt-in, and a second set of deliberately anonymous counters exists alongside it because a consent-gated table cannot distinguish nobody playing from nobody opting in. Github: https://github.com/theoferguson/nyc-map-game

NYC Daily

Integration Health Dashboard

A self-hosted, general-purpose observability dashboard for third-party integrations — a minimal, AI-assisted Sentry or Datadog that any application can report to. Applications send events with an API key, either over a plain POST /api/ingest endpoint or through the published @theof/ihd-sdk npm package; integration and event-type names are free-form, so nothing is hardcoded to a particular domain. The dashboard shows per-integration health at a glance — healthy, degraded, or down — with success rates, a recent-event stream, and trend sparklines drawn from reported metrics, plus a paginated All Events view that is filterable, searchable, sortable, and exportable to CSV, and Datadog-style saved queries rendered as time-series graphs with predicates over metrics, tags, payload fields, severity, and environment. Any failure can be classified on demand by AI (OpenAI gpt-4o-mini) into a category, severity, root cause, and suggested fix, which engineers then acknowledge, resolve, or reopen: detection stays deterministic, the AI only suggests, and a human decides. It is multi-tenant, with GitHub OAuth sign-in, org-scoped projects, and member and admin roles. A companion project, the Integrations Host App, exercises it end to end with live weather, NYT, and NYC campaign-finance integrations emitting real events. Built with React, Vite, TypeScript and Tailwind on the frontend, Node and Express on the backend, with SQLite on a Fly volume, and deployed on Fly.io. Github: https://github.com/theoferguson/integration-health-dashboard

Integration Health Dashboard

Integrations Host App

A dashboard of four live third-party API integrations — National Weather Service alerts, NYC Campaign Finance Board contribution filings, and NYT Top Stories and Bestsellers — each fetched, normalized, and stored on its own hourly, daily, or weekly schedule. It doubles as the real-world reporter for the Integration Health Dashboard: every scheduled refresh reports its own success or failure into IHD through the @theof/ihd-sdk package, the same way a customer application reports into Sentry or Datadog. Built with React, Vite, TypeScript and Tailwind on the frontend and Node and Express on the backend, with SQLite storage, an in-process node-cron scheduler that keeps the writer and reader on the same database instance, and GitHub OAuth admin auth gating manual refreshes and a raw pre-normalization payload viewer. The NYC campaign finance adapter streams the Campaign Finance Board's ~50 MB bulk CSVs rather than buffering them, discovers election cycles from the data library at runtime, and uses ETag/Last-Modified signatures to skip unchanged downloads — all necessary to fit inside a 256 MB Fly.io VM. Deployed on Fly.io. Github: https://github.com/theoferguson/integrations-host-app

Streamlist

Your Letterboxd watchlist, finally streamable. StreamList reads a Letterboxd account's public RSS watchlist feed, checks which of those films are actually streaming right now, and filters the result down to the services you already pay for — turning a long aspirational list into a short one you can press play on tonight. A separate recommendations tab uses Claude to suggest taste-matched films based on what is already on the watchlist. React 18 with Redux Toolkit and Vite on the frontend, Node and Express behind it, streaming availability from the Movie of the Night API, and an in-memory response cache to stay comfortably inside its free tier. The streaming provider is deliberately isolated behind a single route module, so swapping it for Watchmode or JustWatch touches one file and leaves the store and UI untouched. Deployed on Fly.io. Github: https://github.com/theoferguson/streamlist


More Projects

Portfolio Site

The site you are reading this on, built as its own project rather than from a template. A Django REST Framework backend exposes a paginated, public read-only API — writes require an admin session — and a Qwik and Tailwind frontend server-renders each page and fetches that API at request time. The practical consequence is that projects and blog posts are written through the Django admin and appear on the live site immediately, with no rebuild or redeploy in between. The contact form is hardened against the usual drive-by spam with Google reCAPTCHA verification, a honeypot field, and per-IP rate limiting, and every submission is stored with its referrer for later triage. The backend runs on Fly.io against Postgres and serves uploaded media; the frontend deploys separately to Cloudflare Pages behind theojohnferguson.com. Docker Compose brings the whole stack up locally in one command.

NYC Campaign Finance

A full-stack platform for analyzing roughly 400,000 NYC campaign finance donor records. The hard problem is identity, not scale: the same person appears across filings under dozens of spellings of their name, address, employer, and occupation, so nothing useful can be counted until those variants are collapsed. The pipeline runs entity resolution over the raw filings using fuzzy and phonetic matching to merge them into single donors, then works on the resolved graph — detecting donation communities with Louvain community detection over shared giving patterns, and flagging potentially coordinated activity through temporal burst detection, where a cluster of donors gives to the same recipient in an unusually tight window. The architecture is split by language strength: a React and TypeScript frontend, an Express and Knex API, BullMQ workers on Redis to run resolution jobs that take far too long for a request cycle, and a separate FastAPI service in Python carrying the matching and graph work with NetworkX, python-louvain, jellyfish, and thefuzz. PostgreSQL with the pg_trgm extension backs fuzzy search across the whole donor set.

Polybot

A Python trading bot for Polymarket prediction markets, with three modes: paper for validating a strategy against simulated fills, live for real trading through the Polymarket CLOB API, and copy for tracking chosen wallets and replicating their trades at proportional size. The core strategy hunts cheap tail bets priced between 2 and 12 percent, on the thesis that these markets systematically underprice genuine tail risk in political and geopolitical events, while filtering out sports markets, anything expiring within a day, and any book with a spread wider than 10 percent. The risk rules are deliberately hardcoded rather than tunable per trade — a fixed unit size, hard caps on total deployed capital, on any single position, and on aggregate exposure to correlated themes, plus a standing prohibition on averaging down — because the failure mode of a bot like this is not a bad model but a loosened limit. Exits ladder out in stages as a position moves toward multiples of its entry price, with a forced reduction before expiry.

Scabble App

A Scrabble-style dictionary for words that don't exist. Type in any word or phrase — especially complete nonsense — and it returns a straight-faced, entirely fabricated definition written in the register of a real dictionary entry. Underneath the joke it is an exercise in making an LLM feature cheap enough to leave running indefinitely: it uses gpt-4o-mini capped at 300 tokens per response, caches every definition for seven days so repeat lookups never reach the API, and rate-limits to 50 requests per day per IP, which holds the bill to a few cents a month at a thousand requests. Django 5.2 served by Gunicorn with WhiteNoise for static files, SQLite for usage tracking, and an admin dashboard for request analytics. Deployed on Fly.io. Github: https://github.com/theoferguson/scabble-app