← 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
  • ✓
    Finance Dashboard

    Home, Plan, Ledger and Radar at bud.rootstack.dev: the month at a glance, what a normal month costs, why each category changed, and what is coming due. Built on the Firefly III ledger with Bud's own categorization layer.

    OCT 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

BUDfeaturephaserefactor

The Sheet live — grouped budget, split charges, one-offs and a split-it prompt

Features

  • The Sheet shipped on Plan — every line grouped as Spoken for, Essentials, Choices, Savings or One-offs against income, with typical, so-far, budget and trend per line and a paycheck bracket on each bill
  • Spend groups resolve charge > merchant > category — one group_of() reader feeds the Sheet and Home, so both show the same $1,020.12 spoken for
  • One-offs stay in actual spend but leave usual, typical, pace and budget — the $1,819 refrigerator no longer drives Home's "over pace"
  • Split a charge in the Ledger — parts must sum to the cent (save_split() / clear_split(), migration 021), Save stays off until Remaining is $0.00
  • Home sentence under the headline — spoken for, followed habits, and the one-offs set apart
  • Radar cut from 13,471px to 4,716px at 375px — stopped subscriptions fold away, Change opens one row at a time, the Discretionary table is retired
  • "Split it?" Discord prompt — one line per recent charge from a mixed merchant, linking to /ledger?journal=<id>, sent once and forgotten when the charge is dealt with
  • Named lines for labelled payees on an every-other-month rhythm — Hair (Cardinal Salon) sits in Essentials at its monthly share

Bug Fixes

  • Merchant descriptors that drifted (REPUBLICSERVICES vs REPUBLIC SERVICES TRAS PHOENIX) now fold to one key — set-aside fell from $373 to $273.53 a month once Republic stopped counting twice
  • The "could do without" tick set Choices for ungrouped merchants and did nothing for grouped ones — it now always sets Choices, and unticking releases only a Choices it set
  • Plan's .expected class leaked onto Sheet elements through unscoped Radar CSS — renamed under the pl- prefix
  • Split form overflowed a phone and opened with the whole charge on part 2 — fixed with minmax(0,1fr) and a blank second part

Infrastructure

  • Migrations 020 (groups, one-off) and 021 (splits) applied to CT 104
  • Costco seed rule retired — merchant_rules row soft-deleted and the code fallback removed, so new Costco charges wait in Not sorted yet for a split
  • bud/CLAUDE.md gains a "Budget sheet" entry covering precedence, whole_charges(), name folding and the prompt
  • 577 tests pass; bud PRs #21 to #26 and bud-app PRs #11 to #14 merged, each deploy backed up first

Lessons

  • A fold rule needs an exact first-words match — a looser "long word contains a shorter key" rule merged DashPass into DoorDash
  • usd() rounds whole dollars at $100 and up, which hides the cents a split prompt exists to show — format money with :,.2f there
  • Medians do not add, so the Sheet adds only the so-far column and states the rule once in a methodology fold
  • Reading expected numbers from the live API before a test turns a UAT into a checklist the holder can pass in six minutes
  • sed on a CRLF file misses an anchored match until the \r is allowed for, and a prettier run rewrites line endings across the tree

TODO

  • Home's per-category "usual" is a median by day, so a lumpy haircut reads "$216 of about $0"
  • ChoicesLine fetches the whole Plan once per Radar visit — a small choices field on the Radar payload would fix it and refresh
  • Check the merchant-defaults learner after the 06:15 import (grep -E "defaults learned|learn defaults" /opt/firefly/import.log)
BUDaiinfrastructurebrainstorm

Inbox classifier measured, handler brief drafted, and the sweep's cloud write closed

