Public trades are the executions on a Pair, without the accounts involved. Read recent trades with GET /v1/spot/markets/{symbol}/trades or MarketDataService.GetTrades,
stream new ones on the public trades channel, 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.
Trade fields
GET https://api.testnet.polyester.com/v1/spot/markets/BTC-USDT/trades?limit=2{
"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"
}
]
}POST https://api.testnet.polyester.com/marketdata.v1.MarketDataService/GetTrades
Content-Type: application/json
{"symbolId": 1, "limit": 2}{
"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 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. The rules that matter when
you page through history:
limitdefaults to100trades per page, with a maximum of1000.startTimeandendTimeare inclusive bounds on execution time. Neither may be more than 5 minutes in the future, andendTimemay not be beforestartTime.- Send
pageTokenwith the same Pair, bounds, andsideas the request that returned it. An emptynextPageTokenmeans 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.
GET https://api.testnet.polyester.com/v1/spot/markets/BTC-USDT/trades?afterTradeId=0&limit=3{
"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:
{
"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
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.
Apply the page
Apply trades in order, upserting by matchId.
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:
public:spot:market:trades:<symbolId>
public:spot:market:trades:<symbolId>:protoEach 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:
{
"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 whosematchIdyou already hold replaces the earlier copy. - When the subscription reports that missed publications could not be recovered, backfill with
afterTradeIdfrom the highestmatchIdyou hold, then keep applying live arrays.
afterTradeId to the
present. Upserting by matchId merges the two without duplicates or gaps.See Realtime channels for connecting and recovery, Rate limits for quotas, and the trades channel reference for both payload schemas.