> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kuru.io/llms.txt
> Use this file to discover all available pages before exploring further.

# User trades stream

> Replay and follow a user's finalized taker- and active-maker-side executions.

```text theme={null}
userTrades@f
```

This user-scoped stream uses `(feedEpoch, globalUserSeq)` and has no snapshot.
Without `seq`, it starts at the oldest retained trade involving the user. With
`seq`, replay starts strictly after that cursor before joining live delivery.

## Subscribe

```json theme={null}
{"op":"sub","id":8,"t":"userTrades@f","u":2,"feedEpoch":0,"seq":2951}
```

## Decoded event

The shape below is the inner `message` returned by `decodeExchangeWsEnvelope`
or `decodeExchangeWsMessage`. Every binary socket message also carries an outer
`id: bigint`, matching your subscription ID. Use it to route snapshots, replay,
and live updates. See [binary subscription envelopes](/api/exchange/websocket#binary-subscription-envelope).

```ts theme={null}
type ExchangeWsUserTradeLiquidity =
  | {
      kind: "activeFifo";
      slotIndex: number;
      orderId: bigint;
      makerSide: "buy" | "sell";
      remainingBaseAfter: bigint;
      makerFeePps: number;
    }
  | {
      kind: "passiveBand";
      lowPrice: bigint;
      passiveSideRemainingAfter: bigint;
    };

interface ExchangeWsUserTradesFrame {
  wireVersion: 1;
  feedEpoch: bigint;
  view: "finalized";
  kind: "userTrades";
  userId: bigint;
  globalUserSeq: bigint | null;
  globalSeq: bigint | null;
  previousGlobalUserSeq: bigint | null;
  sourceBlock: { blockNumber: bigint; blockId: string };
  trades: Array<{
    marketAddress: string;
    tradeId: bigint;
    recordIndex: number;
    users: [bigint, bigint];
    takerSide: "buy" | "sell";
    price: bigint;
    baseFilled: bigint;
    blockTimestamp: bigint;
    liquidity: ExchangeWsUserTradeLiquidity;
    txHash: string;
    txIdx: number;
    logIdx: number;
    effectiveTakerFeePps: number;
    builderFeePps: number;
    matchEnd: boolean;
    action: { accountId: bigint; executor: string; clientOrderId: string | null };
    operation: { outcome: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; operationIndex: number; replacementSlot: number } | null;
  }>;
}
```

`users` is ordered `[takerId, makerId]`. An active FIFO fill carries the
resting maker's user ID and is delivered to both nonzero users. A passive fill
has no individual maker and uses `0n` as the maker ID. Active fills have distinct
taker and maker IDs; contracts prevent self fills. `takerSide` always describes the taker side,
so the active maker's side is the opposite value.

The stable identity of a fill is `(marketAddress, tradeId, recordIndex)`.

For an explicit replay cut `R`, accept the first publication when
`globalUserSeq > R` and `previousGlobalUserSeq` is `null` or at most `R`.
After that sparse bridge, every subsequent `previousGlobalUserSeq` must equal
the last accepted `globalUserSeq` exactly.

## Fees, source identity, and match boundaries

Fee rates are historical event values in PPS (denominator `10_000_000`).
`liquidity.makerFeePps` applies only to active makers; passive liquidity omits
that field. Prices and sizes retain market price/size precision. The client
calculates fees and positions; the Gateway exposes the inputs only.

`sourceBlock`, `txHash`, `txIdx`, `logIdx`, and `recordIndex` identify the source
record. A packed log becomes separate normalized records: the current projector
emits one trade per publication, rather than packing the complete match into
one `trades` array. Iterate rows in delivery order.

`matchEnd: true` closes a match. A match can span multiple packed logs with
intervening book/hook records; one log can also contain multiple matches.
Takers receive all their active and passive fills. A maker receives only
its own fills and may never receive the match's terminal record. Maker buys use the contract's per-record two-stage quote floor. For active
maker sells, exact quote credit is a cumulative active-ask ceiling delta and
can depend on preceding fills belonging to other makers. A maker-only stream
therefore cannot always reconstruct exact quote credit or fee-adjusted positions
from its own rows. Use complete ordered match data or authoritative indexed
accounting for those amounts.

For example, a taker 101 matching maker 202, passive liquidity, then maker 303
receives three publications. Their rows have `users` values `[101n, 202n]`,
`[101n, 0n]`, and `[101n, 303n]`, with `matchEnd` values `false`, `false`, and
`true`. Maker 202 receives only the first row; maker 303 receives only the third.

Persist unfinished match state together with the replay cursor. If a reconnect
loses that state, replay from before the unfinished match; handle a history gap
by bootstrapping again. The existing epoch and predecessor rules still apply.

See the [response changelog](/api/exchange/user-trades-changelog) for every added
field and its binary offset.

## Canonical event timestamp

Each decoded trade row or order delta event includes `blockTimestamp: bigint`
in Unix seconds. It is the source block's canonical header timestamp and is
identical in live delivery and retained replay. It is not the publication time.
Open-order snapshot rows use their retained creation timestamp, `createdAt`. See the [event timestamp changelog](/api/exchange/user-trades-changelog#kur-1719-canonical-event-timestamps).

## Contract quote arithmetic

For taker sells, quote uses two floors: `floor(floor(price * size / sp) * quoteScale / pp)`.
For passive taker buys, both stages round up. For active taker buys, maintain
`W = sum(price * size)` across only active fills in the match, and charge
`ceil(W_after * quoteScale / (sp * pp)) - ceil(W_before * quoteScale / (sp * pp))`.
Passive fills do not change `W`; reset it at `matchEnd`.

Buy builder fees are `ceil(sum(size) * baseSizeMultiplier * builderFeePps / 10_000_000)`
in raw base atoms. Keep raw-atom precision when updating positions; do not first
round the fee to size precision. The Gateway exposes inputs and performs none
of this accounting. `operation` is optional outcome metadata, not another fill
or an instruction to mutate an order twice.

## Calculating client-side PnL

The following is the Indexer's weighted-average, long-inventory accounting
model. PnL is per user and market, denominated in that market's **quote token**;
it is not automatically a USDC value. Use arbitrary-precision integer arithmetic
(for example, JavaScript `bigint`), never floating-point numbers for accounting.

### 1. Start from a position baseline

Read the [user trades API](/api/data-source/reference/user-trades), including
`data.positions`, `feedEpoch`, and `globalUserSeq`. Subscribe strictly after that
cursor using the replay rules above. Do not reapply the historical trades in
the API response to its already-materialized position baseline.

For each market, keep:

* `B`: open inventory in raw base-token atoms.
* `C`: open cost in raw quote-token atoms.
* `R`: cumulative realized PnL in raw quote-token atoms (signed).
* Market `pp` (price precision), `sp` (size precision), and both token decimals.

Let `baseScale = 10^baseDecimals`, `quoteScale = 10^quoteDecimals`, and
`baseSizeMultiplier = baseScale / sp`. API `openSize` is in size precision, so
convert its exact decimal string to raw atoms by multiplying by
`baseSizeMultiplier`; do not truncate fractional size units. API `openCost`
already uses raw quote atoms. Convert x18 realized PnL using the quote token's
decimals. Do not rebuild cost from a rounded display average entry price.

`data.positions` supplies size and cost, **not cumulative realized PnL**.
To display lifetime `R`, retain a verified realized-PnL baseline or reconstruct
it from complete indexed history at the same cutoff; one page of trade history
is not sufficient. Alternatively initialize `R = 0` and label the result as
realized PnL **since the baseline**, not lifetime PnL. For quote tokens with more
than 18 decimals, an x18 value can lose raw-atom precision; it cannot substitute
for exact raw state when reproducing integer accounting.

The baseline and replay cursor must describe the same indexed state. Preserve
the epoch, accepted cursor, position state, and any unfinished match together.
After an epoch change or unavailable replay history, reload the baseline.

### 2. Calculate each fill's quote and the complete match's fees

Use the contract quote arithmetic above to calculate `q[i]`, the raw quote
amount of fill `i`, and `b[i] = baseFilled[i] * baseSizeMultiplier`. Preserve
delivery order and carry the active-ask accumulator across logs until
`matchEnd`; neither a WebSocket frame nor a log boundary closes a match.

With `D = 10_000_000` (PPS, parts per scale):

```text theme={null}
Q = sum(q[i])
S = sum(b[i])
takerProtocolFee = ceil(Q * effectiveTakerFeePps / D)

taker buy:  builderBaseFee  = ceil(S * builderFeePps / D)
taker sell: builderQuoteFee = ceil(Q * builderFeePps / D)

active maker fee for fill i = floor(q[i] * makerFeePps[i] / D)
```

Maker protocol fees are per-fill quote fees. Taker protocol fees and builder
fees are computed once per complete match, not independently rounded per fill.
Use the historical rates carried by the event, not current fee-tier defaults.
Passive fills have no individual maker position to update.

`builderFeePps` is the builder fee **rate**, not an amount. It is separate from
`effectiveTakerFeePps`: apply both fees to the taker, but do not charge the
builder fee to the active maker. A zero builder rate produces a zero builder
fee. The buy-side builder fee is in raw **base** atoms; the sell-side builder
fee is in raw **quote** atoms. Do not add amounts in these different units.

To reproduce the Indexer's individual trade rows, allocate the taker protocol
fee and sell-side builder fee using `q[i]` as weights; allocate the buy-side
builder fee using `b[i]` as weights. For each nonterminal fill assign
`floor(totalFee * weight[i] / sum(weights))`; the final fill receives the exact
remaining fee. If both the fee and total weight are zero, all allocations are
zero. Apply the resulting legs in order, then publish the taker's position at
`matchEnd`. This preserves both fee totals and per-fill cost-basis rounding.

### 3. Apply fee-adjusted buys and sells

For a **buy**, let `b` be the acquired raw base amount and `q` its raw quote
amount. A taker receives `b - allocatedBuilderBaseFee`; an active maker receives
`b`. Add the corresponding quote protocol fee to cost:

```text theme={null}
B' = B + netBaseReceived
C' = C + q + protocolQuoteFee
R' = R
```

Buy fees are incorporated into inventory/cost, not immediately booked as
realized PnL. In particular, a base-token builder fee reduces inventory while
the quote cost remains invested.

For example, a single-fill taker buy of 2 tokens for 200 USDC with
`effectiveTakerFeePps = 2_000` and `builderFeePps = 10_000` incurs a 0.04 USDC
protocol fee and a 0.002-token builder fee (assuming the token decimals can
represent these amounts exactly). From an empty position, store 1.998 tokens
of inventory and 200.04 USDC of cost. Average entry becomes
`200.04 / 1.998` USDC per token, rather than `200.04 / 2`. At a 100 USDC mark,
unrealized PnL is `1.998 * 100 - 200.04 = -0.24 USDC`; realized PnL is still
zero. Do not also subtract the base builder fee's quote value from realized
PnL: its effect is already reflected in the reduced inventory.

For a **sell**, let `b` be the sold raw base amount and `f` the quote fees:
maker protocol fee for a maker, or allocated taker protocol plus builder quote
fees for a taker. Using the position **before** this fill:

```text theme={null}
m = min(b, B)
releasedCost = C                         if m = B
               floor(C * m / B)          otherwise
matchedGrossQuote = floor(q * m / b)
matchedNetQuote   = floor((q - f) * m / b)

grossRealizedPnl = matchedGrossQuote - releasedCost
realizedPnl      = matchedNetQuote - releasedCost
B' = B - m
C' = C - releasedCost
R' = R + realizedPnl
```

When `B = 0`, released cost and both matched quote amounts are zero. When the
position closes, set `B' = C' = 0`. A sale beyond tracked inventory does not
create a short position: only the matched portion contributes realized PnL.
Do not subtract sell fees a second time after using `matchedNetQuote`.

For a single-fill taker sell of 1 token for 110 USDC, the same rates give a
0.022 USDC protocol fee and a 0.11 USDC builder fee. Net proceeds are
`110 - 0.022 - 0.11 = 109.868 USDC`. If the released cost is 100 USDC, realized
PnL is 9.868 USDC. The Indexer's reported net PnL already includes these fees;
clients displaying that value must not deduct them again.

### 4. Derive average entry and marked PnL

```text theme={null}
averageEntryPriceX18 = floor(C * baseScale * 10^18 / (B * quoteScale))
```

Use zero when `B = 0`. At a mark price `markPriceX18` in quote tokens per base
token, the unrealized value is the marked open inventory minus open cost:

```text theme={null}
markedInventoryX18 = floor(B * markPriceX18 / baseScale)
openCostX18        = floor(C * 10^18 / quoteScale)
unrealizedPnlX18   = markedInventoryX18 - openCostX18
totalPnlX18        = realizedPnlX18 + unrealizedPnlX18
```

Convert signed realized PnL to x18 with integer truncation toward zero, matching
the Indexer. These are quote-token values; a non-USDC quote needs its own USDC
conversion before account-wide aggregation. Mark-price changes affect
unrealized PnL even when this user has no new trades.

For example, with no builder fee, buying 2 tokens for 200 USDC plus a 0.04 USDC
protocol fee creates 2 tokens of inventory at 200.04 USDC cost. Selling 1 token
for 110 USDC with a 0.022 USDC fee releases 100.02 USDC cost and realizes
`110 - 0.022 - 100.02 = 9.958 USDC`. The remaining cost is 100.02 USDC. At a
110 USDC mark, unrealized PnL is 9.98 USDC and total PnL is 19.938 USDC.

### Transfers are also position changes

Trades alone are insufficient to maintain the Indexer's position state.
Deposits, withdrawals, and internal account transfers also adjust inventory
and basis in the token's designated first market, without changing realized
PnL. An incoming transfer uses the recipient's existing average cost; when
there is no open position it uses the latest preceding trade price, or zero
if no price exists. An outgoing transfer removes proportional cost, resetting
inventory/cost to zero if it exhausts or exceeds the position. Internal
transfers apply each account's own basis; they do not transfer the sender's
cost basis to the recipient.

Balance updates do not identify these accounting operations. Unless your
client also consumes complete, ordered transfer history, refresh authoritative
indexed positions rather than treating a balance delta as another trade.


## AsyncAPI

````yaml api/exchange/asyncapi.yaml userTrades
id: userTrades
title: Subscribe to user trades
description: Replay and follow finalized taker- and active-maker-side executions.
servers:
  - id: mainnet
    protocol: wss
    host: gateway.mainnet.kuru.io
    bindings: []
    variables: []
  - id: testnet
    protocol: wss
    host: gateway.testnet.kuru.io
    bindings: []
    variables: []
address: /ws
parameters: []
bindings: []
operations:
  - &ref_1
    id: subscribeUserTrades
    title: Subscribe user trades
    type: receive
    messages:
      - &ref_2
        id: request
        contentType: application/json
        payload:
          - name: userTrades@f subscription
            type: object
            properties:
              - name: op
                type: string
                enumValues:
                  - sub
                required: true
              - name: id
                type: integer
                description: >-
                  JSON integer restricted to JavaScript's exact safe-integer
                  range.
                required: true
              - name: t
                type: string
                enumValues:
                  - userTrades@f
                required: true
              - name: u
                type: integer
                description: Numeric Kuru user ID.
                required: true
              - name: feedEpoch
                type: integer
                description: Feed namespace. Required when seq is supplied.
                required: false
              - name: seq
                type: integer
                required: false
        headers: []
        jsonPayloadSchema:
          type: object
          additionalProperties: false
          required:
            - op
            - id
            - t
            - u
          allOf:
            - if:
                required:
                  - seq
                x-parser-schema-id: <anonymous-schema-61>
              then:
                required:
                  - feedEpoch
                x-parser-schema-id: <anonymous-schema-62>
              x-parser-schema-id: <anonymous-schema-60>
          properties:
            op:
              type: string
              enum:
                - sub
              x-parser-schema-id: <anonymous-schema-58>
            id: &ref_0
              type: integer
              minimum: 0
              maximum: 9007199254740991
              description: >-
                JSON integer restricted to JavaScript's exact safe-integer
                range.
              x-parser-schema-id: ControlInteger
            t:
              type: string
              enum:
                - userTrades@f
              x-parser-schema-id: <anonymous-schema-59>
            u:
              allOf:
                - *ref_0
              description: Numeric Kuru user ID.
              x-parser-schema-id: UserId
            feedEpoch:
              allOf:
                - *ref_0
              description: Feed namespace. Required when seq is supplied.
              x-parser-schema-id: FeedEpoch
            seq:
              allOf:
                - *ref_0
              minimum: 1
              x-parser-schema-id: ReplaySequence
          x-parser-schema-id: UserTradesSubscription
        title: userTrades@f subscription
        example: |-
          {
            "op": "sub",
            "id": 8,
            "t": "userTrades@f",
            "u": 2,
            "feedEpoch": 0,
            "seq": 2951
          }
        bindings: []
        extensions:
          - id: x-parser-unique-object-id
            value: request
    bindings: []
    extensions:
      - id: x-parser-unique-object-id
        value: userTrades
sendOperations:
  - *ref_1
receiveOperations: []
sendMessages:
  - *ref_2
receiveMessages: []
extensions:
  - id: x-parser-unique-object-id
    value: userTrades
securitySchemes: []

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.