This is a clean reference export of the reusable pieces from a private personal finance app. It is meant to show one practical approach to replacing a Spiir-style overview with local data:
- fetch bank transactions through Enable Banking / Nordea
- normalize them into a local ledger
- attempt to categorize new Nordea rows from repeated historical ledger matches
- save local category, note, hashtag, date, split, and review overrides
- rebuild a Spiir-style income/expense overview from that local ledger
- optionally import Storebox receipt JSON for receipt/item analysis
This is not a polished starter app. Treat it as a map and code archive for your own project.
The most important external setup is Enable Banking/Nordea access. Start with docs/enable-banking.md if you want to make the bank fetch work.
Related Danish discussion: r/dkfinance post.
These show the kind of workflows the exported code supports.
backend/app/nordea_service.py: Enable Banking/Nordea transaction fetch, raw storage, normalization, status, and taxonomy helpers.backend/app/spiir_local_ledger_service.py: local transaction ledger, override application, sync from Nordea, history-based category suggestions, split migration/repair, and paged transaction responses.backend/app/spiir_service.py: Spiir-style processed overview, income/expense series, rebuild state, hashtag helpers, and summary output.backend/app/local_ledger_overrides.py: shared override normalization/application rules.backend/app/kvitteringer_service.py: Storebox receipt import, SQLite indexes, item clustering, category overrides, and receipt/Spiir linking helpers.backend/app/reference_api.py: slim FastAPI route wiring for the exported modules.backend/app/config.pyandbackend/app/storage.py: small generic replacements for private app config/storage.frontend/src/*Dashboard.tsx: copied React UI surfaces for Nordea/local-ledger review, overview, and receipts.frontend/src/api.ts,frontend/src/types.ts, and helpers: the API client and shared frontend types used by those dashboards.scripts/enablebanking_probe.py: a local helper for listing banks, creating an auth URL, exchanging a consent code, and fetching transactions.
This repo is meant to contain reusable code and redacted examples only.
- If you still have Spiir access, download the full postings export first.
- Read docs/enable-banking.md and create the Enable Banking app/key.
- Copy
env.exampleto.env, replace placeholders, then source it in your shell. - Install the backend and call
/api/statusto verify local storage paths. - Import the Spiir export, then use
scripts/enablebanking_probe.pyto fetch new bank transactions. - Start the frontend after the backend has data to show.
Before Spiir disappears, download your full postings dataset if you can. This gives you historical transactions and a useful category history for matching future Nordea rows. During Nordea sync, uncategorized new rows can be assigned a category when matching historical rows agree strongly enough.
Save the downloaded JSON as:
data/spiir/raw/all_entries.json
Then run the reference API and import it into the local ledger:
curl http://127.0.0.1:8000/api/spiir/local-ledger/preview
curl -X POST http://127.0.0.1:8000/api/spiir/local-ledger/apply
curl -X POST http://127.0.0.1:8000/api/spiir/rebuild-from-localThe export downloader itself is not part of this reference repo. The reusable import code starts from data/spiir/raw/all_entries.json.
Use Python 3.11 or newer.
From this folder:
python3 -m venv .venv
source .venv/bin/activate
pip install -r backend/requirements.txtUseful environment variables:
export SPIIR_ALT_DATA_DIR="$PWD/data"
export ENABLEBANKING_APP_ID="your-enable-banking-app-id"
export ENABLEBANKING_PRIVATE_KEY_PATH="$PWD/data/local_secrets/enablebanking/$ENABLEBANKING_APP_ID.pem"
export ENABLEBANKING_REDIRECT_URL="https://your-domain.example/enablebanking/callback"
export SPIIR_CUTOVER_DATE="2026-01-01"Or use the template:
cp env.example .env
# edit .env first
set -a
source .env
set +aRun the reference API:
uvicorn app.reference_api:app --app-dir backend --reload --port 8000This reference API intentionally has no auth gate, because the point is to show the relevant routes. Do not expose it directly. Put a password/session layer in front of it before real use.
The crucial external part is getting Enable Banking working with a restricted production app for your own accounts. See the full guide: docs/enable-banking.md.
Minimal command path after you have created the app, saved the private key, linked your accounts, and exported the env vars above:
source .venv/bin/activate
python scripts/enablebanking_probe.py aspsps
python scripts/enablebanking_probe.py auth-url --days 170Open the printed URL, approve access in Nordea, then copy the code query parameter from the redirect URL:
python scripts/enablebanking_probe.py session --code "PASTE_CODE_HERE"That writes data/transactions/enablebanking/latest_session.json. The backend fetcher reads that session and calls /accounts/{uid}/transactions for each linked account.
Fetch the first account manually with the longest available history:
python scripts/enablebanking_probe.py transactions --account-index 0 --strategy longestAPI-driven fetch:
curl -X POST http://127.0.0.1:8000/api/nordea/retrieve/start
curl http://127.0.0.1:8000/api/nordea/retrieve/statusThen sync Nordea rows into the local ledger and rebuild the overview:
curl -X POST http://127.0.0.1:8000/api/spiir/local-ledger/nordea-sync/apply
curl -X POST http://127.0.0.1:8000/api/spiir/rebuild-from-localcd frontend
npm install
npm run devThe copied frontend assumes API routes under /api/.... In a real app, run Vite with a proxy to the FastAPI backend or serve the built frontend behind the same origin.
If you export Storebox receipt JSON, place it outside git and point STOREBOX_SOURCE_DIR at it:
export STOREBOX_SOURCE_DIR="$PWD/data/storebox"Then import or rebuild:
curl -X POST http://127.0.0.1:8000/api/kvitteringer/import/default
curl -X POST http://127.0.0.1:8000/api/kvitteringer/rebuild- no fake sample dataset yet; dashboards need fetched/imported local data before they become useful
- reference API has no auth gate; add one before exposing it beyond your own machine
- frontend surfaces are copied from the working app and still larger than a clean starter UI
- API paths still use Spiir/Nordea naming because they mirror the original replacement flow
- license choice is not included yet; add one before encouraging broad reuse




