Build Splitwise

KINDAreplaces $5/mosaves $60/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A shared expense ledger where the interesting problem is the settlement graph: turning a tangle of who paid for what into the smallest set of transfers that clears it, without ever losing a penny to rounding or to a currency conversion nobody agreed on.

estimated effort multi-daythe files for this build are in the project pack

RuntimeTypeScript with Fastify and PostgreSQLAmountsInteger minor units with an explicit remainder allocationSettlementDebt simplification over the balance graphCurrencyRate recorded at entry, never re-convertedHistoryEvery expense and edit retained

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server and the settlement computation.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Shared money between people needs real transactions and exact integer arithmetic.

    Get it Docker or your package manager. open ↗

    Verify psql connects and integer arithmetic is exact

  3. have readyfree

    Why The settlement graph is uninteresting below about six people, and rounding, multiple payers and mixed currencies all appear together only in real shared spending.

    Get it Reconstruct a holiday or a shared house month with at least forty expenses, several payers and at least two currencies.

    Verify Forty expenses exist across six or more people with two currencies

  4. decidefree

    Why Every uneven split has one and the rule is enforced in code, so it belongs decided rather than emergent.

    Get it Decide the allocation rule and whether it rotates, and write it down where the group can see it.

    Verify A written rule exists and is displayed in the application

  5. decidefree

    Why Debt simplification routes obligations through people who did not transact, which is correct arithmetic and sometimes socially unwelcome.

    Get it Decide whether opting out is available by default, and whether original obligations are always shown.

    Verify A written note states the default and what is displayed

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- groups(id, name, default_currency, created_at, settled_at)
- members(group_id, person_id, joined_at, left_at)
- expenses(id, group_id, description, amount_minor, currency, rate_to_group_currency, paid_at, created_by, deleted_at)
- payers(expense_id, person_id, amount_minor) · more than one person can pay for one expense
- shares(expense_id, person_id, amount_minor, method) · method is equal, exact, percentage or shares
- settlements(id, group_id, from_person_id, to_person_id, amount_minor, currency, confirmed_at)
- edits(expense_id, ordinal, before_json, after_json, by, at) · retained, because a changed expense is a changed balance
- Shares are stored as resolved amounts rather than as a rule, so the remainder allocation is a fact rather than something recomputed differently later.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/splitShared expenses between identified people.
DEFAULT_CURRENCYrequiredEURThe group currency. Individual expenses keep their own.
REMAINDER_RULErequiredrotateWho receives the odd penny. Rotating is fairer than always the first member.
ALLOW_INTERMEDIATION_OPT_OUTrequiredtrueWhether a member can refuse to be routed through in settlement.
REQUIRE_SETTLEMENT_CONFIRMATIONrequiredalwaysPresent as a constant. One-sided settlement is assertion rather than payment.
RATE_SOURCErequiredmanualWhere conversion rates come from. Recorded per expense and never re-applied.

The build, in order

  1. Splitting money without losing a penny

    Every split sums exactly to the amount paid.

    1. Stored as amounts, not as a rule to recompute.

      Files src/split.tssrc/split.test.ts

    2. Deterministic, recorded, and rotated so it is not always the same person.

      Files src/split.ts

    3. A thousand random splits. This is the invariant everything rests on.

      Files tests/split-sum.test.ts

    done when · tick each as it passes
    watch out
    • Splitting an amount that does not divide evenly and rounding each share independently produces a total that does not match what was paid, and over a holiday's worth of expenses the discrepancy becomes visible and nobody can explain it. Allocate the remainder explicitly, rotate who receives it, and assert the sum.
  2. Balances that survive an edit

    Changing an expense changes the balances correctly and visibly.

    1. Across the group. Any non-zero total is a bug in the split maths.

      Files src/balances.tstests/balance-zero.test.ts

    2. Shared money arguments are settled by the record.

      Files src/edits.ts

    3. To the people affected. A silently changed figure is how trust goes.

      Files src/notify.ts

    done when · tick each as it passes
    watch out
    • Editing an expense silently after people have looked at their balances is how shared money becomes an argument, because someone remembers owing a different figure and there is no record of the change. Retain the edit, and surface the balance movement to everyone it affected rather than only updating the number.
  3. The fewest payments that clear it

    A tangled group settles with as few transfers as possible.

    1. Positive and negative balances matched, not every pair reconciled.

      Files src/simplify.tssrc/simplify.test.ts

    2. Simplification must not lose or invent a penny.

      Files src/simplify.ts

    3. Routing a stranger's debt through someone is correct and unwelcome.

      Files src/simplify.ts

    done when · tick each as it passes
    watch out
    • Simplification will happily route a debt through someone who never transacted with either party, which is mathematically correct and socially wrong, since they now owe money to a person they did not deal with. Let people opt out of intermediation, and show the original obligations alongside the simplified transfers.
  4. More than one currency

    A trip with three currencies settles fairly and explicably.

    1. And keep it on the expense. Re-converting changes what people agreed.

      Files src/currency.ts

    2. So a disagreement is about the rate rather than about the arithmetic.

      Files src/display.ts

    3. Conversion rounding is where it usually breaks.

      Files tests/multicurrency.test.ts

    done when · tick each as it passes
    watch out
    • Converting balances at today's rate rather than the rate recorded when the expense happened means the amount someone owes changes daily without anyone spending anything, and on a two week trip that is a real and unexplainable difference. Fix the rate at entry and keep it with the expense.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more personal finance.