Vibecode Splitwise
track this build4 phases, 12 steps, beginner friendly0%The ledger and the debt math are a solved problem; a link-shared DIY version covers a household or one trip in a single sitting, no accounts needed. The gap is not code, it is adoption: Splitwise works because the whole group already has it installed, and nobody downloads your homebrew app for one weekend away. Build it for the people who share your fridge; for everyone else, the moat is other people's phones.
You are building a lean indie version of Splitwise. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Splitwise · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | shared money between people needs transactions and a single source of truth | | Amounts | Integer minor units with an explicit remainder allocation | splitting three ways always leaves a penny and someone has to get it | | Settlement | Debt simplification over the balance graph | the point is fewer payments, not a matrix of everyone owing everyone | | Currency | Rate recorded at entry, never re-converted | converting at settlement time changes what people agreed to | | History | Every expense and edit retained | shared money arguments are settled by the record | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the settlement computation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Shared money between people needs real transactions and exact integer arithmetic. - Get it: Docker or your package manager. - Verify: psql connects and integer arithmetic is exact - [ ] **A real trip's worth of shared expenses** · free - 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 - [ ] **Who gets the odd penny** · free - 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 - [ ] **Your position on intermediation** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. - native iOS and Android apps, push reminders included - your friends' existing accounts and muscle memory - receipt scanning and itemized splits - automatic currency conversion at live rates - settle-up integrations with payment apps If one of those is essential to you, that is the reason to keep paying for Splitwise, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Splitwise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me shared expense tracking that settles a group in as few payments as possible to replace Splitwise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · shared money between people needs transactions and a single source of truth - Amounts: Integer minor units with an explicit remainder allocation · splitting three ways always leaves a penny and someone has to get it - Settlement: Debt simplification over the balance graph · the point is fewer payments, not a matrix of everyone owing everyone - Currency: Rate recorded at entry, never re-converted · converting at settlement time changes what people agreed to - History: Every expense and edit retained · shared money arguments are settled by the record ### Data model (create this before Phase 1) - 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. ### Phase 1 · Splitting money without losing a penny Build: Expenses with several payers and several split methods, all resolved to exact integer amounts. Allocate the remainder deterministically and record who received it. Assert that shares sum to the total. Trap: 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. Done when: Shares always sum exactly to the expense total, asserted across a thousand random splits. A three-way split of an odd amount allocates the remainder deterministically and records who got it. Several payers on one expense sum correctly. Percentage and share-based splits resolve to exact amounts at entry. Do not build yet: No balances, settlement or currency handling. ### Phase 2 · Balances that survive an edit Build: Compute per-person balances from expenses and settlements. Retain every edit and deletion with what changed. Show anyone affected by an edit that their balance moved and why. Trap: 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. Done when: Balances derive from expenses and settlements and reconcile to zero across the group, asserted in a test. An edited expense retains its previous version. Anyone whose balance changed by an edit is shown the change and its cause. Deleting an expense is a recorded deletion rather than a removal. Do not build yet: No settlement or currency yet. ### Phase 3 · The fewest payments that clear it Build: Simplify the debt graph so a group of eight with forty expenses settles in a handful of transfers rather than a matrix. Preserve the exact totals. Let a member opt out of being routed through. Trap: 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. Done when: A group of eight with forty expenses settles in a small number of transfers, verified against the naive pairwise count. Simplified transfers sum exactly to the original balances. A member who opted out of intermediation is never routed through, tested directly. The original obligations remain visible beside the simplified plan. Do not build yet: No currency or settlement recording yet. ### Phase 4 · More than one currency Build: Record each expense in its own currency with the rate used at entry. Never re-convert later. Show balances in the group currency and in each person's own, stating the rate. Trap: 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. Done when: An expense keeps the rate used at entry and balances do not move when rates change, tested by changing the rate afterwards. Balances display in the group currency with the rate stated. A settlement in a different currency records both amounts and its own rate. Rounding across currency conversion still nets to zero. Do not build yet: No settlement confirmation or export yet. ### Phase 5 · Settling up, and remembering that you did Build: Record settlements requiring confirmation from the recipient. Handle a partial payment. Export the full ledger. Close a group without deleting its history. Trap: A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled. Done when: A settlement requires recipient confirmation and shows as pending until then. A partial payment reduces the balance by exactly its amount. Closing a group retains its full history and remaining balances. Export contains expenses, shares, settlements and edits and re-imports identically. Do not build yet: Nothing after this. ### Out of scope (and why) - Moving actual money, which is a payments and regulatory project rather than a ledger one. - Receipt scanning and itemisation, which is where a lot of the practical convenience lives. - Mobile applications, which is how this category is overwhelmingly used in the moment. - Recurring bills between housemates, which is a scheduling layer on top of everything here. ### README must contain - That shares are resolved to exact amounts at entry with the remainder allocated and recorded - That balances net to zero across a group and this is asserted in the test suite - That simplification never routes through someone who opted out of intermediation - That currency rates are fixed at entry and balances do not move when rates change - That a settlement requires confirmation from the recipient ===== AGENTS.md ===== # Agent instructions · Splitwise indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Integer minor units with an explicit remainder allocation, Debt simplification over the balance graph, Rate recorded at entry, never re-converted, Every expense and edit retained. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Splitwise 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Splitting money without losing a penny Every split sums exactly to the amount paid. ### Steps 1. Resolve every split to exact amounts at entry Stored as amounts, not as a rule to recompute. Files: `src/split.ts`, `src/split.test.ts` 2. Allocate the remainder explicitly Deterministic, recorded, and rotated so it is not always the same person. Files: `src/split.ts` 3. Assert the sum in the suite A thousand random splits. This is the invariant everything rests on. Files: `tests/split-sum.test.ts` ### Done when - [ ] Shares always sum exactly to the total - [ ] The remainder is allocated deterministically and recorded - [ ] Several payers sum correctly - [ ] Percentage splits resolve to exact amounts ### 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. ## Phase 2 · Balances that survive an edit Changing an expense changes the balances correctly and visibly. ### Steps 1. Derive balances and assert they net to zero Across the group. Any non-zero total is a bug in the split maths. Files: `src/balances.ts`, `tests/balance-zero.test.ts` 2. Retain every edit Shared money arguments are settled by the record. Files: `src/edits.ts` 3. Surface balance movements from edits To the people affected. A silently changed figure is how trust goes. Files: `src/notify.ts` ### Done when - [ ] Balances net to zero across the group - [ ] Edits retain the previous version - [ ] Affected people see the change and its cause - [ ] Deletion is recorded rather than removal ### 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. ## Phase 3 · The fewest payments that clear it A tangled group settles with as few transfers as possible. ### Steps 1. Simplify the balance graph Positive and negative balances matched, not every pair reconciled. Files: `src/simplify.ts`, `src/simplify.test.ts` 2. Preserve exact totals Simplification must not lose or invent a penny. Files: `src/simplify.ts` 3. Respect intermediation opt-out Routing a stranger's debt through someone is correct and unwelcome. Files: `src/simplify.ts` ### Done when - [ ] A tangled group settles in few transfers - [ ] Simplified transfers sum to the original balances - [ ] An opted-out member is never routed through - [ ] Original obligations stay visible ### 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. ## Phase 4 · More than one currency A trip with three currencies settles fairly and explicably. ### Steps 1. Fix the rate at entry And keep it on the expense. Re-converting changes what people agreed. Files: `src/currency.ts` 2. State the rate wherever a converted figure appears So a disagreement is about the rate rather than about the arithmetic. Files: `src/display.ts` 3. Keep the zero-sum invariant across currencies Conversion rounding is where it usually breaks. Files: `tests/multicurrency.test.ts` ### Done when - [ ] Balances do not move when rates change - [ ] The rate is stated wherever conversion appears - [ ] A cross-currency settlement records both amounts - [ ] Balances still net to zero across currencies ### 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. ## Not in this build - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. ## After v1, if you want it - Reminders for outstanding balances, worded so they do not damage friendships - Categories and a trip summary, which is what people want afterwards - Splitting by shares that change per expense, for households with uneven arrangements ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Shared expenses between identified people. DATABASE_URL=postgres://localhost:5432/split # Required. The group currency. Individual expenses keep their own. DEFAULT_CURRENCY=EUR # Required. Who receives the odd penny. Rotating is fairer than always the first member. REMAINDER_RULE=rotate # Required. Whether a member can refuse to be routed through in settlement. ALLOW_INTERMEDIATION_OPT_OUT=true # Required. Present as a constant. One-sided settlement is assertion rather than payment. REQUIRE_SETTLEMENT_CONFIRMATION=always # Required. Where conversion rates come from. Recorded per expense and never re-applied. RATE_SOURCE=manual
You are building a lean indie version of Splitwise. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Splitwise · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | shared money between people needs transactions and a single source of truth | | Amounts | Integer minor units with an explicit remainder allocation | splitting three ways always leaves a penny and someone has to get it | | Settlement | Debt simplification over the balance graph | the point is fewer payments, not a matrix of everyone owing everyone | | Currency | Rate recorded at entry, never re-converted | converting at settlement time changes what people agreed to | | History | Every expense and edit retained | shared money arguments are settled by the record | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the settlement computation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Shared money between people needs real transactions and exact integer arithmetic. - Get it: Docker or your package manager. - Verify: psql connects and integer arithmetic is exact - [ ] **A real trip's worth of shared expenses** · free - 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 - [ ] **Who gets the odd penny** · free - 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 - [ ] **Your position on intermediation** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. - native iOS and Android apps, push reminders included - your friends' existing accounts and muscle memory - receipt scanning and itemized splits - automatic currency conversion at live rates - settle-up integrations with payment apps If one of those is essential to you, that is the reason to keep paying for Splitwise, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Splitwise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me shared expense tracking that settles a group in as few payments as possible to replace Splitwise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · shared money between people needs transactions and a single source of truth - Amounts: Integer minor units with an explicit remainder allocation · splitting three ways always leaves a penny and someone has to get it - Settlement: Debt simplification over the balance graph · the point is fewer payments, not a matrix of everyone owing everyone - Currency: Rate recorded at entry, never re-converted · converting at settlement time changes what people agreed to - History: Every expense and edit retained · shared money arguments are settled by the record ### Data model (create this before Phase 1) - 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. ### Phase 1 · Splitting money without losing a penny Build: Expenses with several payers and several split methods, all resolved to exact integer amounts. Allocate the remainder deterministically and record who received it. Assert that shares sum to the total. Trap: 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. Done when: Shares always sum exactly to the expense total, asserted across a thousand random splits. A three-way split of an odd amount allocates the remainder deterministically and records who got it. Several payers on one expense sum correctly. Percentage and share-based splits resolve to exact amounts at entry. Do not build yet: No balances, settlement or currency handling. ### Phase 2 · Balances that survive an edit Build: Compute per-person balances from expenses and settlements. Retain every edit and deletion with what changed. Show anyone affected by an edit that their balance moved and why. Trap: 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. Done when: Balances derive from expenses and settlements and reconcile to zero across the group, asserted in a test. An edited expense retains its previous version. Anyone whose balance changed by an edit is shown the change and its cause. Deleting an expense is a recorded deletion rather than a removal. Do not build yet: No settlement or currency yet. ### Phase 3 · The fewest payments that clear it Build: Simplify the debt graph so a group of eight with forty expenses settles in a handful of transfers rather than a matrix. Preserve the exact totals. Let a member opt out of being routed through. Trap: 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. Done when: A group of eight with forty expenses settles in a small number of transfers, verified against the naive pairwise count. Simplified transfers sum exactly to the original balances. A member who opted out of intermediation is never routed through, tested directly. The original obligations remain visible beside the simplified plan. Do not build yet: No currency or settlement recording yet. ### Phase 4 · More than one currency Build: Record each expense in its own currency with the rate used at entry. Never re-convert later. Show balances in the group currency and in each person's own, stating the rate. Trap: 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. Done when: An expense keeps the rate used at entry and balances do not move when rates change, tested by changing the rate afterwards. Balances display in the group currency with the rate stated. A settlement in a different currency records both amounts and its own rate. Rounding across currency conversion still nets to zero. Do not build yet: No settlement confirmation or export yet. ### Phase 5 · Settling up, and remembering that you did Build: Record settlements requiring confirmation from the recipient. Handle a partial payment. Export the full ledger. Close a group without deleting its history. Trap: A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled. Done when: A settlement requires recipient confirmation and shows as pending until then. A partial payment reduces the balance by exactly its amount. Closing a group retains its full history and remaining balances. Export contains expenses, shares, settlements and edits and re-imports identically. Do not build yet: Nothing after this. ### Out of scope (and why) - Moving actual money, which is a payments and regulatory project rather than a ledger one. - Receipt scanning and itemisation, which is where a lot of the practical convenience lives. - Mobile applications, which is how this category is overwhelmingly used in the moment. - Recurring bills between housemates, which is a scheduling layer on top of everything here. ### README must contain - That shares are resolved to exact amounts at entry with the remainder allocated and recorded - That balances net to zero across a group and this is asserted in the test suite - That simplification never routes through someone who opted out of intermediation - That currency rates are fixed at entry and balances do not move when rates change - That a settlement requires confirmation from the recipient ===== AGENTS.md ===== # Agent instructions · Splitwise indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Integer minor units with an explicit remainder allocation, Debt simplification over the balance graph, Rate recorded at entry, never re-converted, Every expense and edit retained. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Splitwise 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Splitting money without losing a penny Every split sums exactly to the amount paid. ### Steps 1. Resolve every split to exact amounts at entry Stored as amounts, not as a rule to recompute. Files: `src/split.ts`, `src/split.test.ts` 2. Allocate the remainder explicitly Deterministic, recorded, and rotated so it is not always the same person. Files: `src/split.ts` 3. Assert the sum in the suite A thousand random splits. This is the invariant everything rests on. Files: `tests/split-sum.test.ts` ### Done when - [ ] Shares always sum exactly to the total - [ ] The remainder is allocated deterministically and recorded - [ ] Several payers sum correctly - [ ] Percentage splits resolve to exact amounts ### 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. ## Phase 2 · Balances that survive an edit Changing an expense changes the balances correctly and visibly. ### Steps 1. Derive balances and assert they net to zero Across the group. Any non-zero total is a bug in the split maths. Files: `src/balances.ts`, `tests/balance-zero.test.ts` 2. Retain every edit Shared money arguments are settled by the record. Files: `src/edits.ts` 3. Surface balance movements from edits To the people affected. A silently changed figure is how trust goes. Files: `src/notify.ts` ### Done when - [ ] Balances net to zero across the group - [ ] Edits retain the previous version - [ ] Affected people see the change and its cause - [ ] Deletion is recorded rather than removal ### 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. ## Phase 3 · The fewest payments that clear it A tangled group settles with as few transfers as possible. ### Steps 1. Simplify the balance graph Positive and negative balances matched, not every pair reconciled. Files: `src/simplify.ts`, `src/simplify.test.ts` 2. Preserve exact totals Simplification must not lose or invent a penny. Files: `src/simplify.ts` 3. Respect intermediation opt-out Routing a stranger's debt through someone is correct and unwelcome. Files: `src/simplify.ts` ### Done when - [ ] A tangled group settles in few transfers - [ ] Simplified transfers sum to the original balances - [ ] An opted-out member is never routed through - [ ] Original obligations stay visible ### 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. ## Phase 4 · More than one currency A trip with three currencies settles fairly and explicably. ### Steps 1. Fix the rate at entry And keep it on the expense. Re-converting changes what people agreed. Files: `src/currency.ts` 2. State the rate wherever a converted figure appears So a disagreement is about the rate rather than about the arithmetic. Files: `src/display.ts` 3. Keep the zero-sum invariant across currencies Conversion rounding is where it usually breaks. Files: `tests/multicurrency.test.ts` ### Done when - [ ] Balances do not move when rates change - [ ] The rate is stated wherever conversion appears - [ ] A cross-currency settlement records both amounts - [ ] Balances still net to zero across currencies ### 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. ## Not in this build - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. ## After v1, if you want it - Reminders for outstanding balances, worded so they do not damage friendships - Categories and a trip summary, which is what people want afterwards - Splitting by shares that change per expense, for households with uneven arrangements ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Shared expenses between identified people. DATABASE_URL=postgres://localhost:5432/split # Required. The group currency. Individual expenses keep their own. DEFAULT_CURRENCY=EUR # Required. Who receives the odd penny. Rotating is fairer than always the first member. REMAINDER_RULE=rotate # Required. Whether a member can refuse to be routed through in settlement. ALLOW_INTERMEDIATION_OPT_OUT=true # Required. Present as a constant. One-sided settlement is assertion rather than payment. REQUIRE_SETTLEMENT_CONFIRMATION=always # Required. Where conversion rates come from. Recorded per expense and never re-applied. RATE_SOURCE=manual
You are building a production product version of Splitwise. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Splitwise · product brief ## Problem The ledger and the debt math are a solved problem; a link-shared DIY version covers a household or one trip in a single sitting, no accounts needed. The gap is not code, it is adoption: Splitwise works because the whole group already has it installed, and nobody downloads your homebrew app for one weekend away. Build it for the people who share your fridge; for everyone else, the moat is other people's phones. ## Product outcome A shared ledger where the pennies always add up, a group of eight settles in three payments, and nobody's balance changes because an exchange rate moved. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Implement the core workflow described in ARCHITECTURE.md ## Explicit non-goals for v1 - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. - native iOS and Android apps, push reminders included - your friends' existing accounts and muscle memory - receipt scanning and itemized splits - automatic currency conversion at live rates - settle-up integrations with payment apps ## Success criteria - Shares are confirmed to sum exactly across a thousand random splits - Balances are confirmed to net to zero across a group - Simplification is confirmed to preserve exact totals - Rate changes are confirmed not to move balances - Settlements are confirmed to require recipient confirmation ===== BRIEF.md ===== # Build brief · Splitwise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me shared expense tracking that settles a group in as few payments as possible to replace Splitwise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · shared money between people needs transactions and a single source of truth - Amounts: Integer minor units with an explicit remainder allocation · splitting three ways always leaves a penny and someone has to get it - Settlement: Debt simplification over the balance graph · the point is fewer payments, not a matrix of everyone owing everyone - Currency: Rate recorded at entry, never re-converted · converting at settlement time changes what people agreed to - History: Every expense and edit retained · shared money arguments are settled by the record ### Data model (create this before Phase 1) - 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. ### Phase 1 · Splitting money without losing a penny Build: Expenses with several payers and several split methods, all resolved to exact integer amounts. Allocate the remainder deterministically and record who received it. Assert that shares sum to the total. Trap: 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. Done when: Shares always sum exactly to the expense total, asserted across a thousand random splits. A three-way split of an odd amount allocates the remainder deterministically and records who got it. Several payers on one expense sum correctly. Percentage and share-based splits resolve to exact amounts at entry. Do not build yet: No balances, settlement or currency handling. ### Phase 2 · Balances that survive an edit Build: Compute per-person balances from expenses and settlements. Retain every edit and deletion with what changed. Show anyone affected by an edit that their balance moved and why. Trap: 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. Done when: Balances derive from expenses and settlements and reconcile to zero across the group, asserted in a test. An edited expense retains its previous version. Anyone whose balance changed by an edit is shown the change and its cause. Deleting an expense is a recorded deletion rather than a removal. Do not build yet: No settlement or currency yet. ### Phase 3 · The fewest payments that clear it Build: Simplify the debt graph so a group of eight with forty expenses settles in a handful of transfers rather than a matrix. Preserve the exact totals. Let a member opt out of being routed through. Trap: 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. Done when: A group of eight with forty expenses settles in a small number of transfers, verified against the naive pairwise count. Simplified transfers sum exactly to the original balances. A member who opted out of intermediation is never routed through, tested directly. The original obligations remain visible beside the simplified plan. Do not build yet: No currency or settlement recording yet. ### Phase 4 · More than one currency Build: Record each expense in its own currency with the rate used at entry. Never re-convert later. Show balances in the group currency and in each person's own, stating the rate. Trap: 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. Done when: An expense keeps the rate used at entry and balances do not move when rates change, tested by changing the rate afterwards. Balances display in the group currency with the rate stated. A settlement in a different currency records both amounts and its own rate. Rounding across currency conversion still nets to zero. Do not build yet: No settlement confirmation or export yet. ### Phase 5 · Settling up, and remembering that you did Build: Record settlements requiring confirmation from the recipient. Handle a partial payment. Export the full ledger. Close a group without deleting its history. Trap: A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled. Done when: A settlement requires recipient confirmation and shows as pending until then. A partial payment reduces the balance by exactly its amount. Closing a group retains its full history and remaining balances. Export contains expenses, shares, settlements and edits and re-imports identically. Do not build yet: Nothing after this. ### Out of scope (and why) - Moving actual money, which is a payments and regulatory project rather than a ledger one. - Receipt scanning and itemisation, which is where a lot of the practical convenience lives. - Mobile applications, which is how this category is overwhelmingly used in the moment. - Recurring bills between housemates, which is a scheduling layer on top of everything here. ### README must contain - That shares are resolved to exact amounts at entry with the remainder allocated and recorded - That balances net to zero across a group and this is asserted in the test suite - That simplification never routes through someone who opted out of intermediation - That currency rates are fixed at entry and balances do not move when rates change - That a settlement requires confirmation from the recipient ===== ARCHITECTURE.md ===== # Architecture · Splitwise ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | shared money between people needs transactions and a single source of truth | | Amounts | Integer minor units with an explicit remainder allocation | splitting three ways always leaves a penny and someone has to get it | | Settlement | Debt simplification over the balance graph | the point is fewer payments, not a matrix of everyone owing everyone | | Currency | Rate recorded at entry, never re-converted | converting at settlement time changes what people agreed to | | History | Every expense and edit retained | shared money arguments are settled by the record | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Splitting | Exact share resolution and remainder allocation. | Any method, provided shares are resolved at entry and sum exactly. | | Balances | Derivation and edit history. | Any implementation, provided balances net to zero and edits are retained. | | Settlement | Debt simplification with opt-outs. | Any algorithm, provided totals are preserved and opt-outs respected. | | Currency | Rates fixed at entry. | Any rate source, provided the rate is stored with the expense and never re-applied. | | Confirmation | Two-sided settlement recording. | Not swappable for one-sided assertion, which is what the tool exists to avoid. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Shared expenses between identified people. - `DEFAULT_CURRENCY` · required · The group currency. Individual expenses keep their own. - `REMAINDER_RULE` · required · Who receives the odd penny. Rotating is fairer than always the first member. - `ALLOW_INTERMEDIATION_OPT_OUT` · required · Whether a member can refuse to be routed through in settlement. - `REQUIRE_SETTLEMENT_CONFIRMATION` · required · Present as a constant. One-sided settlement is assertion rather than payment. - `RATE_SOURCE` · required · Where conversion rates come from. Recorded per expense and never re-applied. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Splitwise product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Integer minor units with an explicit remainder allocation, Debt simplification over the balance graph, Rate recorded at entry, never re-converted, Every expense and edit retained. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled. ===== MILESTONES.md ===== # Delivery milestones · Splitwise Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Splitting money without losing a penny Every split sums exactly to the amount paid. ### Steps 1. Resolve every split to exact amounts at entry Stored as amounts, not as a rule to recompute. Files: `src/split.ts`, `src/split.test.ts` 2. Allocate the remainder explicitly Deterministic, recorded, and rotated so it is not always the same person. Files: `src/split.ts` 3. Assert the sum in the suite A thousand random splits. This is the invariant everything rests on. Files: `tests/split-sum.test.ts` ### Done when - [ ] Shares always sum exactly to the total - [ ] The remainder is allocated deterministically and recorded - [ ] Several payers sum correctly - [ ] Percentage splits resolve to exact amounts ### 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. ## M2 · Balances that survive an edit Changing an expense changes the balances correctly and visibly. ### Steps 1. Derive balances and assert they net to zero Across the group. Any non-zero total is a bug in the split maths. Files: `src/balances.ts`, `tests/balance-zero.test.ts` 2. Retain every edit Shared money arguments are settled by the record. Files: `src/edits.ts` 3. Surface balance movements from edits To the people affected. A silently changed figure is how trust goes. Files: `src/notify.ts` ### Done when - [ ] Balances net to zero across the group - [ ] Edits retain the previous version - [ ] Affected people see the change and its cause - [ ] Deletion is recorded rather than removal ### 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. ## M3 · The fewest payments that clear it A tangled group settles with as few transfers as possible. ### Steps 1. Simplify the balance graph Positive and negative balances matched, not every pair reconciled. Files: `src/simplify.ts`, `src/simplify.test.ts` 2. Preserve exact totals Simplification must not lose or invent a penny. Files: `src/simplify.ts` 3. Respect intermediation opt-out Routing a stranger's debt through someone is correct and unwelcome. Files: `src/simplify.ts` ### Done when - [ ] A tangled group settles in few transfers - [ ] Simplified transfers sum to the original balances - [ ] An opted-out member is never routed through - [ ] Original obligations stay visible ### 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. ## M4 · More than one currency A trip with three currencies settles fairly and explicably. ### Steps 1. Fix the rate at entry And keep it on the expense. Re-converting changes what people agreed. Files: `src/currency.ts` 2. State the rate wherever a converted figure appears So a disagreement is about the rate rather than about the arithmetic. Files: `src/display.ts` 3. Keep the zero-sum invariant across currencies Conversion rounding is where it usually breaks. Files: `tests/multicurrency.test.ts` ### Done when - [ ] Balances do not move when rates change - [ ] The rate is stated wherever conversion appears - [ ] A cross-currency settlement records both amounts - [ ] Balances still net to zero across currencies ### 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. ## M5 · Settling up, and remembering that you did (production only) A payment between people is recorded once and agreed by both. ### Steps 1. Require recipient confirmation One-sided settlement is assertion, not payment. Files: `src/settlements.ts` 2. Handle partial payments People pay round numbers. The remainder stays owed. Files: `src/settlements.ts` 3. Close without deleting The history is what a disagreement a year later is settled by. Files: `src/groups.ts` ### Done when - [ ] Settlements require recipient confirmation - [ ] Partial payments reduce balances exactly - [ ] Closing a group retains its history - [ ] Export round trips the full ledger ### Watch out - A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled. ===== OPERATIONS.md ===== # Operations · Splitwise ## Backup This is a record of money between friends and it is referred to months later. Back up expenses, shares and settlements together, since any one of them alone gives a wrong balance. ## Restore Restore and confirm balances still net to zero, which is the fastest check that nothing partial was lost. A non-zero total after a restore means part of the ledger is missing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The zero-sum invariant is the health check and it should be asserted continuously rather than at release, because a drift means real money is being misreported to real people. ## Incident checklist A balance that does not reconcile is the failure that matters, and the usual cause is a split whose remainder was not allocated. The assertion in the suite covers a thousand random splits precisely because the failing case is always an odd one. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Shares are confirmed to sum exactly across a thousand random splits - [ ] Balances are confirmed to net to zero across a group - [ ] Simplification is confirmed to preserve exact totals - [ ] Rate changes are confirmed not to move balances - [ ] Settlements are confirmed to require recipient confirmation ## Launch constraint Do not market omitted Splitwise capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Shared expenses between identified people. DATABASE_URL=postgres://localhost:5432/split # Required. The group currency. Individual expenses keep their own. DEFAULT_CURRENCY=EUR # Required. Who receives the odd penny. Rotating is fairer than always the first member. REMAINDER_RULE=rotate # Required. Whether a member can refuse to be routed through in settlement. ALLOW_INTERMEDIATION_OPT_OUT=true # Required. Present as a constant. One-sided settlement is assertion rather than payment. REQUIRE_SETTLEMENT_CONFIRMATION=always # Required. Where conversion rates come from. Recorded per expense and never re-applied. RATE_SOURCE=manual
# Splitwise · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | shared money between people needs transactions and a single source of truth | | Amounts | Integer minor units with an explicit remainder allocation | splitting three ways always leaves a penny and someone has to get it | | Settlement | Debt simplification over the balance graph | the point is fewer payments, not a matrix of everyone owing everyone | | Currency | Rate recorded at entry, never re-converted | converting at settlement time changes what people agreed to | | History | Every expense and edit retained | shared money arguments are settled by the record | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the settlement computation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Shared money between people needs real transactions and exact integer arithmetic. - Get it: Docker or your package manager. - Verify: psql connects and integer arithmetic is exact - [ ] **A real trip's worth of shared expenses** · free - 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 - [ ] **Who gets the odd penny** · free - 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 - [ ] **Your position on intermediation** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. - native iOS and Android apps, push reminders included - your friends' existing accounts and muscle memory - receipt scanning and itemized splits - automatic currency conversion at live rates - settle-up integrations with payment apps If one of those is essential to you, that is the reason to keep paying for Splitwise, and the README should say so rather than pretend.
# Build brief · Splitwise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me shared expense tracking that settles a group in as few payments as possible to replace Splitwise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · shared money between people needs transactions and a single source of truth - Amounts: Integer minor units with an explicit remainder allocation · splitting three ways always leaves a penny and someone has to get it - Settlement: Debt simplification over the balance graph · the point is fewer payments, not a matrix of everyone owing everyone - Currency: Rate recorded at entry, never re-converted · converting at settlement time changes what people agreed to - History: Every expense and edit retained · shared money arguments are settled by the record ### Data model (create this before Phase 1) - 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. ### Phase 1 · Splitting money without losing a penny Build: Expenses with several payers and several split methods, all resolved to exact integer amounts. Allocate the remainder deterministically and record who received it. Assert that shares sum to the total. Trap: 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. Done when: Shares always sum exactly to the expense total, asserted across a thousand random splits. A three-way split of an odd amount allocates the remainder deterministically and records who got it. Several payers on one expense sum correctly. Percentage and share-based splits resolve to exact amounts at entry. Do not build yet: No balances, settlement or currency handling. ### Phase 2 · Balances that survive an edit Build: Compute per-person balances from expenses and settlements. Retain every edit and deletion with what changed. Show anyone affected by an edit that their balance moved and why. Trap: 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. Done when: Balances derive from expenses and settlements and reconcile to zero across the group, asserted in a test. An edited expense retains its previous version. Anyone whose balance changed by an edit is shown the change and its cause. Deleting an expense is a recorded deletion rather than a removal. Do not build yet: No settlement or currency yet. ### Phase 3 · The fewest payments that clear it Build: Simplify the debt graph so a group of eight with forty expenses settles in a handful of transfers rather than a matrix. Preserve the exact totals. Let a member opt out of being routed through. Trap: 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. Done when: A group of eight with forty expenses settles in a small number of transfers, verified against the naive pairwise count. Simplified transfers sum exactly to the original balances. A member who opted out of intermediation is never routed through, tested directly. The original obligations remain visible beside the simplified plan. Do not build yet: No currency or settlement recording yet. ### Phase 4 · More than one currency Build: Record each expense in its own currency with the rate used at entry. Never re-convert later. Show balances in the group currency and in each person's own, stating the rate. Trap: 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. Done when: An expense keeps the rate used at entry and balances do not move when rates change, tested by changing the rate afterwards. Balances display in the group currency with the rate stated. A settlement in a different currency records both amounts and its own rate. Rounding across currency conversion still nets to zero. Do not build yet: No settlement confirmation or export yet. ### Phase 5 · Settling up, and remembering that you did Build: Record settlements requiring confirmation from the recipient. Handle a partial payment. Export the full ledger. Close a group without deleting its history. Trap: A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled. Done when: A settlement requires recipient confirmation and shows as pending until then. A partial payment reduces the balance by exactly its amount. Closing a group retains its full history and remaining balances. Export contains expenses, shares, settlements and edits and re-imports identically. Do not build yet: Nothing after this. ### Out of scope (and why) - Moving actual money, which is a payments and regulatory project rather than a ledger one. - Receipt scanning and itemisation, which is where a lot of the practical convenience lives. - Mobile applications, which is how this category is overwhelmingly used in the moment. - Recurring bills between housemates, which is a scheduling layer on top of everything here. ### README must contain - That shares are resolved to exact amounts at entry with the remainder allocated and recorded - That balances net to zero across a group and this is asserted in the test suite - That simplification never routes through someone who opted out of intermediation - That currency rates are fixed at entry and balances do not move when rates change - That a settlement requires confirmation from the recipient
# Agent instructions · Splitwise indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Integer minor units with an explicit remainder allocation, Debt simplification over the balance graph, Rate recorded at entry, never re-converted, Every expense and edit retained. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · Splitwise 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Splitting money without losing a penny Every split sums exactly to the amount paid. ### Steps 1. Resolve every split to exact amounts at entry Stored as amounts, not as a rule to recompute. Files: `src/split.ts`, `src/split.test.ts` 2. Allocate the remainder explicitly Deterministic, recorded, and rotated so it is not always the same person. Files: `src/split.ts` 3. Assert the sum in the suite A thousand random splits. This is the invariant everything rests on. Files: `tests/split-sum.test.ts` ### Done when - [ ] Shares always sum exactly to the total - [ ] The remainder is allocated deterministically and recorded - [ ] Several payers sum correctly - [ ] Percentage splits resolve to exact amounts ### 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. ## Phase 2 · Balances that survive an edit Changing an expense changes the balances correctly and visibly. ### Steps 1. Derive balances and assert they net to zero Across the group. Any non-zero total is a bug in the split maths. Files: `src/balances.ts`, `tests/balance-zero.test.ts` 2. Retain every edit Shared money arguments are settled by the record. Files: `src/edits.ts` 3. Surface balance movements from edits To the people affected. A silently changed figure is how trust goes. Files: `src/notify.ts` ### Done when - [ ] Balances net to zero across the group - [ ] Edits retain the previous version - [ ] Affected people see the change and its cause - [ ] Deletion is recorded rather than removal ### 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. ## Phase 3 · The fewest payments that clear it A tangled group settles with as few transfers as possible. ### Steps 1. Simplify the balance graph Positive and negative balances matched, not every pair reconciled. Files: `src/simplify.ts`, `src/simplify.test.ts` 2. Preserve exact totals Simplification must not lose or invent a penny. Files: `src/simplify.ts` 3. Respect intermediation opt-out Routing a stranger's debt through someone is correct and unwelcome. Files: `src/simplify.ts` ### Done when - [ ] A tangled group settles in few transfers - [ ] Simplified transfers sum to the original balances - [ ] An opted-out member is never routed through - [ ] Original obligations stay visible ### 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. ## Phase 4 · More than one currency A trip with three currencies settles fairly and explicably. ### Steps 1. Fix the rate at entry And keep it on the expense. Re-converting changes what people agreed. Files: `src/currency.ts` 2. State the rate wherever a converted figure appears So a disagreement is about the rate rather than about the arithmetic. Files: `src/display.ts` 3. Keep the zero-sum invariant across currencies Conversion rounding is where it usually breaks. Files: `tests/multicurrency.test.ts` ### Done when - [ ] Balances do not move when rates change - [ ] The rate is stated wherever conversion appears - [ ] A cross-currency settlement records both amounts - [ ] Balances still net to zero across currencies ### 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. ## Not in this build - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. ## After v1, if you want it - Reminders for outstanding balances, worded so they do not damage friendships - Categories and a trip summary, which is what people want afterwards - Splitting by shares that change per expense, for households with uneven arrangements
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Shared expenses between identified people. DATABASE_URL=postgres://localhost:5432/split # Required. The group currency. Individual expenses keep their own. DEFAULT_CURRENCY=EUR # Required. Who receives the odd penny. Rotating is fairer than always the first member. REMAINDER_RULE=rotate # Required. Whether a member can refuse to be routed through in settlement. ALLOW_INTERMEDIATION_OPT_OUT=true # Required. Present as a constant. One-sided settlement is assertion rather than payment. REQUIRE_SETTLEMENT_CONFIRMATION=always # Required. Where conversion rates come from. Recorded per expense and never re-applied. RATE_SOURCE=manual
# Splitwise · product brief ## Problem The ledger and the debt math are a solved problem; a link-shared DIY version covers a household or one trip in a single sitting, no accounts needed. The gap is not code, it is adoption: Splitwise works because the whole group already has it installed, and nobody downloads your homebrew app for one weekend away. Build it for the people who share your fridge; for everyone else, the moat is other people's phones. ## Product outcome A shared ledger where the pennies always add up, a group of eight settles in three payments, and nobody's balance changes because an exchange rate moved. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Implement the core workflow described in ARCHITECTURE.md ## Explicit non-goals for v1 - Moving actual money. - Receipt scanning and itemisation. - Mobile applications. - Recurring bills between housemates. - native iOS and Android apps, push reminders included - your friends' existing accounts and muscle memory - receipt scanning and itemized splits - automatic currency conversion at live rates - settle-up integrations with payment apps ## Success criteria - Shares are confirmed to sum exactly across a thousand random splits - Balances are confirmed to net to zero across a group - Simplification is confirmed to preserve exact totals - Rate changes are confirmed not to move balances - Settlements are confirmed to require recipient confirmation
# Build brief · Splitwise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me shared expense tracking that settles a group in as few payments as possible to replace Splitwise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · shared money between people needs transactions and a single source of truth - Amounts: Integer minor units with an explicit remainder allocation · splitting three ways always leaves a penny and someone has to get it - Settlement: Debt simplification over the balance graph · the point is fewer payments, not a matrix of everyone owing everyone - Currency: Rate recorded at entry, never re-converted · converting at settlement time changes what people agreed to - History: Every expense and edit retained · shared money arguments are settled by the record ### Data model (create this before Phase 1) - 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. ### Phase 1 · Splitting money without losing a penny Build: Expenses with several payers and several split methods, all resolved to exact integer amounts. Allocate the remainder deterministically and record who received it. Assert that shares sum to the total. Trap: 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. Done when: Shares always sum exactly to the expense total, asserted across a thousand random splits. A three-way split of an odd amount allocates the remainder deterministically and records who got it. Several payers on one expense sum correctly. Percentage and share-based splits resolve to exact amounts at entry. Do not build yet: No balances, settlement or currency handling. ### Phase 2 · Balances that survive an edit Build: Compute per-person balances from expenses and settlements. Retain every edit and deletion with what changed. Show anyone affected by an edit that their balance moved and why. Trap: 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. Done when: Balances derive from expenses and settlements and reconcile to zero across the group, asserted in a test. An edited expense retains its previous version. Anyone whose balance changed by an edit is shown the change and its cause. Deleting an expense is a recorded deletion rather than a removal. Do not build yet: No settlement or currency yet. ### Phase 3 · The fewest payments that clear it Build: Simplify the debt graph so a group of eight with forty expenses settles in a handful of transfers rather than a matrix. Preserve the exact totals. Let a member opt out of being routed through. Trap: 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. Done when: A group of eight with forty expenses settles in a small number of transfers, verified against the naive pairwise count. Simplified transfers sum exactly to the original balances. A member who opted out of intermediation is never routed through, tested directly. The original obligations remain visible beside the simplified plan. Do not build yet: No currency or settlement recording yet. ### Phase 4 · More than one currency Build: Record each expense in its own currency with the rate used at entry. Never re-convert later. Show balances in the group currency and in each person's own, stating the rate. Trap: 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. Done when: An expense keeps the rate used at entry and balances do not move when rates change, tested by changing the rate afterwards. Balances display in the group currency with the rate stated. A settlement in a different currency records both amounts and its own rate. Rounding across currency conversion still nets to zero. Do not build yet: No settlement confirmation or export yet. ### Phase 5 · Settling up, and remembering that you did Build: Record settlements requiring confirmation from the recipient. Handle a partial payment. Export the full ledger. Close a group without deleting its history. Trap: A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled. Done when: A settlement requires recipient confirmation and shows as pending until then. A partial payment reduces the balance by exactly its amount. Closing a group retains its full history and remaining balances. Export contains expenses, shares, settlements and edits and re-imports identically. Do not build yet: Nothing after this. ### Out of scope (and why) - Moving actual money, which is a payments and regulatory project rather than a ledger one. - Receipt scanning and itemisation, which is where a lot of the practical convenience lives. - Mobile applications, which is how this category is overwhelmingly used in the moment. - Recurring bills between housemates, which is a scheduling layer on top of everything here. ### README must contain - That shares are resolved to exact amounts at entry with the remainder allocated and recorded - That balances net to zero across a group and this is asserted in the test suite - That simplification never routes through someone who opted out of intermediation - That currency rates are fixed at entry and balances do not move when rates change - That a settlement requires confirmation from the recipient
# Architecture · Splitwise ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | shared money between people needs transactions and a single source of truth | | Amounts | Integer minor units with an explicit remainder allocation | splitting three ways always leaves a penny and someone has to get it | | Settlement | Debt simplification over the balance graph | the point is fewer payments, not a matrix of everyone owing everyone | | Currency | Rate recorded at entry, never re-converted | converting at settlement time changes what people agreed to | | History | Every expense and edit retained | shared money arguments are settled by the record | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Splitting | Exact share resolution and remainder allocation. | Any method, provided shares are resolved at entry and sum exactly. | | Balances | Derivation and edit history. | Any implementation, provided balances net to zero and edits are retained. | | Settlement | Debt simplification with opt-outs. | Any algorithm, provided totals are preserved and opt-outs respected. | | Currency | Rates fixed at entry. | Any rate source, provided the rate is stored with the expense and never re-applied. | | Confirmation | Two-sided settlement recording. | Not swappable for one-sided assertion, which is what the tool exists to avoid. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Shared expenses between identified people. - `DEFAULT_CURRENCY` · required · The group currency. Individual expenses keep their own. - `REMAINDER_RULE` · required · Who receives the odd penny. Rotating is fairer than always the first member. - `ALLOW_INTERMEDIATION_OPT_OUT` · required · Whether a member can refuse to be routed through in settlement. - `REQUIRE_SETTLEMENT_CONFIRMATION` · required · Present as a constant. One-sided settlement is assertion rather than payment. - `RATE_SOURCE` · required · Where conversion rates come from. Recorded per expense and never re-applied. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Splitwise product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Integer minor units with an explicit remainder allocation, Debt simplification over the balance graph, Rate recorded at entry, never re-converted, Every expense and edit retained. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled.
# Delivery milestones · Splitwise Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Splitting money without losing a penny Every split sums exactly to the amount paid. ### Steps 1. Resolve every split to exact amounts at entry Stored as amounts, not as a rule to recompute. Files: `src/split.ts`, `src/split.test.ts` 2. Allocate the remainder explicitly Deterministic, recorded, and rotated so it is not always the same person. Files: `src/split.ts` 3. Assert the sum in the suite A thousand random splits. This is the invariant everything rests on. Files: `tests/split-sum.test.ts` ### Done when - [ ] Shares always sum exactly to the total - [ ] The remainder is allocated deterministically and recorded - [ ] Several payers sum correctly - [ ] Percentage splits resolve to exact amounts ### 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. ## M2 · Balances that survive an edit Changing an expense changes the balances correctly and visibly. ### Steps 1. Derive balances and assert they net to zero Across the group. Any non-zero total is a bug in the split maths. Files: `src/balances.ts`, `tests/balance-zero.test.ts` 2. Retain every edit Shared money arguments are settled by the record. Files: `src/edits.ts` 3. Surface balance movements from edits To the people affected. A silently changed figure is how trust goes. Files: `src/notify.ts` ### Done when - [ ] Balances net to zero across the group - [ ] Edits retain the previous version - [ ] Affected people see the change and its cause - [ ] Deletion is recorded rather than removal ### 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. ## M3 · The fewest payments that clear it A tangled group settles with as few transfers as possible. ### Steps 1. Simplify the balance graph Positive and negative balances matched, not every pair reconciled. Files: `src/simplify.ts`, `src/simplify.test.ts` 2. Preserve exact totals Simplification must not lose or invent a penny. Files: `src/simplify.ts` 3. Respect intermediation opt-out Routing a stranger's debt through someone is correct and unwelcome. Files: `src/simplify.ts` ### Done when - [ ] A tangled group settles in few transfers - [ ] Simplified transfers sum to the original balances - [ ] An opted-out member is never routed through - [ ] Original obligations stay visible ### 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. ## M4 · More than one currency A trip with three currencies settles fairly and explicably. ### Steps 1. Fix the rate at entry And keep it on the expense. Re-converting changes what people agreed. Files: `src/currency.ts` 2. State the rate wherever a converted figure appears So a disagreement is about the rate rather than about the arithmetic. Files: `src/display.ts` 3. Keep the zero-sum invariant across currencies Conversion rounding is where it usually breaks. Files: `tests/multicurrency.test.ts` ### Done when - [ ] Balances do not move when rates change - [ ] The rate is stated wherever conversion appears - [ ] A cross-currency settlement records both amounts - [ ] Balances still net to zero across currencies ### 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. ## M5 · Settling up, and remembering that you did (production only) A payment between people is recorded once and agreed by both. ### Steps 1. Require recipient confirmation One-sided settlement is assertion, not payment. Files: `src/settlements.ts` 2. Handle partial payments People pay round numbers. The remainder stays owed. Files: `src/settlements.ts` 3. Close without deleting The history is what a disagreement a year later is settled by. Files: `src/groups.ts` ### Done when - [ ] Settlements require recipient confirmation - [ ] Partial payments reduce balances exactly - [ ] Closing a group retains its history - [ ] Export round trips the full ledger ### Watch out - A settlement recorded by the payer alone and immediately applied lets one person zero their balance by asserting they paid, which is the exact situation shared expense tracking exists to prevent. Require the recipient to confirm, and show unconfirmed settlements as pending rather than as settled.
# Operations · Splitwise ## Backup This is a record of money between friends and it is referred to months later. Back up expenses, shares and settlements together, since any one of them alone gives a wrong balance. ## Restore Restore and confirm balances still net to zero, which is the fastest check that nothing partial was lost. A non-zero total after a restore means part of the ledger is missing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The zero-sum invariant is the health check and it should be asserted continuously rather than at release, because a drift means real money is being misreported to real people. ## Incident checklist A balance that does not reconcile is the failure that matters, and the usual cause is a split whose remainder was not allocated. The assertion in the suite covers a thousand random splits precisely because the failing case is always an odd one. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Shares are confirmed to sum exactly across a thousand random splits - [ ] Balances are confirmed to net to zero across a group - [ ] Simplification is confirmed to preserve exact totals - [ ] Rate changes are confirmed not to move balances - [ ] Settlements are confirmed to require recipient confirmation ## Launch constraint Do not market omitted Splitwise capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Shared expenses between identified people. DATABASE_URL=postgres://localhost:5432/split # Required. The group currency. Individual expenses keep their own. DEFAULT_CURRENCY=EUR # Required. Who receives the odd penny. Rotating is fairer than always the first member. REMAINDER_RULE=rotate # Required. Whether a member can refuse to be routed through in settlement. ALLOW_INTERMEDIATION_OPT_OUT=true # Required. Present as a constant. One-sided settlement is assertion rather than payment. REQUIRE_SETTLEMENT_CONFIRMATION=always # Required. Where conversion rates come from. Recorded per expense and never re-applied. RATE_SOURCE=manual
$ choose a build depth, inspect the files, then open the complete pack in your agent
Splitwise sits where the friction is lowest: everyone already has an account, the apps are polished, and the balance quietly nags people so you do not have to. Pro mostly sells relief from the free tier's daily expense cap and the ads, plus receipt scanning and automatic currency conversion for trips. The subscription monetizes the fact that the group, not you, picks the tool.
xnative iOS and Android apps, push reminders included
xyour friends' existing accounts and muscle memory
xreceipt scanning and itemized splits
xautomatic currency conversion at live rates
xsettle-up integrations with payment apps
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Splitwise →· no votes, no pay-to-list · just what's real
Splitwise pricing
pro$5/mo · monthly · $60/yr
free tierThe free tier still creates groups, splits and simplifies debts, but caps expense adds at roughly three per day, shows ads, and keeps receipt scanning and automatic currency conversion for Pro.
verified 2026-08-11 · source ↗
Is Splitwise free?
The free tier still creates groups, splits and simplifies debts, but caps expense adds at roughly three per day, shows ads, and keeps receipt scanning and automatic currency conversion for Pro. Paid is Pro at $5/mo (checked 2026-08-11).
Vibecode Splitwise
Kinda. The core of Splitwise is buildable in a weekend with the prompt on this page, but there are real gaps: native iOS and Android apps, push reminders included, your friends' existing accounts and muscle memory. Read the honest list above before committing.
How much does Splitwise cost?
Splitwise costs about $5/month (Pro, checked 2026-08-11), which is $60 per year.
What do I lose by replacing Splitwise?
Honestly: native iOS and Android apps, push reminders included; your friends' existing accounts and muscle memory; receipt scanning and itemized splits; automatic currency conversion at live rates; settle-up integrations with payment apps. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Splitwise?
Yes: Spliit (Share a link, split, settle. No accounts, no ads. Use their instance or run your own.) IHateMoney (Around since 2011, proudly boring, in declared maintenance mode; use their free instance or one docker run.) Cospend (Splitwise for people who already run Nextcloud; pairs with the MoneyBuster Android app.) All 4 curated free alternatives are at vibecodeit.com/splitwise/alternatives. The prompt is for when you want it exactly your way.