Loading post
Jul 21, 2026

僕はベイエリアで働く日本人ソフトウェアエンジニアで、PayPalで数年、いまはフィンテックスタートアップのAtlasで、ずっと決済をやっています。台帳、残高、突合、丸め誤差。お金の移動は文字通り本業です。
割り勘に困っていたかというと、実はそんなに困っていませんでした。友達とはwalicaみたいな既存の割り勘サービスを使うこともあったし、別のグループとはExcelのシートを回して精算していました。どちらもちゃんと機能します。
じゃあなぜ作ったのか。理由は割とシンプルです。
もう一つ、作るからにはtechに詳しくない人にもそのまま渡せるものにしたいと思っていました。アプリのインストールもアカウント登録もなしで、リンクを1本送ったら誰でも参加できる。この設計要件はあとで効いてきます。
そうしてできたのが Settli です。この記事はその「なぜ」と、実際に何ができるのかと、中で一番楽しいエンジニアリング——散らかった支払いの山を最小限の「誰が誰にいくら払う」リストに変換する精算アルゴリズム——の話です。
先に、自分でも引っかかっていた矛盾を書いておきます。「既存アプリはデータの行き先が分からないから嫌だ」と言っておきながら、Settliは友達の精算データを僕のFirebaseプロジェクトに保存します。友達から見れば、僕のアプリも「よく分からない誰かのサーバー」なのでは?
考えた結果、違いは「データを預かるかどうか」ではなく、インセンティブ構造だと思っています。広告モデルのサービスは、データを活用する経済的な動機を構造的に持ちます。個人ツールにはそれがありません。マネタイズしないので、データは精算の表示以外に使い道がない。しかも預かるデータ自体を最小にしてあります——メンバーは「Yudai」「Ken」みたいな打ち込んだ名前だけで、メールアドレスも電話番号もアカウントも存在しません。運営者の顔が見えて(僕です)、聞けば何を保存しているか答えられて、そもそも大したものを保存していない。友達に説明するならこれで十分だと思っています。
SettliはFirebaseを裏に持つNext.jsアプリです。設計の軸は最初に書いた要件そのままで、幹事以外は誰もログインしなくていいこと。
幹事 友達
──── ────
グループ作成 ─┐
(名前, 通貨, リンクかQRを受け取る
メンバー名, │ │
任意の4-6桁 │ ▼
パスコード) │ /tools/settli/join/A7K2M9PX
│ │ │
▼ │ ▼
共有コード取得 ┘─────────▶ グループページが開く。
"A7K2M9PX" 登録なし。インストールなし。
│ (パスコード設定時のみ入力)
▼
支払いをその都度記録
│
▼
「精算」→ 最小限の送金リスト
「各自1回送金して終わり」
主な構成要素です。全部いまのコードに実在します。
createdByフィールドの型は文字通りstring | null。匿名での作成は「おまけモード」ではなく正規ルートです。匿名の幹事には最近触ったグループがlocalStorageに残り、ログインした場合はサーバー側に履歴ページが付きます0/Oと1/Iをあえて除外してあります。このコードは居酒屋のテーブル越しに読み上げられるものなので、「それゼロ? オー?」はサポートで対応するんじゃなくて、アルファベット側で潰すバグです。同じ理由でQRコードのエンドポイントもあります重み機能には一言足しておきます。これこそ既存アプリでは手に入らない類のもので、<!-- TODO(Yudai): 実例に差し替え — 飲まない人は少なめ、子供は0.5人分など、実際のグループのルール -->「この人はこの支出については1人前未満で数える」という、うちのグループの暗黙ルールをそのまま機能にしたものです。グループチャットの口論がプロダクトの仕様になる。自作の配当です。
ここがエンジニアリングの核です。旅行が終わると支払いの山が残ります。素朴にやると支払い単位の精算になって、23件の支払い×5人で最悪23×5回の送金。もちろん誰もそんなことはしないわけですが、じゃあ最小は何回か?
Settliは2段階でやります。
第1段階:すべてを1人1つの残高に畳む。 各支払いについて割り方どおりに負担額を計算し、人ごとに残高 = 払った合計 − 負担すべき合計を出します。正ならグループがその人に借りがある、負ならその人がグループに借りがある。総和は構成上ゼロになります。
第2段階:貪欲マッチング。 債権者と債務者をそれぞれ大きい順に並べ、いちばん大きい債務者がいちばん大きい債権者に払えるだけ払う、を繰り返します。
interface Balance {
memberId: string;
amount: number; // 正 = 債権者、負 = 債務者
}
function settle(balances: Balance[]): Transfer[] {
const transfers: Transfer[] = [];
// これ未満は気にしない。円なら¥0.5、小数通貨なら$0.005。
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;
}
(実コードの簡略版です。本番はメンバー名や通貨ごとの丸め、複数通貨のグルーピングを持ち回りますが、アルゴリズム自体はこの通りです。)
4人旅行の例で見るとこうなります。
第1段階のあとの残高:
Aya +9,000 (Airbnbを払った)
Yudai +1,000
Ken -4,000
Mei -6,000
貪欲マッチング (最大 vs 最大):
Mei ──6,000──▶ Aya (Ayaの残りは +3,000)
Ken ──3,000──▶ Aya (Aya精算完了。Kenの残りは -1,000)
Ken ──1,000──▶ Yudai
23件の支払い → 3回の送金。
正直に書いておくべきことが2つあります。この手の記事がだいたい盛る部分です。
n人に対して送金は最大n − 1回。これが貪欲法の保証です¥10,000を3人で割ると¥3,333.33…。誰かが1円を食います。仕事では、この配賦の丸めというクラスの問題は突合レポートと殺気立った経理Slackの主成分です。割り勘アプリでも結局ポリシーは選ばないといけない。ただ、友達向けにはより平和なポリシーを選べます。
Settliのポリシーは3層です。
表示レベルのルールもあって、円建ての推奨送金額は¥10単位に丸めます。「¥4,237送って」は技術的には正しいけど、社会的には正気じゃないので。
自作は「より良い」という意味ではありません。コスト構造が違うだけです。手放したもの:
得たもの:友達側の手順が「リンクを踏む」だけのツール。うちのグループの実際のルール(重み)がそのまま仕様になっているツール。データの置き場所と使い道を自分で説明できるツール。広告ゼロ。そして、不満に気づいたその週に直せるツール——フィードバックフォームとメンテナが同一人物なので。
ここから試せます: meetyudai.com/tools/settli — 登録不要です。それが設計の出発点だったので。