# Trades

Read a Pair's public trades, stream new ones, and backfill missed trades without gaps using trade IDs.

Public trades are the executions on a Pair, without the accounts involved. Read recent trades with [`GET /v1/spot/markets/{symbol}/trades`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/markets/symbol/trades) or [`MarketDataService.GetTrades`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.MarketDataService/GetTrades), stream new ones on the [public trades channel](https://testnet.polyester.com/docs/api-docs/streams/public-spot-market-trades), and use `afterTradeId` to fill any gap between them. All three are public and need no authentication.

For your own fills, with fees and order IDs, use your account's trades instead; see the [WebSocket session model](https://testnet.polyester.com/docs/developer-docs/shared-concepts/websocket-session-model#event-streams).

## Trade fields

## REST JSON

```http
GET https://api.testnet.polyester.com/v1/spot/markets/BTC-USDT/trades?limit=2
```

```json
{
	"nextPageToken": "cGcBEW1hcmtldGRhdGEudHJhZGVz...",
	"trades": [
		{
			"side": "SELL",
			"matchId": "2185",
			"price": "76059.87",
			"qty": "0.01793867",
			"symbol": "BTC-USDT",
			"executedAt": "2026-11-05T21:37:49.373560213Z"
		},
		{
			"side": "SELL",
			"matchId": "2184",
			"price": "76059.87",
			"qty": "0.00518525",
			"symbol": "BTC-USDT",
			"executedAt": "2026-11-05T21:37:48.403061401Z"
		}
	]
}
```

## ConnectRPC ProtoJSON

```http
POST https://api.testnet.polyester.com/marketdata.v1.MarketDataService/GetTrades
Content-Type: application/json

{"symbolId": 1, "limit": 2}
```

```json
{
	"trades": [
		{
			"symbolId": 1,
			"matchId": "2186",
			"priceTicks": "76059870000000",
			"qtyScaled": "2176832",
			"tsNs": "1793914669484062384"
		},
		{
			"symbolId": 1,
			"matchId": "2185",
			"priceTicks": "76059870000000",
			"qtyScaled": "1793867",
			"tsNs": "1793914669373560213"
		}
	],
	"nextPageToken": "cGcBEW1hcmtldGRhdGEudHJhZGVz..."
}
```

ConnectRPC JSON omits `isBuy` when it is `false`, so a trade without `isBuy`, like these two, was a taker sell.

These responses were captured on 5 November 2026; page tokens are shortened.

| REST         | ConnectRPC   | Meaning                                                                                  |
| ------------ | ------------ | ---------------------------------------------------------------------------------------- |
| `matchId`    | `matchId`    | Trade ID, unique within the Pair. Use it to order and deduplicate trades                 |
| `side`       | `isBuy`      | Taker side: `BUY` (`isBuy: true`) when the taker bought the base Asset, otherwise `SELL` |
| `price`      | `priceTicks` | Execution price in quote units. `priceTicks` has a fixed scale of 9                      |
| `qty`        | `qtyScaled`  | Quantity in base units. `qtyScaled` uses the Pair's `baseQuantityScale`                  |
| `executedAt` | `tsNs`       | Execution time: RFC 3339 in UTC on REST, Unix nanoseconds on ConnectRPC                  |
| `symbol`     | `symbolId`   | The Pair                                                                                 |

`matchId` is an unsigned 64-bit integer carried as a string. Parse it as a 64-bit integer, never as a JavaScript `Number`. See [Scaled integers](https://testnet.polyester.com/docs/developer-docs/connectrpc/scaled-integers) for decoding `priceTicks` and `qtyScaled`.

## Recent trades

Without `afterTradeId`, `GetTrades` returns trades newest first, by execution time with `matchId` as the tie-breaker. Every parameter is listed in the [API reference](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/markets/symbol/trades). The rules that matter when you page through history:

- `limit` defaults to `100` trades per page, with a maximum of `1000`.
- `startTime` and `endTime` are inclusive bounds on execution time. Neither may be more than 5 minutes in the future, and `endTime` may not be before `startTime`.
- Send `pageToken` with the same Pair, bounds, and `side` as the request that returned it. An empty `nextPageToken` means there are no more pages.

REST takes the symbol in the path; ConnectRPC takes `symbolId`. A request outside these rules is rejected with HTTP 400 (`INVALID_ARGUMENT`), and a value that cannot be parsed, such as an unknown `side`, with HTTP 400 (`BAD_REQUEST`). An unknown symbol answers HTTP 404 (`UNKNOWN_SYMBOL`) on REST; an unknown `symbolId` answers HTTP 400 (`invalid_argument`) on ConnectRPC.

## Backfill by trade ID

`afterTradeId` walks a Pair's trades in ascending `matchId` order and never skips one. Use it to fill gaps after a reconnect or downtime. It cannot be combined with `startTime`, `endTime`, `side`, or `pageToken`.

```http
GET https://api.testnet.polyester.com/v1/spot/markets/BTC-USDT/trades?afterTradeId=0&limit=3
```

```json
{
	"completeThroughTradeId": "2184",
	"nextAfterTradeId": "3",
	"nextPageToken": "",
	"trades": [
		{
			"side": "BUY",
			"matchId": "1",
			"price": "76053.36",
			"qty": "0.00262873",
			"symbol": "BTC-USDT",
			"executedAt": "2026-11-05T17:37:39.717089572Z"
		},
		{
			"side": "BUY",
			"matchId": "2",
			"price": "76015.35",
			"qty": "0.00202795",
			"symbol": "BTC-USDT",
			"executedAt": "2026-11-05T17:37:48.61476999Z"
		},
		{
			"side": "BUY",
			"matchId": "3",
			"price": "76015.35",
			"qty": "0.00099033",
			"symbol": "BTC-USDT",
			"executedAt": "2026-11-05T17:37:57.842206051Z"
		}
	]
}
```

| Field                    | Meaning                                                                                                                                       |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `trades`                 | Trades with `matchId` above `afterTradeId`, oldest first, at most `limit`, and never above `completeThroughTradeId`                           |
| `completeThroughTradeId` | Every trade at or below this ID is available and returned by this walk. Newer trades may exist that are not available here yet. It only grows |
| `nextAfterTradeId`       | The `afterTradeId` of your next call. It never moves backwards                                                                                |
| `epochEnds`              | The epochs that ended between `afterTradeId` and `nextAfterTradeId`, with the last trade ID of each. Absent when none ended                   |

On a Pair without trades, `afterTradeId=0` answers `{"nextPageToken":"","trades":[]}` on REST and `{}` on ConnectRPC. Both cursor fields are absent, so treat a missing `nextAfterTradeId` as `0`.

### Epochs

Trade IDs are grouped in epochs. The IDs of epoch `e` are greater than `(e - 1) × 2^40` and less than `e × 2^40`, and a later epoch's IDs are greater than every earlier epoch's. When an epoch ends, the next one starts at a much higher ID, so IDs jump between epochs. Epoch 16, for example, starts after `15 × 2^40` (`16492674416640`), so its first trade is `16492674416641`.

A walk that crosses the end of an epoch lists it in `epochEnds`:

```json
{
	"epochEnds": [
		{ "epoch": "1", "lastTradeId": "0" },
		{ "epoch": "2", "lastTradeId": "1099511627776" }
	]
}
```

An epoch that ended without trades reports `lastTradeId` equal to its start, `(e - 1) × 2^40`, as both entries here do. Do not treat the jump between epochs as missing trades; `afterTradeId` already accounts for it.

### Walk to the present

1. Start from the last trade you hold

   Set `afterTradeId` to the highest `matchId` you have applied, or `0` to start from the Pair's first trade.

2. Apply the page

   Apply `trades` in order, upserting by `matchId`.

3. Continue or stop

   Call again with `afterTradeId` set to `nextAfterTradeId`. When `nextAfterTradeId` equals the `afterTradeId` you sent, nothing newer is available yet: stop, or retry later with backoff.

A page with fewer than `limit` trades can still be followed by more trades once they become available, so always decide from `nextAfterTradeId`, not from the page size.

## Live trades

Subscribe to the Pair's public trades channel:

```text
public:spot:market:trades:<symbolId>
public:spot:market:trades:<symbolId>:proto
```

Each publication is an ordered array of 1 to 256 trades, at most 64 KiB, oldest first. JSON publishes `{"events":[…]}` with the REST trade fields; Protobuf publishes `marketdata.v1.MarketTradeBatch`, whose `events` repeat `marketdata.v1.MarketTrade`. A JSON publication looks like this:

```json
{
	"events": [
		{
			"symbol": "BTC-USDT",
			"matchId": "2190",
			"side": "SELL",
			"price": "76075.09",
			"qty": "0.00131449",
			"executedAt": "2026-11-05T21:37:52.340853094Z"
		},
		{
			"symbol": "BTC-USDT",
			"matchId": "2191",
			"side": "SELL",
			"price": "76075.08",
			"qty": "0.01307918",
			"executedAt": "2026-11-05T21:37:52.340853094Z"
		}
	]
}
```

Several trades can share an execution time, as here, when one order fills against several resting orders. Order them by `matchId`, not by time.

## Ordering and deduplication

- Apply trades in array order, and arrays in publication order. Within one Pair, trades arrive in execution order.
- Upsert by `matchId`. A trade can arrive again after a reconnect or a service restart, possibly in a different array; a trade whose `matchId` you already hold replaces the earlier copy.
- When the subscription reports that missed publications could not be recovered, backfill with `afterTradeId` from the highest `matchId` you hold, then keep applying live arrays.

> **Subscribe before you backfill**
>
> Subscribe to the live channel first and buffer what arrives, then walk `afterTradeId` to the present. Upserting by `matchId` merges the two without duplicates or gaps.

See [Realtime channels](https://testnet.polyester.com/docs/developer-docs/market-data/realtime-channels) for connecting and recovery, [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits) for quotas, and the [trades channel reference](https://testnet.polyester.com/docs/api-docs/streams/public-spot-market-trades) for both payload schemas.
