spltty
Shared-expense tracking in plain markdown ledgers, with the arithmetic owned by
a CLI instead of a spreadsheet — and a generated CLAUDE.md so an AI agent can do
the transcription while spltty does the math.
The data is markdown tables you can read, diff and grep. spltty owns the parts
that must be exact: appending rows, resolving split groups, computing who owes
whom, and recording settlements.
mkdir ~/finances && cd ~/finances
spltty install # asks who splits what, scaffolds the workspace
spltty add "Groceries" -v 84.20 -p Ana -m cash -r Both
spltty totals
Install
Homebrew
brew install thiagodiniz/spltty/spltty
# from a checkout, before the tap exists:
brew install --build-from-source ./Formula/spltty.rb
RubyGems (needs Ruby >= 3.1)
gem install spltty
Linux/Unix script — installs the gem, falls back to a per-user install and
tells you if the bin dir is not on your PATH:
curl -fsSL https://raw.githubusercontent.com/thiagodiniz/spltty/main/install.sh | sh
From a checkout (development). Ruby is pinned via mise
(.tool-versions → ruby 3.4.2); bin/spltty selects it automatically, so the CLI
runs from any directory without activating mise:
mise install && bundle install
./bin/spltty help
rake install # build the gem and put `spltty` on your PATH
Setting up a workspace
spltty install [PATH] creates everything a tracker needs in a folder (the current
directory by default):
.spltty/config.json participants, split groups, payment methods, ledger cache
accounts/ one ledger per account being tracked
sources/INDEX.md tracker for raw statements/receipts
CLAUDE.md generated workflow guide for the AI agent
.claude/skills/ingest/ the /ingest skill for raw inputs
It asks for the participants (at least one is required), a shared split group, the
ledgers to create, optional payment-method defaults, and whether to git init.
Every step but the participants can be skipped — pressing enter through it yields
one monthly ledger named expenses.
Non-interactively:
spltty install ~/finances -y -P Ana,Bruno -s Ana:60,Bruno:40 -l HOME,TRIP --no-git
| Flag | Meaning |
|---|---|
-P, --participants |
comma-separated names (required) |
-g, --group / -s, --split |
shared group name and NAME:PCT,NAME:PCT split (default: Both, even) |
-l, --ledgers |
comma-separated ledger names (default: expenses) |
--single |
create ledgers as single files instead of per-month |
--no-git / --git |
skip or force git init |
-f, --force |
overwrite an existing workspace config |
Every command afterwards finds the workspace by walking up from the current
directory for .spltty/config.json — like git. Override with --config /
SPLTTY_CONFIG.
Usage
spltty install [PATH] # scaffold a new workspace (accounts, sources, CLAUDE.md)
spltty add [options] # (alias: a) append an entry (flags + interactive confirm)
spltty list # (alias: ls) list discovered ledgers (also runs the notes<->config sync)
spltty sync # sync ledger config between notes-file headers and config.json
spltty totals [LEDGER..] # per-person totals & settlement per ledger (--combined to merge)
spltty settle [LEDGER..] # record a settlement (one ledger = cash; several/none = offsets + optional cash)
spltty groups # list split groups (global + per-ledger)
spltty groups add NAME # define/update a split group (in a ledger header, or --global)
spltty groups rm NAME # remove a split group
spltty methods # list configured payment methods
spltty methods add NAME # add/update a payment method in the config
spltty help # show every command with its arguments/options
spltty add --help # options for a single command
Add or update a payment method without hand-editing the config:
spltty methods add "Visa 1234" -s visa-1234 -p Ana -r Both -b 10
# -s slug -p paid-by -r responsible -b bill-day (1-31 or "last")
# Only the properties you pass are set; existing ones are preserved.
add takes the title as a positional argument; everything else is a flag
with a short alias. It prompts for any required field you omit, applies
payment-method defaults, shows the resolved row, and asks before writing. Pass
--yes/-y to skip prompts (required fields must then be supplied).
Unknown ledger names are treated as typos, not new accounts. -l CAS when
you meant CASA would otherwise scaffold a junk ledger that then shows up in
every totals run. Interactively you get a menu — create it, or pick the one you
meant:
$ spltty add "Coffee" -l CAS -v 12.90
Ledger "CAS" is not known yet.
1) Create a new ledger "CAS"
2) HOME
3) TRIP
Which ledger? [1]: 2 # or type HOME
With --yes there is nobody to ask, so it errors and lists the known ledgers.
Creating a ledger from a script needs --create-ledger:
spltty add "Mooring" -l BOAT -v 40 -p Ana -m cash -y --create-ledger
Examples
# Positional title + short flags:
spltty add "Leite 1L" -l HOME -v 14.20 -p Ana -m cash
# Card entry — Paid By/Responsible/Date default from the card's config:
spltty add "Extra" -l HOME -v 66.69 -m "Visa 1234" -y
# Multi-currency entry:
spltty add "Metro" -l TRIP -o 3.75 -c CAD -v 14.20 -p Bruno -m Wise -y
# Fully interactive (prompts for what's missing):
spltty add
Arguments & options
| Flag | Short | Meaning |
|---|---|---|
<title> |
Expense description — positional (first bare argument) | |
--ledger |
-l |
Ledger name (default: default_ledger in config; case-insensitive) |
--date |
-d |
YYYY-MM-DD (default: today, or the card's bill day) |
--value |
-v |
Amount in R$ (12.5, 12,50, 1.234,56 all accepted) |
--paid-by |
-p |
Who fronted the money |
--method |
-m |
Payment instrument; matches a configured payment method |
--responsible |
-r |
Who bears the cost — a split-group name (Both, …) or a person (Ana) |
--paid-responsible |
-pr |
Shortcut: sets both Paid By and Responsible (e.g. -pr Ana); -p/-r override |
--source |
-s |
Provenance (default text) |
--orig-value |
-o |
Original amount — multi-currency schema only |
--currency |
-c |
Original currency — multi-currency schema only |
--create-ledger |
Create --ledger when it doesn't exist (required with --yes) |
|
--yes |
-y |
Non-interactive; no prompts or confirmation |
--config |
-C |
Config path (default .spltty/config.json; or SPLTTY_CONFIG) |
--accounts-dir |
-A |
Override accounts dir (or SPLTTY_ACCOUNTS_DIR) |
Config (.spltty/config.json)
Only payment_methods is authored by hand. The ledgers block is optional and
auto-maintained: on every run the CLI scans accounts/ and records what it
finds. Discovery treats as a ledger:
- a root file named
<NAME>.ledger.md(single-file ledger), or - a subfolder containing
YYYY-MM.mdfiles (monthly ledger, e.g.HOME).
Everything else is ignored. Structural fields (type/file/dir/schema) are
set automatically; title, default_responsible, default_currency, and
notes are user overrides that discovery preserves. See config.example.json.
Notes-header sync
The three tunable fields — title, default_responsible, default_currency —
also live in each ledger's *.notes.md file, in a YAML frontmatter header under a
spltty: key, so the config sits next to the rules it documents:
---
spltty:
default_responsible: Both
groups:
Both: {Ana: 65, Bruno: 35}
---
# WEDDING — Wedding — Notes
…
spltty sync (and spltty list) keep the header and config.json in agreement.
The notes file is the source of truth. Each ledgers entry carries a
synced_at timestamp; the direction is decided by comparing it to the notes
file's filesystem mtime:
- notes file edited more recently (or never synced) → the header wins and updates config.
- config newer, or the header is missing the field → config is copied into the header.
Every field is optional: only keys present on the winning side are merged, an
absent key is never cleared, and a ledger with no tunable config gets no
spltty: block. Any other frontmatter keys you add are left untouched. Don't
hand-edit synced_at — the sync manages it (and stamps it to the post-write
mtime so a write never ping-pongs). spltty add does not run this sync.
Split groups
A split group is a named split — name → {participant: percentage} — such as
Both → {Ana: 70, Bruno: 30} or Presente → {Ana: 100}. Percentages are
integers and should sum to 100 (a mismatch only warns).
Each ledger's own groups live in its notes header, under spltty.groups, and are
synced into config.json (as ledgers.<NAME>.groups) by the same
notes↔config sync above:
---
spltty:
default_responsible: Both
groups:
Both: {Ana: 70, Bruno: 30}
Presente: {Ana: 100}
---
# HOME — … — Notes
When the identical definition (same name + participants + percentages) appears
in two or more ledgers, it is promoted to a shared global group at the top
level of config.json ("groups": { … }). Global groups are config-only — they
are never written back into any header, and the sync only manages ledger-specific
groups. A ledger group shadows a global group of the same name (e.g. a global
Both 50/50 vs HOME's own Both 70/30).
Manage groups without hand-editing YAML:
spltty groups # list global + per-ledger groups
spltty groups add Both -s Ana:70,Bruno:30 -l HOME # write into HOME's header
spltty groups add Both -s Ana:50,Bruno:50 --global # write a global group
spltty groups rm Both -l HOME # remove from a ledger (or --global)
Consumed by spltty totals: when computing settlement, each row's
Responsible value is resolved against these groups (ledger header groups first,
then global). A match splits the cost by the group's percentages; anything
unmatched is treated as a person (100%). There is no Both (NN/NN T/C)
string-parsing anymore — the split lives in the group. (spltty add still writes
the Responsible cell verbatim; it does not yet validate it against known groups.)
Totals & settlement
spltty totals compiles per-person Paid, Owes, Net = Paid − Owes, a
spending breakdown, and a greedy Settlement (who pays whom):
spltty totals # one report per ledger (reference ledgers flagged)
spltty totals HOME TRIP # only these ledgers
spltty totals --combined # merge all non-reference ledgers into ONE settlement
A monthly ledger (HOME) merges all its YYYY-MM.md files into one per-ledger
report. Splits are resolved via split groups (above).
Reference ledgers
A ledger that intentionally duplicates rows already recorded in another one — a project tracker mirroring shared spend, so the project's cost is visible in one place — must never be summed with the primaries; that double-counts. Mark it in its notes header:
---
spltty:
reference: true
---
Reference ledgers are flagged in per-ledger totals, excluded from --combined
(and listed as skipped), and refused in a cross-ledger settle — settle them on
their own in cash mode.
Each payment method may define an optional slug — a short alias you can pass
to -m instead of the full name. Both are matched case-insensitively, and the
row always stores the full canonical name:
"Visa 5678": { "slug": "visa-5678", "paid_by": "Bruno", "responsible": "Bruno", "bill_day": "last" }
Then spltty add "Coffee" -l HOME -v 12 -m visa-5678 fills Paid By / Responsible /
Date from that card. If a method has no default for a field, interactive mode
prompts for it (and --yes errors). spltty methods lists every method with its
slug.
Settling up
spltty settle records a settlement (the Settlement: line in totals is
only a suggestion). A settlement is written as a normal table row — a transfer
where Paid By = debtor and Responsible = creditor — so it flows through
the totals math and offsets the debt exactly. No table is ever hand-edited.
spltty settle HOME # cash-settle HOME in full (Pix row in the current month file)
spltty settle TRIP -v 500 -y # partial cash settle, non-interactive
spltty settle # global offset mode: zero opposite-direction ledgers, rest stays open
spltty settle -v 2000 # global: offsets + R$ 2000 cash, smallest ledger first
spltty settle HOME WEDDING # offset mode restricted to these two ledgers
Two modes, chosen by how many ledgers you pass:
- One ledger — cash mode. A real payment for that ledger's outstanding
balance (or
-vpart of it, titled(partial)), never touching other ledgers. Payment method defaults toPix(-moverrides). - None/several ledgers — offset mode. None = all non-reference ledgers.
Ledgers whose debt points opposite to the combined direction are zeroed with
mirrored rows (payment method
offset— no real money moves): e.g. HOME has Bruno owing Ana 7000 and WEDDING has Ana owing Bruno 5000 ⇒ WEDDING gets an offset row Ana → Bruno 5000, HOME the mirror Bruno → Ana 5000, leaving 2000 open in HOME. Without-vthat is all that happens — the remainder stays open (with nothing to offset, it just prints the outstanding and writes nothing). With-v X, X is then recorded as a cash payment against the remainder. Offsets and cash are both allocated smallest-outstanding ledger first.
Rows are stamped with today's date (-d overrides; a card method's bill day is
ignored) and Source = settle (-s overrides). The plan is previewed and
confirmed before writing (-y skips). Ledgers marked reference: true never join
offset mode — settle them on their own in cash mode.
Tests
bundle exec rake test