Skip to main content
The enriched trade stream is the live trade tape with two things added to every print: who traded, and where that trader stands in the market right after the fill. It runs on the same market data WebSocket as the raw tape, as two opt-in topics. Read this page before you build a trade feed that shows trader names or per-trade PnL.

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:
Both topics require a credential. Authenticate with API-key headers or a JWT, as described in Authenticated connections. An anonymous connection (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 normal SubscribeRequest (tag 0x10) and name the topics explicitly:
  • Use the trade-tape id. contract_id is the same id you use for the raw trades topic. 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 topics list selects, and trades.enriched.pnl does not imply trades.enriched.
  • Add to an existing subscription. Subscriptions are additive, so you can send these topics in a second SubscribeRequest for 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.
To drop the enriched topics and keep the rest of the market, send an 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.
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

A TradePnL 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:
  1. The print is published with provisional PnL, usually within milliseconds.
  2. When the fill confirms, the confirmed PnL replaces it. If the values are the same, nothing is sent. If they differ, trades.enriched.pnl is sent again with the same seq and the confirmed values.
  3. If the fill never lands, the same seq is sent again with status UNAVAILABLE. 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 a READY update for the same seq.
trades.enriched is not republished.

Ordering and gaps

Records can arrive out of seq 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 seq is below one you have seen.
  • Tolerate gaps. A seq can 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:
In a browser, pass the JWT through 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.