> For the complete documentation index, see [llms.txt](https://docs.holons.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.holons.io/software/scoring.md).

# Scoring

The **Scoring** domain in `@holons/core/scoring` is the engine that turns a holon's [REA event stream](/software/rea-accounting.md) into per-user scores. It is the working implementation of the [value equation](/getting-started/glossary.md#value-equation) — the concept referenced across the rest of the docs.

Where REA decides *what counts as a recorded event* and [Council](/software/council.md) / [DNA](/software/dna.md) decide *what the holon stands for*, scoring decides *how much each event is worth*. Every UI in the [Harvest](/software/harvest-dashboard.md) monorepo calls the same scoring functions, so "what's my score?" produces the same answer in the web app, the Telegram bot, the CLI, and via the [MCP server](/software/mcp-server.md).

## The pipeline

```
        ┌────────────────────────────┐
        │   REA events (rea_events)   │
        └──────────────┬──────────────┘
                       │
                       ▼
        ┌────────────────────────────┐
        │   REAAggregator             │
        │   → UserAggregates          │
        │   → currencyBalances        │
        └──────────────┬──────────────┘
                       │
                       ▼
        ┌────────────────────────────┐
        │   calculateUserScore()      │
        │   with ScoreEquation        │
        └──────────────┬──────────────┘
                       │
                       ▼
                    score
```

Three steps, three pieces of state:

1. **REA events** — every action that should count produced an event (see [REA Accounting](/software/rea-accounting.md)).
2. **Aggregates** — the aggregator reduces the event stream to per-user counters (`UserAggregates`) and per-currency balances.
3. **Score** — a pure function multiplies aggregates by the equation's weights.

## The value equation

```ts
interface ScoreEquation {
  initiated: number;
  completed: number;
  sent: number;
  received: number;
  hours: number;          // legacy, see migration
  collaboration: number;
  wants: number;
  offers: number;
  currencies: Record<string, number>;  // per-currency weights
}
```

`DEFAULT_EQUATION` ships with sensible starting values:

| Field             | Default | Meaning                                             |
| ----------------- | ------- | --------------------------------------------------- |
| `initiated`       | 1       | Quests created                                      |
| `completed`       | 2       | Quests completed (deliberately worth 2× initiating) |
| `sent`            | 1       | Appreciations sent                                  |
| `received`        | 1       | Appreciations received                              |
| `collaboration`   | 1       | Time-logging events                                 |
| `wants`           | 1       | Wants declared                                      |
| `offers`          | 1       | Offers declared                                     |
| `currencies.hour` | 1       | Hours logged (canonical currency)                   |

That "completed 2× initiated" default isn't accidental — it encodes one of the holon's most basic principles: finishing things matters more than starting them.

Each holon can change every weight. The equation is loaded from the holon's settings lens, cached synchronously for instant UI access, and live-updated via `subscribeToEquationChanges()`.

## User aggregates

```ts
interface UserAggregates {
  initiated: number;    // quests initiated
  completed: number;    // quests completed
  sent: number;         // appreciations sent
  received: number;     // appreciations received
  hours: number;        // sum of hours logged
  collaboration: number;// time-logging events
  wants: number;        // wants declared
  offers: number;       // offers declared
}
```

These are produced by `REAAggregator` from the event stream, or by `toAggregates()` from legacy data shapes. `ZERO_USER_AGGREGATES` is a useful default while REA queries are in flight.

## The score formula

```
score = aggregates.initiated     × equation.initiated
      + aggregates.completed     × equation.completed
      + aggregates.sent          × equation.sent
      + aggregates.received      × equation.received
      + aggregates.collaboration × equation.collaboration
      + aggregates.wants         × equation.wants
      + aggregates.offers        × equation.offers
      + Σ currencyBalances[c]    × equation.currencies[c]
```

Plain weighted sum. No magic, no neural net, no decay (yet). Every weight is multiplied by its aggregate; everything is added. The result is a single number per user.

For per-currency balances (`currencyBalances: Record<string, number>`), the score adds one term per currency the user holds, weighted by `equation.currencies[currency]`.

## Operations

The public functions exported by `@holons/core/scoring`:

| Function                                               | Purpose                                                      |
| ------------------------------------------------------ | ------------------------------------------------------------ |
| `calculateUserScore(aggregates, equation?, balances?)` | Pure: total score for one user                               |
| `calculateScoreFromUserData(userData, equation?)`      | Convenience: aggregates + score in one call                  |
| `calculateAllUserScores(holonId, equation?)`           | Score every user in a holon                                  |
| `getScoreBreakdown(aggregates, equation?, balances?)`  | Per-field contribution (for "where do my points come from?") |
| `getActionScore(actionType, equation?)`                | What's one of these actions worth right now?                 |
| `calculatePercentageShare(userScore, allScores)`       | One user's slice of the total                                |
| `calculateTaskCompletionScores(...)`                   | Specialized: per-task completion delta                       |
| `loadEquation(holonId)`                                | Read the holon's equation from settings                      |
| `getCachedEquation(holonId)`                           | Synchronous read of the cached equation                      |
| `preloadEquation(holonId)`                             | Warm the cache                                               |
| `subscribeToEquationChanges(holonId, cb)`              | Live updates                                                 |
| `migrateEquation(raw)`                                 | Fold legacy / loose-shape data into canonical form           |

The split between **pure** scoring helpers (`calculateUserScore`, `getScoreBreakdown`, …) and **impure** loaders (`loadEquation`, `subscribeToEquationChanges`) means every UI can render a score live from cached state without round-tripping to the substrate.

## Migration: legacy `hours` → `currencies.hour`

Historically the equation had a top-level `hours` weight. Newer code reads `currencies.hour`. `migrateEquation()` folds the legacy shape into the new one on load, and also folds flat top-level currency keys (`equation.euro = 0`) — the shape telegram-ui historically wrote — into `currencies.euro`.

The migration is idempotent and safe to call on already-migrated data. The deprecated top-level `hours` field is preserved (set equal to `currencies.hour`) so unmigrated consumers keep computing the same score.

## Subscriptions and caching

`getCachedEquation(holonId)` returns the cached equation synchronously, useful in render-paths that can't await. The cache is populated by `preloadEquation()` and kept fresh by `subscribeToEquationChanges()`, which listens to the underlying settings lens.

This pattern is why a slider in the web dashboard can instantly re-render every user's score when a holon adjusts a weight — the cache updates, the pure score function re-runs, every UI hears about it.

## MCP tool surface

The [MCP server](/software/mcp-server.md) exposes **9 tools** in the `scoring` domain:

* per-user score (`calculate_user_score`)
* all-user scoring (`calculate_all_scores`)
* score breakdown (`get_score_breakdown`)
* per-action delta (`get_action_score`)
* equation load/migration (`load_equation`, `migrate_equation`)
* and the supporting variants

Agents connected via MCP can answer "what's my contribution?" and "what would happen if we changed the equation?" without writing any score-computation code of their own.

## See also

* [REA Accounting](/software/rea-accounting.md) — the event stream this domain consumes
* [Glossary: value equation](/getting-started/glossary.md#value-equation) — the protocol concept
* [Glossary: epoch](/getting-started/glossary.md#epoch) — the time windows over which scores are distributed
* [Funding Flow](/getting-started/funding-flow.md) — how scores feed splitter/threshold distributions


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.holons.io/software/scoring.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
