# Ledger CLI: Plain Text Accounting

> Why I log every transaction in one plain text file with Ledger CLI: double-entry basics, the account tree, multi-currency metals and stocks, daily queries.

Author: Elnur Badalov
Published: 2026-07-24
Updated: 2026-07-24
Tags: linux, lifeisgood
Source: https://elnurbda.com/posts/62-ledger-cli/
License: CC BY 4.0, please cite the source URL.

---

## Introduction

Every transaction I make - every bus ride, every coffee, every gift - goes into one plain text file. No banking app, no aggregator, no spreadsheet. Just a text file and [Ledger](https://ledger-cli.org/), a command-line double-entry accounting tool.

If [presentations can be code](/posts/45-presenterm), accounting definitely can.

> **Note:** All names, banks, tickers and amounts in this post are made up. The structure is exactly how I run mine; the numbers are not.

### Purpose

1. **Understand plain text accounting** - double-entry in one minute, and why a text file beats an app.
2. **Structure a journal** - the account tree, multi-currency handling (USD, EUR, gold, silver, stocks, crypto) and the file conventions I settled on.
3. **Query it** - the aliases and commands I actually run, including a few that answer suspiciously specific questions.

### Prerequisites

- A terminal.
- `ledger` installed: `apt install ledger`, or `nix shell nixpkgs#ledger` - I am a NixOS person, after all.
- The discipline to spend two minutes a day logging. This is the hard part; the tool is the easy part.

## Why a Text File and Not an App

- Want your full financial history on someone else's server, categorized by their ML model?
  - **Banking app**: yes, by default.
  - **Ledger**: it is a file on your disk. `grep` is the ML model.
- Want version control for your money?
  - **Ledger**: `git add finances.ledger && git commit -m "July"`
- Want gold grams, EUR, stocks and crypto in the same report as your main currency?
  - **Excel**: prepare the VLOOKUP of your nightmares.
  - **Ledger**: built in. One flag converts everything.
- Want to know how much one specific doner place has cost you since 2025?
  - **Ledger**: one command, milliseconds.

And it is free, works offline, and the file will still open in 30 years.

## Double-Entry in One Minute

Every transaction moves value between at least two accounts, and the postings must sum to zero. All accounts live under five roots. The comment header at the top of my file is the whole theory:

```
; Assets - Bank Accounts, Wallet, Investments, Loans credited
; Income - Paychecks, Gifts received, Dividends, Interest
; Expense - Groceries, Taxes, Gifts given, Donations
; Liabilities - Mortgage, Credit cards, Student loans, Loans owed
; Equity - Untrackable errors or Opening Balances
```

One sign convention to internalize: **Income is negative.** Money flows _from_ Income _into_ Assets, so a salary posting is `-3000 USD` on the Income side. It feels wrong for a day and then it clicks.

## The Journal

A routine day in the file looks like this:

```
2026-07-10
	Assets:Cash:Bank:WeeWeeBank:Checking
	Expenses:Groceries:PurPurMarket			7.12 USD
	Expenses:Transport:Transit:Bus			0.60 USD
	Expenses:Food:Restaurants:Doner			9.80 USD
```

The conventions I converged on:

- **One transaction per day per funding account.** The account without an amount is _elided_ - Ledger balances it automatically (here: `-17.52 USD` leaves Checking). No payee line for routine days; I name transactions only when they are worth remembering ("Buy gold", "Pay Day").
- **A subaccount per venue and per person.** `Expenses:Food:Cafes:<CafeName>`, `Expenses:Gifts:Monetary:<Person>`, `Expenses:Transport:Taxi`. The tree grows organically - a new place is just a new line - and "how much did I ever spend there" becomes free.
- **Day one is an `Opening Balances` transaction** against `Equity:OpeningBalances`, with whatever numbers you remember. Precision comes later; starting matters more.

A payday - income negative in the wild, with the day's expenses and cashback netted into the same transaction:

```
2026-07-02 Pay Day
	Assets:Cash:Bank:WeeWeeBank:Checking
	Income:Salary:Employer				-3000.00 USD
	Expenses:Groceries:PurPurMarket			17.61 USD
	Income:Cashback:Bank:WeeWeeBank			-5.00 USD
```

Debts are assets. Lending moves money from the bank into `Assets:Receivable:<Person>`, repayment moves it back, and if it is never coming back - reclassify and accept reality:

```
2026-03-01
	Assets:Receivable:Friend			100 USD
	Assets:Cash:Bank:WeeWeeBank:Checking

2026-04-01
	Assets:Cash:Bank:WeeWeeBank:Checking		100 USD
	Assets:Receivable:Friend

2026-06-30 Clearing Receivables
	Assets:Receivable:Friend			-50 USD
	Expenses:Gifts:Monetary:Friend			50 USD
```

The receivable balance is always the honest answer to "who owes me what".

## More Than One Currency

In Ledger everything is a _commodity_: USD is one, EUR is one, a gram of gold is one, a share of ACME is one. Buying an asset records the total cost with `@@`:

```
2026-01-05 Buy gold and silver
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:PreciousMetals:Gold		5 GOLD @@ 545 USD
	Assets:Investments:PreciousMetals:Silver	20 SILVER @@ 26 USD
	Expenses:Fees:MetalPremium			21 USD
```

Note the premium split into its own expense - the bank's margin over spot is a fee, not an investment. Stocks and crypto work the same way:

```
2026-01-06
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:Stocks:Broker		2 ACME @@ 505 USD
	Assets:Crypto:Exchange				0.5 COIN @@ 60 USD
```

Even exchanging cash is just a commodity purchase:

```
2026-01-10
	Assets:Cash:Pocket:Checking			100 EUR @@ 108 USD
	Assets:Cash:Bank:WeeWeeBank:Checking
```

Market prices are `P` directives that I update by hand every month or two:

```
P 2026-07-01 EUR 1.08 USD
P 2026-07-01 GOLD 109 USD
P 2026-07-01 SILVER 1.30 USD
P 2026-07-01 ACME 260 USD
```

Plain `ledger bal Assets` then reports each commodity separately; add `-X USD` and the whole portfolio collapses into one USD number at the latest known prices.

## Where Did the Cash Go

Pocket cash never reconciles. Instead of pretending it does, I book the difference honestly:

```
	Expenses:FX:Loss				2.05 USD ; idk where
```

Those entries accumulate into the real, measured cost of carrying physical cash. `Income:FX:Gain` exists too, for the rarer opposite case of finding money on your own shelf.

## Daily Workflow

Aliases, because I run these constantly:

```bash
alias lg='ledger'
alias lgb='ledger b'
alias lgba='ledger b Assets'
alias lgbe='ledger b Expenses'
alias lgbi='ledger b Income'
```

At the end of the day I append one transaction from the bank app history and one from memory for cash. To lower the friction I pre-create empty date lines for the week ahead:

```
2026-07-25
2026-07-26
2026-07-27
```

They parse fine, and an empty date staring at you is a surprisingly effective nag to fill it.

## The Queries I Actually Run

```bash
# net worth, per commodity
lgba -f finances.ledger

# net worth, one number
lgba -f finances.ledger -X USD

# liquid money only - boolean filters on account names
lgba -f finances.ledger -X USD and not gold and not crypto and not saving

# this month's burn and income
lgbe -f finances.ledger --begin "2026-07-01"
lgbi -f finances.ledger --begin "2026-07-01"

# monthly expense totals across the whole history
lg -f finances.ledger reg ^Expenses -X USD -M --collapse

# lifetime damage in one supermarket chain
lgb -f finances.ledger PurPurMarket
```

The query language is just account regexes plus `and`/`or`/`not` plus date windows. That is enough to answer almost anything.

## What the File Knows About You

After a while, the fun part is asking questions you never planned to ask:

```bash
# most frequent expense accounts - where does life actually happen
lg -f finances.ledger reg ^Expenses -F "%(account)\n" | sort | uniq -c | sort -rn | head

# how many distinct restaurants have I tried
lg -f finances.ledger accounts Expenses:Food:Restaurants | wc -l

# savings rate in one line
lg -f finances.ledger bal ^Income ^Expenses -X USD --depth 1

# average cost of a taxi ride vs a bus ride - you already know, but now in numbers
lg -f finances.ledger bal Expenses:Transport:Taxi -X USD
lg -f finances.ledger reg Expenses:Transport:Taxi | wc -l
```

I will keep my own numbers to myself, but I promise some of them surprised me.

## Tips

### 1. LLM

Once an LLM has the [Ledger manual](https://ledger-cli.org/doc/ledger3.html) and your file, it writes any query you can describe in words, finds patterns you did not think to look for, and catches inconsistencies. This is the same trick as with [Presenterm](/posts/45-presenterm): plain text formats and LLMs are a natural pair.

### 2. Guard the Account Names

A typo silently creates a new account - `Expenses:Grocery` next to `Expenses:Groceries` will split your data without a single error. Run `ledger accounts` once in a while and read the list; renames are one `sed` away.

### 3. Start Stupid, Split Later

Do not design the perfect taxonomy upfront. Log everything to `Expenses:Food` and split into subaccounts the day you have a question that needs the split. The file is text; restructuring it is editing, not migration.

### 4. Edit in Neovim

The [vim-ledger](https://github.com/ledger/vim-ledger) plugin adds syntax highlighting, folding, account completion and amount alignment for `.ledger` files, which makes a growing journal far easier to read and keep consistent.

## Conclusion

One text file under git, two minutes a day. In return, any question about my money is one command away - in a format no vendor can take away or discontinue.

## References

- [Ledger CLI](https://ledger-cli.org/) and the [full manual](https://ledger-cli.org/doc/ledger3.html)
- [plaintextaccounting.org](https://plaintextaccounting.org/) - the wider ecosystem
- Alternatives in the same family: [hledger](https://hledger.org/), [beancount](https://beancount.github.io/)

## Appendix: A Sample Journal

Everything from this post in one small, valid file. Save it as `finances.ledger` and start querying:

```
; Assets - Bank Accounts, Wallet, Investments, Loans credited
; Income - Paychecks, Gifts received, Dividends, Interest
; Expense - Groceries, Taxes, Gifts given, Donations
; Liabilities - Mortgage, Credit cards, Student loans, Loans owed
; Equity - Untrackable errors or Opening Balances

P 2026-01-01 EUR 1.08 USD
P 2026-01-01 GOLD 105 USD
P 2026-01-01 SILVER 1.20 USD
P 2026-01-01 ACME 250 USD

2026-01-01 Opening Balances
	Assets:Cash:Bank:WeeWeeBank:Checking		1500 USD
	Assets:Cash:Bank:WeeWeeBank:Saving		2000 USD
	Assets:Cash:Pocket:Checking			120 USD
	Assets:Cash:Pocket:Checking			50 EUR
	Liabilities:StudentLoans:University		-4500 USD
	Equity:OpeningBalances

2026-01-02 Pay Day
	Assets:Cash:Bank:WeeWeeBank:Checking
	Income:Salary:Employer				-3000.00 USD
	Expenses:Groceries:PurPurMarket			17.61 USD
	Income:Cashback:Bank:WeeWeeBank			-5.00 USD

2026-01-03
	Assets:Cash:Bank:WeeWeeBank:Checking
	Expenses:Food:Cafes:Coffee			3.20 USD
	Expenses:Transport:Transit:Bus			0.60 USD
	Expenses:Transport:Transit:Bus			0.60 USD
	Expenses:Subscriptions:Music			11.99 USD

2026-01-04
	Assets:Cash:Pocket:Checking
	Expenses:Food:Restaurants:Doner			9.80 USD
	Expenses:FX:Loss				2.05 USD ; idk where

2026-01-05 Buy gold and silver
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:PreciousMetals:Gold		5 GOLD @@ 545 USD
	Assets:Investments:PreciousMetals:Silver	20 SILVER @@ 26 USD
	Expenses:Fees:MetalPremium			21 USD

2026-01-06
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:Stocks:Broker		2 ACME @@ 505 USD
	Assets:Crypto:Exchange				0.5 COIN @@ 60 USD

2026-01-07
	Assets:Receivable:Friend			100 USD
	Assets:Cash:Bank:WeeWeeBank:Checking

2026-01-10
	Assets:Cash:Pocket:Checking			100 EUR @@ 108 USD
	Assets:Cash:Bank:WeeWeeBank:Checking

2026-01-15
	Assets:Cash:Bank:WeeWeeBank:Checking		100 USD
	Assets:Receivable:Friend

2026-01-16
2026-01-17
2026-01-18
```