For the complete documentation index, see llms.txt. This page is also available as Markdown.

Expenses

Shared cost logging, splitting, and per-user balance accounting

The Expenses domain in @holons/core/expenses tracks shared monetary expenses inside a holon: who paid, what for, in which currency, and how the cost should be split among participants. From those raw records it computes per-user balances and a credit matrix showing who owes what to whom.

Where REA Accounting is the event ledger for all value flows in a holon, Expenses is the specialized layer for monetary ones. Every expense action produces REA events that feed Scoring, but the Expenses domain owns its own data model because monetary balances need stronger guarantees (currency normalization, integer-safe arithmetic, multi-currency netting) than a generic event stream provides.

Concepts

Expense

interface Expense {
  id: AgentId;
  date: number;                // Unix epoch ms
  amount: number;
  currency: string;            // normalized: lowercase, singular, a–z only
  description: string;
  paidBy: AgentId;
  splitWith: AgentId[];
  picture?: string | null;     // optional Telegram file_id for a receipt
}

type AgentId = string | number;

A few notes on the shape:

  • IDs are loose. Telegram stores numeric user IDs; the web app uses UUID strings. The domain accepts both and normalizes at the storage edge.

  • Currency is normalized. "EUR", "euros", "Euro" all become eur via normalizeCurrency(). This is what makes multi-currency totals additive without per-call casing checks.

  • splitWith can be loose. Older records may arrive as a non-array. coerceSplitWith() is the canonical normalizer.

  • Pictures are optional. Receipts are stored as Telegram file IDs—external blob storage stays out of the domain.

Balance

A single number per user per currency. Positive means the holon owes them; negative means they owe the holon.

Credit matrix

An NxN matrix where [i][j] is the net amount user i is owed by user j. Row sums give per-user net balances; column sums give "how much does each user owe in total."

This is what lets the bot answer "settle up with Laura" by reading one cell of the matrix rather than reconstructing the full debt graph each time.

Operations

Creation and editing

Function
Purpose

createExpense(input)

Build an Expense from a CreateExpenseInput

addParticipant(expense, userId)

Add a user to splitWith (pure)

removeParticipant(expense, userId)

Remove (pure)

toggleParticipant(expense, userId)

Convenience toggle (pure)

splitAmongAll(expense, allUsers)

Spread the cost across every user in a holon

Operations are pure—they return a new Expense. Persistence is done by the calling UI through HoloSphere.

Balance computation

Function
Purpose

computeBalances(expenses, users)

Full BalancesResult — matrix + per-user nets

computeCreditMatrix(expenses, userIds)

Just the matrix

computeUserCurrencyBalance(expenses, userId, currency)

One user, one currency

coerceSplitWith(value)

Defensive normalizer for legacy data

normalizeCurrency(currency)

Canonical currency code

Aliases for legacy call sites:

  • calculateBalance = computeUserCurrencyBalance

  • calculateCreditMatrix = computeCreditMatrix

All computation is pure. A web UI rendering "you owe Laura €12.50" runs the same function as the Telegram bot running /balance.

How a split works

The conventional even-split:

For multi-currency holons, the same logic runs independently per currency. There is no implicit exchange rate—balances stay in the currency they were incurred in, and users can clear them in whichever currency makes sense.

Integration with REA

When an expense is created, the corresponding REAEventFactory emits:

  • one expense:paid event with the payer as provider,

  • one share event per participant, with the participant as receiver of the cost obligation.

These events feed scoring via the standard aggregator pipeline. A holon that values "covering shared costs" can give weight to expense:paid events in its value equation, so contributing financially becomes a recognized form of participation alongside hours and appreciations.

Storage layout

Expenses are persisted in the expenses lens of their holon. Each expense is keyed by its id (Telegram message ID or UUID); reads use the standard getAll pattern.

The Telegram bot's persistence shape is the authoritative one — both the web app and any future UI normalize to it on write. This is what lets the same holon's expenses be edited from any interface without per-UI translation.

MCP tool surface

The MCP server exposes 6 tools in the expenses domain:

  • expense CRUD (expense_create, expense_get, expense_delete)

  • split helpers (split_expense)

  • balance computation (per-user, full matrix)

An MCP-connected agent can log a shared meal, calculate who owes whom, or settle a balance entirely through tool calls.

See also

  • REA Accounting — the event ledger expenses emit into

  • Scoring — how monetary contributions become recognized points

  • Library — sibling resource-sharing domain (with deposit accounting)

  • MCP Server — the tool surface

Last updated

Was this helpful?