A headless, deterministic CLI that learns categorization rules from your Spendee history, fixes a selected period, and links transfers between your own wallets.
No LLM, local database, or browser is required. Data is fetched into memory for each run and discarded when the command exits.
Warning
The Spendee adapter uses the web application's Firebase/Firestore backend. It is unofficial, may break without notice, and is not endorsed by Spendee. Review Spendee's terms and use it at your own risk.
- Learns only from transactions before the requested period.
- Requires at least 3 observations and 80% category agreement by default.
- Lets the latest 12 matching transactions override stale older categorization when they meet the same threshold.
- Shows unanimous 2/2 matches as review-only suggestions; they are never auto-written.
- Checks every transaction in the period, including already-categorized ones.
- Leaves an already-correct category untouched.
- Keeps Spendee's category when no rule is strong enough by default.
- Can instead assign unknown expenses and incomes to configured fallback categories.
- Detects same- and cross-currency transfers between selected wallets.
- Compares cross-currency transfers through Spendee's normalized USD value.
- Never matches transfers more than one day apart by default.
- Previews everything unless
--commitis explicitly supplied.
Python 3.11 or newer is required.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"The installed command is spendee.
Only email/password Spendee accounts are currently supported. Authentication is fully headless.
export SPENDEE_EMAIL="you@example.com"
export SPENDEE_PASSWORD="your-password"
spendee auth checkFor VPS secret mounts, point to a file containing only the password:
export SPENDEE_PASSWORD_FILE=/run/secrets/spendee-passwordFirebase ID tokens are refreshed automatically during long runs. Credentials and tokens are never written by the CLI.
Create a config and edit the wallet list. Wallet IDs are preferable because names can change.
spendee config init
spendee wallets list
spendee config validateConfiguration is resolved from --config, then SPENDEE_CONFIG, then config/config.yaml, then $XDG_CONFIG_HOME/spendee/config.yaml.
wallets:
include: [] # Empty selects all visible wallets; IDs or exact names are accepted.
exclude: []
categorization:
min_observations: 3
min_agreement: 0.80
recent_observations: 12
suggestion_min_observations: 2
suggestion_min_agreement: 1.00
fallback: spendee # spendee | other
other_expense_category: others
other_income_category: income
prefer_wallet_rules: true
transfers:
max_days: 1
fx_tolerance_pct: 3.0
auto_score: 80
review_score: 60CLI options override the file where an override is available.
Set recent_observations or suggestion_min_observations to 0 to disable that layer.
Preview:
spendee categorize --from 2026-06-01 --to 2026-06-30Write the previewed category changes and automatic transfer matches:
spendee categorize --from 2026-06-01 --to 2026-06-30 --commitLimit a run to selected wallets:
spendee categorize \
--from 2026-06-01 \
--to 2026-06-30 \
--wallet 00000000-0000-4000-8000-000000000000Use --json for automation. A non-zero exit means at least one read or write failed.
Suggestions are included in the preview as SUGGESTION, but --commit writes only confident UPDATE decisions.
Preview an explicit correction by transaction ID and exact category name or ID:
spendee transactions correct TRANSACTION_ID --category TransportWrite it after review:
spendee transactions correct TRANSACTION_ID --category Transport --commitThe correction is written directly to Spendee. It becomes training history for later periods without a local database.
spendee doctor
spendee wallets list [--json]
spendee categories list [--json]
spendee transactions list [--from DATE] [--to DATE] [--wallet ID]
spendee transactions correct TRANSACTION_ID --category NAME_OR_ID [--commit]
spendee transfers detect --from DATE --to DATE [--commit]
spendee rules inspect --before DATE
spendee config validate
For a target period [from, to]:
- Transactions dated before
fromare trusted history. Pending items, transfers, and uncategorized items are excluded from training. - History is grouped by direction (expense/income) and normalized merchant. Wallet-specific rules are preferred, with a global rule as fallback.
- The most frequent category wins only when the group meets both thresholds. Ties never produce a rule.
- When there is more history than the recent window, a qualifying rule from the latest matching transactions overrides the all-time rule. This adapts to category changes without forgetting older evidence.
- A unanimous group meeting only the suggestion threshold is shown for review and is never auto-written.
- Every non-transfer target transaction is evaluated. Matching existing categories are kept; differing categories are proposed for update.
- Unknowns retain their current Spendee category, or use direction-specific
othercategories when configured.
Transfer candidates are evaluated first so they cannot be recategorized as ordinary income or spending. Pairing requires different selected wallets, opposite signs, a maximum one-day date difference, and either an exact same-currency amount or normalized USD values within the configured FX tolerance. Equally strong pairings are marked for review and never auto-linked.
The package has deliberately small boundaries:
spendee_client.py: authentication and remote Firestore adaptermodels.py: provider-neutral wallet, category, and transaction modelscategorization.py: pure in-memory rule learning and decisionstransfers.py: pure transfer candidate matchingservice.py: fetch/evaluate/commit orchestrationcli.py: presentation and exit codes
The pure modules do not know about HTTP, Firestore, Typer, or persistence. This makes them straightforward to test and replace with another data provider.
python -m ruff check .
python -m pytest -q
python -m buildNever use real financial exports in tests or issues. See SECURITY.md and CONTRIBUTING.md.
MIT. See LICENSE.