Skip to content

Repository files navigation

subscription-tracker

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.

How it works

Once a day, a Cloud Run Job triggered by Cloud Scheduler:

  1. 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).
  2. Runs each candidate message through a cheap heuristic pre-filter (subject keywords, List-Unsubscribe header) to discard messages that are clearly not signup confirmations before spending an LLM call on them.
  3. Classifies the survivors with Gemma 4 (via the Gemini API), which returns a structured {is_subscription, site_name, domain}.
  4. Appends one row per newly discovered domain to the tracking Google Sheet — deduplicated by domain, never editing an existing row. An unsubscribed checkbox is added unchecked and is exclusively owner-managed from then on.
  5. 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/.

Status

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.

Project structure

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).

Requirements

  • 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)

Setup

git clone <this-repo>
cd subscription-tracker
uv sync
cp .env.example .env

Fill 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.

One-time OAuth consent

Before the first run, obtain a gmail.readonly refresh token interactively:

uv run python scripts/bootstrap_oauth.py

This 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.

Running locally

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.

Testing, linting, type-checking

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 scan

Tests 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.

Deployment

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.

Documentation

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)

License

MIT

About

Daily Gmail scan for subscription confirmation emails, tracked in a Google Sheet.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages