Fintech · own product · 2026
Frendex — Expense Splitting
My own product, launching soon.
- Flutter
- NestJS
- TypeScript
- API Development
- Mobile App Development

My own product, launching soon. Splitting a bill is easy. Keeping it straight over a whole trip is not — and that is where these apps fall apart. Once a group stops trusting the number at the top of the screen, the app is finished. Money is counted in whole cents, never decimals: a three-way split of $100 still adds up to exactly $100. Balances are never stored — they are recomputed every time, so they cannot drift. Currencies never merge. Settle-up nets down to the fewest transfers. Four split strategies, 33 screens, an 11-controller API, with unit and e2e tests on the money logic.

How these apps actually fail
Not at launch — three months in, when the balances have quietly stopped matching the expenses and nobody can work out who is right. Once a group stops trusting the number at the top of the screen the product is finished, because being correct was the only thing it was ever for.
Three specific causes. Floating-point money, where splitting $100 three ways gives $99.99 and a few hundred of those makes the ledger measurably wrong. Stored balances, where any failed write or race leaves a running total permanently disagreeing with the records beneath it. And settlement sprawl, where four people who all owe each other end up with five payments to make, most of which cancel out.

Four split strategies, one engine
Equal, exact, percentage and share-based all pass through a single function with deterministic remainder distribution, so the parts always sum to the whole regardless of which strategy produced them.
A three-way split of $100 becomes 3334 + 3333 + 3333 — exactly 10000 cents, with the leftover penny assigned rather than lost. A 50/50 split of an odd total gives 51 and 50. A 1:3 share split of 100 gives 25 and 75.
Splits that do not reconcile are rejected at the API rather than saved and quietly absorbed into somebody's balance — and that refusal is itself a test case.

Currency is part of the debt, not a display setting
Two people who share a dollar group and a rupee group hold two entirely independent balances. No amount of arithmetic merges them without an exchange rate, so the ledger keys on debtor, creditor and currency, and never sums across currencies.
Currency belongs to the group and is chosen once, at creation — so every debt inside a trip keeps that currency for the rest of its life.
Getting people in matters as much as the maths: contacts matching, QR scanning across a table, friend codes, and a group invite link that goes out through whatever the phone already has.

The settle-up algorithm, and why balances are derived
Net positions are computed per member, then debtors and creditors are sorted by magnitude and greedily matched — producing the minimum set of transfers that clears the group, computed independently within each currency. Five payments become two.
That matters because it is an algorithm solving a behavioural problem: people simply do not settle up when it requires five transfers, including one for fifty cents that nobody will ever make.
A settlement is a record in its own right, not a delete. Expenses and settlements are both append-only, and the balance is derived by walking them on every read. That costs more compute than a running total — and it cannot drift, which in a product whose entire value is being right about money is not a close trade.

Staying in sync without refetching everything
When a friend adds an expense, the naive fix is to reload the whole app on the next push. This does something better.
Every push carries a cursor in its data payload. The client calls the sync endpoint on app start, on resume from background, and whenever a push lands, passing the last cursor it stored. The response names exactly which endpoints are now stale — activity, friends, groups, balances, and the specific group and expense ids affected — so the app refreshes one screen instead of everything.
Cold starts get a starting cursor plus a full-refresh instruction. A client that has been away too long is told to treat everything as stale. Cursors are normalised before comparison, because two timestamps for the same instant can sort differently as strings, and that is the difference between working and silently skipping a change.
The mapping from event type to stale screens lives on the server, so a new activity type invalidates the right screens without shipping a client update.
Tested, not asserted
The split maths has its own unit suite, with tests named for the exact failures they prevent: distributes equal remainder cents without losing money; supports exact, percentage, and share-based splits; rejects invalid totals.
Two Jest configurations run the pure maths and the full API independently, so the money logic can be tested fast enough that there is no excuse not to run it.
Flutter with BLoC on the front — 134 files, 18,961 lines, 33 screens, 12 BLoCs, light and dark themes, biometric unlock. NestJS behind it with 11 controllers, validated inputs, rate limiting, and an OpenAPI specification generated from the built application rather than maintained by hand.