HANDS-OFF macOS PIPELINE · RUNS UNATTENDED

From a billing text
to a paid bill — zero clicks

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.

0
Pipeline Stages
0
Python Modules
0
External Services
0
Lines of Code
0
Hard Re-Pay Lock
The Workflow

Eight stages, one orchestrator

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.

idle · press play
The Architecture

A hub-and-spoke of single-purpose modules

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.

app.py

stage-aware orchestrator

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.

parse_bill() _zelle_safety_gate() _trigger_zelle() · subprocess _run_pipeline()
System Design

Everything secret stays on the machine

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.

LOCAL · ON-DEVICE
Messages chat.db
~/Library/Messages
FDA
macOS Keychain
TMobile_*, BoA_*, ZELLE_*
SECRET
Per-bill state JSON
~/.tmo_state · 0600 · atomic
0600
Browser profile
~/.tmo_browser_profile
COOKIES
Bill PDF
~/Downloads · sha256
ENGINE · PYTHON + PLAYWRIGHT
app.py orchestrator
8-stage state machine
download_bill.py
T-Mobile · stealth
zelle_pay.py
BoA · isolated subprocess
LaunchAgent
days 6–10 · 9 AM
EXTERNAL · OVER TLS
T-Mobile portal
bill PDF + posted date
Bank of America
Zelle multi-step flow
Gmail SMTP/SSL
:465 · 4 email types
reads local secret drives a browser network egress (TLS) writes durable state
Safety & Security Model

It would rather do nothing than pay twice

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.

Hard re-pay lock

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.

Dry-run by default

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.

Cap + exact recipient

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.

Atomic, 0600 state

State writes via tempfile + os.replace() so a crash mid-write never corrupts. Directory 0700, file 0600 — it holds confirmation IDs and amounts.

Env-first, Keychain-second

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.

Session isolation

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.

⛔  _zelle_safety_gate() — every check must pass before a cent moves

Already confirmed?
→ refuse (hard lock)
Attempted, unconfirmed?
→ refuse w/o --force
Amount > 0?
→ nothing to pay
Amount ≤ cap?
→ manual review
Recipient set?
→ refuse
✓ all clear → Stage 6 Zelle send
Scheduling

Wakes itself five mornings a month

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.

Billing windowdays 6 – 10 · 09:00

StartCalendarInterval × 5

Five dict entries in the plist, one per day. Each retriggers the same orchestrator.

Won't wake the Mac alone

Pair with pmset repeat wakeorpoweron MTWRFSU 08:55:00 so the machine is awake before launchd fires.

Safe to re-run

Exits clean if no SMS in 14 days, the bill isn't new, or it was already paid — no wasted MFA pushes.

Needs Full Disk Access

Granted to /sbin/launchd (scheduled) or Terminal.app (manual) so it can read chat.db.