Loading post
Jul 21, 2026

I'm a Japanese software engineer in the Bay Area. I spent years doing payments at PayPal and now do payments at Atlas, a fintech startup. Ledgers, balances, reconciliation, rounding errors — moving money is literally my day job.
Was I desperate for a bill-splitting app? Honestly, no. My friends and I already used existing services like walica (a popular Japanese registration-free splitter) for some trips, and with another group we just passed around an Excel sheet. Both worked fine.
So why build one? The reasons are pretty simple:
One more design goal from the start: whatever I built had to be something I could hand to non-technical friends as-is — no app install, no account signup, just send one link and anyone can join. That requirement pays off later.
The result is Settli. This post covers the "why," a tour of what it actually does, and the most fun piece of engineering inside — the settlement algorithm that turns a messy pile of payments into a minimal "who pays whom how much" list.
Let me get an awkward tension out of the way first. I just said I dislike not knowing where my data goes — and yet Settli stores my friends' settlement data in my Firebase project. From their point of view, isn't my app just "somebody's random server" too?
After chewing on this, I think the difference isn't whether data is held, but the incentive structure around it. An ad-funded service has a structural economic motive to exploit data. A personal tool has none — there's no monetization, so the data has no use beyond rendering the settlement sheet. And the data itself is minimized by design: members are just typed-in names like "Yudai" and "Ken." No email addresses, no phone numbers, no accounts exist at all. The operator has a face (mine), can answer exactly what's stored if asked, and isn't storing much to begin with. As an explanation I can give my friends, I think that holds up.
Settli is a Next.js app with Firebase behind it. The design axis is the requirement from the top: nobody but the organizer ever needs to log in.
Organizer Friends
───────── ───────
Create group ─┐
(name, currency, Receive link or QR
member names, │ │
optional 4-6 │ ▼
digit passcode)│ /tools/settli/join/A7K2M9PX
│ │ │
▼ │ ▼
Get share code ─┘────────▶ Group page opens.
"A7K2M9PX" No signup. No install.
│ (passcode prompt only if set)
▼
Log payments as they happen
│
▼
"Settle up" → minimal transfer list
"Everyone sends one payment, done"
The main pieces — all of which exist in the code today:
createdBy field is literally typed string | null. Anonymous creation isn't a degraded mode; it's a first-class path. Members are just names typed into a form ("Yudai," "Ken," "Aya"), not user accounts. Anonymous organizers get their recent groups remembered in localStorage; logged-in ones get a server-side history page.0/O and 1/I — because these codes get read aloud across izakaya tables. "Is that a zero or an O?" is a bug you fix in the alphabet, not in support. A QR endpoint exists for the same reason.The weights feature deserves a sentence, because it's exactly the kind of thing you can't get from an off-the-shelf app: <!-- TODO(Yudai): replace with the real group rule — lighter share for non-drinkers, kids count as 0.5, etc. --> it encodes my group's unwritten rule that certain people count as less than one full share for certain expenses. A group-chat argument became a product feature. That's the dividend of building your own.
Here's the engineering core. A trip ends and you're left with a pile of payments. Done naively, settlement is per-payment: 23 payments × 5 people means up to 23×5 transfers. Nobody does that, obviously — so what's the minimum?
Settli does it in two stages.
Stage 1: collapse everything into one balance per person. For each payment, compute each participant's share according to the split type, then per person: balance = total paid − total owed. Positive means the group owes you; negative means you owe the group. By construction the balances sum to zero.
Stage 2: greedy matching. Sort creditors and debtors descending, then repeatedly have the largest debtor pay the largest creditor as much as possible.
interface Balance {
memberId: string;
amount: number; // positive = creditor, negative = debtor
}
function settle(balances: Balance[]): Transfer[] {
const transfers: Transfer[] = [];
// Below this we don't care: ¥0.5 for yen, $0.005 for decimal currencies.
const threshold = 0.5;
const creditors = balances
.filter((b) => b.amount > threshold)
.sort((a, b) => b.amount - a.amount);
const debtors = balances
.filter((b) => b.amount < -threshold)
.map((b) => ({ ...b, amount: -b.amount }))
.sort((a, b) => b.amount - a.amount);
let i = 0, j = 0;
while (i < creditors.length && j < debtors.length) {
const pay = Math.min(creditors[i].amount, debtors[j].amount);
transfers.push({ from: debtors[j].memberId, to: creditors[i].memberId, amount: pay });
creditors[i].amount -= pay;
debtors[j].amount -= pay;
if (creditors[i].amount <= threshold) i++;
if (debtors[j].amount <= threshold) j++;
}
return transfers;
}
(This is a simplified version of the real code — production carries member names, per-currency rounding, and multi-currency grouping — but the algorithm is exactly this.)
A concrete four-person trip:
Balances after stage 1:
Aya +9,000 (paid the Airbnb)
Yudai +1,000
Ken -4,000
Mei -6,000
Greedy matching (largest vs largest):
Mei ──6,000──▶ Aya (Aya's remainder: +3,000)
Ken ──3,000──▶ Aya (Aya settled. Ken's remainder: -1,000)
Ken ──1,000──▶ Yudai
23 payments → 3 transfers.
Two things worth stating honestly — the parts blog posts usually oversell:
n people you get at most n − 1 transfers. That's the greedy guarantee.Split ¥10,000 three ways and you get ¥3,333.33… — someone eats the extra yen. At work, this class of problem — allocation rounding — is the main ingredient of reconciliation reports and tense accounting Slack channels. A bill-splitting app still has to pick a policy; it just gets to pick a friendlier one.
Settli's policy has three layers:
There's also a display-level rule: suggested transfers in yen round to ¥10 units, because "send ¥4,237" is technically correct and socially insane.
Building your own doesn't mean "better." It means a different cost structure. What I gave up:
What I got: a tool whose entire onboarding for friends is "tap this link." A tool whose split rules match my actual group's actual norms. A tool where I can explain exactly where the data lives and what it's used for. Zero ads. And a tool where every annoyance gets fixed the same week I hit it — because the feedback form and the maintainer are the same person.
Try it here: meetyudai.com/tools/settli — no signup. That was the whole starting point.