Personal budgeting system built on Actual Budget (self-hosted) with automated transaction imports from Canadian banks and Splitwise integration.
- Install Docker and Node.js (v20+)
- Clone this repo and install dependencies:
npm install
- Start Actual Budget:
./start.sh
- Open http://localhost:5006, create a budget and your accounts
- Copy the config template and fill in your values:
cp config/accounts.example.yml config/accounts.yml
- Fill in the config — see Finding Account UUIDs below
The config needs Actual Budget account UUIDs (not names). To find them:
node src/cli.js balancesThis prints all accounts with their UUIDs. Copy the UUIDs into config/accounts.yml.
You need at minimum the actual.serverURL, actual.password, and actual.budgetId filled in before running this. The budget ID (Sync ID) is found in Actual Budget UI under Settings > Advanced.
All banks are imported through the CLI. Drop files into imports/ and run:
# Import everything (CSV + OFX)
node src/cli.js import ./imports/
# Preview without importing
node src/cli.js import --dry-run ./imports/
# Import a specific file
node src/cli.js import ./imports/td_chequing-2026-03.ofx
# Force a specific account (overrides auto-detection)
node src/cli.js import --account ws_chequing file.csvSafe to re-import the same file — duplicates are skipped automatically.
The CLI resolves the target account in order: --account flag → CSV header detection → filename match.
CSV files (EQ Bank, Wealthsimple) are auto-detected by their headers — no special naming needed. OFX files need the account key (from config/accounts.yml) somewhere in the filename:
| Bank | Account Key | Example Filename |
|---|---|---|
| TD Chequing | td_chequing |
td_chequing-2026-03.ofx |
| TD Visa | td_visa |
td_visa-2026-03.ofx |
| CIBC Chequing | cibc_chequing |
cibc_chequing-march.ofx |
| Amex | amex |
amex-2026-03.ofx |
| EQ Savings | eq_savings |
(auto-detected by CSV header) |
| WS Chequing | ws_chequing |
(auto-detected by CSV header) |
| WS Credit | ws_credit |
(auto-detected by CSV header) |
| Bank | Format | Detection |
|---|---|---|
| TD / CIBC / Amex | OFX | File contains OFXHEADER or has .ofx/.qfx extension |
| EQ Bank | CSV: Transfer date, Description, Amount, Balance |
Header: "Transfer date" |
| Wealthsimple Chequing | CSV: date, transaction, description, amount, balance, currency |
Header: "transaction" + "balance" |
| Wealthsimple Credit Card | CSV: transaction_date, post_date, type, details, amount, currency |
Header: "post_date" + "type" |
Wealthsimple desktop PDF exports can be converted into review CSVs for chequing and credit card activity. The converter reads PDF text/layout data only; phone screenshots and image-only PDFs are not supported because they require OCR.
Create a chequing PDF:
- Open Wealthsimple in a desktop browser
- Go to Activity
- Filter Account to Chequing
- Set the desired timeframe
- In Safari/macOS, choose File -> Export as PDF from the menu bar, or use your browser's print/save PDF equivalent
- Save the PDF into
imports/
Create a credit card PDF:
- Open Wealthsimple in a desktop browser
- Open the Wealthsimple credit card page, or Activity filtered to Wealthsimple credit card
- Set the desired timeframe if the page offers one
- Export/save the page as a PDF
- Save the PDF into
imports/
Convert the PDF to a review CSV, inspect it, then import separately:
# Chequing
node src/cli.js ws-pdf ./imports/ws-chequing.pdf --account ws_chequing --out ./imports/ws_chequing-review.csv
node src/cli.js import --dry-run ./imports/ws_chequing-review.csv
# Credit card
node src/cli.js ws-pdf ./imports/ws-credit.pdf --account ws_credit --out ./imports/ws_credit-review.csv
node src/cli.js import --dry-run ./imports/ws_credit-review.csvThe PDF converter does not connect to Actual Budget and does not import anything. It writes only the CSV columns expected by the existing Wealthsimple import parser. If rows are skipped, the CLI prints counts and up to five privacy-safe examples with reason and page position. Use --force to overwrite an existing review CSV, and --report ./imports/ws-report.json for a privacy-minimal diagnostic summary.
Track Wealthsimple investment account balances (RRSP, TFSA, FHSA, Non-registered) in Actual Budget using holdings report CSVs. USD positions are converted to CAD using the live Bank of Canada exchange rate.
- Create tracking accounts in Actual Budget for each investment account (off-budget)
- Find their UUIDs:
node src/cli.js balances - Add to
config/accounts.yml:accounts: ws_rrsp: "uuid" ws_tfsa: "uuid" ws_fhsa: "uuid" ws_non_registered: "uuid"
- Download the holdings report CSV from Wealthsimple (Accounts → Download → Holdings report)
- Drop it in
imports/ - Run:
# Preview node src/cli.js holdings --dry-run ./imports/holdings-report-2026-04-10.csv # Update balances node src/cli.js holdings ./imports/holdings-report-2026-04-10.csv
Repeat monthly to track net worth over time. Each run creates a balance adjustment transaction per account to match the current market value.
Sync shared expenses from Splitwise into Actual Budget. Handles split transactions so your budget reflects your actual share, not the full payment.
- Register an app at https://secure.splitwise.com/apps and get your API key
- Create one on-budget account in Actual Budget: "Splitwise Clearing"
- Create or choose a category for temporary review, e.g. "Splitwise"
- Find the account/category UUIDs:
node src/cli.js balances node src/cli.js categories
- Add to
config/accounts.yml:splitwise: apiKey: "your-api-key" accounts: splitwise_clearing: "uuid" categories: splitwise_review: "uuid"
# List recent Splitwise expenses
node src/cli.js splitwise expenses
# Show who owes whom
node src/cli.js splitwise balances
# Sync: match expenses to bank transactions, show proposed splits, confirm
node src/cli.js splitwise sync
# Sync with custom date range
node src/cli.js splitwise sync --since 2026-01-01Run splitwise sync from an interactive terminal when you want it to write to Actual. Non-interactive runs only print the proposed actions and exit without applying changes.
- Import bank statements first (OFX/CSV)
- Run
splitwise sync— fetches last 60 days of Splitwise expenses - For each expense, it shows what it wants to do:
- You paid: split the bank transaction into your share + transfer to Splitwise Clearing
- They paid: create a categorized expense in Splitwise Clearing
- Settlement: match the bank-side settlement transaction and convert it to a transfer to/from Splitwise Clearing
- You confirm before any changes are applied
- Already-processed expenses are skipped on re-runs
If a bank transaction has not been imported yet, interactive sync can create an uncleared placeholder in the bank/card account you choose. Later statement imports can match that placeholder by account, date, and amount.
Settlement matching ignores transactions that are already Actual transfers. If you already converted a settlement manually, leave that Splitwise item skipped or mark it manually in config/splitwise-state.json after verifying it is truly handled.
Older Splitwise sync runs used separate off-budget receivable/payable accounts. You can leave that history in place and use the new Splitwise Clearing workflow going forward. Legacy applied IDs are still respected, so old items should not import again, but changed or deleted legacy Splitwise expenses do not have enough saved metadata for automatic repair. Review those manually if the old off-budget balances look wrong, then close or adjust the old accounts once you are comfortable with the clearing account balance.
Use Tailscale (free) to access Actual Budget from your phone or share with others.
- Install Tailscale on the server machine, your phone, and any other devices
- All devices join the same Tailscale network (tailnet)
- Access Actual Budget at
http://<server-tailscale-hostname>:5006from any device
- They create their own Tailscale account (free)
- Share your server device with them via Tailscale admin console
- Optionally use ACLs to restrict access to port 5006 only
- They access the same URL in their browser — same password, same budget
Actual Budget supports simultaneous access from multiple devices. Changes sync automatically.
If the server runs on one laptop and you run CLI tools from another:
- Both machines need Tailscale
- Update
config/accounts.ymlon the CLI machine:actual: serverURL: "http://<server-tailscale-hostname>:5006"
- Everything else works the same — the CLI talks to the server over Tailscale
If your server is a Mac running Colima for Docker, create a LaunchAgent so Actual Budget starts automatically on login. If you also run Home Assistant, combine them into one plist so Colima only starts once.
cat <<'EOF' > ~/Library/LaunchAgents/com.server.colima.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.server.colima</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>colima start --cpu 4 --memory 4 && docker start actual_budget && docker start homeassistant</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/colima-server.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/colima-server.err.log</string>
</dict>
</plist>
EOF
launchctl load ~/Library/LaunchAgents/com.server.colima.plistRemove the docker start homeassistant line if you don't run Home Assistant.
Note: If colima is installed via MacPorts, use the full path (e.g., /opt/local/bin/colima). Check with which colima.
launchctl unload ~/Library/LaunchAgents/com.server.colima.plist
rm ~/Library/LaunchAgents/com.server.colima.plistcat /tmp/colima-server.out.log
cat /tmp/colima-server.err.logThe server needs to stay awake. No third-party apps needed:
# Keep awake on AC power, screen off after 1 minute
sudo pmset -c sleep 0 displaysleep 1 disksleep 0
# Revert to defaults
sudo pmset -c sleep 1 displaysleep 10 disksleep 10Server and API versions must match. To upgrade:
- Check latest version at https://actualbudget.org/docs/releases/
- Update
VERSIONinstart.sh - Update
imagetag indocker-compose.yml - Run
npm install @actual-app/api@<same-version> - Restart:
./start.sh
If you see out-of-sync-migrations errors, the versions are mismatched.
budgeting/
├── start.sh # Start/restart Actual Budget container
├── docker-compose.yml # Alternative to start.sh (for docker compose)
├── src/
│ ├── cli.js # CLI entry point
│ ├── config.js # Loads config/accounts.yml
│ ├── actual.js # Actual Budget API wrapper
│ ├── holdings.js # Wealthsimple holdings report parser
│ ├── parsers/
│ │ ├── index.js # Auto-detection + filename-based account resolution
│ │ ├── ofx.js # OFX parser (TD, CIBC, Amex)
│ │ ├── eqbank.js # EQ Bank CSV parser
│ │ ├── wealthsimple.js # Wealthsimple CSV parser (chequing + credit card)
│ │ └── utils.js # Shared utilities (imported_id generation)
│ └── splitwise/
│ ├── client.js # Splitwise API client
│ ├── sync.js # Sync orchestrator (match, propose, apply)
│ ├── matcher.js # Match Splitwise expenses to bank transactions
│ └── state.js # Track processed expense IDs
├── config/
│ └── accounts.example.yml # Config template
├── imports/ # Drop CSV/OFX files here (gitignored)
└── data/ # Actual Budget server data (gitignored)