# How Budget Splitting and Settlement Calculation Work in TREK

> Uncover how TREK handles budget splitting and settlement calculation. Learn about its three-stage pipeline for expense division, multi-currency netting, and payment minimization.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: deep-dive
- Published: 2026-07-09

---

**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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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

```typescript
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

```typescript
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

```typescript
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`](https://github.com/mauriceboe/TREK/blob/main/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.