Features

  • Classifier measured for the first time since July — 536 emails in a week: 56% unknown, 29% coupon, 3% purchase
  • Hand-read of 50 unknown emails — marketing, newsletters, app notifications and account notices, with no missed receipts
  • Revised classify prompt tried offline on the same emails — unknown fell from 54% to 39%, short of the 25% target, and one real order was lost, so it stays unshipped
  • Repair pass for the model's replies — recovers every broken reply ("confidence:99) and folds invented labels like promotion back onto the taxonomy
  • Handler vision brief drafted — sender rules before the model, then a bills-to-Radar or coupon handler chosen on two weeks of logged data

Bug Fixes

  • The Catch-up Sweep no longer writes to the cloud project — since July its active version sent non-purchase mail to nwiyddpjdeogtyjeelyh and read its Gmail search date back from there; both nodes now use Taproot
  • Radar API tests no longer fail once the calendar passes the 17th — the tests pin today, and the fake tables gain alert_settings

Infrastructure

  • n8n database backed up before the patch (database-20261022-pre-email-log-repoint.sqlite); verified on the next sweep: 25 rows on Taproot, 0 errors
  • PRs #18, #19 and #20 merged; CLAUDE.md corrected on email_log and the CT 102 address pin

Lessons

  • The August cutover changed only the workflow's draft — n8n runs the workflow_history row named by activeVersionId, so a migration is verified by reading the active version
  • Broken JSON was 1 in 100, not 1 in 12 — the old figure came from a different prompt and was never re-measured
  • A 3B model cannot hold seven classes apart — coupon collapsed into marketing as soon as marketing existed
  • fetchall() on n8n's execution data was killed for memory on a 2 GB container — reading one row at a time worked
  • Node's built-in fetch gives up after 300 s without headers — a CPU-only Ollama queued behind a sweep batch needs node:http

TODO

  • Read the handler brief and answer its six questions
  • Delete the Vercel project bud past the 2FA prompt, then remove .vercel/ and the skip-list lines; decide on the Vercel project bud-app
  • Rebuild the missing supabase-taproot-migration-closeout-plan.md with the Catch-up Sweep's cloud rows on its list
BUDfeatureinfrastructurephase

Receipt joins live — matcher, Ledger receipts, and a housekeeping sweep

Features

  • Receipt matcher shipped — ledger purchases pair with receipt emails on shop name, date and amount (0.5 / 0.3 / 0.2), and only an exact amount is auto-accepted
  • No alias list — a shared first word and a distinctive amount (cents, one charge within three days) route to review instead, cutting near misses from 23 to 16 with the accepted pairs unchanged
  • Ledger shows the receipt behind a transaction — matched ones list their items, suggested ones offer "Same purchase" and "Not the same"
  • A person's answer is final — an approved pair leaves the matching pool, a refused pair is never offered again
  • A category correction keeps its receipt — migration 019 carries matched_purchase_id through both supersede_enrichment() overloads
  • The daily import now categorises and matches after it runs — run-import.sh logged "categorised" and "receipts matched" on its first scheduled run
  • Plan page finished and merged (set-aside vault, yearly-charge nudges)

Bug Fixes

  • Dateless receipts no longer match any month — the day their email arrived stands in (124 of 208 receipts had no purchase date)
  • Alert cron follows New York 07:00 across daylight saving, guarded by TZ=America/New_York date
  • supersede_enrichment() had execute rights for anon and authenticated on CT 104 despite migration 008 — revoked

Infrastructure

  • Migration 018 dropped the five anon_read_* policies; legacy Supabase pages and @supabase/supabase-js removed from bud-app
  • Four dead KM n8n workflows retired with a backup, seven secrets removed from n8n's compose file, and its address pinned to 192.168.1.152
  • Anthropic client and its config removed from bud — Ollama is the only model path

Lessons

  • create or replace function with a different argument list adds an overload instead of replacing — a second supersede_enrichment was found only by listing pg_proc
  • Diffing each matcher run against the last caught two bugs the totals hid — a first-word rule that lowered good matches and a 4.99 paired with a 5.00 round-up
  • The import job never ran the categorizer, so new transactions had no enrichment row until someone ran the backfill by hand
  • tar from Windows exits non-zero on ownership errors — an && chain silently skipped the container rebuild

