Self-hosted personal finance tracking, built around Swiss e-banking.
FinPilot imports your bank's e-banking exports and categorizes every transaction, and gets better each time you correct it. It keeps a lean view of your ETF and stock holdings with honest pricing (stale quotes and failed FX lookups are flagged, never papered over), and exposes a small, key-authenticated API so a personal AI agent can read your financial state and fix data mistakes, with no access to anything else.
It runs my own household's finances daily. If you're here to read code, start with Architecture and the import pipeline under Features.
- Bank statement import: camt.053 XML (ISO 20022), Valiant/PostFinance/Viseca CSV, plus a generic column-mapper for anything else, all with automatic dedup.
- Auto-categorization that learns: keyword rules → merchant database → transaction history → a per-user ML classifier, falling back to AI only for genuine unknowns. The AI fallback is a per-user setting: each user brings their own Anthropic API key, any OpenAI-compatible endpoint (Ollama, OpenRouter, OpenAI), or a custom gateway, and can enable/disable it and test it from Settings.
- Cumulus settlement auto-reset: detects the monthly Migros Cumulus (Cembra) card-bill payment on the linked bank account and resets the credit card balance to zero, idempotently, so re-importing the same statement is a no-op.
- Holdings-only portfolio: current ETF/stock positions with live pricing, a staleness indicator, and explicit FX-rate warnings instead of silent fallbacks. No AI stock analysis or trade recommendations: that experiment was tried, didn't work, and was removed.
- Selective watchlist: track a handful of tickers (price + day change) without implying you hold them or want advice on them.
- Customizable dashboard: a widget registry with per-user visibility and ordering.
- Recurring-payment detection: surfaces subscriptions and bills from transaction history with an upcoming-payments view.
- Budgets & savings goals: simple caps and targets checked against real transaction data.
- Read-only Saxo Bank sync: pulls real brokerage positions in without any order-placement capability.
- Agent gateway: a thin,
X-API-Key-authenticated API surface so a personal AI agent can read financial state and fix data mistakes (merchant renames, recategorization, corrections) without touching the database directly.
Imported and categorized bank and card activity |
Holdings with live prices and performance |
Monthly income, expenses, and category breakdown |
graph TD
Browser["Browser"] --> NextJS["Next.js 16 (App Router)"]
NextJS -->|same-origin proxy: /api/* route handler| API["FastAPI"]
API --> Services["Service layer\n(import, categorize, settlement, portfolio, ...)"]
Services --> DB[(PostgreSQL 16)]
GmailJob["Gmail scheduler\n(background)"] -.->|poll bank email notifications| Services
SaxoJob["Saxo scheduler\n(background)"] -.->|periodic position sync| Services
Services -->|price quotes| Yahoo["Yahoo Finance"]
Services -->|read-only holdings sync| Saxo["Saxo Bank OpenAPI"]
Services -->|categorization fallback only| Claude["Anthropic Claude"]
Agent["Personal AI agent"] -->|X-API-Key, /api/agent/*| API
The frontend never calls the backend cross-origin: frontend/src/app/api/[...path]/route.ts
proxies same-origin requests to the FastAPI service, so the browser only ever
talks to Next.js. Two background schedulers run inside the API process:
services/gmail_scheduler.py (polls for bank email notifications) and
services/saxo_scheduler.py (periodic read-only position sync), both started
in app/main.py's lifespan handler.
| Layer | Technology |
|---|---|
| Frontend | Next.js 16 (App Router), TypeScript, Tailwind CSS, TanStack Query, Recharts |
| Backend | FastAPI, Python 3.12, SQLAlchemy (async), Alembic |
| Database | PostgreSQL 16 |
| Auth | JWT (PyJWT) for the dashboard, X-API-Key for agent endpoints |
| ML | scikit-learn (per-user TF-IDF + SGD categorization classifier), thefuzz for merchant matching |
| External data | Yahoo Finance (quotes), Saxo Bank OpenAPI (read-only sync), Anthropic Claude (categorization fallback) |
| Infra | Docker Compose |
- Docker & Docker Compose
- Python 3.12+ (local backend dev only)
- Node 20+ (local frontend dev only)
git clone https://github.com/your-org/finpilot.git
cd finpilot
cp .env.example .envEdit .env, at minimum setting SECRET_KEY and POSTGRES_PASSWORD:
# Generate a strong key:
python3 -c "import secrets; print(secrets.token_urlsafe(48))"docker compose up -dThe api container runs alembic upgrade head automatically on boot (see
backend/MIGRATIONS.md if you're migrating an
existing pre-Alembic database rather than starting fresh).
| Service | Default URL |
|---|---|
| Web (Next.js) | http://localhost:3010 |
| API (FastAPI) | http://localhost:8001 |
| API docs | http://localhost:8001/docs (dev only, disabled when ENVIRONMENT=production) |
SEED_EMAIL=you@example.com SEED_PASSWORD=changeme \
docker compose exec api python seed.pyAdd --demo to also generate ~6 months of deterministic, entirely fake demo
data (salary/rent/groceries/insurance, a full Cumulus settlement cycle, a
small ETF/stock portfolio, budgets, a savings goal, recurring expenses, and a
watchlist) so every feature has something to show:
docker compose exec api python seed.py --demoAll variables are documented in .env.example.
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
yes | none | JWT signing key (≥ 32 chars) |
POSTGRES_PASSWORD |
yes | none | Postgres password |
POSTGRES_USER / POSTGRES_DB |
no | finpilot |
Postgres user/database name |
DB_PORT / API_PORT / WEB_PORT |
no | 5432 / 8001 / 3010 |
Host ports for Compose |
ENVIRONMENT |
no | development |
Set to production to disable /docs |
CORS_ORIGINS |
no | http://localhost:3000,http://localhost:3010 |
Allowed CORS origins |
ANTHROPIC_API_KEY |
no | none | Server-wide default Anthropic key for the AI categorization fallback; each user can override it (and pick a different provider) in Settings |
ANALYSIS_MODEL |
no | claude-haiku-4-5-20251001 |
Default model for the categorization fallback |
See .env.example for the full, current list.
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest tests/ -x -qTests run fully offline against an in-memory SQLite database: no Postgres,
network access, or Anthropic API key required. CI (.github/workflows/ci.yml)
runs this suite against Postgres on every push/PR, plus a frontend type-check
and production build.
cd frontend
npm ci
npx tsc --noEmit
npm run build # production build
npm run dev # dev server on :3000docker compose exec api alembic upgrade headSee backend/MIGRATIONS.md for the baseline
reconstruction history and which path applies to an existing database.
External AI agents authenticate with the X-API-Key header (issued per-user
by seed.py, or visible in account settings). All agent endpoints live under
/api/agent/* and share the same service layer as the dashboard, so behavior
can't drift between the two. See the API docs (dev only) or
backend/app/api/endpoints/agent.py /
agent_finance_router.py /
agent_portfolio_router.py
for the route map.



