Overview
Every question you ask goes through a two-stage pipeline: a fast intent router that classifies your question without burning tokens, followed by a Claude Sonnet specialist that runs a deterministic tool-use loop against your real data.Stage 1: Intent router
The router assigns one of five domain labels:cashflow, investment, debt, wealth, or market.
Step 1 — Regex classifier (no LLM call)
About 70% of queries are classified instantly by matching domain-specific keyword patterns:Step 2 — Haiku fallback
If a query matches multiple domains, or matches none, a one-shot Claude Haiku call resolves it:Stage 2: Specialist tool-use loop
The specialist is Claude Sonnet with all tools for the matched domain pre-registered. Claude decides which tools to call, in what order, and when it has enough information to answer.query_transactions + get_budgets) run concurrently via asyncio.gather. Tools with dependencies run sequentially within the same loop iteration.
SSE event stream
The/ai/stream endpoint sends Server-Sent Events. The Flutter app reacts to each event in real time:
Widget responses
When the answer is better expressed as a chart or card, Claude returns structured JSON in thedone event widgets field, which Flutter’s widget_renderer.dart renders natively:
Background pipelines
Two background jobs run outside the request cycle:Daily insights (APScheduler — 2 AM daily)
TheAnomalyAlerts agent runs over the last 90 days of transactions and writes alerts to the user_insights Supabase table. The Dashboard reads from this table in real time.
Three detection methods run on every cycle:
Weekly newsletter (APScheduler — Sunday 3 AM)
TheNewsletterGenerator agent compiles a portfolio summary, top movers, and AI-curated market commentary. Delivered via push notification.
Conversation memory
The Flutter app sends the last 10 messages (5 exchanges) as context with each/stream request. The backend converts Flutter’s {text, isUser} format to Claude’s {role: "user"|"assistant", content} format before passing to the specialist.
All conversations are persisted to Supabase (ai_conversations table) so context survives app restarts.
Security
- Every request carries a Supabase JWT in
Authorization: Bearer <token> - The backend validates the JWT using Supabase’s JWKS endpoint before any data access
- All Supabase queries use the authenticated service client with RLS enforced — a user can never query another user’s data through the AI
- The raw notification text (not the full notification object) is all that reaches Claude for parsing