# Compute rewards

Every hour, each Mac that worked gets a score s = A · T · q. The hour's release from compute.mlx is shared by weight × score, capped at κ/24 MLX per unit, and paid by one compute_reward transaction into locked balances.

Everything here is in `lib/compute/rewards.ts` (`planHour`, `settleCompute`) and `lib/compute/seats.ts` (`deliveryT`, `pairUp`), and the ledger's `compute_reward` handler in `lib/chain/ledger.ts`. Hours are UTC clock hours.

## The score of one Mac for one hour

```text
s = A · T · q

A = m / 60
    m = the distinct minutes of the hour in which the Mac was available (at most 60)

T = min(1, done / max(1, m / p))
    done = complete results the server accepted from this Mac this hour
    p    = minutes per job that count as full delivery:
           15 s on a dedicated seat, 12 s on a free seat

q = quality(ok, bad)
```

### A: availability

A minute counts when, during it, the server saw any of these from the Mac (`noteHeartbeat`, `noteJob`):

- a heartbeat with an eligible state (see [Free seats](/protocol/free-seats#what-counts-as-available)), from a Mac linked to a wallet;
- a job handed out to it;
- a complete result from it.

A Mac that is paused or off is not available.

### T: delivery

`done` counts complete results the server took: the job the Mac was handed, once, and no faster than the timing floor (see [Jobs and checks](/protocol/jobs#results)). It does not wait for the comparison with other Macs. A wrong answer still counts as delivered. It is caught by `q` instead.

The target depends on the seat. A Mac whose wallet holds a dedicated seat (starter seats count) is measured against one job per 15 available seconds (`DEDICATED_MIN_PER_JOB`). A Mac on a free seat is measured against one short job per 12 seconds (`FREE_CYCLE_S`): the longest throttle wait plus a few seconds of work. A Mac that takes every job it is offered reaches `T = 1`.

Hours before `DELIVERY_V2_AT` are scored with the older, looser target for dedicated seats, so they replay as they were paid.

### q: quality

`ok` and `bad` count, over the Mac's whole life (`cr:q2:<pk>`, from 27 Oct 2026), how often its answers agreed with the majority of Macs of its GPU group that ran the same job (see [Jobs and checks](/protocol/jobs#verdicts)). With `n = ok + bad`:

```text
n below 10:   q = (ok + 10 − n) / 10    (checks not made yet count as passed)
from 10 on:   q = ok / n
```

So a new Mac starts at 1, and every failed check counts from the first one. An earlier version kept q at 1 for any Mac below 10 checks, so a script could fail every check and stay at 1 by linking a fresh device key before reaching it.

## Rows: one per wallet

`planHour` groups the Macs that scored above zero by the wallet they are linked to. Each wallet gets one row with a weight `w` and a score:

| Wallet | Weight `w` | Score |
| --- | --- | --- |
| Seats carried | Σ units carried (`carryOrder`) | Σ (units × s) / Σ units |
| Free seats | 0.125 for each Mac without a seat to carry (`COMPUTE_FREE_WEIGHT`) | Σ (w × s) / Σ w |

The row's weight is the two added together, and its score the weighted average, at most 1. Every Mac that worked counts: each seat is carried by one Mac, biggest with biggest (the Mac linked first breaks a tie), and earns the units of the smaller class; every other Mac earns on a free seat. See [Pairing seats with Macs](/protocol/seats#pairing-seats-with-macs). The seat part and the free part are each capped at κ/24 per unit, and only the free part is scaled by the free-seat share. At most 5 free-seat Macs on one network count in an hour ([Fair use](/resources/fair-use)). Until 29 Oct 2026 a wallet without a seat scored by its best Mac only and Macs beyond its seats earned nothing.

## The hour's release

```text
B   = compute.mlx's MLX balance when the hour is planned
E_h = λ_h · B
λ_h = 1 − 2^(−1 / half-life in hours) = 1.9768e-5
```

The half-life is 4 years (`COMPUTE_HALF_LIFE_DAYS`, `COMPUTE_LAMBDA_HOUR` in `lib/chain/curve.ts`). MLX that is released but not earned is never moved. It stays in `compute.mlx` and goes into later releases.

## Who gets what

```text
S        = Σ over rows of w · score
reward_r = min( κ/24 · w · score ,  E_h · w · score / S )      κ = 8 MLX per unit per day
```

Then the free-seat cap. If the free rows together got more than 10% of `E_h` (`COMPUTE_FREE_SHARE`), every free row is scaled down by the same factor so that together they get exactly 10% of `E_h`. Rows with seats are not scaled up.

Each reward is rounded down to whole base units (6 decimals). Rows that round to zero are dropped.

At most, a seat earns 8 MLX per unit a day and a free seat 1 MLX a day (0.0417 an hour). The [seat table](/protocol/seats#the-seats) gives each seat's cap.

## Settlement

The produce cron runs every minute and calls `settleCompute`:

1. It takes a short Redis lock (`cr:lock`), so two runs never settle together.
2. It starts at the hour after the last settled one (`cr:last`), or at `COMPUTE_REWARDS_START`.
3. An hour is settled once it has ended and its last heartbeats are in. A run catches up a few hours at most.
4. `planHour` computes the payouts, and the sequencer sends one system transaction:

```text title="compute_reward body"
{
  type:    "compute_reward",
  hour:    <start of the hour, ms since 1970, a whole UTC hour>,
  payouts: [ { to: <wallet>, amount: <MLX base units, string>, weight: <w>, score: <score> }, … ]
}
```

The ledger checks it again before it pays:

- `hour` is a whole UTC hour, not before `COMPUTE_REWARDS_START`, and already over.
- The hour has not been paid before (`cr:<hour>` in the ledger store). This makes settlement idempotent: a second attempt fails with "That hour was already paid", which `settleCompute` treats as done.
- Each payout goes to an existing, non-protocol wallet, and no wallet twice.
- `0 < weight` and `0 < score ≤ 1`.
- `amount ≤ κ · weight / 24`, plus one base unit for rounding.
- `compute.mlx` holds enough.

Each payout moves MLX from `compute.mlx` into a `compute` lock on the wallet, one lock per UTC day. See [Lock-ins](/protocol/lock-ins). An hour with nothing to pay sends no transaction, and is still marked settled.

`compute_reward` is not added to each wallet's transaction list: 24 a day would push real history out. The day's lock is the record, and the wallet shows one "Mined on your Macs" row per day.

## Mining referrals

Since 29 Oct 2026 the hour's payouts also pay the miners' inviters (`planReferrals` in `lib/compute/referrals.ts`, checked again by the ledger in `compute_reward`). A wallet's inviter is the `referrer` recorded when it was created, and it never changes. Since 29 Oct 2026 16:30 UTC every new wallet must name one (`create_account` without an existing `referrer` is refused: the invite code is the inviting wallet's name). Wallets with a linked Mac at that time are **epoch 0** (`epoch0` system transaction, `epoch0: true` on the account): no upstream, so the chain stops at them and nobody above them is paid on their mining. For each payout, up to three wallets above the miner are paid a share of the payout:

| Tier | Level 1 / 2 / 3 |
| --- | --- |
| `normal` | 20% / 4% / 0.8% |
| `kol` | 25% / 5% / 1% |
| `strategic` | 30% / 6% / 1.2% |

Each inviter is paid at its own tier (`ref_tier` sets it, publicly). An inviter qualifies for an hour only when:

- one of its own linked Macs checked in within the last 7 days;
- none of those Macs checks in from the same network as the miner's Macs (an opaque hash of the network, kept 7 days, `wk:net:<pk>`; never the address);
- it isn't [flagged as suspicious](/network/accounts#suspicious).

A share that doesn't qualify is not paid and stays in the budget; the levels above are still paid. Free seats' mining counts. Bonuses are not mining, so they earn no bonus themselves. They come from `community.mlx`, within a budget of 300 million MLX (`stats.refMined` counts what was paid); when it is spent, no new bonus is paid. Each bonus is a lock of kind `referral` on the inviter, ending when the payout's own lock ends, and it folds into the balance the same way mined MLX does.

```endpoint try
GET /api/referrals/{name}
```

| Field | Type | Notes |
| --- | --- | --- |
| `tier` | string | `normal`, `kol` or `strategic` |
| `invitedBy` | string or null | the wallet's own inviter |
| `levels` | [number, number, number] | wallets invited at levels 1, 2 and 3 (recounted every 10 minutes) |
| `today` | string | bonus settled today (UTC), base units |
| `locked` | string | bonus still locked, base units |
| `earned` | string | all bonus ever paid, base units |
| `budget` | object | `total` and `used`, base units |
| `live` | boolean | whether bonuses are being paid |

## Reading it

Every reward ever paid, reconciled against the balance of `compute.mlx`:

```endpoint try
GET /api/compute
```

A wallet's MLX from hours not yet settled, and how fast it is growing, is in `GET /api/account/<name>/mining`. See [Compute](/network/compute).

## What the hourly plan does not include

- No bond after a lock-in ends. The whitepaper's §11.6 describes one. The code doesn't check one today.
- No fee recycling. Paid jobs are not live, so nothing is recycled into `compute.mlx` yet (whitepaper §11.4).
