Skip to main content
What you’ll build: a live holder count for a market, event, or position that ticks as wallets enter and exit, with historical candles backfilled from REST. Useful for a “holders” counter on a market page or for spotting markets gaining holders fast.
polymarket_holder_metrics streams real-time holder counts for positions, conditions (markets), and events, batched per block. It answers questions like “is this market gaining holders?” and “how concentrated is this position?” without polling. This recipe subscribes to the families you care about, tracks holder growth over time, and backfills history from REST.

When to use this

  • A “holders” counter on a market or event page that ticks up as wallets enter.
  • Spotting momentum: an event whose holder count is climbing fast.
  • Showing a position’s distribution: total balance and cost basis across all holders.

The three families

You subscribe with at least one id array, and only the families you filter for are delivered. Up to 500 identifiers total per subscription.
FilterEventRow
position_idsholder_metrics_position_batchPositionHolderMetricsRow
condition_idsholder_metrics_condition_batchConditionHolderMetricsRow
event_slugsholder_metrics_event_batchEventHolderMetricsRow
This is the only one of the PnL v3 rooms whose filters are all optional, but it still needs at least one id array to return data. Each row carries ts (Unix seconds), block, and holder_count. Position rows add total_balance and total_cost_basis, plus condition_holder_count and event_holder_count so a single position subscription can also report the holder counts of its parent market and event.

Step 1: subscribe to what you are watching

Pass the ids for the surfaces on screen. A market page tracking one event and its markets passes both event_slugs and condition_ids.
import { StructWebSocket } from "@structbuild/sdk";

const ws = new StructWebSocket({ apiKey: "sk_live_xxx" });
await ws.connect();

await ws.subscribe("polymarket_holder_metrics", {
  event_slugs: ["us-election-2028"],
  condition_ids: ["0xabc..."],
});
Only the families whose id array you supplied are delivered, so the subscription above yields holder_metrics_event_batch and holder_metrics_condition_batch but not position batches.

Step 2: track holder growth for a market or event

Keep the latest count per id and, if you want a trend, append each tick to a small series so you can show direction.
const latestHolders = new Map<string, number>();
const eventSeries = new Map<string, { ts: number; holders: number }[]>();

ws.on("holder_metrics_event_batch", (event) => {
  for (const row of event.data) {
    latestHolders.set(row.event_slug, row.holder_count);
    const s = eventSeries.get(row.event_slug) ?? [];
    s.push({ ts: row.ts, holders: row.holder_count });
    eventSeries.set(row.event_slug, s);
  }
  renderHolderTrend();
});

ws.on("holder_metrics_condition_batch", (event) => {
  for (const row of event.data) {
    latestHolders.set(row.condition_id, row.holder_count);
  }
});
A rising holder_count between ticks is the clearest signal that a market is drawing fresh participation.

Step 3: read a position’s balance and cost basis

Position rows carry the distribution figures. total_balance is the combined outcome-token balance across all holders and total_cost_basis is what they paid in aggregate, so the two together describe how much capital is committed to that position.
ws.on("holder_metrics_position_batch", (event) => {
  for (const row of event.data) {
    renderPositionStats(row.position_id, {
      holders: row.holder_count,
      balance: row.total_balance,
      costBasis: row.total_cost_basis,
      marketHolders: row.condition_holder_count,
      eventHolders: row.event_holder_count,
    });
  }
});

Backfilling historical candles from REST

The room streams changes going forward. To draw a holder-growth chart with history, pull candles from the REST history methods, then attach the live stream on top by the same id.
const { data: candles } = await client.holders.getPositionHoldersHistory({
  position_id: "123456789...",
});
The matching methods are getPositionHoldersHistory, getMarketHoldersHistory, and getEventHoldersHistory, one per grain. Seed the chart from the candles, then let the stream extend it.

Push without a socket: webhooks

If you do not want to hold a socket open, the same data is available as webhooks: position_holder_metrics, condition_holder_metrics, and event_holder_metrics. Each requires its id array (position_ids, condition_ids, or event_slugs respectively) and delivers the same row as the matching stream family. Register one with a POST /v1/webhooks body specifying url, event, and filters. Use webhooks for server-side counters and the stream for live UIs.

Follow-on

Last modified on June 13, 2026