An autonomous pipeline that reacts to a T-Mobile billing SMS, scrapes the bill PDF, parses per-line charges, emails a styled summary, and pays the shared pool over Bank of America Zelle — all gated behind hard safety locks and idempotent state.
Every run is the same idempotent march through 8 stages, all driven by app.py::_run_pipeline().
State at ~/.tmo_state/bill_<YYYY-MM>.json decides what's already done. Press play and watch the flow.
app.py is the only entrypoint that touches every subsystem. Each spoke owns exactly one concern — and the two browser-driving spokes are deliberately kept in separate sessions.
Owns the PDF parser, the Zelle safety gate, and the per-stage idempotency logic. Spawns zelle_pay.py as a subprocess so the Bank of America browser session is fully isolated from the T-Mobile one. Any uncaught failure routes to a failure-alert email and a non-zero exit.
Credentials, session cookies, and transaction state never leave the Mac. Three external services are touched; everything else — Keychain, state, browser profile, the SMS database — is local and locked down.
Money movement is fenced behind a default-dry-run flag, an amount cap, an exact recipient match, and a hard idempotency lock that even --force cannot bypass.
zelle_confirmed_at in state is permanent. Once a payment confirms, the pipeline refuses to send again — --force won't override it. To truly reset you must rm the state file.
ZELLE_LIVE_SEND=0 stops at BoA's Review screen and saves zelle_review_dryrun.png. Only =1 ever clicks Pay. Dry-runs never write an attempted flag.
Amount is checked against ZELLE_AMOUNT_CAP (default $300). Recipient is matched by exact accessible name on the Pay button — never a fuzzy "Edit/Delete <name>" sibling.
State writes via tempfile + os.replace() so a crash mid-write never corrupts. Directory 0700, file 0600 — it holds confirmation IDs and amounts.
Secrets resolve from env vars, then macOS Keychain. No credential is hardcoded; phone last-4 for MFA selection is pulled from Keychain too. OTPs are read but never logged.
zelle_pay.py runs as a separate subprocess, so the BoA browser context never shares memory or cookies with the live T-Mobile session in the parent.
A macOS LaunchAgent fires auto_process.sh at 9 AM on days 6–10. Because the run is fully idempotent, firing five times is safe — it exits early the moment the work is already done.
Five dict entries in the plist, one per day. Each retriggers the same orchestrator.
Pair with pmset repeat wakeorpoweron MTWRFSU 08:55:00 so the machine is awake before launchd fires.
Exits clean if no SMS in 14 days, the bill isn't new, or it was already paid — no wasted MFA pushes.
Granted to /sbin/launchd (scheduled) or Terminal.app (manual) so it can read chat.db.