What it is
A bill splitter that handles the three situations my friends actually get into: a trip where six people pay for things in four currencies, a flat where rent and utilities recur, and a mahjong night where money moves every hand.
The existing apps do the first case adequately and the third not at all. Mahjong isn’t a shared expense: it’s a sequence of small settlements with winners, losers, partner arrangements and someone who paid for the food. Modelling that as “split a bill five ways” produces a number nobody accepts.
How it works
There are three split workflows rather than one generic one. Travel splits handle uneven participation across days. General splits cover recurring shared costs. Mahjong splits are scored pot by pot: winners, losers, partners, and meal money kept separate from play.
Multi-currency covers SGD, CNY, USD, MYR and HKD, with a per-split FX rate pulled from MAS. Fixing the rate to the split rather than the moment of viewing means the numbers stop moving after the trip ends, which is the only behaviour anyone will tolerate.
Settlement has two views, because people want two different things. “Fewest transfers” optimises the graph down to the minimum number of payments. “By activity” keeps person-to-person debts intact so you can see who owes what for which dinner. The first is efficient. The second is the one that ends arguments.
The parts that had to be right
Money maths and settlement optimisation are unit-tested, because a cent of drift across forty transactions destroys trust in a tool like this permanently. The mahjong scoring engine was ported from an earlier app of mine and kept intact rather than reimplemented.
The stack is Next.js 16 on Supabase (Postgres, Auth, Storage and Realtime) with TanStack Query on the client. Row Level Security is the sole security boundary: there is no privileged server path that could quietly bypass it. Splits are shared by invite link and update live for everyone in them. Receipt photos attach to entries, and charts show where the money went.
Where it stands
Shipped, running as an installable PWA, and iterated on for about six months, mostly in response to arguments it failed to settle.