# Points

Lighter Robinhood (`lighter:robinhood`) runs a points program. It gives points
to the venue accounts that trade on it. Mosaiq executes through pooled venue
accounts, so the points go to the Mosaiq accounts, not to each trader.

Mosaiq keeps 10% of these points. Clients earn the other 90%, in proportion to
their fees on the venue:

```text
client pool = venue points × (1 - Mosaiq share), rounded down to 6 places
your fees   = sum over your fills on the venue in the period of
              max(0, fees.exchange + fees.mosaiq + fees.priority),
              rounded half to even to 6 places
your points = client pool × your fees / (sum of all clients' fees),
              rounded down to 6 places
```

The Mosaiq share is 0.1. `fees` is the `fees` object of each fill in
[`GET /v1/userFills`](/api/user-fills). [`GET /v1/points`](/api/points) shows
the points that an address earned in each period. Today Mosaiq shares only the
points of Lighter Robinhood.

Lighter runs the points program. Mosaiq makes no promise about the value of
venue points.

These points are venue points. They are not the `points` of
[`GET /v1/portfolioHistory`](/api/portfolio-history), which are samples of
account value.

## Periods

Lighter gives points during the week and in a weekly drop on Friday. Lighter
publishes no API for points, so Mosaiq reads them on the Lighter points page
and records them.

Mosaiq shares the points one period at a time. A period is a UTC time window
from `startMs`, included, to `endMs`, excluded. Periods follow the weekly
drops, from Friday 00:00 UTC to the next Friday 00:00 UTC, unless the venue
uses other bounds. Each period of a venue starts where the previous one ends.
So the periods have no gap, and no fill is in two periods.

Mosaiq records a period after the venue's drop for that period. The period
appears in `GET /v1/points` when Mosaiq records it. Mosaiq computes the split
once, at that time. `GET /v1/points` reads the recorded split, so a recorded
figure changes only with a [correction](#corrections).

## What counts

Only client accounts take part. Client accounts are all accounts except the
Mosaiq fee and House accounts. The fills of these two accounts never count, and
these accounts earn no points.

Your fees in a period follow these rules:

* A fill is in the period when its `createdAtMs` in `GET /v1/userFills` is at
  or after `startMs` and before `endMs`.
* Each of your fills on the venue counts with the fees shown in
  `userFills[].fees`: `exchange + mosaiq + priority`. An absent `priority`
  counts as 0.
* A fill with a total fee below 0, such as a maker rebate, counts as 0. It does
  not reduce the fees of your other fills.
* The liquidation fills of your account count, with their fees. The
  liquidation penalty does not count. It is not in `userFills[].fees`.
* Fills on other venues do not count.
* A fill whose `fillId` starts with `xc:` does not count. It is a match between
  two Mosaiq traders on Lighter, and Lighter records no trade for it.

If your fees in a period are 0, you earn no points for that period.

[`GET /v1/fees`](/api/fees) publishes the fee rates. `userFills[].fees` shows
the fees of each fill.

## Example week

Lighter gives Mosaiq 10,000 points for the period from Friday 2026-10-02
00:00 UTC to Friday 2026-10-09 00:00 UTC. The client pool is
10,000 × 0.9 = 9,000 points. Three clients and the House account trade on
`lighter:robinhood` in that period:

| Account | Fees shown in `userFills` | `feesUsdc` | `earnedPoints` |
| --- | --- | --- | --- |
| Client A | 30 USDC | `30` | `6000` |
| Client B | 10 USDC on taker fills, and a 2 USDC rebate on one maker fill | `10` | `2000` |
| Client C | 5 USDC | `5` | `1000` |
| House | 40 USDC | `0` | `0` |

B's maker fill has a total fee of -2 USDC, so it counts as 0. B's fees are 10
USDC, not 8. The House fees do not count. The clients' fees add up to 45 USDC,
the `clientFeesUsdc` of the period. A earns 9,000 × 30 / 45 = 6,000 points, B
earns 9,000 × 10 / 45 = 2,000 points, and C earns 9,000 × 5 / 45 = 1,000
points. The clients earn the whole pool, and Mosaiq keeps 1,000 points.

Mosaiq records the period on Monday 2026-10-12 at 09:00 UTC. From then on,
`GET /v1/points` returns this period for client B:

```json
{
  "startMs": 1790899200000,
  "endMs": 1791504000000,
  "revision": 1,
  "recordedAtMs": 1791795600000,
  "venuePoints": "10000",
  "mosaiqShare": "0.1",
  "clientPoolPoints": "9000",
  "clientFeesUsdc": "45",
  "feesUsdc": "10",
  "earnedPoints": "2000"
}
```

The example response on the [`GET /v1/points`](/api/points) page is client A's.
It shows this period and the period before it.

## A period without client fees

If no client has fees above 0 in a period, no client earns points for that
period. Mosaiq keeps all the venue points of that period. Nothing carries over
to the next period.

## Rounding

Mosaiq rounds the client pool down to 6 decimal places. It rounds the fees of
each client to 6 decimal places, half to even. It rounds the points of each
client down to 6 decimal places. So the sum of client points never exceeds the
client pool. What the rounding leaves, less than 0.000001 point per client,
stays with Mosaiq.

Half to even rounds a value that is exactly halfway to the even digit:
0.0000025 becomes 0.000002, and 0.0000035 becomes 0.000004.

For example, take a client pool of 100 points and three clients with the same
fees. Each client earns 100 / 3 points, rounded down to 33.333333. The clients
earn 99.999999 points together, and Mosaiq keeps the 0.000001 point that is
left.

## Corrections

A correction is a new revision of a recorded period, with the same `startMs`
and `endMs`. Mosaiq records a correction when Lighter reverses points that it
gave for the period. Mosaiq can also record a correction for late fills. A late
fill is a fill of the period that Mosaiq recorded after the period itself. The
recorded split does not count a late fill until a correction.

A correction computes the split again, from the venue points of the new
revision and every fill recorded at that time. It keeps the Mosaiq share and
the rule of the first revision. A correction can raise or lower your points for
the period.

`revision` is 1 for the first record of a period, and each correction adds 1.
`recordedAtMs` is when Mosaiq recorded the revision that the response shows.
`GET /v1/points` shows the latest revision of each period. Mosaiq keeps the
earlier revisions as a record.

## Check your figure

You can compute `feesUsdc` and `earnedPoints` of a period from your own fills.
Read your fills of the period from `GET /v1/userFills`, with `startTime` set to
`startMs` and `endTime` set to `endMs - 1`. `endTime` is inclusive, so
`endMs - 1` is the last millisecond of the period. For client A in the example:

```http
GET /v1/userFills?user=0x00000000000000000000000000000000000000a1&startTime=1790899200000&endTime=1791503999999
```

Then do these steps:

1. While `hasMore` is `true`, pass `nextCursor` as `cursor` and read the next
   page.
2. Keep the fills whose `venue` is the venue of the period.
3. For each fill, compute `max(0, exchange + mosaiq + priority)` from its
   `fees`. Count an absent `priority` as 0.
4. Add the results, and round the sum to 6 decimal places, half to even. The
   result is `feesUsdc`.
5. Compute `clientPoolPoints × feesUsdc / clientFeesUsdc`, and round it down
   to 6 decimal places. The result is `earnedPoints`.

`clientPoolPoints` is `venuePoints × (1 - mosaiqShare)`, rounded down to 6
decimal places.

If your sum is higher than `feesUsdc`, the period has late fills of yours.
They count only after a correction.
