# How TREK's Budget Addon Handles Expense Splitting and Settlement

> Discover how TREK's Budget addon simplifies expense splitting and settlement. Learn about its automated payment calculation and debt balancing system for efficient trip financial management.

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

---

**TREK's Budget addon automatically splits trip expenses among assigned members and calculates the minimal set of payments required to settle debts using a greedy matching algorithm that converts all amounts to a base currency and balances payer credits against member debits.**

The TREK repository includes a sophisticated Budget addon that handles multi-currency expense tracking and automated debt settlement. Understanding how TREK's expense splitting and settlement system works reveals a robust backend architecture that persists granular payment data in SQLite tables and applies currency-aware algorithms to minimize transfer complexity across trip participants.

## Database Architecture for Budget Tracking

The settlement system relies on four interconnected tables that separate expense metadata from payment responsibility.

**`budget_items`** stores the core expense data including `total` amount, `currency` code, and an optional `exchange_rate` field. When a trip creator records a new expense, this row captures the transaction details while remaining agnostic about who actually pays.

**`budget_item_members`** creates the split relationship by linking users to specific items. Each row contains a `paid` boolean flag that the UI toggles when marking members as settled, though this flag serves display purposes only and does not affect the mathematical settlement calculation.

**`budget_item_payers`** records the actual monetary sources. When one or more users contribute cash toward an expense, rows here store the `amount` each payer contributed. If this table contains no entries for an item, the system assumes the total amount listed in `budget_items` represents the full payment.

**`budget_settlements`** persists manual transfers between users after the algorithm suggests settlement flows. These historic transactions are automatically factored into future balance calculations to prevent double-counting debts.

## The Settlement Calculation Engine

The core logic resides in [`server/src/services/budgetService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/budgetService.ts), where two primary functions orchestrate the debt resolution process.

### Per-Person Balance Aggregation

The `getPerPersonSummary()` function aggregates financial responsibility across all trip expenses. It calculates each member's total assigned share of expenses and subtracts any amounts they have already paid via explicit payer rows or previous settlements. The method returns an array containing `user_id`, `total_assigned`, `total_paid`, and `items_count` for every participant.

### Currency Normalization

Before running calculations, the system converts all monetary values to a common *base* currency (defaulting to the trip's currency). If a `budget_items` row contains a frozen `exchange_rate`, that value is applied directly. Otherwise, the system calls `getRates()` to fetch live foreign exchange rates and applies the current conversion to normalize multi-currency trips.

### The Greedy Settlement Algorithm

The `calculateSettlement()` function implements a flow minimization algorithm that produces the smallest number of transactions required to zero all balances. The implementation follows these steps:

```ts
// 1️⃣ Load all items, members and payers for the trip
const items   = db.prepare('SELECT * FROM budget_items WHERE trip_id = ?').all(tripId);
const members = db.prepare('SELECT … FROM budget_item_members …').all(tripId);
const payers  = db.prepare('SELECT … FROM budget_item_payers …').all(tripId);

// 2️⃣ For each item compute the total paid in the *base* currency
const paidBase = payers.reduce(
  (a, p) => a + toBase(p.amount, item.currency, item.exchange_rate), 0);

// 3️⃣ Split the paid amount evenly among the members that are assigned to the item
const sharePerMember = paidBase / members.length;

// 4️⃣ Update the balance map:
//   – each payer gets a credit for what they actually paid
//   – each member gets a debit for the equal share
ensure(payerId, …).balance += toBase(p.amount, …);
ensure(memberId, …).balance -= sharePerMember;

// 5️⃣ Apply persisted settlements (recorded transfers)
ensureSettled(s.from_user_id, …).balance += s.amount;
ensureSettled(s.to_user_id, …).balance   -= s.amount;

// 6️⃣ Greedy matching of remaining debtors ↔ creditors
while (debtors && creditors) {
  const transfer = Math.min(debtor.amount, creditor.amount);
  flows.push({ from: debtor, to: creditor, amount: round(transfer) });
  debtor.amount   -= transfer;
  creditor.amount -= transfer;
}

```

The algorithm orders debtors and creditors by outstanding amount and matches the largest debts against the largest credits, yielding the minimal number of transfers in practice without attempting computationally expensive combinatorial optimization.

## Working with the Budget Service API

The NestJS wrapper in [`server/src/nest/budget/budget.service.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/budget/budget.service.ts) exposes these methods for client applications.

### Listing Budget Items

Retrieve all expenses for a specific trip:

```ts
import { BudgetService } from './server/src/nest/budget/budget.service';

async function showBudget(tripId: string) {
  const svc = new BudgetService();
  const items = await svc.list(tripId);
  console.log('Budget items →', items);
}

```

### Calculating Per-Person Balances

