Skip to content

madsostergaard/spirex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

spirex

A self-hosted alternative to Spiir, built on Actual Budget with bank sync via Lunch Flow.

Spiir is shutting down. spirex (Spiir Exit) helps you migrate your data and set up a self-hosted replacement with ongoing bank sync for Danish (and other EU) banks. I have tested it for Lån og Spar so far, and it works.

What you get

  • Actual Budget - open source, self-hosted personal finance app with envelope budgeting
  • Spiir → Actual converter - imports your full Spiir export (accounts, categories, transactions, transfers)
  • Lunch Flow - automated bank sync for 2400+ banks including Danish banks

Prerequisites

  • Docker (or OrbStack on macOS)
  • Node.js (for npx)
  • Python 3.10+ and uv

1. Start Actual Budget

cd docker
docker compose up -d

Open http://localhost:5006, set a password, and create a budget.

For a permanent setup on a home server, consider putting Actual behind a reverse proxy with HTTPS.

2. Import your Spiir data

Export from Spiir

Download your data from Spiir. Three formats are supported:

Format File Transfer detection
Full CSV alle-poster-*.csv Yes
Simple CSV simpel-poster-*.csv No (no IDs)
JSON alle-poster-*.json Yes

Use the full CSV or JSON export for the best result, they contain IDs needed to correctly detect transfers between accounts.

Run the converter (import)

cd converter
uv sync
uv run python cli.py import /path/to/alle-poster-2026-04-10.csv \
  --password YOUR_ACTUAL_PASSWORD \
  --budget "The Name of your Budget"

Options:

flag default description
--url http://localhost:5006 Actual Budget server URL
--password (prompted) Actual Budget password
--budget (required) Budget name in Actual Budget

The converter will:

  1. Parse the Spiir export (auto-detects format)
  2. Create accounts matching your Spiir accounts
  3. Create category groups and categories preserving the Spiir hierarchy
  4. Import all transactions with deduplication (safe to run multiple times)
  5. Import transfers between accounts

3. (Optional) Create rules based on transactions

There's an optional command in the cli that can extract rules based on the given data. The rule creation algorithm is very simple:

  • group transactions by description
  • for each group find the most common (Main category, category)
  • if more than X% of the transactions agree on the category, create rule

The default threshold is 80%.

cd converter
uv sync
uv run python cli.py rules /path/to/alle-poster-2026-04-10.csv \
  --password YOUR_ACTUAL_PASSWORD \
  --budget "The Name of your Budget"

Extra options:

flag description default
--threshold Minimum confidence to create a rule [0.0, 1.0] 0.8
--min-count Minimum number of transactions needed to create a rule 2

When the rules have been inferred, you have the option to either accept all rules, decline them or decide for each individual rule.

Once accepted the rules are imported into Actual Budget.

4. Set up bank sync with Lunch Flow

Lunch Flow connects your bank to Actual Budget via open banking. It supports Danish banks (Danske Bank, Nordea, Jyske Bank, etc.) and 2400+ institutions globally.

Connect your bank

  1. Sign up at lunchflow.app
  2. Go to Connections and add your bank
  3. Go to DestinationsAdd Destination → select API
  4. Copy your API token

Setup merchant normalization

To have the new transactions look more like the ones we expect from Spiir, we can create a normalization function in Lunch Flow for both the Merchant and Description fields.

  1. On lunchflow.app go to Connections
  2. For each account open settings
  3. Go into the Merchant tab, and enable Advanced Mode.
  4. Insert the following into the template field
(it.remittanceInformationUnstructuredArray ?? [])
  .join(" ")
  .replace(
    /^(kontaktløs\s+)?(Dankort(-køb)?|MobilePay køb|Betalingsservice|Visa\/Dankort)\s+/i,
    "",
  )
  .replace(/\s+(Nota|Notanr|Aftalenr\.|Trans\.nr\.)\s+\S+$/i, "")
  .replace(/\s+Nota\s+nr\.\s+\S+$/i, "")
  .replace(/^[A-Z]{3}\s+[\d,.]+\s+Kurs\s+[\d,.]+\s+Nota\s+nr\.\s+\d+\s*/i, "")
  .replace(/\s*[-]\s*\d+$/, "")
  .replace(/\s+\d+$/, "")
  .replace(/\.+$/, "")
  .replace(/\s+/g, " ")
  .trim() ||
  it.creditorName ||
  "Unknown";

This will normalize the bank text and put it into the Merchant field.

Repeat for the Description tab, using the same template.

Get your Actual Budget Sync ID

In Actual Budget: SettingsShow advanced settings → copy the Sync ID.

Run actual-flow

npx @lunchflow/actual-flow

The interactive setup will ask for:

  • Lunch Flow API token
  • Actual Budget server URL
  • Actual Budget password
  • Actual Budget Sync ID

Configure account mappings

On first run, select Configure account mappings to map each Lunch Flow bank account to the corresponding Actual Budget account. This ensures transactions from your bank land in the right account. This is also where you choose which date to start syncing from, so don't skip this step.

If you imported from Spiir first, your Actual accounts will already exist, just match them up by name.

Note

The Lunch Flow account names are probably pretty meaningless to you. They are by default just the account types (at least from Lån & Spar), so creating the account mappings becomes a lot easier if you rename the accounts in the Lunch Flow web-UI before configuring the account mappings.

Automate with cron

To sync daily, add a cron job:

crontab -e
0 8 * * * npx @lunchflow/actual-flow import

Automate with nix + home-manager (macOS)

Add a launchd agent to your home-manager config:

launchd.agents.actual-sync = {
  enable = true;
  config = {
    Label = "com.spirex.actual-sync";
    ProgramArguments = [ "${pkgs.nodejs}/bin/npx" "@lunchflow/actual-flow" "import" ];
    StartCalendarInterval = [{ Hour = 8; Minute = 0; }];
    StandardOutPath = "/tmp/actual-sync.log";
    StandardErrorPath = "/tmp/actual-sync.err";
  };
};

For NixOS (home server), use a systemd timer instead:

systemd.user.services.actual-sync = {
  Unit.Description = "Sync bank transactions to Actual Budget";
  Service = {
    Type = "oneshot";
    ExecStart = "${pkgs.nodejs}/bin/npx @lunchflow/actual-flow import";
  };
};

systemd.user.timers.actual-sync = {
  Unit.Description = "Daily Actual Budget bank sync";
  Timer = {
    OnCalendar = "daily";
    Persistent = true;
  };
  Install.WantedBy = [ "timers.target" ];
};

Project structure

spirex/
├── README.md
├── converter/             # Spiir → Actual Budget importer
│   ├── cli.py             # CLI entry point
│   ├── spiir_parser.py    # Parses all 3 Spiir export formats
│   ├── actual_importer.py # Imports into Actual via actualpy
│   ├── rule_inference.py  # Infers categorisation rules from history
│   └── pyproject.toml
└── docker/
    └── docker-compose.yaml

TODO list

This is a pretty simple project, so it's already fairly complete.

However, there are probably many fancier things that could be done to create meaningful rules based on exported data such as:

  • better normalization of transaction descriptions
  • use some kind of text embeddings to make semantic clusters and derive rules from that
  • probably something with large language models

Contributing

PRs welcome. Run tests with:

cd converter
uv sync
uv run pytest

License

MIT

About

spirex is a self-hosted alternative to Spiir using Actual Budget with bank sync via Lunch Flow. It supplies utilities for importing data and basic rules from spiir data.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages