← HOME

PROJECT

Bud

ACTIVE

Inbox intelligence

PythonFastAPIDockern8nSupabase

ABOUT

Python/FastAPI service that processes Gmail, classifies purchases and notifications, and feeds structured data into Current OS and other services. Deployed on Taproot via Docker.

FEATURES

SHIPPED

  • PurchaseHandler

    Classifies Gmail purchase receipts and extracts structured data — merchant, amount, items. Deployed on Taproot via Docker.

    MAY 2026

IN PROGRESS

  • Gmail Classification

    n8n workflow connects Gmail OAuth to Bud's classification endpoint. Routes notifications, receipts, and digests to appropriate handlers.

PLANNED

  • Inbox Intelligence

    Surfaces patterns across inbox data — recurring charges, unusual spend, subscription changes. Feeds summary into Current OS.

CHANGELOG

BUDfeaturebuginfrastructure

Pipeline live — Gmail trigger activated, handler resilience fixed

Features

  • Gmail purchase router workflow published and active in n8n — polls inbox every minute for new emails
  • Classification pipeline end-to-end: Gmail trigger → Claude Haiku classify → confidence-based routing → handler POST or Supabase log
  • Read status filter set to all emails (read + unread) — prevents missed classification if email is opened before n8n polls

Bug Fixes

  • Handler crashed on emails where Claude returned null for amount — PurchaseExtraction.amount was required, now optional
  • email_log audit trail never written when extraction failed — crash happened in extract() before store() was reached; moved email_log write before compliance checks
  • Handler returned 200 with error body but route still reported status: "ok" — now returns "partial" when email is logged but no purchase row created
  • Deployed model mismatch: EmailPayload on docker-host was missing unsubscribe_link and is_read fields — scp'd the updated model file

Infrastructure

  • Lost session reconstructed from git state and uncommitted files — committed as dab51e7
  • Google Cloud OAuth, n8n Gmail credential, and workflow import confirmed already complete from prior session
  • Gmail Trigger node had stale "Every Day" poll entries from JSON import — removed, set to Every Minute
  • Deployment confirmed: scp to /opt/bud/ on docker-host, docker compose build && up -d

Lessons

  • When deploying via scp, every changed file must be pushed — not just the files edited in the current session; a model file mismatch caused an AttributeError that only surfaced at runtime
  • n8n workflow JSON imports can carry orphaned poll configuration entries that prevent activation — the .trim() error gave no indication which node or field was the problem
  • Handler pipelines should write the audit log (email_log) unconditionally at the top of store(), not after validation — a crash in extract() leaves zero trace otherwise

TODO

  • Confirm end-to-end with a real purchase email landing in both email_log and purchases tables — fix deployed but not yet validated
BUDinfrastructureaifeature

PurchaseHandler live — Steps 2–4 complete, n8n up

Features

  • PurchaseHandler local test passed — 3/3 email formats (Amazon, DoorDash, Netflix) extracted and written to Supabase
  • Handler deployed on docker-host — FastAPI container running at 192.168.1.153:8001, health endpoint confirmed from network
  • n8n operational — CT 102 (192.168.1.152:5678) created, Docker installed, n8n running and survives reboot
  • Gmail API enabled on Google Cloud project life-dashboard — OAuth credentials next

Bug Fixes

  • Claude Haiku wraps JSON extraction responses in ```json ``` fences despite explicit instructions — stripped before model_validate_json() in claude_client.py

Infrastructure

  • Supabase schema live — email_log and purchases tables with RLS, indexes, and soft delete
  • .env.example committed — documents SUPABASE_URL, SUPABASE_SERVICE_KEY, ANTHROPIC_API_KEY
  • services.md created at ~/.claude/services.md — global inventory of cloud platforms, APIs, and self-hosted services; updated by /wrap going forward
  • n8n compose file uses N8N_SECURE_COOKIE=false — required for HTTP access on local network

Lessons

  • Smaller models (Haiku) reliably ignore "no markdown formatting" instructions — fence stripping is a required defensive layer, not optional
  • scp with ~ fails in PowerShell; full Windows paths (C:\Users\nrisa\...) required — write files locally and copy over rather than fighting heredocs or rsync
  • Services inventory needs to exist before the second integration, not after — build the index early so "do I already have a GCP project?" has a real answer

TODO

  • Step 5: create Google OAuth 2.0 credentials in life-dashboard, wire Gmail trigger in n8n
  • Add bud to save-state project registry via /ship
BUDfeatureinfrastructureai

PurchaseHandler built — schema live, handler ready for local test

Features

  • PurchaseHandler complete — extract → validate → store pipeline wired to /api/v1/handle/purchase
  • Extraction prompt with three few-shot examples (Amazon order, DoorDash delivery, Netflix subscription) — Haiku model, JSON-only output
  • HandlerResponse returns email_log_id and purchase_id on success — n8n gets traceable IDs, not just a status string
  • amount > 0 and non-empty vendor validation gates every write — no silent bad data reaches Supabase

Infrastructure

  • email_log and purchases tables live in Supabase — RLS enabled, dedup index on message_id, FK from purchases into email_log
  • Migration files in migrations/ numbered 001/002 — ordering enforces the FK dependency at run time
  • Sync Supabase client wrapped in asyncio.to_thread — avoids supabase-py async API fragility across 2.x minor versions
  • email_log row written before purchases insert — if the purchases write fails, the audit trail exists and carries the error

Lessons

  • Supabase service_role bypasses RLS by design — adding a write policy is redundant and misleading; only anon/authenticated reads need explicit policies
  • supabase-py async API changed names between 2.x minor versions (acreate_client vs create_async_client) — sync + asyncio.to_thread is the portable fix
  • Prompt templates with JSON few-shot examples can't use .format() — JSON braces read as format variables; .replace() per slot avoids the trap
  • Write the audit log row first, then the derived row — partial failures are recoverable; a missing anchor row is not

TODO

  • Step 2 checkpoint: venv setup, .env, curl tests with three email formats against localhost
  • Steps 3–6 after checkpoint: Docker deployment on docker-host, n8n LXC, Gmail trigger workflow, end-to-end validation