Get a summary of who owes or is owed money:

```ts
async function summary(tripId: string) {
  const svc = new BudgetService();
  const summary = await svc.perPersonSummary(tripId);
  // → [{ user_id, username, total_assigned, total_paid, items_count }]
  console.log(summary);
}

```

### Computing Settlement Flows

Generate the optimized payment instructions:

```ts
async function settlement(tripId: string, baseCurrency?: string) {
  const svc = new BudgetService();
  // `baseCurrency` is the currency the UI wants the result in (e.g. "USD")
  const result = await svc.settlement(tripId, baseCurrency, 'EUR');
  console.log('Balances:', result.balances);
  console.log('Transfer flows:', result.flows);
}

```

### Recording Manual Settlements

Persist a payment between users to update future calculations:

```ts
async function settle(tripId: string, fromUserId: number, toUserId: number, amount: number) {
  const svc = new BudgetService();
  const settlement = await svc.createSettlement(tripId, {
    from_user_id: fromUserId,
    to_user_id: toUserId,
    amount,
  }, /* createdByUserId */ fromUserId);
  console.log('Created settlement →', settlement);
}

```

### Marking Members as Paid

Toggle the visual paid status for UI feedback:

```ts
async function markPaid(itemId: string, tripId: string, userId: number) {
  const svc = new BudgetService();
  const member = await svc.toggleMemberPaid(itemId, tripId, `${userId}`, true);
  console.log('Member marked paid →', member);
}

```

## UI Integration and Payment Workflows

The frontend interacts with the settlement system through specific HTTP endpoints that map to the service methods.

When a user edits the **Persons** column in the budget interface, the front-end sends a `PUT /budget/:id` request that triggers `BudgetService.updateMembers` to modify `budget_item_members` associations. Clicking a member avatar toggles the `paid` flag via `BudgetService.toggleMemberPaid`, which updates the database row but only affects the green outline indicator in the UI.

The actual settlement calculation occurs when the UI calls `GET /budget/settlement`, implemented by `BudgetController.settlement` and forwarded to `BudgetService.settlement` → `calculateSettlement`. This endpoint returns the net balances, the greedy flow list, and any historic settlements for the trip.

When a user confirms a suggested transfer, the UI POSTs to `/budget/settlements`, which invokes `BudgetService.createSettlement` → `createSettlement` to persist the transfer in `budget_settlements`. Subsequent calculations automatically incorporate this payment to adjust the affected users' balances.

## Summary

- **Four-database design** separates expense items (`budget_items`), split assignments (`budget_item_members`), explicit payers (`budget_item_payers`), and historic settlements (`budget_settlements`).
- **Currency handling** converts all amounts to a base currency using frozen exchange rates from the expense record or live rates via `getRates()`.
- **Balance calculation** occurs in `getPerPersonSummary()`, which aggregates shares and subtracts payments to determine net positions.
- **Greedy optimization** in `calculateSettlement()` matches debtors with creditors by amount to minimize the total number of required transactions.
- **API exposure** through `BudgetService` provides methods for listing items, calculating summaries, generating flows, and recording settlements.

## Frequently Asked Questions

### How does TREK handle multiple currencies in expense splitting?

The Budget addon automatically converts all expenses to a common base currency (defaulting to the trip currency) before running calculations. If an expense item includes a frozen `exchange_rate` in `budget_items`, that rate is used for consistency. Otherwise, the system fetches live FX rates via `getRates()` to normalize amounts, ensuring accurate splitting even when trip members pay in different currencies.

### What is the difference between marking a member as paid and recording a settlement?

The `paid` flag in `budget_item_members` is a UI-only indicator that toggles a green avatar outline to show visual progress; it does not affect the mathematical settlement. Recording a settlement via `createSettlement()` persists an actual monetary transfer in `budget_settlements`, which permanently adjusts the users' balances and factors into future calculations. The settlement algorithm only considers payer rows and explicit settlements, not the visual paid flag.

### How does the algorithm minimize the number of payments needed?

`calculateSettlement()` implements a greedy matching algorithm that orders all debtors and creditors by their outstanding amounts. It repeatedly matches the largest remaining debtor with the largest creditor, creating a transfer for the smaller of the two amounts. This approach produces the minimal practical set of transactions without exhaustive combinatorial search, as implemented in [`server/src/services/budgetService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/budgetService.ts).

### Where are the exchange rates stored for budget calculations?

Exchange rates are handled dynamically rather than stored permanently. When `calculateSettlement()` processes an expense, it checks for a frozen `exchange_rate` value in the `budget_items` row. If absent, the system calls `getRates()` to retrieve current foreign exchange data. This design allows the settlement engine to handle historical expenses with locked rates while supporting real-time conversion for new entries.