Skip to content

A headless watcher that monitors a loyalty-program account, detects any change to its activity history (including edits and deletions of old rows), and alerts me on Telegram. It only reads my own account. About 900 lines of Python with 60 unit tests, built in three days.

Architecture

fetch, diff, alert, then persist. Each run captures the account’s data, diffs it against stored state, sends one message summarizing changes, and only then saves the new state.

  • Capture. A logged-in headless Chromium session listens to the page’s own network responses and takes the activity data from its JSON API instead of parsing a virtualized DOM. A “warm up” request through the homepage satisfies the site’s edge bot protection before deep links.
  • Identity. Each row is hashed over (date, description, amount), not its raw columns, so the same transaction hashes identically whether it came from the CSV download or the fallback table scrape.
  • Diff. A multiset diff using Counters of row hashes handles legitimately repeated identical rows. A removed row and an added row sharing an identity key are reported as one modified row. Rows that merely scroll out of the roughly four-month visible window are suppressed (older than a 110-day cutoff and older than the oldest visible row).
  • State. SQLite: a single-row snapshot (CHECK (id = 1)), a permanent archive keyed by row hash with first and last seen, a run log, and a flags table.

Interesting problems

MFA on a headless server. The server can’t complete an interactive login, so authentication happens on a laptop in a real browser, and Playwright’s storage_state (cookies and localStorage) is copied up. Frequent runs keep the session alive, and when it does expire the watcher sends a single alert and a “recovered” message later.

At-least-once alerting. If sending the alert fails, state is deliberately not saved, so the next run re-diffs against the old state and retries. Failure alerts are rate-limited by persisted flags (one after 3 consecutive failures).

Operations. A dry-run mode, user-level cron (no root), secrets in a mode-600 file, a deploy script that never overwrites session or state files, and tests for the parser, diff, database, message formatting and Telegram client.