TODO

  • Phone check of receipts on the Ledger at 360px, and a first live "Same purchase" click
  • Kuma's n8n monitor still points at .182; the Vercel project bud is still to delete
  • Learn aliases from "Same purchase" clicks and fix vendor extraction ("Chase" or "unknown")
BUDinfrastructureaitooling

SoFi outage closed — outage triage built: detector, gate, and on-call runner

Features

  • SoFi outage closed — Checking matches the bank to the cent after the re-authorization and the Oct 21 import
  • Diagnose checks (ops/bud-ops/diag/) — read-only, redacted verdicts for per-bank freshness, balance drift, the import job, containers, crons, backups, Kuma, n8n, and disk
  • Detector — one Discord message per new failure, one "Cleared" when it ends, warnings recorded and never posted; RUNBOOK.md is the single source of fix text
  • The gate — an SSH forced command with fixed verbs, budgets counted from an audit log, and two kill switches; no verb writes to Firefly, reads a .env file, or runs SQL
  • On-call runner — Task Scheduler runs headless Sonnet every 30 minutes on the subscription; each new failure gets one triage, reported through the gate
  • Drill A passed — an injected stale-SoFi failure produced one 4-turn triage with no denied calls and no actions; the next tick stayed silent
  • Nightly Postgres backup — every database plus roles, 14 days kept (same disk; no offsite copy yet)

Bug Fixes

  • Bud data freshness no longer false-alarms on quiet inboxes — email_log stores only purchase emails, so its age tracked purchases, not the sweep
  • Gate diag now loads the ops .env — found by testing through the gate, which checked the wrong n8n address
  • Gate --why takes the rest of the line — ssh flattens a quoted reason into separate words

