Skip to content

Repository files navigation

Kist

A self-hosted, local-first personal finance & career command center.

A kist is an old word for the chest where you keep what's valuable — at home, in your own hands. This one turns scattered personal finances and career signals into one decision cockpit. Runs entirely on your machine — a Flask backend, a single SQLite file, and a vanilla-JS frontend with no build step.

▶ Try the live demo — the real UI with a fake persona ("Alex Demo"), fully clickable, read-only. Static snapshots on GitHub Pages; nothing you click is saved and no backend is involved.

Your data never leaves your machine. Everything lives in one local SQLite file, stored outside the repo by default (~/Library/Application Support/Kist on macOS, ~/.local/share/kist on Linux) — no database server, no cloud account. This repo ships no personal data, a first-run wizard sets you up, and a demo mode masks every figure for safe screenshots.

What problems does it solve?

Mostly my own. Too many apps to check, a spreadsheet I kept forgetting to update, and a nagging feeling that every finance tool out there wants my data in its cloud more than it wants to help me. So I built the small local tool I actually wanted — nothing revolutionary, it just answers my own everyday questions. If it answers yours too, great; that's the only reason it's public.

  • "My money is scattered across 10 apps and a spreadsheet" — one dashboard: net worth, cash-flow, debts, goals, investments, taxes.
  • "When will I actually reach my goal?" — month-by-month projections: goal ETA at your savings pace, FIRE/work-optional crossover, overpay-vs-invest scenarios with saved-interest math.
  • "Should I sell my vested stock? Overpay the mortgage? Convert currency now?" — opinionated, data-grounded guidance: an FX signal engine with a historical backtest, RSU Monte-Carlo on real volatility, debt-overpayment simulations.
  • "I don't trust cloud finance apps" — local-first by design; optional cloud integrations touch only public market data, never your numbers.
  • "Is the world nervous today?" — a 🌍 Risk Radar in the Market tab: VIX, gold, oil and USD with explicit thresholds blended into one calm/elevated/hot reading, backfilled a month and refreshed daily — a measurable take on the meme "pizza index" idea (it gives context; it doesn't predict).
  • "AI assistants read my finances on someone else's server" — here the AI is built in and runs on your machine: a local LLM reviews your recommendations, narrates forecasts and answers questions grounded in your own numbers — with an explicit, off-by-default switch if you ever want a cloud model's second opinion.
  • "Forecasts that admit what they don't know" — research-grounded modeling: short-horizon range forecasts (EWMA volatility + empirical quantiles, because the direction of a single stock or FX rate is not predictable and the app doesn't pretend otherwise) and long-horizon labeled scenario bands. The forecast journal grades itself daily (band-coverage vs an 80% target) and self-calibrates on its own past errors (conformal calibration). Every band can be explained in one sentence.

Quick start

git clone https://github.com/grzella/kist-finance.git
cd kist-finance
pip install -r requirements.txt
./run.sh                      # → opens http://127.0.0.1:8321 (prefers ./.venv if present)

That's it. On first launch a setup wizard walks you through:

  1. Modules — enable only what fits your life (see below).
  2. Data — three ways to start: load a fake sample persona to look around, start empty and type your own numbers, or pick "Set it up with an AI assistant" — a copyable onboarding prompt that has any AI assistant interview you and fill the app tab by tab (with a privacy warning: cloud assistants take your answers off localhost).
  3. Integrations — optional; skip freely, the app is fully functional offline.

Re-run the wizard anytime at http://127.0.0.1:8321/#wizard. Use a different port with PORT=8400 ./run.sh.

Modules

Core (always on):

Module What it does
📊 Dashboard net worth and key figures at a glance
💧 Cash-flow income vs. expenses, monthly surplus
💡 Recommendations rule-engine guidance from your data (+ optional AI): loan-vs-market compared after capital gains tax, one definition of essential costs, a cushion that counts cash + a haircut on brokerage (retirement accounts separately), and a memory — each recommendation shows since when it has been open, resolved ones are kept, and each gets an outcome (done / rejected / obsolete; a rewording with new numbers supersedes the old entry and keeps its age) with a monthly review of the execution rate
💎 Wealth assets and net worth over time, with snapshots
🧾 Fixed Expenses recurring costs grouped by entity (personal/business/rental…) and subscription type (work/entertainment/health/other), each in its own currency (converted at the cached rate) with a monthly/yearly billing toggle, month-to-month trend, an "invoiced" tag for expense/tax tracking, and data-driven cost hints (e.g. which subscriptions bill monthly and what an annual plan would save) — an item you don't touch just carries its last amount forward, so there's no monthly copy-paste
🥧 Allocation portfolio breakdown vs. targets, 5/25 drift
🎯 Goals savings goals with ETA as a range
🔮 Forecasts range forecasts, FIRE crossover with an inflation-indexed target, contribution growth, an after-tax series and a cone bootstrapped from a benchmark's real returns, a 24-month net-worth cone with scenario toggles (bonus, employer stock ±30%, USD ±10%, real terms), forecast calibration per ticker × horizon (coverage judged on non-overlapping windows, so a gap within noise reads "too few windows" instead of a false alarm; miss direction, Winkler score, VIX regime), stress test + guardrails
📐 Ratios eleven personal-finance ratios with traffic lights vs targets (savings rate, essential share, cushion months, debt service / income, liquid / debt, investment share, employer concentration, real estate share, FX exposure, effective cost of debt, net worth m/m) — stored as a monthly series plus a weekly wealth point, so you can see each trend over time
🛠️ Control Center status, backups, security review, data inventory, reminders; every scheduled task's last failure is visible here, and when the server runs older code than the files on disk a banner offers a one-click restart. An 🎓 AI improvement tab holds everything AI: mode, the ask box, the prompt log with 👍/👎, learned lessons, a red/amber/green checklist of the AI learning loop and a research-backed improvement roadmap

Optional — toggle in the wizard, disabled ones disappear from the UI:

Module What it adds
🏠 Loans principal/interest split, effective rate, overpayment scenarios, history with event markers (overpayments and bank corrections drawn as points, not sawtooth), a monthly reference-rate refresh task (PLN base)
🏛️ Taxes consolidated tax sources + payment calendar, including capital gains tax on the year's RSU sales — logged in the RSU tab, due next April, and deducted from net worth as a reserve until paid
📈 Market & FX watchlist, price analytics, currency signal engine with backtest, daily 🌍 Risk Radar, on-demand keyless history backfill
💎 Equity / RSU a vest schedule built from the grant list (legacy grants expire, new ones start on their own dates, cash-vest included), a sales log that feeds the tax reserve, net-vs-gross values everywhere downstream (cash-flow, goals, FIRE), Monte-Carlo projection on a block bootstrap of real returns (the same engine scores its own backtest and forecast journal), sell-vs-hold guidance
🚁 Business revenue/costs of self-employment or a side company
💼 Career inbound job offers, market barometer (a missing month is called out with the collector's last error, never silently skipped), commit-activity tracker, and a leader's evidence log: dated impact / visibility / scope / feedback / learning entries with a metric and proof link, the week in four numbers, a 90-day plan checklist
🏡 Property analysis deep-dive for a property-purchase goal

Connecting your own services (all optional)

The app runs fully offline. Live market data and alerts are opt-in:

  • Market data (stock/FX quotes) — the app reads public prices from a Supabase table:
    1. Create a free Supabase project with tables market_prices (ticker, date, close, currency) and market_watchlist (ticker, notes).
    2. Put keys in .env (copy .env.example): SUPABASE_URL=…, SUPABASE_ANON_KEY=….
    3. Feed the table daily however you like — e.g. an n8n workflow pulling quotes from Yahoo/Stooq. Without this, market views simply show "no data".
    • For any watchlist symbol the nightly sync doesn't cover deeply, a per-ticker Deepen history action backfills its full history straight from Yahoo (keyless, no setup) so the chart and indicators have depth.
  • Data-freshness alerts (n8n → Telegram) — importable workflow in integrations/n8n/ that messages you when the pipeline goes stale. Setup guide in its README.
  • Market barometer (built-in collectors) — the Career tab's barometer tracks your roles (configurable roles + geography, edited in the tab) as several series: 📈 interest (Google Trends search interest — keyless, with history, collected monthly by the app's barometer_collect schedule, one-off backfill() for history), 🏢 open roles (a monthly snapshot of real postings for your roles in Europe on the public Greenhouse/Lever/Ashby boards of an editable company watchlist — keyless, barometer_watchlist), 🌍 IT postings in Europe (Indeed Hiring Lab sector indices for GB/DE/FR, keyless history since 2020, barometer_hiringlab) 🗂️ job boards (monthly count of postings per role on No Fluff Jobs, keyless, barometer_boards; the region is boards_region in the barometer config) and 🎯 openings (real posting counts via JSearch/Google for Jobs — set RAPIDAPI_JSEARCH_KEY). Shown as an index (base 100) + trend per role×stream, not a raw count, next to your inbound (with a 3-month average) and an average-offered-range chart. External collectors still work via POST /api/market-barometer — two importable n8n workflows (barometer-jsearch-collector.json, barometer-apify-collector.json) remain an alternative; see their README for the methodology.
  • Stress test & withdrawal guardrails — a deterministic "financial fire drill" (stocks −25%, rates +2pp, income stops) plus a Guyton-Klinger dynamic-withdrawal policy with guardrails in Forecasts; allocation drift follows the 5/25 rebalancing rule with editable targets.
  • Keeping the forecast journal learning daily — the self-calibration only advances when the cycle runs, and the cycle is triggered from /api/health, i.e. whenever you happen to open the app. Open it irregularly and the journal gets multi-day holes, so the bands calibrate on a patchy error history. forecast-daily.sh closes that: it calls the engine directly (no HTTP, no running server), refreshes quotes, runs the cycle, takes the risk-radar reading, and logs the current band coverage so the log itself tells you whether learning is on track. Schedule it once — macOS launchd (a StartCalendarInterval agent) or a cron line such as 40 7 * * * /path/to/kist/forecast-daily.sh. Missing history can also be seeded in one shot: market.backfill_forecasts() replays walk-forward forecasts over past data using only what was known on each day, so the conformal calibration starts with real material instead of waiting a quarter.
  • Built-in private AI — the app never requires an LLM to function, and it ships the full client (server/llm_local.py) for a local llama.cpp server, so AI features run on your machine and your numbers never leave it. See "Built-in private AI" below.
  • Commit tracker — set commit_repos / commit_author in settings or COMMIT_REPOS / COMMIT_AUTHOR env vars.

Built-in private AI

AI is a core feature and is designed to run on your machine. The app ships the whole AI stack (client, grounding, prompt log, safety checks); the only thing it cannot ship is the model weights themselves (gigabytes, licensed separately) — you fetch a model once with a single command below, and the AI features switch on. No model running? Every AI feature degrades gracefully to "offline" and the rest of the app is unaffected.

How the local LLM works. You run a small open model (recommended: Qwen3 8B — its toggleable thinking mode measurably helps multi-step financial math; any GGUF works) with llama.cpp's llama-server — a local process exposing an OpenAI-compatible API on localhost. The app talks to it over HTTP; no API keys, no data egress, no per-call cost. Because it speaks the OpenAI API, the same setup works with LM Studio or Ollama. Where the answer must be machine-readable, the app sends a JSON Schema and llama.cpp enforces it at the token level (GBNF grammars), so the output always parses against the schema. On Qwen3 the app toggles thinking per task: on for analysis, off for quick structured calls.

brew install llama.cpp        # or build from source; any OpenAI-compatible server works
llama-server -hf bartowski/Qwen3-8B-GGUF:Q4_K_M --port 8080 --api-key <secret> \
    --spec-type ngram-simple  # free speedup: drafts repeated n-grams (great for JSON), no second model needed

Then set LOCAL_LLM_KEY=<secret> (and optionally LOCAL_LLM_URL) in .env. Control Center shows the model's status, and the security review actively probes the local server to confirm it rejects keyless requests. Without a running server, AI features simply report "offline"; nothing breaks.

What the AI actually does in the app:

  • AI second opinion on Recommendations — the rule engine computes recommendations from your data; the AI reviews them (agrees/disagrees, what's missing) with your own numbers as context. One click in the Recommendations tab.
  • Forecast narration — turns the self-learning forecast journal into a plain-language why.
  • A grounded ask-anything box (Control Center → AI improvement) plus a keyless /api/llm/chat hook for your own scripts.
  • It queries your actual numbers — the local model gets one tool: a read-only SQL SELECT against your database (server/db_tools.py). Asked "how far am I from my goals?", it queries the actual tables instead of guessing from text snippets. Defense in depth: the connection is opened read-only at the SQLite level, only a single SELECT passes validation, results are capped, and every round-trip lands in the prompt log — and the security review actively pentests this guard (injection/DDL/stacked-query payloads must all be refused). Servers without tool support just fall back to plain answers.

AI mode — local by default, cloud strictly opt-in. The Control Center AI mode switch governs all of the above. Default: local only — every AI call stays on your machine. Flip to local + Claude and the app asks both engines, then synthesizes one verdict from the two answers; the cloud model defaults to Anthropic's newest (claude-fable-5, configurable via CLOUD_LLM_MODEL). The UI warns plainly that this mode sends the prompt and snippets of your data to Anthropic — set your own ANTHROPIC_API_KEY in .env to enable it. All AI answers are framed by a financial-analyst system prompt (explicit assumptions, scenario ranges, opportunity-cost/tax, a one-line bottom line), and every question/answer is recorded in a local prompt log (Control Center) so you can see what was asked and whether the AI helps.

Local RAG — answers grounded in your own numbers. Before the AI answers, the app hands it the matching snippets of your data — goals, wealth items, offers, business entries, saved analyses, plus computed recommendations and reminders — from a local rag_chunks index (pure stdlib, fully offline). So the model reasons about your figures. The index maintains itself: any data write marks it stale and it reindexes before the next AI answer (plus a scheduled refresh); the Refresh memory button in Control Center remains for forcing it.

Retrieval is BM25 (lexical) out of the box, and upgrades to a BM25 + semantic hybrid if you point it at a local embedding server — then a question can match by meaning even with different words or across languages ("saving for retirement" finds your "pension account"):

# recommended: Qwen3-Embedding-0.6B (~600 MB, strong multilingual retrieval)
llama-server -hf Qwen/Qwen3-Embedding-0.6B-GGUF:Q8_0 --embeddings --pooling last \
  --batch-size 2048 --ubatch-size 2048 --port 8081
# in .env:
LOCAL_EMBED_URL=http://127.0.0.1:8081/v1

Embeddings are stored per chunk (L2-normalized) and cosine similarity is computed in plain Python — no vector DB, no SQLite extension. Without an embedding server everything stays lexical; Control Center shows how many chunks are embedded. Two hard-won notes: raise --ubatch-size — non-English text tokenizes densely, and with the default 512 a ~2000-char chunk can crash llama-server mid-reindex (the app then quietly falls back to lexical); and run the server persistently (launchd/systemd), because the self-maintaining reindex embeds new data only while the server is up — if it's down, the rebuilt index degrades to BM25 until the next reindex with the server alive. That degrade-and-recover contract is covered by a regression test. A reindex carries the embeddings of unchanged chunks over, so after a small data change it takes seconds and runs by itself before the next question.

Optionally add a third retrieval stage — a reranker: the hybrid picks ~20 candidates, a small cross-encoder re-orders them by true relevance and only the top few reach the model. Same graceful pattern:

llama-server -hf gpustack/bge-reranker-v2-m3-GGUF --embedding --pooling rank --port 8082
# in .env:
LOCAL_RERANK_URL=http://127.0.0.1:8082/v1

Experience distillation — the assistant learns from its good answers. When an answer is genuinely useful, one click (💡 Learn from this, in the prompt log of the AI improvement tab) asks the model to compress that Q&A into a single transferable lesson — the method and the pitfall, not your specific numbers. The lesson is stored, indexed into the same RAG memory, and injected as guidance on future similar questions — so the assistant gets better over time without any retraining or weight changes (the pattern from AI Agents in Depth, ch. 8). It's deliberately human-gated: only answers you mark become lessons, and a 🧠 Learned experiences panel lets you prune any that don't hold up. Everything stays local.

Ratings and the AI improvement tab. Every logged answer gets 👍 or 👎. A rating does not change the answer: it is attributed to the lessons that were in that answer's context, so the lessons list shows how often each lesson was used and how its answers were rated, and flags the ones that hurt more than they help. 💡 Learn from this counts as a 👍. At most one lesson enters the context at a time, so similar lessons cannot crowd out your data. The tab's checklist sorts the learning loop red / amber / green (broken / your move / OK or runs by itself): model, RAG memory, ratings, lessons, quality eval, recommendation outcomes, price forecasts, data freshness and failing scheduled tasks. Below it, a roadmap lists improvement ideas with the evidence behind each and the criterion that tells you it worked, including the ones that were rejected and why.

Quality eval on a schedule. Point the ai_eval_cmd setting at your eval harness (a JSON list of arguments, run without a shell; the app appends --record <data>/eval/run-<date>.json, and the harness writes {ts, model, passed, total} there). The daily ai_eval task runs it only when there is a reason: the model changed, 30 days passed or a gold*.json question set changed, and only while the local model is online. The tab shows the score over time.

Market brief — daily & weekly, written by the AI. The Market tab keeps two briefs: a daily one (regenerated every morning) and a weekly one (Monday mornings) — toggle between them in the tab. Each is generated from your cached quotes and the risk-radar state by the engine your AI mode selects (local only, or cloud-first with local fallback in local+Claude mode), schema-locked so it always renders. The daily view has a 🔄 Fetch latest button: it pulls fresh quotes and rewrites the brief on the spot (e.g. you open the app at noon and want more than the morning run). You can still paste your own brief (weekly box) — a saved brief is never overwritten by a failed generation. Cadence is editable in Control Center → Data → Schedules.

Risk-radar Telegram alert. When the radar composite goes hot (🔴, ≥4/8), the app can ping you on Telegram — a signal to investigate, not to act. Two ways, pick either:

  1. Built-in (app running): set TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID in .env — the daily radar snapshot sends the alert itself (at most once a day, with the drivers and the AI one-liner).
  2. Standalone (works with the app off): import integrations/n8n/risk-radar-telegram-alert.json into n8n — it recomputes the same composite from Yahoo every morning and alerts on the same threshold.

Backups

Your data is one local SQLite file, so a backup is a copy of it. Control Center's Data backup card writes a consistent snapshot (SQLite's online-backup API, WAL-safe) into a folder your desktop Google Drive / Dropbox / iCloud client already syncs. The app itself talks to no cloud API and holds no OAuth keys — your own sync client pushes the file. Common synced folders are auto-detected; the last 14 snapshots are kept. For at-rest encryption, pip install cryptography and set BACKUP_KEY in .env — snapshots are then Fernet-encrypted before they touch the cloud.

Data & privacy

  • Everything is local out of the box. No account, server or cloud dependency: the whole app runs against a single local SQLite file. Live market data and alerts are the only cloud touchpoints, and they're opt-in (see above).
  • Your data lives outside the repo by default. A fresh clone stores its database in a per-user app-data dir (~/Library/Application Support/Kist on macOS, ~/.local/share/kist on Linux) — so a stray git add can't stage your finances. An existing ./.finance/ keeps working in place, and FINANCE_PROJECT_DIR=/path ./run.sh overrides the location.
  • .finance/, .env, and backups/ are git-ignored — never commit them.
  • seed.py refuses to overwrite existing data (use --force only on a throwaway DB).
  • Demo mode (Control Center or ?demo) masks all figures with a 0-1 pattern and hides chart axis values — safe screenshots.
  • Language: the UI is English-only; i18n contributions welcome.
  • Saved analyses know when they are stale: any analysis snapshot with an as_of date shows a warning once the underlying data (RSU file, loans, wealth, expenses) changed after it.

Security & tests

This repo is built to be safe to fork and contribute to:

  • Loopback hardening (no auth by design). The API is keyless because the model is "one user, on their own machine, over loopback." A before_request guard defends the two attacks that need only your browser: it rejects any non-loopback Host (DNS rebinding) and blocks cross-origin state-changing requests (CSRF). Defense in depth: a strict Content-Security-Policy (script-src 'self') neutralizes injected markup even in unescaped fields, external data (e.g. Supabase market text) is HTML-escaped, and RAG/DB content is framed to the model as data, not instructions. Do not expose the app beyond 127.0.0.1 — see SECURITY.md for the full threat model.
  • server/security_review.py — a pentest-style suite: secret scan of the working tree and the full git history, dangerous-pattern static analysis (eval/exec, shell, SQL injection, debug, network binds), maintainer personal-data audit, config hygiene, endpoint smoke tests, an active probe of any local LLM server (must reject keyless requests), an active pentest of the AI's SQL tool (injection/DDL/stacked-query payloads must all be refused, the connection read-only at the SQLite layer), and an active pentest of the web guard — a forged Host and a cross-origin write must both return 403, so a regression fails the build.
  • Runs three ways: Control Center button, CLI (cd server && python -m security_review --ci), and GitHub Actions on every push/PR + weekly (.github/workflows/security.yml).
  • Test suite — pytest in tests/ (endpoint smoke across every GET route, goals/wealth CRUD, forecast math incl. the bootstrap engine, the grant schedule and tax reserve, RAG ranking incl. the semantic hybrid, backup/restore round-trip, the wizard's module→view gating, scheduled-task failure tracking, the AI improvement checklist, rating attribution, the restart endpoint) — about 190 tests.
  • Personal-data audit as a test — two tests scan every tracked file for a maintainer marker list (names, employer, cities, private hosts/ports, key shapes) and fail the build on a hit; the same list guards the security_review --ci gate. CI enforces a coverage floor and a bandit security-lint gate; Dependabot watches dependencies. Run locally: python -m pytest -q.
  • LLM contract tests (tests/test_llm_contract.py) — a distinct kind of test for the AI surface: the model is mocked and the tests assert the invariants the app guarantees around it (rather than what it says). Graceful degradation when the model is offline, the shared pipeline always logs and returns a best, local mode never calls the cloud, and a failed brief never overwrites the last good one — the harness contract, kept deterministic and fast.

Tech

Python 3 + Flask, SQLite (WAL, single file), vanilla JS + Chart.js. Self-contained SQLite layer (server/db.py) — no dependencies beyond Flask. Domain logic lives in server/planner_*.py (core, wealth, expenses, goals, career, debts, recs, business, cashflow, allocation, freshness, ops, fire) behind the planner facade; metrics.py (ratios, trajectory), em.py (leader's log), market.py (prices, RSU, forecasts, FX).

Changelog

See CHANGELOG.md.

Contributing

Issues and PRs welcome — see CONTRIBUTING.md for setup, the two CI gates, and ground rules. Good first areas: i18n (new languages), market-data adapters, new forecast models, and UX. Clone, run ./run.sh, load sample data via the wizard, and you have a working playground.

License

MIT © Łukasz Grzella