Polymarket Toolkit Cookbook
May 26, 2026 · View on GitHub
Runnable snippets use the same helpers as examples/ (src/index.ts). Requires Node 18+ and:
npx tsx script.ts
# Or, with no extra packages (Node 22+):
node --experimental-strip-types script.ts
No dependencies entries are required in this repo for these snippets; npx tsx pulls a runner on demand when your toolchain supports it.
1) Get a live midpoint price (REST)
English
You already know which outcome token you care about (from Gamma clobTokenIds). Polymarket’s CLOB exposes a compact midpoint read — ideal for dashboards where polling every few seconds is enough.
import { fetchClobMidpoint } from "./src/index.ts";
const tokenId = process.argv[2];
if (!tokenId) throw new Error("usage: node script <token_id>");
console.log(await fetchClobMidpoint(tokenId));
Example output
{ mid: '0.525' }
中文(本地化)
手里已经有 Gamma 返回的某个 token_id(某条 outcome 的 CLOB 资产 id)时,最快看到「中间价」的方式就是打 midpoint。适合做轻量轮询:别一上来就挂 WebSocket,先确认这个字段是不是你要的口径。
import { fetchClobMidpoint } from "./src/index.ts";
const tokenId = process.argv[2];
if (!tokenId) throw new Error("用法:传入 token_id");
console.log(await fetchClobMidpoint(tokenId));
2) Stream order book updates (WebSocket)
English
Subscribe to the market channel with one or more asset ids. Below: print a few book / best_bid_ask frames, then exit. Production code should add reconnect, backoff, and periodic PING as described in Polymarket’s CLOB websocket docs.
import {
createClobMarketWebSocket,
sendClobMarketSubscribe,
} from "./src/index.ts";
const tokenId = process.argv[2];
if (!tokenId) throw new Error("usage: node script <token_id>");
let seen = 0;
const ws = createClobMarketWebSocket({
onOpen: () => sendClobMarketSubscribe(ws, [tokenId], { customFeatureEnabled: true }),
onMessage: (msg) => {
const batch = Array.isArray(msg) ? msg : [msg];
for (const raw of batch) {
const m = raw as { event_type?: string; bids?: unknown[]; asks?: unknown[] };
const et = m.event_type;
const bookLike = et === "book" || (Array.isArray(m.bids) && Array.isArray(m.asks));
if (bookLike || et === "best_bid_ask" || et === "price_change") {
console.log(et ?? "book_snapshot", m.bids?.[0], m.asks?.[0]);
if (++seen >= 3) ws.close();
}
}
},
});
setTimeout(() => ws.close(), 12_000);
Example output (truncated)
book_snapshot { price: '0.01', size: '1041555.39' } { price: '0.99', size: '530334.25' }
price_change ...
中文(本地化)
要看盘口「推着走」,用 CLOB 的 market 通道最省事:把 token_id 丢进订阅消息,监听 book / best_bid_ask 即可。下面这段是教学用的最短路径——真实上线记得加重连、退避和心跳,不然 Wi‑Fi 抖一下你就断在半路。
import {
createClobMarketWebSocket,
sendClobMarketSubscribe,
} from "./src/index.ts";
const tokenId = process.argv[2];
if (!tokenId) throw new Error("用法:传入 token_id");
let seen = 0;
const ws = createClobMarketWebSocket({
onOpen: () => sendClobMarketSubscribe(ws, [tokenId], { customFeatureEnabled: true }),
onMessage: (msg) => {
const batch = Array.isArray(msg) ? msg : [msg];
for (const raw of batch) {
const m = raw as { event_type?: string; bids?: unknown[]; asks?: unknown[] };
const et = m.event_type;
const bookLike = et === "book" || (Array.isArray(m.bids) && Array.isArray(m.asks));
if (bookLike || et === "best_bid_ask" || et === "price_change") {
console.log(et ?? "book_snapshot", m.bids?.[0], m.asks?.[0]);
if (++seen >= 3) ws.close();
}
}
},
});
setTimeout(() => ws.close(), 12_000);
3) Scan active markets from Gamma
English
Discovery starts at Gamma: filter active=true, sort client-side if needed, then drill into clobTokenIds for pricing.
import { fetchGammaMarkets } from "./src/index.ts";
const markets = await fetchGammaMarkets({ limit: 5, active: true });
console.log(markets.map((m: { question?: string }) => m.question));
Example output
[
'Russia-Ukraine Ceasefire before GTA VI?',
'Will GPT-6 be released by December 31, 2025?',
...
]
中文(本地化)
做「扫市场」通常从 Gamma 开始:active=true 先兜一圈活跃盘,再挑你关心的 slug / conditionId,最后才落到 CLOB 的 token 维度。别反着做——没有 Gamma 元数据,你很难解释某个 token 到底代表哪条问题。
import { fetchGammaMarkets } from "./src/index.ts";
const markets = await fetchGammaMarkets({ limit: 5, active: true });
console.log(markets.map((m: { question?: string }) => m.question));
4) Pull the Data API PnL leaderboard
English
This endpoint returns rank, userName, pnl, and proxyWallet — handy when you want a typed ranking object distinct from the LB /profit array shape.
import { fetchDataLeaderboardPnL } from "./src/index.ts";
const rows = await fetchDataLeaderboardPnL({ limit: 10, offset: 0 });
console.log(rows[0]);
Example output
{
rank: '1',
userName: 'SomeTrader',
pnl: 1234567.89,
proxyWallet: '0xabc...',
vol: 9876543.21,
...
}
中文(本地化)
Data API 的 leaderboard 行结构更「报表向」:带 rank、vol、userName。如果你在做内部排行榜或想把链上地址和展示名对齐,这个端点往往比纯 LB 数组少写几层适配。
import { fetchDataLeaderboardPnL } from "./src/index.ts";
const rows = await fetchDataLeaderboardPnL({ limit: 10, offset: 0 });
console.log(rows[0]);
5) Resolve a username to a proxy wallet (ranked users)
English
resolveLbUsernameToProxyWallet scans LB /profit pages and matches name / pseudonym. Unranked accounts will not resolve — that is expected.
import { resolveLbUsernameToProxyWallet } from "./src/index.ts";
console.log(await resolveLbUsernameToProxyWallet("Theo4", 4));
Example output
0x56687bf447db6ffa42ffe2204a05edaa20f55839
中文(本地化)
把「显示名 → 0x」这件事不要想得太神奇:实现就是翻利润榜分页做大小写不敏感匹配。没在榜上出现的号,脚本诚实返回 null 才是对的——这时让用户自己从页面复制地址,比瞎猜一个链上身份安全得多。
import { resolveLbUsernameToProxyWallet } from "./src/index.ts";
console.log(await resolveLbUsernameToProxyWallet("Theo4", 4));
6) Fetch one page of positions
English
Positions power both profile views and Brier inputs. Start with offset=0, then increase by limit while a full page is returned.
import { fetchPositionsPage } from "./src/index.ts";
const user = process.argv[2];
if (!user) throw new Error("usage: node script <0xaddress>");
console.log(await fetchPositionsPage(user, { limit: 5, offset: 0 }));
Example output (shape)
[
{
title: 'Some market title',
outcome: 'Yes',
size: 123.45,
avgPrice: 0.42,
curPrice: 0.55,
redeemable: false,
...
}
]
中文(本地化)
仓位接口是典型「满页再翻」:limit 张满 100 就 offset += 100 继续拉。第一页就能验证地址是否写对、字段是不是你期望的版本——别等全量拉完才发现 user 传错。
import { fetchPositionsPage } from "./src/index.ts";
const user = process.argv[2];
if (!user) throw new Error("用法:传入 0x 地址");
console.log(await fetchPositionsPage(user, { limit: 5, offset: 0 }));
7) Fetch a slice of on-chain activity
English
Use type to focus on TRADE, REDEEM, etc. Cursor with end follows Polymarket’s Data API semantics used in the skills — see skills/polymarket-profile/SKILL.md for full pagination.
Cap warning (issue #1): Community reports ~4000 effective rows on
/activity; pages beyond that may return identical JSON. UsefetchActivityPagesorpm activity/examples/16-activity-cap-probe.tsto detectDUPLICATE_PAGE/STALE_CURSOR. For audit PnL useskills/polymarket-pnl(pagination_incompletefield).
import { fetchActivityPage, fetchActivityPages } from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
console.log(await fetchActivityPage(user, { limit: 20, type: "TRADE" }));
// Safer multi-page probe:
const probe = await fetchActivityPages(user, { limit: 500, maxPages: 10, type: "TRADE" });
console.log(probe.warnings);
Example output
[
{ type: 'TRADE', side: 'BUY', usdcSize: 12.34, title: '...', timestamp: 1710000000 },
...
]
中文(本地化)
做流水分析时先用 type=TRADE 把噪声砍掉一半。真要审计级回放,去看 profile / pnl 两个 SKILL 里写的分页细节:同一秒挤满一页时的边界条件,处理不好会漏单。
上限警告(issue #1): /activity 有效深度大约 4000 行;继续翻页可能返回相同 JSON。用 fetchActivityPages 或 ./bin/pm activity / examples/16 检测;审计级 PnL 看 polymarket-pnl 的 pagination_incomplete。
import { fetchActivityPages } from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
const probe = await fetchActivityPages(user, { limit: 500, maxPages: 10, type: "TRADE" });
console.log({ rows: probe.rows.length, warnings: probe.warnings });
8) Load event metadata (events?slug=)
English
Markets embed their parent event. Use that slug to fetch richer metadata (tags, copy, timing).
import { fetchGammaEventsBySlug } from "./src/index.ts";
const slug = process.argv[2];
if (!slug) throw new Error("usage: node script <event_slug>");
console.log(await fetchGammaEventsBySlug(slug));
Example output
[ { title: 'What will happen before GTA VI?', category: 'Pop-Culture', tags: [ ... ] } ]
中文(本地化)
如果你要画「事件级」信息架构,别只盯着单条 market:父级 event 才有更完整的叙事字段和标签。Gamma 的 events?slug= 就是干这个的。
import { fetchGammaEventsBySlug } from "./src/index.ts";
const slug = process.argv[2];
if (!slug) throw new Error("用法:传入 event slug");
console.log(await fetchGammaEventsBySlug(slug));
9) Compare LB PnL windows for one address
English
fetchLbProfitForAddress accepts all | 7d | 30d. Empty arrays simply mean “no row for that window” — common for inactive wallets.
import { fetchLbProfitForAddress } from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
for (const w of ["all", "7d", "30d"] as const) {
const rows = await fetchLbProfitForAddress(user, w);
console.log(w, rows[0] ?? "(empty)");
}
Example output
all { amount: 12345.67, name: 'SomeTrader', ... }
7d (empty)
30d (empty)
中文(本地化)
短期窗口为空不代表程序坏了,很多时候就是这段时间没产生可统计的 leaderboard 行。做展示时把「空数组」解释给用户,比硬显示 0 更诚实。
import { fetchLbProfitForAddress } from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
for (const w of ["all", "7d", "30d"] as const) {
const rows = await fetchLbProfitForAddress(user, w);
console.log(w, rows[0] ?? "(empty)");
}
10) Compute a Brier score from settled positions
English
computeBrierScoreFromSettledPositions filters redeemable=true, treats avgPrice as the forecast, and maps wins to actual=1. It is the same simplified model described in skills/polymarket-brier/SKILL.md.
import {
computeBrierScoreFromSettledPositions,
fetchPositionsPage,
} from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
const positions = await fetchPositionsPage(user, { limit: 500, offset: 0 });
console.log(computeBrierScoreFromSettledPositions(positions as never[]));
Example output
{ brier: 0.1823, n: 36, wins: 29 }
中文(本地化)
Brier 看的是「预测概率 vs 真实结果」,跟仓位大小无关。这里用 avgPrice 当初概率是刻意简化:真要做论文级严谨,再去读 brier SKILL 里关于 DCA、SPLIT 仓位的注意项。
import {
computeBrierScoreFromSettledPositions,
fetchPositionsPage,
} from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
const positions = await fetchPositionsPage(user, { limit: 500, offset: 0 });
console.log(computeBrierScoreFromSettledPositions(positions as never[]));
11) Watch redeemable positions without private keys
English
fetchRedeemablePositionsPage reads the public Data API with redeemable=true. After Polymarket's pUSD-era redemption shipped, normal user redemption happens inside the official app. This helper is the read-only status lane for agents: it surfaces every row the Data API still flags as redeemable=true, including losing tokens whose currentValue is 0 (correct — they redeem to $0). summarizeRedeemablePositions groups by conditionId and sums currentValue, so losing rows contribute 0 rather than being inflated to their size. The helper does not sign transactions or redeem funds.
import {
fetchRedeemablePositionsPage,
resolveRedeemMode,
summarizeRedeemablePositions,
} from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
const rows = await fetchRedeemablePositionsPage(user, { limit: 100 });
console.log({
mode: resolveRedeemMode({ lowWatermark: 25 }),
...summarizeRedeemablePositions(rows as never[]),
});
Example output (losing rows surfaced honestly as $0)
{
mode: 'low_watermark',
redeemableCount: 3,
conditionCount: 3,
estimatedRedeemableValue: 0,
topConditions: [
{ conditionId: '0x...', slug: 'btc-updown-5m-...', count: 1, estimatedCurrentValue: 0 },
...
]
}
中文(本地化)
官方 pUSD redeem 流程负责正常领取,agent 侧需要的是只读「看板口径」:这个钱包当下还有哪些行被 Data API 标成 redeemable=true、payable 价值(currentValue)多少、是否低于策略资金水位。注意 losing token redeem 拿 $0,所以 currentValue=0 是正确口径,helper 不会把 size 误当作 payable 拉高数字。只读公开 API,可直接接 dashboard、日报、agent 工作流。
import {
fetchRedeemablePositionsPage,
resolveRedeemMode,
summarizeRedeemablePositions,
} from "./src/index.ts";
const user = process.argv[2] ?? "0x63ce342161250d705dc0b16df89036c8e5f9ba9a";
const rows = await fetchRedeemablePositionsPage(user, { limit: 100 });
console.log({
mode: resolveRedeemMode({ lowWatermark: 25 }),
...summarizeRedeemablePositions(rows as never[]),
});
See also
examples/— numbered demos (npx tsx examples/01-fetch-gamma-markets.ts, ornode --experimental-strip-types …on Node 22+)src/index.ts— exported helpers this cookbook calls