Inspiration

Every expense-splitting app stops at the same place: it tells you who owes whom, and then nothing happens. Someone still has to send a Venmo request, someone still forgets, and the balance sits there for weeks. I wanted to close that loop; an app where settling up isn't a separate manual step, it's a button.

What it does

Fair Split lets a group track shared expenses (a trip, a dinner, rent) and automatically computes each member's balance. Instead of suggesting every possible payment between every pair of people, it runs a debt-simplification algorithm to find the minimum number of transactions needed to settle the whole group. Each suggested payment is backed by a real Stripe Connect transfer, clicking "Record" moves real money (in Stripe's test mode) between connected accounts and returns a real, verifiable transfer ID.

How we built it

  • Frontend/backend: Next.js 14 (App Router) + TypeScript + Tailwind, with Server Actions for all mutations
  • Data: SQLite via better-sqlite3; Groups, Members, Expenses, ExpenseSplits, and Settlements
  • Payments: Stripe Connect (Express accounts), each group member has a real connected account; settlements call stripe.transfers.create() directly
  • Algorithm: a greedy min-transaction debt simplifier, matches the largest creditor with the largest debtor repeatedly until the group nets to zero
  • Design: a custom "travel ledger" visual system (Fraunces serif for headers/amounts, Inter for UI, IBM Plex Mono for account/transfer IDs) with settlement suggestions styled as ticket stubs, and a "SETTLED" stamp animation on a completed transfer
  • Workflow: built solo, with Cursor handling implementation from detailed, incremental prompts while I drove architecture, the settlement algorithm, and the Stripe integration

Sample verified transfer from testing: tr_1U8S8dIDZeW0vlN5CNlXEXwI

Challenges I ran into

Stripe Connect account types took real debugging: accounts created for accepting payments ("merchant" configuration) can't receive transfers, only accounts with "recipient" configuration can. I hit this as a live transfers.create() error mid-build, traced it to account capabilities, and fixed my test account setup accordingly. I also hardened the settlement flow after finding that a rapid double-click could theoretically trigger two real transfers for the same suggested payment,fixed with a synchronous submission lock.

Accomplishments that I'm proud of

Every transfer in this app is real: not mocked, not simulated client-side. The settlement math is also verifiably correct: my debt-simplification algorithm was tested against known cases (e.g., one creditor with multiple debtors) and produces the exact minimum transaction set.

What I learned

Stripe Connect's account-type model (merchant vs. recipient capabilities) isn't obvious until you hit it in practice. I also learned a lot about scoping a solo hackathon build: cutting multi-currency, real auth, and custom split percentages early let me spend the available time on the two things that actually mattered, a real payment integration and a settlement algorithm that's provably correct.

What's next for Fair Split

Real (live-mode) bank payouts instead of test mode, multi-currency support for international trips, and a two-leg payment flow (debtor pays the platform via Checkout, platform pays the creditor) instead of the simplified single-leg transfer used in this demo.

Built With

Share this project:

Updates

Submission history