What you get
Both topics carry the same prints, keyed by the same
seq. Subscribe to one or
both.
When to use it instead of trades
Use the raw trades topic
when you need every print as fast as possible and do not need identity. It has
no added latency, it covers every venue, and it is available to anonymous
connections.
Use the enriched topics when you want to show a name, avatar, or PnL next to
each print without calling another API per row. You pay for that with a short
delay, a smaller set of venues, and a credential.
Venue coverage
A print with no taker wallet is never published. That rules out venues whose
public tape is anonymous, such as Kalshi. Backfilled history is not published
either: the stream carries live prints only.
Connecting
Use the market data gateway:kairos.marketdata.public.v1) that asks for either
topic gets an ErrorResponse with action: "subscribe" and
message: "Topic requires authentication: enriched-trade" (or enriched-pnl).
Nothing else in that request is subscribed.
Subscribing
Send a normalSubscribeRequest (tag 0x10) and name the topics explicitly:
- Use the trade-tape id.
contract_idis the same id you use for the rawtradestopic. For Polymarket that is the numeric market id, not the condition id. See Venues and products. - Opt in by name. Neither topic is in the default set that an empty
topicslist selects, andtrades.enriched.pnldoes not implytrades.enriched. - Add to an existing subscription. Subscriptions are additive, so you can
send these topics in a second
SubscribeRequestfor a market you already follow. - Nothing is replayed. The gateway forwards frames as they are published. It sends no snapshot or history on subscribe. Load earlier prints from Trade history.
UnsubscribeRequest (tag 0x11) with the topic names in repeated string
field number 4. Every name in that field must be trades.enriched or
trades.enriched.pnl. An UnsubscribeRequest without field 4 drops every
topic for the market, enriched ones included.
Messages
Every frame is[1-byte tag][protobuf payload]. Each batch covers one market.
EnrichedTradeBatch (tag 0x20)
EnrichedTrade:
TraderIdentity:
TradePnLBatch (tag 0x21)
TradePnL:
MarketPnL is the position of the print’s taker (trade.taker_address) across
every outcome of the market, right after this fill. On-chain venues publish one
print per wallet, so the other side’s PnL arrives on its own print.
TokenPosition:
The PnL is a snapshot at the fill.
mark_price and unrealized_pnl_usd are not
updated as the market moves. For a live valuation, re-mark tokens with your
own prices. For how cost basis and realized PnL work, see
Positions and PnL.enriched_trade.proto
enriched_trade.proto
Save this as
proto/kairos/enriched/v1/enriched_trade.proto, next to the
kairos/v1 files from the Protobuf schema page.
It imports common.proto and trade.proto.Status values
Identity status
A
PENDING identity is not republished for that print. Later prints by the same
wallet carry the result once the lookup finishes. Cache identities by
(provider, wallet) and apply the newest READY or ANONYMOUS value to every
row from that wallet.
PnL status
Sequencing and joining the topics
Each market has its own sequence.seq increments by one per print, in the
order prints reached the enrichment step. epoch identifies the sequence space:
it changes when the enrichment service restarts, and seqs from different epochs
are not comparable. Every key you build should include epoch.
Join the two topics on (provider, contract_id, epoch, seq). trade_id
identifies the same print and works as a join key too.
A print is published on both topics together, once its identity and its PnL
have both resolved or timed out. This holds even if you subscribe only to
trades.enriched, so a print never shows up there before its PnL exists. The
two frames usually arrive back to back, but either can arrive first. Create the
row from whichever arrives first and fill in the other half when it lands.
Update in place
ATradePnL can arrive more than once for the same seq. Treat every
TradePnL as an upsert: replace the PnL you hold for that seq. Do not append
a second row.
This happens for Polymarket prints seen before on-chain confirmation:
- The print is published with provisional PnL, usually within milliseconds.
- When the fill confirms, the confirmed PnL replaces it. If the values are the
same, nothing is sent. If they differ,
trades.enriched.pnlis sent again with the sameseqand the confirmed values. - If the fill never lands, the same
seqis sent again with statusUNAVAILABLE. Remove that PnL. If the wallet’s previous print on the market is still in view, fall back to its PnL. A late confirmation can restore the print with aREADYupdate for the sameseq.
trades.enriched is not republished.
Ordering and gaps
Records can arrive out ofseq order. A print whose lookups finish quickly is
published before an earlier print that is still waiting. Within one batch,
records are in ascending seq. Across batches, they are not.
- Key by seq, don’t assume order. Store rows by key and sort them for
display. Don’t drop a record because its
seqis below one you have seen. - Tolerate gaps. A
seqcan arrive up to 20 seconds after its neighbours, the PnL deadline. One still missing after that was lost and will not arrive. If you hold rows back to fill gaps, cap the wait. A few seconds covers almost every print. - Tolerate duplicates. An upsert by key makes a repeated record harmless.
- Handle a new epoch. Records from an old epoch are still real prints. Keep them, and start a new sequence space for the new epoch.
- Reconnect cleanly. After a reconnect, re-send your
SubscribeRequests and load anything you missed from Trade history. The gateway does not replay enriched frames.
Latency
The stream trades a little latency for completeness. A print is released when its identity and PnL are ready, and never later than the 20-second PnL deadline. Identity lookups give up after about 1.5 seconds. In testing, the median delay from the raw print to the published row with PnL is around 8 ms, p90 around 100 ms, and p99 around 2 seconds. These are typical figures, not a guarantee. The slow tail is mostly a sell that waits for the ledger to record its opening buy, and the first identity lookup for a wallet not seen before.TypeScript example
This Node.js client subscribes to both topics for one Polymarket market, decodes the frames, and merges them into one row per print. It uses@bufbuild/protobuf and
ws.
Generate the bindings from the kairos/v1 protos and enriched_trade.proto:
Sec-WebSocket-Protocol instead of headers,
as shown in Authenticated connections,
and set ws.binaryType = "arraybuffer".
Limits and caveats
- Credential required. Anonymous connections cannot subscribe.
- Limited venues. Identity covers Polymarket and predict.fun. PnL covers Polymarket and predict.fun. Prints without a taker wallet are not published.
- Live only. No history, snapshot, or replay on subscribe. Backfilled prints are not published.
- PnL is per fill. It is the taker’s position right after the print, marked at the latest price seen then. It is not a live portfolio value.
- Gaps are possible. A record lost in transit is not resent. Use Trade history as the source of truth for the tape.
- Connection limits apply. Each subscription counts toward the connection limits like any other topic.