Infrastructure

  • Kuma push monitors for the detector, the email sweep, and the runner; the n8n monitor repointed to .152 after CT 102 flipped addresses again
  • "Ping Kuma" node patched into the Catch-up Sweep (workflow_entity and workflow_history) so sweep liveness is measured directly
  • Branches feat/ops-triage and fix/freshness-purchase-only merged (PRs #14, #13)

Lessons

  • SimpleFIN's errlist stayed empty during a bank "Auth required" outage — per-bank balance-date age is the honest health signal
  • A stale persistent CLAUDE_CODE_OAUTH_TOKEN shadows the login for scheduled tasks — claude -p returned 401 under Task Scheduler while the same script worked interactively
  • PowerShell 5.1 piping text to ssh adds a BOM and a CR — sshd silently skipped the authorized_keys line; a base64 script fixed it
  • No flag keeps the global CLAUDE.md out of a headless run, and --bare needs an API key — the gate, not the prompt, has to be the fence

TODO

  • Drill B: stop firefly-importer-1 and watch the agent restart it through the gate
  • Reserve 192.168.1.152 for CT 102 in the router
  • Offsite backup copy and a Proxmox vzdump job
  • Bring the bud wiki pages up to date (last touched 2026-05-30)
BUDfeatureinfrastructurephase

Finance dashboard live — Ledger, Radar, Home, and Plan at bud.rootstack.dev

Features

  • Finance Steps 6–9 built, deployed, and merged — Home, Plan, Ledger, and Radar serve from https://bud.rootstack.dev behind Cloudflare Access
  • Ledger ("Why Column"): monthly variance per category with the drivers behind each change, and category corrections made from the page
  • Merchant rules are data — the merchant_rules table replaces the hardcoded list, and a tick-box on any ledger row creates a rule
  • Radar: a recurrence engine forecasts expected bills and pay, flags late payments with a reason, and lists subscriptions with a discretionary total
  • Balances are real — opening balances set from SimpleFIN's figures put all nine bank accounts on the bank's number to the cent
  • Chase history backfilled — 1,232 transactions from 42 statement PDFs (Dec 2024 to Sep 2026), every statement reconciled to its own printed totals
  • Home: the month at a glance — spending against "usual so far" (the day-by-day median of up to six earlier months), measured through the bank's last reported day
  • Plan: what a normal month costs and what is left — typical income minus typical spending minus money set aside for quarterly and annual bills
  • Budgets are effective-dated — the budget for a month is the latest row at or before it, so a change never rewrites history
  • Checking line on Home and Radar — balance, usual balance for the day of the month, and the lowest projected point before payday
  • Discord alerts fire on change only — a newly missed payment, an obligation entering its warning window, or checking projected under the floor

Bug Fixes

  • bud-app's Vercel api/ routes removed — /api/purchases was reachable with no authentication
  • A payment landing before its usual day no longer makes the same month's usual day the next expected date
  • A third-paycheck month no longer skews the typical month — pay is identified by cadence and size, and extra-pay months are left out

Infrastructure

  • Twelve Uptime Kuma monitors share one Discord notification — radar health, data freshness, the daily import job, the dashboard, n8n, and Firefly
  • Migrations 008–016 applied to CT 104 — supersede_enrichment(), merchant_rules, obligations and series settings, budgets, alert_settings
  • n8n prunes executions older than 7 days, and its database was compacted with VACUUM INTO after CT 102's root disk filled (grown 7.8 GB to 12 GB)
  • Bud — Gmail Purchase Router unpublished — zero successes in 839 runs; the Ollama Catch-up Sweep is the real ingestion path
  • Gmail OAuth credential re-authorized after expiring 2026-10-09 with invalid_grant
  • The web/ review-queue app retired and removed from the repo

Lessons

  • An import job can fail and still log success — the importer could not resolve SimpleFIN's hostname, /autoimport answered "Seems to have worked!", and only docker logs showed the error
  • Without an opening balance a Firefly total is only net movement since the feed began — every balance was wrong until the opening figures were set
  • Measuring "so far" through today makes every month look underspent — the bank feed trails by about two days, cards by two to three
  • The median survives a mortgage paid twice; the mean does not — and category medians do not sum to the total's median
  • Statement descriptors carry a city and state the live feed lacks — untrimmed, every merchant looks new the month the feed takes over
  • A new Kuma monitor's "Default" notification badge does not tick the toggle — each monitor needs it ticked by hand
  • A session's transcript can be missing from disk when it closes — /resume and Switchboard had nothing to restore, and a plan one minute into drafting was lost

TODO

  • Redraft the Bud next-steps plan — post-Step-9 action items, the documented wishlist, and the Radar/Ledger phone layout
  • Write merchant rules for the ~550 uncategorized rows from the Chase history import
  • Delete the Vercel project bud by hand — it still holds the old Supabase settings
  • Remove the cloud service_role key and Kroger secret from n8n's compose file
  • Push docs/notes-2026-10-12 and open its PR — two commits are local only
  • Find out why session transcripts are not staying on disk
BUDfeatureinfrastructurebug

Chase connected — card-aware categorizer, 451/451 transactions enriched, repos backed up

Features

  • Chase Freedom Unlimited (0706) and Flex (7501) linked through SimpleFIN and imported as Firefly Debt liabilities 75 and 76 — the ~59% of outflow that only appeared as lump CHASE CRD payments now has line-item detail
  • Opening balances derived from the September statements and cross-checked two ways (statement roll-forward and live balance) — both cards land on Chase's balance to the cent once in-transit charges post
  • Categorizer is now credit-card aware — counterparty prefers the Expense/Revenue account, and an on_liability flag routes card payments, returned payments, and opening balances to transfer
  • 451/451 Firefly transactions categorized — 161 new rows, merchant rules user-approved, one manual override (a $150 Zelle that was an Airbnb, filed as travel)
  • "Invisible" spend (paid to unconnected cards) drops from $17,074 to $82.30 — only a single Comenity store-card payment still hides its detail
  • simplefin_sync_accounts.py maps accounts by unique ID prefix, repeatable in one request, all-or-nothing, with a config backup before every write
  • simplefin_backfill_once.sh runs one wider import window and restores the 30-day config through an EXIT trap, even on failure

Bug Fixes

  • Card purchases had the card's own name as their merchant — the ledger query took max() over every non-asset account, and a card journal has two, so the card won whenever it sorted after the merchant
  • The 2026-09-08 returned autopay would have counted as $2,347.40 of spend — now classified as a transfer that reverses a payment
  • Sync script printed account IDs truncated to 24 characters, unusable as --map input, and accepted any string as a mapping (including the bare prefix ACT-)
  • .env.example was never committed — the .env.* ignore rule swallowed the template along with real env files
  • Maintenance scripts vanished from the container on rebuild — they had been docker cp'd in; now baked into the image with COPY scripts/

Infrastructure

  • A Supabase service_role JWT found hardcoded in an n8n workflow export during a pre-push history scan — scrubbed from all 16 commits with git filter-repo --replace-text before the first push, verified absent from every object
  • Private GitHub remotes created for bud and bud-app — a month of finance work no longer lives on one disk
  • .gitattributes forces LF for *.sh — core.autocrlf=true would otherwise hand CT 100 a script bash can't parse
  • tests/test_categorizer.py added — 35 cases from real ledger descriptors, the first tests in the repo
  • Liability start amounts set through PUT /api/v1/accounts/{id} — the Firefly v6.6.6 edit form reported success while writing nothing

Lessons

  • A newly linked SimpleFIN account carries ~30 days of history, not 90 — a 90-day import window returned only duplicates of what the first pull already had
  • A "Success" banner is not evidence of a write — Firefly accepted the edit form, dropped the start amount, and only a direct account_meta query showed nothing changed
  • Statement balances beat live balances for an opening-balance anchor — the live figure already included charges still in transit and would have double-counted them once they posted
  • Rules written before a data shape exists fail silently on it — the categorizer assumed every non-asset account was a merchant until a card made that false
  • Scan full history, not HEAD, before a first push — the leaked key sat in a July commit and in the current tree, and an unpushed repo is the only point where scrubbing beats rotating
  • A bounced autopay costs more than a fee — losing the grace period turned a returned payment into $74.14 of interest

TODO

  • bud-app api/ routes have no caller auth — must verify the Supabase JWT before any deploy
  • Delete or repoint the n8n "Gmail historical backfill" workflow that still hardcodes the cloud service_role key
  • Move merchant rules to a Taproot merchant_rules table once the Step 6 dashboard has an edit UI
  • Rebuild the Step 6 dashboard plan — the finance plan and vision brief are both missing from disk
BUDfeatureinfrastructurebug

Catch-up Sweep live — watermark-based Gmail coverage, backfill gap confirmed closed

Features

  • Catch-up Sweep workflow published to n8n (ID: NXwPAwurVz56Mfdx) — runs every 30 minutes, fetches up to 25 emails from Gmail starting 1h before the watermark in email_log
  • Watermark logic: queries MAX(received_at) from email_log, subtracts 1h safety buffer, builds after:YYYY/MM/DD Gmail filter — incremental by design, idempotent on re-run
  • Ollama classification (qwen2.5:3b) wired into the sweep — same Parse Classification logic as the main pipeline, handles code fences and "null" string normalization
  • Historical backfill workflow ported to Ollama (gmail-historical-backfill.json v3) — covers Apr 17–May 16 gap period, manual-trigger, limit 50

Bug Fixes

  • Prefer: resolution=ignore-duplicates header alone returned 409 on duplicate inserts — PostgREST requires ?on_conflict=message_id as a URL query param in addition to the Prefer header; added to both sweep and backfill Log Unclassified nodes

Infrastructure

  • n8n CLI workflow import: docker cp file.json n8n:/tmp/ && docker exec n8n n8n import:workflow --input=/tmp/file.json — usable when n8n API key is unavailable or the workflow was never registered
  • Bud CLAUDE.md updated: Gmail OAuth re-auth note added (SSH tunnel pattern: ssh -L 5678:localhost:5678 root@192.168.1.152), Steps 6-7 marked complete
  • Historical gap validated via direct Supabase row count: 78 rows in email_log for Apr 24–May 16; first backfill batch (50 emails) returned all 409 duplicates, confirming the Jul 5 work had already covered the period

Lessons

  • PostgREST resolution=ignore-duplicates needs two things: the Prefer header AND ?on_conflict=<column> in the URL — the header alone silently returns 409 and the error looks identical to a real constraint violation
  • Expired Gmail OAuth in n8n manifests as ETIMEDOUT on a Google IP — looks like a network failure, not a credential error; check n8n Credentials before debugging connectivity
  • A gap in email_log doesn't mean a gap in coverage — the Jul 5 backfill had already filled Apr 24–May 16; first backfill run hitting 409 on all 50 rows was the confirmation signal, not a failure
BUDinfrastructureaibug

Ollama migration complete — 24 dropped purchases recovered, pipeline validated

Features

  • Extraction backend swapped from Claude Haiku to local Ollama (qwen2.5:3b on CT 100:11434) — zero API costs, inference stays on the homelab
  • All 24 purchase emails dropped during the API credit outage recovered — 39 active purchases now in Supabase, zero errors remaining
  • _fix_null_strings() added to ollama_client.py — walks the parsed JSON dict before Pydantic validation to replace "null" strings with Python None
  • HTML fallback in n8n "Build Handler Payload": when plain-text body is under 500 characters, strips and uses HTML body instead — small models extract nothing useful from 50-word plain-text bodies

Bug Fixes

  • n8n "Build Handler Payload" read email.payload.headers (raw Gmail API format) — n8n's Gmail node returns a parsed object with top-level fields (from.value[0], subject, text); rebuilt JS to match the actual output shape
  • n8n HTTP Request timeout (300s) fired before Ollama processed deep-queue items — 24 concurrent requests queue sequentially; item 24 waits 23×67s ≈ 25 min before Ollama starts it; raised to 1800s in n8n and httpx
  • docker restart was used after code edits — command reuses the baked image and silently runs old code; switched to docker compose up --build -d

Infrastructure

  • src/services/ollama_client.py — new extraction service using httpx against Ollama's /v1/chat/completions endpoint; 1800s timeout; JSON fence stripping; null-string normalization
  • src/config.py updated: anthropic_api_key now optional (defaults to ""); ollama_url and ollama_model added
  • Migrations 003 (unsubscribe support) and 004 (amount nullable on purchases) applied
  • scripts/find_dupes.py — one-shot soft-delete deduplication script; idempotent; dry-run flag
  • workflows/backfill/ — n8n recovery workflow JSON export and Python backfill runner

Lessons

  • docker restart reuses the baked image — Python file changes are invisible until docker compose up --build -d; the container logs look healthy the whole time
  • n8n manual-trigger workflows cannot be triggered via REST API; POST /api/v1/workflows/{id}/run returns "POST method not allowed"; the UI Execute button is the only path
  • Ollama queues inferences sequentially — n8n's HTTP timeout counts from when the request is sent, not when Ollama starts it; large concurrent batches will always exceed any reasonable n8n timeout
  • Small models (qwen2.5:3b) emit "field": "null" as a string, not JSON null — Pydantic's date and float parsers reject it; the fix is a pre-validation dict walk, not a prompt change

TODO

  • Bud Web UI — Step 1 scaffold at bud.understorylabs.co (Split Brain: Vercel reads, Taproot acts); plan at ~/.claude/plans/bud-web-ui-plan.md
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