How Budget Splitting and Settlement Calculation Work in TREK

TREK computes group travel settlements through a three-stage pipeline that splits expenses in integer cents, nets multi-currency balances using frozen exchange rates, and applies a greedy algorithm to generate the minimal set of payment transfers.

TREK is an open-source travel planning application that handles complex group budgeting scenarios. Understanding how budget splitting and settlement calculation work in TREK reveals a sophisticated cent-precision system designed to eliminate floating-point errors and optimize payment flows across multiple currencies.

Stage 1: Budget Splitting with Cent-Precision

The splitEqualShares function at lines 92-101 in server/src/services/budgetService.ts handles the division of expenses. To avoid floating-point drift, the system works entirely in integer cents using totalCents = Math.round(total * 100).

For an equal split, the function calculates a base share of Math.floor(totalCents / n) where n is the member count. The remainder (totalCents % n) is distributed deterministically using const startIndex = itemId % n, ensuring that the same budget item always distributes extra cents to the same members across edits.

TREK supports three distinct split modes:

  • Equal split: Automatic division when no custom amounts are provided
  • Custom split: Explicit amounts stored in budget_item_members.amount bypass the equal-share routine
  • Ticket split: Itemized sub-expenses stored as separate budget items, each processed individually

Stage 2: Settlement Calculation and Currency Netting

The calculateSettlement function (lines 13-56 in server/src/services/budgetService.ts) aggregates all credits and debits per member. For each budget item, the engine credits the payer(s) with toTrip(p.amount, item.currency, item.exchange_rate) and debits members with either their custom amount or their equal share.

Foreign currency conversion uses the frozen exchange rate captured in item.exchange_rate at creation time, falling back to live rates supplied via opts.rates only when no frozen rate exists. This ensures historical expenses remain valued consistently regardless of current rate fluctuations.

The system then applies existing settlements from budget_settlements, converting each through settleToTrip to cancel already-paid portions before calculating final balances.

Stage 3: Greedy Settlement Optimization

The settlement engine (lines 24-34 in server/src/services/budgetService.ts) minimizes payment flows through a greedy matching algorithm. It separates members into debtors (balance < -0.01) and creditors (balance > 0.01), sorts both lists descending by absolute amount, and iteratively matches the largest remaining debtor against the largest creditor.

Each transfer is created for the smaller of the two amounts, and the process repeats until one list is exhausted. The final amounts are rounded to the display currency only after all conversion work completes, ensuring precision is maintained throughout the calculation.

Implementation Examples

Equal Split with Deterministic Rounding

import { splitEqualShares } from './budgetService';

// Split €123.45 among three members, item ID 42
const shares = splitEqualShares(123.45, [
  { user_id: 7 },
  { user_id: 12 },
  { user_id: 19 },
], 42);

/* Remainder 2 cents distributed starting at index (42 % 3 = 0) */
console.log(shares);
// { '7': 41.15, '12': 41.15, '19': 41.15 }

If the total were €123.46, the extra cent would go to member 7 because startIndex is 0.

Calculating Trip Settlement

import { calculateSettlement } from './budgetService';

const rates = { USD: 1.09, GBP: 0.85 };

const result = calculateSettlement('trip-123', {
  base: 'EUR',
  rates,
  tripCurrency: 'EUR',
});

console.log('Balances:', result.balances);
console.log('Payment flows:', result.flows);

The flows array contains the minimal set of transfers (e.g., "User 7 pays User 12 €23.40").

Creating Custom-Split Expenses

import { createBudgetItem } from './budgetService';

await createBudgetItem('trip-123', {
  name: 'Dinner at restaurant',
  total_price: 100,
  members: [
    { user_id: 7, amount: 30 },
    { user_id: 12, amount: 20 },
    { user_id: 19, amount: 50 },
  ],
});

Explicit amount values skip the equal-share calculation and use the provided figures directly.

Summary

  • Budget splitting and settlement calculation in TREK operates through three distinct stages: cent-precision splitting, multi-currency netting, and greedy flow optimization.
  • The splitEqualShares function in server/src/services/budgetService.ts eliminates floating-point errors by working in integer cents and distributes remainder cents deterministically using the item ID as a rotation seed.
  • Foreign currency expenses convert to the trip currency using frozen exchange rates stored at creation time, ensuring historical consistency.
  • The settlement engine produces the minimal number of payment transfers by matching debtors against creditors in descending order of amount.

Frequently Asked Questions

How does TREK prevent rounding errors when splitting expenses?

TREK converts all monetary values to integer cents before division. The splitEqualShares function calculates base shares using Math.floor(totalCents / n) and distributes the remainder one cent at a time based on itemId % n, ensuring deterministic and consistent rounding across edits.

What happens when trip members use different currencies?

The calculateSettlement function normalizes all amounts to the trip's canonical currency using the toTrip utility. It prioritizes frozen exchange rates captured when the expense was created, falling back to live rates only if no frozen rate exists.

How does TREK determine who pays whom in the final settlement?

The system separates members into debtors (negative balances) and creditors (positive balances), sorts both lists by amount, and runs a greedy matching algorithm. It always matches the largest remaining debtor with the largest creditor, creating a transfer for the smaller amount, producing the minimal set of payments required to settle all debts.

Can I specify exact amounts for certain members while splitting the rest equally?

Yes. When creating a budget item, providing explicit amount values in the members array triggers custom split mode. The system uses these exact amounts for specified members. If mixture is needed (some custom, some equal), you would need to calculate equal shares manually or use the ticket split mode which creates separate items for each sub-expense.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →