Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.struct.to/llms.txt

Use this file to discover all available pages before exploring further.

A trending feed is a list ranked by recent activity (volume, trade count, unique traders) that updates as activity changes. Struct exposes the data in two layers: REST listings to seed initial state, and the corresponding stream rooms to push deltas. Polymarket data is split into two surfaces, events (multi-market questions) and markets (single conditions), so this recipe covers both side by side.

When to use this

  • Discovery surfaces: homepage rails, “what’s hot now” widgets, category browsers.
  • Live leaderboards ranked by 24h volume, trade count, or unique traders.
  • Any UI where a row’s metrics dictate ranking and the ranking itself shifts in real time.

Choose your surface

SurfaceWhen to useRESTStream
EventsMulti-market questions (election, tournament, weekly series). Rows include nested markets[].client.events.getEventspolymarket_events_stream
MarketsSingle condition rows (one outcome pair). Rows include outcomes[] and clob_rewards[].client.markets.getMarketspolymarket_markets_stream
Both stream rooms push full rows (not deltas) for whichever rows changed since the last flush tick, so client merging is by primary key (id for events, condition_id for markets) and re-ranking happens on each tick.

Step 1: seed from REST

import { StructClient } from "@structbuild/sdk";

const client = new StructClient({ apiKey: "sk_live_xxx" });

const { data: events } = await client.events.getEvents({ limit: 50 });
const { data: markets } = await client.markets.getMarkets({ limit: 50 });

const trendingEvents = [...events].sort(
  (a, b) => (b.metrics?.["24h"]?.volume ?? 0) - (a.metrics?.["24h"]?.volume ?? 0),
);

const trendingMarkets = [...markets].sort(
  (a, b) => (b.metrics?.["24h"]?.volume ?? 0) - (a.metrics?.["24h"]?.volume ?? 0),
);
Both row shapes carry a metrics map keyed by timeframe (1m, 5m, 30m, 1h, 6h, 24h, 7d, 30d) with volume, fees, txns, and unique_traders per window. Pick the timeframe that matches the rail’s framing and sort by it client-side.

Step 2: subscribe to deltas

Both stream rooms accept the same filters as their REST list counterparts and push every matching row that changed.
import { StructWebSocket } from "@structbuild/sdk";

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

await ws.subscribe("polymarket_events_stream", {
  interval_ms: 1000,
  mode: "filter",
  filter: { timeframe: "24h", min_volume: 10000 },
});

await ws.subscribe("polymarket_markets_stream", {
  interval_ms: 1000,
  mode: "filter",
  filter: { timeframe: "24h", min_volume: 50000 },
});

ws.on("events_stream_update", (event) => {
  for (const row of event.data) eventsById.set(row.id, row);
  rerankEvents();
});

ws.on("markets_stream_update", (event) => {
  for (const row of event.data) marketsByCondition.set(row.condition_id, row);
  rerankMarkets();
});
See Events Stream and Markets Stream for the full payload schemas.

Step 3: re-rank on each tick

Keep an in-memory map keyed by primary key, merge each pushed row in place, then re-sort. Quiet rows produce zero messages, so re-ranking is bounded by how many rows actually shifted.
const eventsById = new Map<string, PolymarketEvent>();
const marketsByCondition = new Map<string, MarketResponse>();

function rerankEvents() {
  const ranked = [...eventsById.values()].sort(
    (a, b) => (b.metrics?.["24h"]?.volume ?? 0) - (a.metrics?.["24h"]?.volume ?? 0),
  );
  renderTrendingEvents(ranked.slice(0, 20));
}

function rerankMarkets() {
  const ranked = [...marketsByCondition.values()].sort(
    (a, b) => (b.metrics?.["24h"]?.volume ?? 0) - (a.metrics?.["24h"]?.volume ?? 0),
  );
  renderTrendingMarkets(ranked.slice(0, 20));
}

Choosing a cadence

Each stream room offers four flush slots: interval_ms of 500, 1000, 3000, or 10000. Pick 1000 for an interactive trending rail, 3000 for a sidebar widget, 10000 for a low-traffic dashboard. Each (interval_ms, mode) pair is its own slot; up to 8 slots per room.

Common combinations

GoalFilter
Crypto trendingcategories=["crypto"], timeframe="24h", min_volume=50000
New and busystart_time={now-86400}, min_unique_traders=50
Search-driven trendingsearch="election", timeframe="24h" (3 to 100 chars, case-insensitive substring on title)
Rewarded markets onlyhas_rewards=true, timeframe="24h" (markets stream only)
Track by idmode="ids", event_slugs=[...] or mode="ids", condition_ids=[...]

Follow-on

If trending also needs to highlight markets paying CLOB rewards, the Markets Stream row carries a clob_rewards[] array and a total_daily_rate summary field per row.
Last modified on April 28, 2026