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.md files (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 -v part of it, titled (partial)), never touching other ledgers. Payment method defaults to Pix (-m overrides).
  • 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 -v that 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