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.
PROJECT
Bud
Inbox intelligence
PythonFastAPIDockern8nSupabase
ABOUT
FEATURES
SHIPPED
- ✓PurchaseHandlerMAY 2026
Classifies Gmail purchase receipts and extracts structured data — merchant, amount, items. Deployed on Taproot via Docker.
- ✓Finance DashboardOCT 2026
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.
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
.expectedclass leaked onto Sheet elements through unscoped Radar CSS — renamed under thepl-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_rulesrow soft-deleted and the code fallback removed, so new Costco charges wait in Not sorted yet for a split bud/CLAUDE.mdgains 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:,.2fthere- 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
sedon a CRLF file misses an anchored match until the\ris 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"
ChoicesLinefetches the whole Plan once per Radar visit — a smallchoicesfield 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
unknownemails — marketing, newsletters, app notifications and account notices, with no missed receipts - Revised classify prompt tried offline on the same emails —
unknownfell 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 likepromotionback 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
nwiyddpjdeogtyjeelyhand 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.mdcorrected onemail_logand the CT 102 address pin
Lessons
- The August cutover changed only the workflow's draft — n8n runs the
workflow_historyrow named byactiveVersionId, 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 —
couponcollapsed intomarketingas soon asmarketingexisted 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
fetchgives up after 300 s without headers — a CPU-only Ollama queued behind a sweep batch needsnode:http
TODO
- Read the handler brief and answer its six questions
- Delete the Vercel project
budpast the 2FA prompt, then remove.vercel/and the skip-list lines; decide on the Vercel projectbud-app - Rebuild the missing
supabase-taproot-migration-closeout-plan.mdwith 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_idthrough bothsupersede_enrichment()overloads - The daily import now categorises and matches after it runs —
run-import.shlogged "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 foranonandauthenticatedon CT 104 despite migration 008 — revoked
Infrastructure
- Migration 018 dropped the five
anon_read_*policies; legacy Supabase pages and@supabase/supabase-jsremoved from bud-app - Four dead
KMn8n workflows retired with a backup, seven secrets removed from n8n's compose file, and its address pinned to192.168.1.152 - Anthropic client and its config removed from bud — Ollama is the only model path
Lessons
create or replace functionwith a different argument list adds an overload instead of replacing — a secondsupersede_enrichmentwas found only by listingpg_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
tarfrom 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 projectbudis 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.mdis 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
.envfile, 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_logstores only purchase emails, so its age tracked purchases, not the sweep - Gate
diagnow loads the ops.env— found by testing through the gate, which checked the wrong n8n address - Gate
--whytakes 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
.152after CT 102 flipped addresses again - "Ping Kuma" node patched into the Catch-up Sweep (
workflow_entityandworkflow_history) so sweep liveness is measured directly - Branches
feat/ops-triageandfix/freshness-purchase-onlymerged (PRs #14, #13)
Lessons
- SimpleFIN's
errliststayed empty during a bank "Auth required" outage — per-bankbalance-dateage is the honest health signal - A stale persistent
CLAUDE_CODE_OAUTH_TOKENshadows the login for scheduled tasks —claude -preturned 401 under Task Scheduler while the same script worked interactively - PowerShell 5.1 piping text to
sshadds a BOM and a CR — sshd silently skipped theauthorized_keysline; a base64 script fixed it - No flag keeps the global
CLAUDE.mdout of a headless run, and--bareneeds an API key — the gate, not the prompt, has to be the fence
TODO
- Drill B: stop
firefly-importer-1and watch the agent restart it through the gate - Reserve
192.168.1.152for CT 102 in the router - Offsite backup copy and a Proxmox vzdump job
- Bring the
budwiki 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.devbehind 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_rulestable 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/purchaseswas 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 INTOafter CT 102's root disk filled (grown 7.8 GB to 12 GB) Bud — Gmail Purchase Routerunpublished — 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,
/autoimportanswered "Seems to have worked!", and onlydocker logsshowed 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 —
/resumeand 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
uncategorizedrows from the Chase history import - Delete the Vercel project
budby hand — it still holds the old Supabase settings - Remove the cloud
service_rolekey and Kroger secret from n8n's compose file - Push
docs/notes-2026-10-12and 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 CRDpayments 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_liabilityflag routes card payments, returned payments, and opening balances totransfer - 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.pymaps accounts by unique ID prefix, repeatable in one request, all-or-nothing, with a config backup before every writesimplefin_backfill_once.shruns one wider import window and restores the 30-day config through anEXITtrap, 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
--mapinput, and accepted any string as a mapping (including the bare prefixACT-) .env.examplewas 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 withCOPY scripts/
Infrastructure
- A Supabase
service_roleJWT found hardcoded in an n8n workflow export during a pre-push history scan — scrubbed from all 16 commits withgit filter-repo --replace-textbefore the first push, verified absent from every object - Private GitHub remotes created for
budandbud-app— a month of finance work no longer lives on one disk .gitattributesforces LF for*.sh—core.autocrlf=truewould otherwise hand CT 100 a script bash can't parsetests/test_categorizer.pyadded — 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_metaquery 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_rolekey - Move merchant rules to a Taproot
merchant_rulestable 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)fromemail_log, subtracts 1h safety buffer, buildsafter:YYYY/MM/DDGmail 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.jsonv3) — covers Apr 17–May 16 gap period, manual-trigger, limit 50
Bug Fixes
Prefer: resolution=ignore-duplicatesheader alone returned 409 on duplicate inserts — PostgREST requires?on_conflict=message_idas 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_logfor 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-duplicatesneeds two things: thePreferheader 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
ETIMEDOUTon a Google IP — looks like a network failure, not a credential error; check n8n Credentials before debugging connectivity - A gap in
email_logdoesn'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:3bon 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 toollama_client.py— walks the parsed JSON dict before Pydantic validation to replace"null"strings with PythonNone- 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 restartwas used after code edits — command reuses the baked image and silently runs old code; switched todocker compose up --build -d
Infrastructure
src/services/ollama_client.py— new extraction service using httpx against Ollama's/v1/chat/completionsendpoint; 1800s timeout; JSON fence stripping; null-string normalizationsrc/config.pyupdated:anthropic_api_keynow optional (defaults to"");ollama_urlandollama_modeladded- Migrations 003 (unsubscribe support) and 004 (
amountnullable on purchases) applied scripts/find_dupes.py— one-shot soft-delete deduplication script; idempotent; dry-run flagworkflows/backfill/— n8n recovery workflow JSON export and Python backfill runner
Lessons
docker restartreuses the baked image — Python file changes are invisible untildocker 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}/runreturns "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 JSONnull— 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.amountwas required, now optional email_logaudit trail never written when extraction failed — crash happened inextract()beforestore()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:
EmailPayloadon docker-host was missingunsubscribe_linkandis_readfields — 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 inextract()leaves zero trace otherwise
TODO
- Confirm end-to-end with a real purchase email landing in both
email_logandpurchasestables — 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 beforemodel_validate_json()inclaude_client.py
Infrastructure
- Supabase schema live —
email_logandpurchasestables with RLS, indexes, and soft delete .env.examplecommitted — documentsSUPABASE_URL,SUPABASE_SERVICE_KEY,ANTHROPIC_API_KEYservices.mdcreated at~/.claude/services.md— global inventory of cloud platforms, APIs, and self-hosted services; updated by/wrapgoing 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
scpwith~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
budto 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
HandlerResponsereturnsemail_log_idandpurchase_idon success — n8n gets traceable IDs, not just a status stringamount > 0and non-emptyvendorvalidation gates every write — no silent bad data reaches Supabase
Infrastructure
email_logandpurchasestables live in Supabase — RLS enabled, dedup index onmessage_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_logrow written beforepurchasesinsert — 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_clientvscreate_async_client) — sync +asyncio.to_threadis 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