What I wanted to solve
A single 'simplify debts' button hides an important choice. Closed loops can be cancelled without changing who owes whom, but collapsing open chains may make two people counterparties for the first time. I rebuilt CashFlow around showing that trade-off instead of quietly choosing it for the group.
What made it difficult
- Money must stay in integer minor units while the graph is reduced thousands of times.
- Cycle-only clearing must preserve every party's net position and never introduce a counterparty.
- Full path simplification has to disclose every relationship it creates.
- The engine needs to handle obligation networks with thousands of edges and report when an optimality cap is reached.
- Group roles govern expenses, exports, membership, and settlement actions.
- A calculated obligation is not proof that money moved between people.
How it works
- 1
Expense ledger
The server stores group expenses, roles, audit events, and integer-minor-unit amounts in PostgreSQL.
- 2
Obligation graph
Members are nodes and the actual who-owes-whom relationships are directed, capacity-limited edges.
- 3
Cycle clearing
A maximum-circulation solver removes value that can flow around closed loops while conserving each party's position.
- 4
Path compensation
The aggressive mode shortens open chains toward the net-position floor and records any new counterparty pairs it creates.
- 5
Browser engine and graph
The dependency-free clearing package and hand-built canvas renderer let a user switch modes without a server round trip.
- 6
Product boundary
The surrounding app handles authorization, realtime updates, exports, receipt parsing, and the separate workflow for recording payments.
Choices I made
Keep obligations, not only final balances
A balance-only solver loses the relationships that existed before simplification. I keep the obligation graph so the engine can prove that cycle mode created no new counterparties and can name the pairs introduced by path mode.
Treat safe clearing as maximum circulation
A capacity-respecting circulation preserves every party's net position. Maximising that circulation maximises the debt cancelled inside closed loops without manufacturing a relationship outside the original graph.
Make the aggressive trade-off visible
Path compensation can clear more debt, but only by shortening chains. The UI reports the extra amount cleared alongside the exact people who would become new counterparties.
Run the comparison in the browser
The standalone clearing package has no runtime dependencies and works on integer amounts. Switching between the original, cycle-only, and path modes is immediate and does not send the graph back to the server.
Keep money in integer minor units
Clearing repeatedly adds and subtracts edge capacities. Integer amounts keep the conservation checks exact and prevent fractional-cent drift from entering the instructions.
How I tested it
- The repository reports 129 passing tests across the web app, server, and clearing package.
- Clearing tests check net-position conservation, integer inputs, no-new-counterparty guarantees, forbidden pairs, and the difference between cycle and path modes.
- The seeded benchmark covers friend groups, unstructured graphs, and tiered supply-chain fixtures up to 13,935 obligation pairs.
- Server tests cover authentication, group authorization, audit scope, settlement confirmation, and fixed-point storage.
- The verify command runs lint, all three test suites, the server TypeScript build, and the production web build.
Known failure modes
- A purely hierarchical or acyclic obligation graph gives cycle mode nothing to cancel.
- Path mode can create relationships between people who did not previously owe each other.
- A configured circulation iteration limit can stop the refinement before it proves optimality, which the result reports.
- Currency conversion context does not remove exchange-rate or rounding policy decisions.
- External receipt OCR can fail and may require consent for third-party data handling.
What works now
- The documented five-person example reduces ₹4,800 across eight IOUs to ₹550 across four payments in full-simplification mode.
- Cycle mode guarantees zero new counterparties; path mode reports every new pair it creates.
- On the 13,935-pair regional benchmark, cycle mode cleared 22.88% and path mode cleared 64.02%; the latter introduced 6,703 pairs.
- The clearing engine is a standalone dependency-free TypeScript package that runs in the browser.
- Role-aware groups, recipient-confirmed settlements, an audit history, exports, and live updates.
Two answers to ‘simplify’
CashFlow does not present one simplification as universally correct. It keeps the original graph beside two different guarantees so the group can choose the trade-off it actually wants.
- Cancel loops removes maximum circulation while keeping the original counterparty set.
- Simplify all also shortens open chains toward the minimum outstanding total.
- Both preserve every party's net position exactly in integer minor units.
- The aggressive result lists the new pairs it creates instead of hiding them behind a smaller payment count.
What it doesn't do yet
- CashFlow records obligations and confirmations. It does not hold funds or connect to a bank.
- The hosted application is a public beta, not an audited financial product.
- The large benchmark networks are seeded fixtures, not private production transaction data.
- The optional C++/WebAssembly minimum-transfer solver remains in the repository, but it is not the engine behind the browser's obligation-clearing comparison.
- A user should not enter financial data they would be uncomfortable storing in a hosted PostgreSQL database.