Daily automation that scans a personal Gmail inbox for subscription / signup confirmation emails ("welcome to...", "confirm your account", "benvenuto") and maintains a Google Sheet listing every service that holds the owner's email address — so the owner can review it and, where they choose to, request deletion of their data (e.g. under GDPR Art. 17) without manually searching years of inbox history.
It is a small, single-purpose personal tool, not a general-purpose product: one Gmail account, one Google Sheet, no UI beyond the Sheet itself. See Non-goals in the architecture document for what it deliberately does not do.
Once a day, a Cloud Run Job triggered by Cloud Scheduler:
- Lists mailbox messages since the last run — a full scan on the very first run, an incremental Gmail History API diff on every run after that (falling back to a date-based scan if the history watermark has expired).
- Runs each candidate message through a cheap heuristic pre-filter
(subject keywords,
List-Unsubscribeheader) to discard messages that are clearly not signup confirmations before spending an LLM call on them. - Classifies the survivors with Gemma 4 (via the Gemini API),
which returns a structured
{is_subscription, site_name, domain}. - Appends one row per newly discovered domain to the tracking Google
Sheet — deduplicated by domain, never editing an existing row. An
unsubscribedcheckbox is added unchecked and is exclusively owner-managed from then on. - Persists a resumable checkpoint every 25 messages, so a crash, kill, or timeout partway through a run picks back up close to where it left off on the next run instead of restarting or duplicating work.
The full design — requirements, data model, sequence diagrams, testing
and CI/CD strategy — is written up in
docs/architecture/architecture.md
(approved, v1.0). Structural decisions made after the design was frozen
are recorded as ADRs in docs/adr/.
Implemented and verified end to end against a real mailbox and
production spreadsheet (v0.3.0 — see CHANGELOG.md and
docs/how-to-verify/first-full-scan.md
for the walkthrough and its actual output). The GCP infrastructure
(Cloud Run Job, Cloud Scheduler, Secret Manager) has since been
decommissioned; the code remains as a working reference and can be
redeployed by following docs/infra.md.
src/subscription_tracker/
runner.py Orchestrates one run: window selection, the
pre-filter → classifier → ledger pipeline,
checkpointing, the structured run summary.
gmail_client.py Thin wrapper over the Gmail API — the only
module that talks to Gmail.
classifier.py Thin wrapper over the Gemini API — classifies
one candidate message via Gemma 4.
prefilter.py Pure heuristic pre-filter, no I/O.
ledger.py Thin wrapper over the Sheets API — the only
module that talks to Sheets.
retry.py Shared exponential-backoff retry for the three
API boundaries above.
config.py Loads config.yaml (tunable, non-secret params).
config.yaml The actual tunable values (Gemma model, batch size).
scripts/
bootstrap_oauth.py One-time, interactive OAuth consent flow —
run manually, not part of the daily job.
tests/ One test module per source module (unit tests
plus boundary tests against fakes of Gmail/
Sheets/Gemini — see architecture §9).
docs/
architecture/ The approved architecture document.
adr/ Decision records for post-freeze choices.
how-to-verify/ Executed verification walkthroughs.
infra.md Real infrastructure reference (GCP project,
service accounts, Cloud Run Job, Scheduler).
- Python 3.14+
- uv for dependency management
- A personal Gmail account (not Google Workspace)
- A GCP project with the Gmail, Sheets, Gemini, and (for deployment) Cloud Run / Cloud Scheduler / Secret Manager APIs enabled
- A Gemini API key on the paid tier (required so email content is contractually excluded from model training — see NFR-02)
git clone <this-repo>
cd subscription-tracker
uv sync
cp .env.example .envFill in .env with your own values (never commit it — it's
git-ignored). See .env.example for the full list:
GOOGLE_CLOUD_PROJECT, GMAIL_OAUTH_CLIENT_ID/_SECRET,
GMAIL_OAUTH_REFRESH_TOKEN, GEMINI_API_KEY, SPREADSHEET_ID.
Before the first run, obtain a gmail.readonly refresh token
interactively:
uv run python scripts/bootstrap_oauth.pyThis opens a browser for the Google consent screen and prints the
refresh token — copy it into GMAIL_OAUTH_REFRESH_TOKEN (in .env
locally, or into Secret Manager for a deployed job) and remove it from
.env once stored.
uv run --env-file .env python -m subscription_tracker.runner(uv run does not load .env on its own — the --env-file flag is
required.) Sheets access uses Application Default
Credentials rather than the
Gmail OAuth token, per ADR 0001.
Non-secret, tunable pipeline parameters (the Gemma model, the
checkpoint batch size) live in config.yaml,
not in the environment.
uv run ruff format --check . # formatting
uv run ruff check . # lint
uv run mypy --strict . # static typing
uv run pytest --cov # unit + boundary tests, with coverage
uv run pip-audit # dependency vulnerability scanTests exercise real behaviour through the real code path; the only
mock points are the three genuine third-party API boundaries (Gmail,
Sheets, Gemini), each tested against a fake implementing its narrow
interface — see architecture §9.
All four of the above run in CI (.github/workflows/ci.yml) on every
pull request and push to main, alongside a container build and a
gitleaks secret scan.
docker build -t subscription-tracker .The image runs python -m subscription_tracker.runner against
environment variables (plain for non-secret identifiers, Secret
Manager–injected for credentials in production). Full deployment
details — the GCP project, the Cloud Run Job, the Cloud Scheduler
trigger, IAM bindings, and how to run everything manually or locally —
are in docs/infra.md.
| Document | Contents |
|---|---|
docs/architecture/architecture.md |
Full design: requirements, data model, sequence flows, security, testing/CI strategy, roadmap |
docs/adr/ |
Decision records for choices made after the architecture was frozen |
docs/infra.md |
Real GCP/infrastructure reference |
docs/how-to-verify/ |
Executed, reproducible verification walkthroughs |
CHANGELOG.md |
Release history (Keep a Changelog, SemVer) |