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.

client.trader.getTraderOutcomePnl returns one row per outcome a wallet has ever traded, with current_shares_balance, realized_pnl_usd, and supporting fields. That single response covers both halves of a portfolio view: open positions (still held) and closed positions (fully exited). The same row shape is pushed live by polymarket_trader_positions, so a portfolio UI seeded from REST can stay in sync with one merge key.

When to use this

  • Wallet portfolio pages with separate “Open” and “Closed” sections.
  • Leaderboard drilldowns where clicking a trader expands to their bets.
  • “My open bets” UI for an authenticated user, with realized PnL surfaced for closed exits.

Open vs closed model

A position is open when current_shares_balance > 0: the wallet still holds outcome tokens, so the row has both unrealised exposure and realized PnL from any partial sells. A position is closed when current_shares_balance === 0: the wallet has fully exited (sold or redeemed), and only realized_pnl_usd remains relevant. The same row carries both halves of the lifecycle, which means you do not need two endpoints. Filter once in client code.

Step 1: fetch the position book

Use min_shares: 0 to include closed rows; bump it to drop dust positions.
import { StructClient } from "@structbuild/sdk";

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

const { data: rows } = await client.trader.getTraderOutcomePnl({
  address: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
  min_shares: 0,
  timeframe: "lifetime",
});

const open = rows.filter((r) => (r.current_shares_balance ?? 0) > 0);
const closed = rows.filter((r) => (r.current_shares_balance ?? 0) === 0);

Step 2: split and sort

Open positions usually rank by current mark-to-market value. Closed positions rank by realized PnL.
const sortedOpen = [...open].sort(
  (a, b) =>
    (b.current_shares_balance ?? 0) * (b.latest_price ?? 0) -
    (a.current_shares_balance ?? 0) * (a.latest_price ?? 0),
);

const sortedClosed = [...closed].sort(
  (a, b) => (b.realized_pnl_usd ?? 0) - (a.realized_pnl_usd ?? 0),
);

Step 3: scope to a timeframe

Pass timeframe to limit realized_pnl_usd (and any windowed counters) to a window: 1d, 7d, 30d, or lifetime. Use this to drive period switchers on the portfolio header.
const { data: last7d } = await client.trader.getTraderOutcomePnl({
  address: "0x...",
  timeframe: "7d",
});
You can also narrow the response to a specific event or market via event_slug or condition_id, useful for “this trader’s bets on the election” drilldowns.

Step 4: keep it live

Subscribe to polymarket_trader_positions with the same address. Every trader_position_update carries (trader, position_id) plus current_shares_balance and realized_pnl_usd, so merging into the REST-seeded map is one-to-one.
import { StructWebSocket } from "@structbuild/sdk";

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

await ws.subscribe("polymarket_trader_positions", {
  traders: ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"],
});

const positions = new Map<string, TraderPosition>();
for (const row of rows) positions.set(row.position_id, row);

ws.on("trader_position_update", (event) => {
  const row = event.data;
  positions.set(row.position_id, row);
  if (row.current_shares_balance === 0) movePositionToClosed(row.position_id);
  else upsertOpenPosition(row.position_id, row);
});
When current_shares_balance flips to 0, move that row from the Open list to the Closed list in your UI. When it ticks up from 0, the trader has re-entered an old position; move the row back.

Common combinations

GoalParameters
Lifetime portfolio (open + closed)address=0x...&min_shares=0&timeframe=lifetime
Weekly performanceaddress=0x...&timeframe=7d
Drop dustaddress=0x...&min_shares=10
Bets on one eventaddress=0x...&event_slug=us-election-2028
Sort by realized PnLaddress=0x...&sort_by=realized_pnl_usd

Pagination

For traders with long histories, the SDK’s paginate helper walks every page without manual cursor handling.
import { paginate } from "@structbuild/sdk";

for await (const row of paginate(
  (params) => client.trader.getTraderOutcomePnl(params),
  { address: "0x...", min_shares: 0 },
)) {
  ingest(row);
}
See Pagination for how offset and pagination_key interact.

Follow-on

For aggregated PnL summary cards next to the position list (global, per-event, per-market roll-ups), see the Trader PnL room. It pairs cleanly with this recipe: positions on the left, summary numbers on the right, both fed from the same wallet address.
Last modified on April 28, 2026