<div align="center">

# rustledger

**A blazing-fast Rust implementation of [Beancount](https://beancount.github.io/)**

Parse and validate your ledger faster than Python beancount.

[![crates.io](https://img.shields.io/crates/v/rustledger)](https://crates.io/crates/rustledger)
[![npm](https://img.shields.io/npm/v/@rustledger/wasm)](https://www.npmjs.com/package/@rustledger/wasm)
[![Packaging status](https://repology.org/badge/tiny-repos/rustledger.svg)](https://repology.org/project/rustledger/versions)
[![docs.rs](https://img.shields.io/docsrs/rustledger-core)](https://docs.rs/rustledger-core)

[![CI](https://github.com/rustledger/rustledger/actions/workflows/ci.yml/badge.svg)](https://github.com/rustledger/rustledger/actions/workflows/ci.yml)
[![Compatibility](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/rustledger/rustledger/compatibility/.github/badges/compat-badge.json)](https://github.com/rustledger/rustledger/actions/workflows/compat.yml)

[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
[![Liberapay](https://img.shields.io/liberapay/receives/rustledger.svg?logo=liberapay)](https://liberapay.com/rustledger)

</div>

______________________________________________________________________

## Why rustledger?

| | |
|---|---|
| **10-30x faster** | Parse and validate large ledgers in milliseconds ([see benchmarks](#performance)) |
| **No dependencies** | No Python runtime, no libraries to install |
| **Drop-in replacement** | Compatible `bean-*` CLI commands for easy migration |
| **Full compatibility** | Parses any valid beancount file |
| **Editor support** | LSP server for VS Code, Neovim, Helix, and more |
| **AI-ready** | MCP server for AI assistants, plus `ag-rledger` — an agent-native CLI with JSON envelopes |
| **Runs anywhere** | WebAssembly support for browser and Node.js |
| **Better errors** | Detailed error messages with source locations |
| **31 built-in plugins** | Plus Python plugin compatibility via WASI sandbox |
| **Bank import** | CSV/OFX import with auto-detection, dedup, and categorization |
| **Investment returns** | Money-weighted (XIRR) and time-weighted returns, robust over imperfect ledgers: a built-in beangrow replacement |
| **Budgeting** | Budgeted vs actual spending from Fava-compatible `custom "budget"` directives, calendar-accurate pro-rating |
| **Capital gains** | Realized gains per tax lot, short vs long term, with exact `@@` proceeds, calendar-correct holding periods, and optional per-lot annualized return (`--irr`) |

<details>
<summary><strong>Comparison with other tools</strong></summary>

| Feature | rustledger | Python beancount | hledger | ledger-cli |
|---------|------------|------------------|---------|------------|
| **Language** | Rust | Python | Haskell | C++ |
| **Speed** | Very fast | Slow | Fast | Fast |
| **Beancount syntax** | Native | Native | Via conversion | No |
| **Query language** | BQL (100% compat) | BQL | Custom | Custom |
| **LSP server** | Built-in | No | Via plugin | No |
| **WASM support** | Yes | No | Partial | No |
| **Plugin system** | Native + Python | Python | Haskell | Custom |
| **Active development** | Yes | Maintenance | Yes | Limited |

**When to use rustledger:**

- You use Beancount syntax and want speed
- You want a single binary with no runtime dependencies
- You need LSP editor integration
- You want to use existing Python plugins

**When to use Python beancount:**

- You need Fava web interface (until rustledger integration)
- You have complex Python plugins with C extensions

**When to use hledger:**

- You prefer hledger's syntax and reports
- You need time-tracking features

</details>

## Install

| Platform | Command |
|----------|---------|
| **macOS** | `brew install rustledger` |
| **Linux** | `brew install rustledger` |
| **Arch Linux** | `yay -S rustledger-bin` |
| **Fedora/RHEL** | `sudo dnf copr enable robcohen/rustledger && sudo dnf install rustledger` |
| **Windows** | `scoop bucket add rustledger https://github.com/rustledger/scoop-rustledger && scoop install rustledger` |
| **Cargo** | `cargo binstall rustledger` or `cargo install rustledger` |
| **Nix** | `nix run github:rustledger/rustledger` |
| **Docker** | `docker run --rm -v "$PWD:/data" ghcr.io/rustledger/rustledger /data/ledger.beancount` |
| **Binaries** | [GitHub Releases](https://github.com/rustledger/rustledger/releases) |
| **npm (WASM)** | `npm install @rustledger/wasm` |
| **npm (MCP)** | `npx @rustledger/mcp-server` ([Model Context Protocol](https://modelcontextprotocol.io) server) |

<sub>Missing your platform? [Open an issue](https://github.com/rustledger/rustledger/issues/new) to request it.</sub>

**Coming from Python beancount?** See the [Migration Guide](docs/migration/index.md) for command equivalents and plugin mapping.

## AI & agents

Two complementary ways to drive rustledger from AI tools:

- **MCP server** — `npx @rustledger/mcp-server` exposes rustledger to AI assistants (Claude, Cursor, …) over the [Model Context Protocol](https://modelcontextprotocol.io). Best for **interactive** assistants that call tools mid-conversation.

- **`ag-rledger`** — an *agent-native* CLI that mirrors every `rledger` command but emits a single structured **JSON envelope** per call instead of human-readable text, so coding agents and scripts parse results directly (no output scraping). Each envelope carries `ok`, a typed `exit_code`, the command's output under `result`, and a HATEOAS `next_action` hint; pass the inner command's `--json` to get fully structured `result.data`. Best for **headless** agents and shell pipelines. It ships beside `rledger` in the [release archives](https://github.com/rustledger/rustledger/releases) (or build it with `cargo build -p rustledger --bin ag-rledger --no-default-features --features ag-rledger`):

  ```console
  $ ag-rledger check --json ledger.beancount
  {"ok":true,"exit_code":0,"result":{"data":{"error_count":0}},"next_action":"…"}
  ```

  Same subcommands as `rledger` (`check`, `query`, `format`, `report`, `extract`, …) — only the I/O is JSON.

## Quick Start

```bash
rledger check ledger.beancount
rledger query ledger.beancount "SELECT account, SUM(position) GROUP BY account"
```

## CLI Commands

| Command | Description |
|---------|-------------|
| `rledger check` | Validate ledger files with detailed error messages |
| `rledger query` | Run BQL queries (interactive shell or one-shot) |
| `rledger format` | Auto-format beancount files |
| `rledger report` | Generate balance, account, and statistics reports |
| `rledger add` | Add transactions interactively or via quick mode |
| `rledger doctor` | Debugging tools for ledger issues |
| `rledger extract` | Import transactions from CSV/OFX bank statements with auto-detection, dedup, and categorization |
| `rledger price` | Fetch commodity prices from online sources |
| `rledger lint` | Non-fatal advisory passes (e.g., detect inter-account transfer pairs) |
| `rledger config` | Manage rustledger configuration (alias: `cfg`) |
| `rledger completions` | Generate shell completions |
| `rledger-lsp` | Language Server Protocol for editor integration |

Python beancount users can install `bean-check`, `bean-query`, etc. wrapper scripts via `rledger compat install`.

<details>
<summary><strong>Report subcommands</strong></summary>

| Subcommand | Alias | Description |
|------------|-------|-------------|
| `balances` | | All account balances |
| `balsheet` | `bal` | Balance sheet report |
| `income` | `is` | Income statement |
| `journal` | `register` | Transaction register |
| `holdings` | | Investment holdings |
| `returns` | | Investment returns: money-weighted (XIRR) + time-weighted, with `--by-group` breakdown (a built-in beangrow replacement) |
| `capgains` | | Realized capital gains/losses per tax lot (short vs long term) |
| `budget` | | Budgeted vs actual spending (Fava-compatible `custom "budget"`) |
| `networth` | | Net worth over time |
| `accounts` | | List all accounts |
| `commodities` | | List all commodities |
| `prices` | | Price entries |
| `stats` | | Ledger statistics |

</details>

<details>
<summary><strong>Doctor subcommands</strong></summary>

Debugging and diagnostic tools:

| Subcommand | Description |
|------------|-------------|
| `lex` | Dump lexer tokens (alias: `dump-lexer`) |
| `parse` | Parse and show directives |
| `context` | Show transaction context at a line number |
| `linked` | Find transactions by link (`^link`) or tag (`#tag`) |
| `missing-open` | Generate missing Open directives |
| `list-options` | List all available beancount options |
| `print-options` | Print options parsed from a file |
| `stats` | Display ledger statistics |
| `display-context` | Show inferred decimal precision context |
| `roundtrip` | Round-trip parse/format test |
| `directories` | Validate directory hierarchy against accounts |
| `region` | Print transactions in a line range with balances |
| `generate-synthetic` | Generate synthetic test files |

```bash
# Debug a parsing issue at line 42
rledger doctor context ledger.beancount 42

# Find all transactions with a link
rledger doctor linked ledger.beancount ^trip-2024

# Generate Open directives for accounts missing them
rledger doctor missing-open ledger.beancount >> ledger.beancount
```

</details>

<sub>Run `rledger <command> --help` for all options.</sub>

<details>
<summary><strong>CLI examples</strong></summary>

```bash
# Validate with plugins
rledger check --native-plugin auto_accounts ledger.beancount

# Interactive query shell
rledger query ledger.beancount

# One-shot query
rledger query ledger.beancount "SELECT date, narration WHERE account ~ 'Expenses:Food'"

# Reports
rledger report ledger.beancount balances
rledger report ledger.beancount stats

# Format in place
rledger format --in-place ledger.beancount
```

</details>

## Import & Categorization

rustledger includes a complete bank import pipeline (`rledger extract`) with automatic CSV/OFX parsing, duplicate detection, transaction categorization, and balance reconciliation.

### CSV Import with Auto-Detection

```bash
# Auto-detect delimiter, date format, and column roles
rledger extract bank-statement.csv --auto -a Assets:Bank:Checking

# Check against existing ledger to avoid duplicates
rledger extract statement.csv --auto -a Assets:Bank --existing ledger.beancount

# Append balance assertion from bank statement
rledger extract statement.csv --auto -a Assets:Bank --balance 5000.00

# List available importers from config
rledger extract --list-importers --config importers.toml
```

### Importers Configuration

Create `importers.toml` for reusable import profiles:

```toml
[[importers]]
name = "chase"
account = "Assets:Bank:Chase"
date_column = "Transaction Date"
amount_column = "Amount"
date_format = "%m/%d/%Y"

[importers.mappings]
"AMAZON" = "Expenses:Shopping"
"WHOLE FOODS" = "Expenses:Groceries"
```

```bash
rledger extract --importer chase chase-statement.csv
```

Config is searched in: current directory, the user config directory (`$RLEDGER_CONFIG_DIR` if set, else `~/.config/rledger/importers.toml`), or via `--config`.

### Transaction Categorization

A 3-tier pipeline automatically categorizes transactions:

1. **Rules engine** — substring, regex, and exact match patterns from `importers.toml`
1. **Merchant dictionary** — ~75 built-in patterns across 10 categories (groceries, dining, transport, subscriptions, etc.)
1. **ML categorization** — TF-IDF + a self-contained Multinomial Naive Bayes classifier (pure `std`, no ML framework dependency)

### Transfer Detection

Automatically identifies inter-account transfers by matching opposite-sign amounts within date windows, with keyword boosting for common transfer indicators.

### Duplicate Detection

Fuzzy matching on date + amount + payee/narration prevents importing the same transaction twice. Structural fingerprinting (BLAKE3) provides stable hashes for comparison.

### Balance Reconciliation

The `--balance` flag appends a balance assertion directive matching your bank statement's ending balance, helping verify import accuracy.

### Custom WASM Importers

Beyond the built-in CSV and OFX importers, third parties can ship importers as sandboxed `.wasm` modules — no need to fork the workspace:

```bash
# Single file
rledger extract --wasm-importer ./my-bank.wasm statement.dat -a Assets:Bank

# Or scan a directory of .wasm modules (also configurable via importers.toml)
rledger extract --wasm-importer-dir /etc/rledger/importers.d statement.dat -a Assets:Bank
```

WASM importers implement the same `Importer` trait as the built-ins via the [`wasm_importer_main!`](crates/rustledger-plugin-types/src/guest.rs) macro and run inside the same wasmtime sandbox as directive plugins (no FS / network / WASI; configurable memory + fuel caps). See [`examples/wasm-importer-csv-example`](examples/wasm-importer-csv-example/) for a reference implementation.

## Crates

| Crate | Description |
|-------|-------------|
| [`rustledger`](https://crates.io/crates/rustledger) | CLI tool (`rledger check`, `rledger query`, etc.) |
| [`rustledger-core`](https://crates.io/crates/rustledger-core) | Core types: Amount, Position, Inventory |
| [`rustledger-parser`](https://crates.io/crates/rustledger-parser) | Lexer and parser with error recovery |
| [`rustledger-loader`](https://crates.io/crates/rustledger-loader) | File loading and includes |
| [`rustledger-booking`](https://crates.io/crates/rustledger-booking) | Interpolation and 7 booking methods |
| [`rustledger-validate`](https://crates.io/crates/rustledger-validate) | Validation error codes |
| [`rustledger-query`](https://crates.io/crates/rustledger-query) | BQL query engine |
| [`rustledger-completion`](https://crates.io/crates/rustledger-completion) | Editor-agnostic completion logic (shared by LSP + WASM) |
| [`rustledger-plugin`](https://crates.io/crates/rustledger-plugin) | 31 built-in plugins + Python plugin support |
| [`rustledger-plugin-types`](https://crates.io/crates/rustledger-plugin-types) | Shared plugin type definitions |
| [`rustledger-importer`](https://crates.io/crates/rustledger-importer) | Import framework: built-in CSV/OFX, plus a `WasmImporter` host loader for third-party `.wasm` importers |
| [`rustledger-ops`](https://crates.io/crates/rustledger-ops) | Pure operations — ML categorization, LLM prompts, dedup, transfer detection, balance reconciliation, merchant dictionary |
| [`rustledger-lsp`](https://crates.io/crates/rustledger-lsp) | Language Server Protocol for editor integration |
| [`rustledger-wasm`](https://crates.io/crates/rustledger-wasm) | WebAssembly bindings for JavaScript/TypeScript |
| [`rustledger-ffi-component`](https://github.com/rustledger/rustledger/tree/main/crates/rustledger-ffi-component) | Primary embedding surface — FFI via WASI Preview 2 / Component Model (typed WIT contract) |
| [`rustledger-ffi-wasi`](https://crates.io/crates/rustledger-ffi-wasi) | Legacy WASI (wasip1) JSON-RPC FFI for embedding — slated for removal |

<details>
<summary><strong>Booking methods (7)</strong></summary>

| Method | Description |
|--------|-------------|
| `STRICT` | Lots must match exactly (default) |
| `STRICT_WITH_SIZE` | Exact-size matches accept oldest lot |
| `FIFO` | First in, first out |
| `LIFO` | Last in, first out |
| `HIFO` | Highest cost first |
| `AVERAGE` | Average cost basis |
| `NONE` | No cost tracking |

</details>

<details>
<summary><strong>Built-in plugins (31)</strong></summary>

| Plugin | Description |
|--------|-------------|
| `auto_accounts` | Auto-generate Open directives |
| `auto_tag` | Automatically tag transactions |
| `box_accrual` | Accrual accounting for boxed periods |
| `gain_loss` | Split capital gains into gain/loss accounts |
| `long_short` | Split capital gains by holding period |
| `check_average_cost` | Validate average cost bookings |
| `check_closing` | Zero balance assertion on account close |
| `check_commodity` | Validate commodity declarations |
| `check_drained` | Ensure accounts are drained before close |
| `close_tree` | Close descendant accounts |
| `coherent_cost` | Enforce cost OR price (not both) |
| `commodity_attr` | Validate commodity attributes |
| `currency_accounts` | Auto-generate currency trading postings |
| `effective_date` | Override posting date via metadata |
| `forecast` | Generate recurring transactions |
| `generate_base_ccy_prices` | Create base currency price entries |
| `implicit_prices` | Generate price entries from transaction costs |
| `leafonly` | Error on postings to non-leaf accounts |
| `noduplicates` | Hash-based duplicate transaction detection |
| `nounused` | Warn on unused accounts |
| `onecommodity` | Single commodity per account |
| `pedantic` | Enable all strict validations |
| `rename_accounts` | Rename accounts via metadata |
| `rx_txn_plugin` | Link related transactions |
| `sellgains` | Cross-check capital gains against sales |
| `split_expenses` | Split expenses across accounts |
| `unique_prices` | One price per day per commodity pair |
| `unrealized` | Calculate unrealized gains |
| `valuation` | Mark-to-market valuation |
| `zerosum` | Group transactions that sum to zero |

Additionally, `document_discovery` auto-discovers documents from `option "documents"` directories.

**Python plugins**: Run existing Python beancount plugins via CPython-WASI sandbox.

</details>

<details>
<summary><strong>Plugin support</strong></summary>

rustledger supports three types of plugins:

**Native plugins** (built-in, fastest):

```bash
# Run a native plugin from CLI
rledger check --native-plugin implicit_prices ledger.beancount

# Or declare in your beancount file (auto-detected as native):
# plugin "beancount.plugins.auto_accounts"
```

**Python file plugins** (via WASM sandbox):

```bash
# Declare in your beancount file:
# plugin "/path/to/my_plugin.py"
```

**WASM plugins** (sandboxed WebAssembly):

```bash
# Load a WASM plugin
rledger check --plugin /path/to/plugin.wasm ledger.beancount
```

**How Python plugins work:**

- File-based plugins (`.py` files) run in a sandboxed CPython compiled to WebAssembly
- No system Python installation required
- Plugins cannot access the filesystem or network (sandbox isolation)
- Compatible with most pure-Python beancount plugins

**Limitations:**

- Module-based plugins (`beancount.plugins.xyz`) only work if rustledger has a native implementation
- Plugins with C extensions won't work (numpy, pandas, etc.)
- No network access (price fetching plugins need alternatives)

</details>

## Editor Integration

rustledger includes a full-featured Language Server (`rledger-lsp`) for IDE support:

- Real-time diagnostics
- Autocompletion (accounts, currencies, payees)
- Go to definition / find references
- Hover information with account balances
- Rename refactoring
- Document formatting

See [LSP setup guide](crates/rustledger-lsp/README.md) for VS Code, Neovim, Helix, Zed, and Emacs.

## Performance

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rustledger/rustledger/benchmarks/.github/badges/validation-chart.svg">
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/rustledger/rustledger/benchmarks/.github/badges/validation-chart.svg">
  <img alt="Validation Benchmark" src="https://raw.githubusercontent.com/rustledger/rustledger/benchmarks/.github/badges/validation-chart.png" width="100%">
</picture>

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rustledger/rustledger/benchmarks/.github/badges/balance-chart.svg">
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/rustledger/rustledger/benchmarks/.github/badges/balance-chart.svg">
  <img alt="Balance Report Benchmark" src="https://raw.githubusercontent.com/rustledger/rustledger/benchmarks/.github/badges/balance-chart.png" width="100%">
</picture>

<sub>Benchmarks run nightly on 10K transaction ledgers. [View workflow →](https://github.com/rustledger/rustledger/actions/workflows/bench.yml)</sub>

<details>
<summary><strong>Benchmark details</strong></summary>

**What's measured:**

- **Validation**: Parse ledger + validate (balance assertions, account opens, etc.)
- **Balance Report**: Parse + compute all account balances

**Memory efficiency:**
rustledger typically uses 3-5x less memory than Python beancount thanks to Rust's zero-cost abstractions and efficient data structures.

**Run locally:**

```bash
# Quick comparison (requires nix)
nix develop .#bench
./scripts/bench.sh

# Criterion micro-benchmarks
cargo bench -p rustledger-core
cargo bench -p rustledger-parser
```

See [benchmarking.md](docs/development/benchmarking.md) for detailed benchmark documentation.

</details>

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.

**Documentation:**

- [Architecture](docs/reference/architecture.md) - Crate structure and data flow
- [BQL Reference](docs/reference/bql.md) - Query language guide
- [Importing](docs/guides/importing.md) - CSV/OFX bank import tutorial
- [Validation errors](docs/reference/errors.md) - Error code reference
- [API docs](https://docs.rs/rustledger-core) - Rust API documentation

By submitting a pull request, you agree to the [Contributor License Agreement](CLA.md).

## License

[GPL-3.0-only](LICENSE)

**Commercial licensing available** - [contact us](https://rustledger.github.io/#contact) for proprietary license options.

## Funding

rustledger is free and open source. If you find it useful, consider supporting development:

[![Support on Liberapay](https://img.shields.io/badge/Support%20on-Liberapay-F6C915?logo=liberapay)](https://liberapay.com/rustledger)
