Skip to content

Settling up

A jar’s balance isn’t a column — it’s the running difference between everything one member paid for others minus everything those others paid back. When the trip ends, you settle: each person makes one or two transfers and the balances zero out.

The unit of obligation is receipt_splits. Each receipt with N people on it produces N split rows: who owes whom, how much, in the jar’s base currency. The pre-settlement view of the jar is just the sum of those rows, grouped by debtor → creditor pair.

settlements then subtract from that view. A settlement row is “X paid Y the amount Z on date D” — same shape as a split row, opposite sign. The net balance is splits minus settlements per pair.

The aggregation is a pure function:

balance(jar) = group_by_pair(receipt_splits) - group_by_pair(settlements)

Both sides live in src/lib/balances.ts and are derived live; nothing is denormalised.

Settlements come from the user, not the system — Splitjar doesn’t move money. You log that a transfer happened: who paid, who received, how much, when.

The endpoint is POST /api/groups/:idOrSlug/settlements with { from, to, amount, currency, paidAt, note? }. Both members must be in the jar; the amount is in the jar’s base currency.

If you try to settle more than the outstanding balance between two people, the request is refused with a 400 oversettlement. Reasoning:

  • A settlement should reduce the outstanding obligation, not invert it
  • Logging “I paid Steve $200” when the actual debt was $50 silently flips Steve into owing you $150
  • That’s almost always a typo, and it’s caught most often when a settle-up tour breaks down (“wait, why does Steve owe me money now?”)

A small grace band exists so a round-to-the-nearest-dollar settlement on a $42.73 debt isn’t blocked. The guard kicks in when the overshoot is meaningful.

When every pair-balance is zero (or close enough that the per-pair grace allows it), the jar is “settled”. The owner can mark it closed from the jar header. Closed jars are read-only — no new receipts, no new splits — but exports, balances, and history stay accessible forever.

Why we don’t compute “minimum transfers”

Section titled “Why we don’t compute “minimum transfers””

A correctly-played jar has at most N-1 settlements between N people. Some products try to compute the minimum number of transfers to balance everyone — solving the cyclic-debt graph for the smallest spanning set.

We don’t, because:

  • It’s a graph theory problem with multiple equally-valid answers; any choice is opinionated
  • People settle along social lines, not optimal-transfer-graph lines (“I’ll pay Steve, he’ll cover the others”)
  • The few-percent reduction in transfers isn’t worth the surprise of telling someone “actually pay this random person, not the one you owe”

Splitjar shows the per-pair balance and lets you settle in whatever order makes sense to your group.