Skip to main content

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:
Haiku returns a domain label in ~200ms. The cost per call is negligible (< $0.001).

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.
Tools that fetch independent data (e.g. 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 the done 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)

The AnomalyAlerts 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)

The NewsletterGenerator 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