An AI-powered agent that automatically tracks your SaaS subscriptions and billing spend — reads your Gmail, classifies receipts with LLMs, logs to Google Sheets, and emails a weekly digest.
Zero manual input after setup. Works with any merchant globally.
- Fetches billing emails via Gmail API using targeted query +
label:purchases - Rule-based noise filter (no LLM cost) drops OTPs, MF statements, marketing
- AI classification:
Renewal/Trial/Cancelled/One-time/Noise - Hybrid extraction: regex pre-extractor for Indian receipts (₹, SIP) + LLM for everything else
- One row per merchant — deduplicates, updates in-place on re-charge
- Auto-calculates
Next_Renewalfrom charge date when LLM can't determine it - Google Sheets with colour-coded status, dropdown, sorted newest-first
- Weekly HTML digest: total spend, INR conversion, trial alerts, spend analysis by category
- LLM fallback chain: Groq → Gemini 3.1 Flash Lite → Gemma 3 27B
verify_llm.pypre-flight check before running
Google Sheet — subscription tracker

| Layer | Technology |
|---|---|
| Language | Python 3.10+ |
| LLM orchestration | CrewAI |
| LLM abstraction | LiteLLM |
| Prompt optimization | DSPy |
| Primary LLM | Groq llama-3.3-70b-versatile (free, 100k TPD) |
| Fallback LLM | Google gemini-3.1-flash-lite-preview (free, 500 RPD) |
| Second fallback | Google gemma-3-27b-it (free, 14.4k RPD) |
| Gmail | Gmail API v1 — OAuth 2.0 |
| Storage | Google Sheets API v4 — service account |
| Currency | open.er-api.com free tier |
| Config | python-dotenv |
| Scheduling | Windows Task Scheduler / cron |
| Container | Docker |
Gmail Inbox
│
▼
fetch_emails() ← Gmail API + SUBSCRIPTION_QUERY + label:purchases
│
▼
is_noise_email() ← Rule engine: sender domain + subject patterns (free, no LLM)
│ pass
▼
looks_like_billing_email() ← Keyword check
│ pass
▼
classify_email() ← CrewAI + Groq/Gemini → Renewal|Trial|Cancelled|One-time|Noise
│
▼
_rule_based_extract() ← Regex: ₹ amounts, SIP fields, fund names (fast, free)
+
extract_billing_info() ← CrewAI + LLM → merchant, amount, currency, period, renewal
│
▼
Google Sheets ← find_merchant_row() → upsert row, sort by Last_Charged desc
│
▼ (Monday / --digest flag)
Weekly Digest Email ← HTML: spend totals, INR conversion, trial alerts, analysis
subscription-agent/
├── main.py # Orchestrator: fetch → filter → classify → extract → sheet
├── config.py # Env vars and constants
├── requirements.txt
├── Dockerfile
├── verify_llm.py # Pre-flight: check Groq + Gemini availability before run
├── sort_sheet.py # One-time utility: sort existing sheet by Last_Charged
├── fix_state.py # Utility: reset processed email state
│
├── agents/
│ └── classifier.py # Email classification prompt + CrewAI agent
│
├── services/
│ ├── gmail.py # Gmail OAuth, fetch, send
│ ├── sheets.py # Sheets read/write, formatting, sort
│ └── digest.py # Digest builder, INR conversion, spend analysis
│
├── core/
│ ├── extractor.py # LLM extraction, rule-based extractor, retry + fallback
│ ├── rule_engine.py # Noise filter, billing keyword detector, HTML sanitizer
│ └── state_manager.py # state.json, dedup, Gemini quota tracking
│
└── logs/
└── logger.py
git clone <your-repo-url>
cd subscription-agent
pip install -r requirements.txt| Key | Where | Cost |
|---|---|---|
| Groq | console.groq.com | Free |
| Gemini | aistudio.google.com/app/apikey | Free |
GROQ_API_KEY=gsk_xxxxxxxxxxxxxxxxxxxx
GEMINI_API_KEY=AIza_xxxxxxxxxxxxxxxxxxxx
MY_EMAIL=you@gmail.com
DIGEST_RECIPIENT=you@gmail.com- Go to console.cloud.google.com → New Project
- Enable Gmail API and Google Sheets API
- OAuth consent screen → External → add scopes:
https://www.googleapis.com/auth/gmail.readonlyhttps://www.googleapis.com/auth/gmail.modifyhttps://www.googleapis.com/auth/gmail.send
- Add your Gmail as a test user
- Credentials → OAuth client ID → Desktop app → download → rename to
client_secret.json - Credentials → Service Account → create → Keys → JSON → rename to
credentials.json - Copy the service account email
- Create a Google Sheet named exactly:
Subscription Tracker - Share it with the service account email → Editor
python main.pyA browser opens → sign in → allow access. Creates token_account1.json for all future runs.
# Process new billing emails
python main.py
# Send weekly digest only (without processing emails)
python main.py --digestCheck app.log for output:
Get-Content app.log -Tail 30- Open Task Scheduler → Create Basic Task
- Set trigger: Daily, repeat every 6 hours
- Action: Start a Program
- Program:
"C:\Program Files\Python312\python.exe" - Arguments:
main.py - Start in:
E:\Tracker\subscription-agent
- Program:
For weekly digest, create a second task:
- Trigger: Weekly on Monday 9:00 AM
- Arguments:
main.py --digest
docker build -t subscription-agent .Windows (PowerShell):
docker run --rm `
-v "${PWD}/credentials.json:/app/credentials.json" `
-v "${PWD}/client_secret.json:/app/client_secret.json" `
-v "${PWD}/token_account1.json:/app/token_account1.json" `
-v "${PWD}/state.json:/app/state.json" `
-v "${PWD}/.env:/app/.env" `
subscription-agent| Column | Description |
|---|---|
| First_Seen | Date first receipt was detected |
| Last_Charged | Date of most recent charge |
| Merchant | Service name (Vercel, Notion, GitHub, etc.) |
| Plan | Plan tier (Pro, Team, Starter, etc.) |
| Amount | Latest charge amount |
| Currency | USD / INR / EUR etc. |
| Billing_Period | monthly / annual / one-time |
| Status | Active / Trial / Cancelled / One-time — colour coded |
| Annual_Projection | Amount × 12 if monthly, else the annual amount |
| Next_Renewal | Extracted renewal date |
| Email_Source | Sender email address |
| Status | Colour |
|---|---|
| Active | 🟢 Green |
| Trial | 🟡 Yellow |
| Cancelled | 🔴 Red |
| One-time | 🔵 Blue |
| Priority | Model | Trigger |
|---|---|---|
| 1 — Primary | Groq llama-3.3-70b-versatile |
Default |
| 2 — Fallback | Google gemini-3.1-flash-lite-preview |
Groq daily/per-minute limit |
| 3 — Second fallback | Google gemma-3-27b-it |
Gemini 503 unavailable |
- Per-minute limits → waits the
retryDelayfrom the error response, then retries - Daily limits → immediately switches to next model for the rest of the run
- If all models are exhausted → email is skipped (not marked seen, reprocessed next run)
- Run
verify_llm.pybeforemain.pyto confirm availability
Full CrewAI traces (agents, tasks, LLM calls, latency, tokens) are sent to LangSmith when the key is set.
- Sign up at smith.langchain.com → copy your API key
- Add to
.env:
LANGSMITH_API_KEY=ls__xxxxxxxxxxxxxxxxxxxx
LANGSMITH_PROJECT=subscription-tracker- Run normally — traces appear automatically, no code changes needed
Tracing is fully optional. If LANGSMITH_API_KEY is not set, the agent runs without it.
A test suite of 60 labeled billing emails (real + synthetic) is in eval/test_cases.json. Run it with:
uv run python eval/run_eval.pyPrompt optimized with DSPy BootstrapFewShotWithRandomSearch using 4 few-shot demonstrations.
| Field | Accuracy |
|---|---|
| Merchant | 57/60 (95%) |
| Amount | 60/60 (100%) |
| Currency | 60/60 (100%) |
| Billing Period | 57/60 (95%) |
| Overall | 97.5% |
Improvement: 83.8% → 97.5% (+13.7 points) via DSPy prompt optimization.
Extraction uses a hybrid approach: regex pre-extractor for Indian receipts (SIP, ₹ amounts, fund names) with LLM as fallback. Rule-based results always win for billing_period to prevent LLM from misclassifying SIP deductions as one-time.
To add more test cases, append entries to eval/test_cases.json following the existing format.
Contributions are welcome! Here are good areas to improve:
- New noise patterns — add sender domains or subject keywords to
core/rule_engine.py - New rule-based extractors — add regex patterns to
_rule_based_extract()incore/extractor.pyfor specific email formats - Additional LLM providers — add a new model string to the fallback chain in
core/extractor.pyandagents/classifier.py - Multi-account support —
authenticate_gmail()accepts anaccount_nameparam, extendmain.pyto loop over accounts - Cron/Linux support — add a cron setup guide to this README
- Tests — unit tests for
rule_engine.py,extractor.pyparsing, andsheets.pyhelpers
git clone <repo-url>
cd subscription-agent
python -m venv .venv
.venv\Scripts\activate # Windows
source .venv/bin/activate # Linux/Mac
pip install -r requirements.txt
cp .env.example .env # fill in your keys- Run
verify_llm.pyto confirm LLM connectivity - Test with a small email batch (
max_results=5ingmail.py) - Do not commit
credentials.json,client_secret.json,token_*.json,.env, orstate.json
.env
credentials.json
client_secret.json
token_*.json
state.json
app.log
Ensure these are in .gitignore before pushing.
