POST /api/accounts/{id}/adjust-balance wrote its delta as 'expense' (down)
or 'income' (up), so every downward correction surfaced as uncategorized
spending in get_monthly_spending, and every upward one as income. Opening
balances were written as 'income' too, kept out of the income total only by
a description != 'Opening balance' filter.
Backend:
- adjust_balance (routers/accounts.py) and create_account (crud.py) now
write type='adjustment' with the signed amount. Balances are unaffected:
SUM(amount - fee) ignores type.
- get_monthly_spending drops the description filter; spending and income
select purely by type.
- TransactionBase gains 'adjustment' for reads; TransactionCreate does not,
so only the server can write one.
- Migration j0e1f2a3b4c5 retypes historical rows: unlinked income/expense
rows with the exact generated descriptions. Downgrade restores the
typing from the sign of the amount.
Tests: cli/tests/test_adjustment.py covers spending/income exclusion, the
balance landing on target, and the migration + downgrade on a scratch DB.
test_cli's opening-balance assertion updated to the new type. 65 passed.
Docs: CLAUDE.md type list and invariant 6; spec in
docs/superpowers/specs/2026-09-30-adjustment-transaction-type.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fin
A self-hosted, single-user personal net worth tracker. fin aggregates bank accounts, investment portfolios, Splitwise balances, and receivables into a single EUR-denominated view, with daily automated snapshots and live stock prices.
Features
- Dashboard — current net worth broken down by account group, plus recent transactions
- Spending — monthly expense breakdown by category with a pie chart
- Portfolio — investment account holdings with live USD prices (via yfinance), P&L per position, and EUR conversion
- History — net worth over time as a line chart; snapshots taken automatically at midnight
- Transactions — full transaction log across all accounts
- Quick-add flows — mobile-friendly sheet for logging expenses, transfers, Splitwise splits/settlements, and stock trades
- Settings — manage accounts (bank, investment, RSU, Splitwise, receivable) and spending categories; manual balance override per account
- Multi-currency — EUR base currency; exchange rates via Frankfurter API
Quick Start
git clone <repo>
cd fin
docker compose up -d
The app is available at http://localhost.
Environment / Config
Set in docker-compose.yml under the api service:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:////data/fin.db |
SQLite path inside the container |
FIN_API_TOKEN |
(unset — open) | Bearer token the API requires; leave unset for no auth |
CLI / MCP environment variables (set in the shell or MCP client config):
| Variable | Default | Description |
|---|---|---|
FIN_API_URL |
(required) | Base URL of the fin API, e.g. https://fin.example.com |
FIN_API_TOKEN |
(unset) | Bearer token for the API (only if the API enforces auth) |
FIN_MCP_ALLOW_WRITES |
(unset — read-only) | Set to 1 to enable the 24 write tools in the MCP server |
FIN_MCP_TRANSPORT |
stdio |
stdio, or streamable-http to run as a network service |
FIN_MCP_HOST / FIN_MCP_PORT |
0.0.0.0 / 8000 |
Bind address, streamable-http only |
FIN_MCP_PATH |
/mcp |
Path MCP is served at, streamable-http only |
Data is persisted in a named Docker volume (sqlite_data).
MCP server
fin-mcp exposes the full fin API as structured tools. Default mode is
read-only (15 tools). Set FIN_MCP_ALLOW_WRITES=1 to additionally enable
24 write tools (create/update/delete transactions, accounts, stock trades, etc.).
Enum-valued arguments are typed, so the client rejects invalid values before the
call. create_transaction deliberately accepts only expense and income:
two-leg writes must go through create_transfer, create_settlement,
create_split_expense or create_splitwise_i_paid, or the ledger ends up with
an orphan leg.
Use find_account / find_category to turn a name into the integer id the
other tools take. Both lists are also published as the resources
fin://accounts and fin://categories.
create_transaction, create_transfer and create_settlement accept an
optional client_ref. Retrying a failed call with the same key returns the
write that already happened instead of booking it twice — worth setting on any
write an agent might have to retry.
Transports. stdio (default) suits clients that spawn the process.
streamable-http runs it as a service — it binds FIN_MCP_HOST:FIN_MCP_PORT
and serves MCP at FIN_MCP_PATH:
FIN_API_URL=http://localhost:8000 FIN_MCP_TRANSPORT=streamable-http fin-mcp
In Kubernetes it runs as its own Deployment and ClusterIP Service — see
deploy/k8s/fin-mcp.yaml. That manifest has no HTTPRoute on purpose: the MCP
server has no auth of its own, so with writes enabled it must stay unreachable
from outside the cluster unless FIN_API_TOKEN is set on both the API and the
MCP server.
Install:
pip install -e 'cli/[mcp]'
Run manually:
export FIN_API_URL=https://fin.example.com
fin-mcp # stdio; connect via your MCP client
Claude Desktop / Claude Code config (~/.claude/mcp.json or equivalent):
{
"mcpServers": {
"fin": {
"command": "fin-mcp",
"env": {
"FIN_API_URL": "https://fin.example.com",
"FIN_API_TOKEN": "...",
"FIN_MCP_ALLOW_WRITES": "0"
}
}
}
}
Remove FIN_API_TOKEN if the API runs without auth. Change FIN_MCP_ALLOW_WRITES to "1"
to permit write operations (confirm amounts, accounts, and dates with the user before each call).
Architecture
The backend is a Python FastAPI app backed by SQLite via SQLAlchemy; APScheduler runs a nightly job to fetch stock prices (yfinance) and currency rates (Frankfurter) and record a net worth snapshot. The frontend is a React + TypeScript SPA served by Nginx, communicating with the API through the same Nginx container via reverse proxy.