> For the complete documentation index, see [llms.txt](https://docs.reya.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.reya.xyz/developers/devnet/executions-and-settlement/executions-and-trade-history.md).

# Executions & Trade History

Confirmed fills are delivered two ways: a **real-time stream** for live trading, and **REST history** endpoints for backfill and reconciliation. Both cover spot and perp. This page explains which channel is authoritative, how to identify and de-duplicate a fill, and exactly how the history pagination works.

## Real-time: the executions channels are the source of truth

A fill is only real once it has **settled on-chain**, and that is precisely what the executions channels report:

* `/v2/wallet/{address}/spotExecutions` — confirmed **spot** fills
* `/v2/wallet/{address}/perpExecutions` — confirmed **perp** fills
* `/v2/wallet/{address}/executionBusts` — **failed** settlements, unified across spot and perp

Executions are **split per product**; busts are a **single unified channel**. A fill appears on your executions channel only after on-chain settlement succeeds, and once it appears it is final — it never reverses. A match that fails settlement appears on `executionBusts` instead. A given match produces **exactly one** event, on exactly one of the two — never both, so there is nothing to double-count.

This is the read side of the lifecycle described in [Trade Settlement](/developers/devnet/executions-and-settlement/settlement.md); for the failure path and why the order-submission response and `orderChanges` are **not** trade confirmations, see [Trade Busts](/developers/devnet/executions-and-settlement/trade-busts.md).

{% hint style="info" %}
Drive your trade ledger and position accounting from your **executions channel** (`spotExecutions` / `perpExecutions`) — not from the order-submission response and not from `orderChanges`, both of which are matching-engine-level signals that fire before settlement.
{% endhint %}

## Reading an execution

An `ORDER_MATCH` **perp** execution is a single match record carrying **both sides**. You receive it on your wallet channel whether you were the taker or the maker — determine your side from `takerAccountId` vs `makerAccountId`, and read your economics from the corresponding `taker*` / `maker*` fee and PnL fields. Treat maker fields as optional because legacy V2 rows may not carry the full counterparty shape. `ADL` and `MARKET_CLOSE` executions are always single-sided: they have no maker and omit every `maker*` field rather than returning `null`, account `0`, or another placeholder. A **spot** execution uses the same taker/maker naming: `takerAccountId`, `makerAccountId`, `takerFee`, and maker-side order fields where available.

* **Unique identity & de-duplication — `sequenceNumber`.** Every execution carries a monotonic `sequenceNumber` that increases by 1 for each execution on its stream. Use `(channel, sequenceNumber)` as the idempotency key to drop duplicates and to detect gaps after a reconnect. The same applies to `executionBusts`, which carries its own `sequenceNumber`.
* **Timestamps are milliseconds.** All `timestamp` fields are the **on-chain block timestamp** of the settlement, in POSIX milliseconds (`block_timestamp × 1000`) — for an execution, the block in which the fill **settled** on-chain; for a bust, the block in which settlement **failed**. It is chain-side time, **not** the matching-engine match time. Because block timestamps have second-level granularity, several fills in the same block share one timestamp (see pagination below).

### Fee v3 fields

For a new Fee v3 perp fill, `takerFee` is the taker's gross signed rUSD debit, and the execution also carries the four settlement buckets it was split into (API `3.3.0`, additive):

| Field               | Meaning                                                                  |
| ------------------- | ------------------------------------------------------------------------ |
| `protocolFeeCredit` | Portion of `takerFee` credited to the protocol fee collector             |
| `referrerFeeCredit` | Portion credited to the taker's referrer (`0` when there is no referrer) |
| `takerRebateCredit` | Portion routed to the taker-rebate settlement bucket                     |
| `poolFeeCredit`     | Portion credited to the passive pool                                     |

All four are rUSD amounts and, whenever they are present, they sum exactly:

```
takerFee = protocolFeeCredit + referrerFeeCredit + takerRebateCredit + poolFeeCredit
```

`takerRebateCredit` is part of the gross taker debit: `takerFee` is what the fill charged. The rebate appears as a separate credit to the taker, which shows on the wallet's [transfer history](/developers/devnet/executions-and-settlement/transfer-history.md) as a `PERP_TAKER_REBATE` entry, so report `takerFee` as the fee of the fill and take the net cost from the ledger rather than computing `takerFee − takerRebateCredit` here.

The four component fields are present together or absent together. Executions settled before Fee v3 carry `takerFee` only; the components are never synthesized for them. `makerFee` is omitted on every Fee v3 fill because the phase has no maker fee or rebate leg; it appears only on pre–Fee v3 executions that recorded a maker debit or credit. Do not require either field or infer that a missing field is an indexing failure.

The full field lists are in the [WebSocket Info API Reference](/developers/devnet/api-reference/websocket-api-reference.md) under the `perpExecutions`, `spotExecutions`, and `executionBusts` channels.

## Mapping an order to its fills

A current PerpOB `ORDER_MATCH` execution carries a **`fillId`** — a stable identifier for that individual fill — plus the order ids of the orders that produced it. Both **spot** and **perp** order matches carry `takerOrderId` and `makerOrderId`; legacy V2 executions can omit `fillId` and these order-id fields. Non-order executions such as `ADL` and `MARKET_CLOSE` are not backed by an ME fill, so `fillId` and non-meaningful order ids are omitted.

To reconcile an order with its settled fills, **match executions by `takerOrderId` / `makerOrderId`** and use `fillId` as the unique key for each individual fill (the same `fillId` appears on an [`executionBust`](/developers/devnet/executions-and-settlement/trade-busts.md) if that fill is later busted). When an order response or `orderChanges` update includes `firstFillId` and `fillCount`, that range identifies the contiguous fill ids produced by that order update.

{% hint style="info" %}
Order-side fill ranges are contiguous: `[firstFillId, firstFillId + fillCount − 1]`. Use the executions channel for full per-fill economics and settlement status.
{% endhint %}

## Historical: the REST endpoints

To backfill or reconcile, pull history per product:

* `GET /v2/wallet/{address}/perpExecutions`
* `GET /v2/wallet/{address}/spotExecutions`
* `GET /v2/wallet/{address}/executionBusts` (unified spot + perp)

Each returns executions where the wallet participated in, or was affected by, the execution. Results are capped at **100 per request** and ordered **newest-first**. The response is an envelope:

```json
{
  "data": [ /* up to 100 execution objects, newest first */ ],
  "meta": {
    "limit": 100,
    "count": 100,
    "startTime": 1756733679000,
    "endTime": 1756733379000
  }
}
```

`meta.startTime` is the timestamp of the **newest** row in the page and `meta.endTime` the **oldest** — i.e. the time range this page covers (`startTime >= endTime`, since the page is newest-first).

## Pagination — `startTime` / `endTime`

{% hint style="warning" %}
**`startTime` and `endTime` are millisecond POSIX timestamps, not sequence numbers.** They filter on the execution's on-chain block timestamp, inclusive on both ends (`block_timestamp >= startTime` and `<= endTime`). Pass them in **milliseconds** (e.g. `1756733379000`).
{% endhint %}

* Omit both to get the most recent 100.
* `startTime` only → executions at or after that time (still newest-first, capped at 100).
* `endTime` only → executions at or before that time.
* Both → the inclusive window between them.

**To page backward through history**, walk the window using the oldest timestamp you've received:

1. Request with no `startTime`/`endTime` (or your desired upper bound) → newest 100.
2. Take `meta.endTime` (the oldest timestamp in the page) and issue the next request with `endTime = meta.endTime`.
3. Repeat until a page returns fewer than 100 rows — you've reached the end of the window.

Because the bound is **inclusive**, consecutive pages can overlap on rows sharing the boundary timestamp (block timestamps have second-level granularity, so several fills can share one). De-duplicate across pages by `sequenceNumber` — that's what makes the walk robust. (You can also set the next `endTime` to `meta.endTime - 1` to skip the boundary, but de-duping by `sequenceNumber` is the safer habit since it also covers same-timestamp ties.)

## Where the money went

Executions describe the trade; the collateral it moved is on the wallet's [transfer history](/developers/devnet/executions-and-settlement/transfer-history.md). A Fee v3 perp fill appears there as signed entries per account — `PERP_TAKER_FEE` for the gross debit, `PERP_TAKER_REBATE` for what came back, `PERP_REFERRER_REBATE` and `PERP_POOL_REBATE` on the referrer's and pool's accounts. Use `fillId` to link entries to executions; historical perp fee entries may omit `fillId` and `symbol`.

## Keeping your trade ledger in sync

The goal is a local trade ledger that matches the authoritative on-chain executions. Three cases cover it:

* **Steady state:** consume `spotExecutions` / `perpExecutions` live and key your ledger on `sequenceNumber`, processing each execution once.
* **After a disconnect:** you may have missed events while offline. Backfill the gap from the REST history endpoint (windowed by time) and merge by `sequenceNumber` — already-seen fills de-dupe away, missed fills fill the gap.
* **Failures:** a match that busted never appears on your executions channel, so there is nothing to reverse. If you track busts, read them from the unified `executionBusts` (stream or REST). A fill is on executions **xor** busts, never both.
