Personal finance CLI — a self-hosted Plaid.
Scrapes your bank and brokerage accounts using browser automation, stores everything locally in SQLite, and gives you a single command-line interface to query across all of them.
cargo install till-cliOr build from source:
git clone https://github.com/thrashr888/till
cd till
cargo install --path .Requires uv for Python scraper execution and Chromium for browser automation.
# Set credentials (stored in macOS Keychain)
till creds set --source schwab
# Sync your accounts
till sync --source schwab
# View accounts
till accounts --pretty
# View recent transactions
till transactions --days 30 --pretty
# View positions
till positions --pretty| Source | Status | Provides |
|---|---|---|
| Charles Schwab | Working | Accounts, transactions, positions |
| E*Trade | Working | Accounts, positions |
| Chase | Working | Credit cards, transactions |
| American Express | Skeleton | Credit cards, transactions |
| Bank of America | Skeleton | Accounts, transactions |
| Fidelity | Skeleton | Accounts, positions |
| Morgan Stanley | Skeleton | Accounts, positions |
| Wells Fargo | Skeleton | Accounts, transactions |
List discovered sources:
till sourcestill sync # All enabled sources
till sync --source schwab # Single sourcetill accounts [--source X] [--type brokerage] [--pretty]
till transactions [--source X] [--days 30] [--category groceries] [--pretty]
till positions [--source X] [--pretty]
till balances [--pretty]
till history --account-id X [--pretty]till creds set --source schwab # Store username/password in Keychain
till creds get --source schwab # Check if credentials exist
till creds delete --source schwab # Remove credentialsEnvironment variable fallback: TILL_SCHWAB_USERNAME / TILL_SCHWAB_PASSWORD.
till export > backup.json
till export --source schwab > schwab.json
till import < backup.jsonJSON format matches scraper output — you can manually create JSON files for banks without scrapers.
till log # Recent sync history
till log --source schwab # Filter by sourcetill scaffold fidelityCreates scrapers/till_scrapers/fidelity/ with:
manifest.json— plugin metadatascraper.py— scraper class extendingBaseScraper
# Run scraper without saving to DB
till test --source fidelity
# Visible browser for debugging
till test --source fidelity --headful
# Pause after login for DOM inspection
till test --source fidelity --pause
# Save page HTML for offline selector iteration
till test --source fidelity --save-html
# Replay saved HTML (no re-authentication needed)
till test --source fidelity --replay /tmp/till_fidelity_page.html~/.config/till/config.toml:
[schwab]
enabled = true
transaction_account = "...1234"
[chase]
enabled = true
[browser]
headless = false
timeout = 600Rust handles CLI, config, credentials, SQLite storage, and output formatting. Python handles browser automation via Playwright with stealth plugins. They communicate via JSON on stdout.
till (Rust CLI)
├── config.toml loader
├── macOS Keychain credentials
├── SQLite with upsert semantics
├── JSON import/export
└── pretty table renderer
scrapers (Python/Playwright)
├── base.py — shared browser setup + replay mode
├── registry.py — manifest.json plugin discovery
└── <source>/scraper.py — per-bank extraction logic
Rust has zero per-bank knowledge. Sources self-register via manifest.json. The till-scrape --list command discovers available plugins at runtime.
SQLite at ~/.config/till/till.db. All syncs use INSERT ... ON CONFLICT DO UPDATE so re-running is always safe.
MIT