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; for the failure path and why the order-submission response and orderChanges are not trade confirmations, see Trade Busts.
Reading an execution
Each fill's execution price is reported in price. For the cumulative matching-engine average across an order's fills, see avgFillPx (introduced in API 3.5.4). This order-level field does not change the per-fill execution format or confirm settlement.
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 monotonicsequenceNumberthat 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 toexecutionBusts, which carries its ownsequenceNumber.Timestamps are milliseconds. All
timestampfields 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 breakdown fields
Under the current fee model, takerFee is the perp taker's gross signed rUSD debit. The execution also carries the four settlement buckets it was split into (API 3.3.0, additive):
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:
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 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 under the legacy fee model carry takerFee without this breakdown; the components are never synthesized for them. makerFee is omitted under the current fee model because there is no maker fee or rebate leg. It appears only on legacy 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 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 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.
Historical: the REST endpoints
To backfill or reconcile, pull history per product:
GET /v2/wallet/{address}/perpExecutionsGET /v2/wallet/{address}/spotExecutionsGET /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:
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
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).
Omit both to get the most recent 100.
startTimeonly → executions at or after that time (still newest-first, capped at 100).endTimeonly → 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:
Request with no
startTime/endTime(or your desired upper bound) → newest 100.Take
meta.endTime(the oldest timestamp in the page) and issue the next request withendTime = meta.endTime.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. A perp fill under the current fee model 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/perpExecutionslive and key your ledger onsequenceNumber, 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.
Last updated