@pipeworx/mcp-polymarket 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # mcp-polymarket
2
2
 
3
- Polymarket MCP — prediction-market data via Gamma + CLOB public APIs.
3
+ Polymarket MCP — prediction-market data via Gamma + CLOB + Data public APIs.
4
4
 
5
- Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1683+ live data sources.
5
+ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1704+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
6
6
 
7
7
  ## Tools
8
8
 
@@ -17,6 +17,10 @@ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents
17
17
  | `polymarket_event_books` | Batched CLOB orderbooks for EVERY tradable market in one Polymarket event — single round trip via the CLOB batch /books endpoint. Use before any multi-leg strategy (partition arbitrage "SELL/BUY EVERY LEG", basket trades) to check per-leg depth: theoretical overround means nothing if half the legs are 50-share books. Returns legs[] with {slug, question, yes_price, best_bid, best_ask, yes_bids[], yes_asks[]} where bids are sorted best(highest)-first and asks best(lowest)-first as {price, size} objects. Pass include_no=true to also fetch NO-side books (doubles payload — only needed for NO-leg strategies). Caps at 80 legs (highest yes_price kept; truncated_legs reports the cut). |
18
18
  | `polymarket_trades` | Recent EXECUTED trades (the fills tape) for a Polymarket market — actual money that changed hands, newest first. Each trade: side (BUY/SELL), outcome (Yes/No or the option name), size (shares), price, timestamp, and the trader's wallet/pseudonym. Use for "what's the recent order flow", "is smart money buying Yes", "how much just traded and at what price". DISTINCT from polymarket_orderbook (resting/unfilled orders — intent) and polymarket_price_history (the CP time-series). Pass a market slug or numeric id (same input as polymarket_market). |
19
19
  | `polymarket_holders` | Largest position holders for a Polymarket market, per outcome — who holds the most Yes and the most No shares, with share amounts and trader pseudonyms. Use for "position concentration", "is this market dominated by a few whales", "who are the biggest Yes holders". Reveals conviction/concentration that price alone hides. Pass a market slug or numeric id (same input as polymarket_market). |
20
+ | `polymarket_wallet_positions` | Open and closed positions for one Polymarket wallet — size, entry basis, current mark, and realized/unrealized P&L per position, plus portfolio-level exposure and concentration (how much of the wallet's capital sits in its single largest position — a concentration measure, not a measure of trading skill). Wallet addresses are public on-chain identifiers (0x…), looked up directly — no name/ENS resolution. Omit `status` to get OPEN + CLOSED together in one call; pass a specific status to filter. Use for "what is this wallet holding", "how exposed is this wallet to market X", "is this wallet up or down overall". |
21
+ | `polymarket_wallet_activity` | Paginated activity feed (trades, redeems, splits, merges, rebates) for one Polymarket wallet over a period — newest first. Use for "what has this wallet done recently", "show me this wallet's trade history", "did this wallet trade market X". Distinct from polymarket_wallet_positions (current holdings) and polymarket_wallet_performance (aggregated P&L) — this is the raw event-by-event tape. Follow `next_cursor` to page past the current window. |
22
+ | `polymarket_wallet_performance` | Lifetime and time-series P&L, volume, and activity stats for one Polymarket wallet — profile stats (distinct markets traded, biggest single win, profile join date) plus a cumulative P&L history on the requested interval/fidelity grid. States fee treatment explicitly: `realized_pnl` in each point is already NET of taker fees paid; maker rebates and other non-trading income (rewards, referrals) are reported separately in `wallet_income` and are additive on top, not already included in `realized_pnl`. Use for "is this wallet profitable overall", "how has this wallet's P&L moved over time", "how active is this wallet". Coverage: Polymarket's own PnL ledger starts from when the wallet's positions were first tracked — `source_fidelity` on each point says whether it is a native observation or synthesized onto a finer grid. |
23
+ | `polymarket_resolution_status` | Resolution lifecycle state and timestamps for one Polymarket market: initialized / posed / proposed / challenged / reproposed / disputed / resolved (or active/arbitration), whether it was disputed, the reporter (UMA_OO / Chainlink / EOA), and — once resolved — the exact resolution timestamp and per-outcome payouts. Use for "has this market actually settled yet", "when did market X resolve", "was this resolution disputed" — distinct from polymarket_market's `closed`/`active` flags, which reflect trading status, not oracle finality. Pass a market slug or numeric id (same input as polymarket_market). |
20
24
 
21
25
  ## Quick Start
22
26
 
@@ -62,7 +66,7 @@ directly, instead of just this one's:
62
66
  }
63
67
  ```
64
68
 
65
- Both URLs reach the same gateway and the same 1683+ data sources. The
69
+ Both URLs reach the same gateway and the same 1704+ data sources. The
66
70
  only difference is which pack's tools are listed **directly**; `ask_pipeworx`
67
71
  reaches all of them from either one.
68
72
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pipeworx/mcp-polymarket",
3
- "version": "0.1.2",
4
- "description": "Polymarket MCP — prediction-market data via Gamma + CLOB public APIs.",
3
+ "version": "0.1.3",
4
+ "description": "Polymarket MCP — prediction-market data via Gamma + CLOB + Data public APIs.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -26,7 +26,7 @@
26
26
  "@cloudflare/workers-types": "^4.20260405.1"
27
27
  },
28
28
  "pipeworx": {
29
- "sourceHash": "v1-365918dcfdc0136edbc3acdcae8a184416773e78d288a422baba5648bd2fe8f5",
30
- "sourceCommit": "4d51421bd010ad9a322c2245b70b401604912eff"
29
+ "sourceHash": "v1-08f66627cb010cc5e0f3d33c4503d5f9fe6ad9a7c6a05b302950f9f7fd051e48",
30
+ "sourceCommit": "eb436cf60f06c9b79701a15cd1670609580544a5"
31
31
  }
32
32
  }
package/server.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.pipeworx-io/polymarket",
4
4
  "title": "Polymarket",
5
- "description": "Polymarket MCP — prediction-market data via Gamma + CLOB public APIs.",
6
- "version": "0.1.2",
5
+ "description": "Polymarket MCP — prediction-market data via Gamma + CLOB + Data public APIs.",
6
+ "version": "0.1.3",
7
7
  "websiteUrl": "https://pipeworx.io/packs/polymarket",
8
8
  "repository": {
9
9
  "url": "https://github.com/pipeworx-io/mcp-polymarket",
package/src/index.ts CHANGED
@@ -671,11 +671,13 @@ function collapse(s: string): string {
671
671
  return s.replace(/\s+/g, ' ').trim();
672
672
  }
673
673
  /**
674
- * Polymarket MCP — prediction-market data via Gamma + CLOB public APIs.
674
+ * Polymarket MCP — prediction-market data via Gamma + CLOB + Data public APIs.
675
675
  *
676
676
  * Polymarket runs binary-outcome prediction markets on Polygon. The Gamma API
677
677
  * (gamma-api.polymarket.com) exposes market and event metadata. The CLOB API
678
- * (clob.polymarket.com) exposes price history. Both are public; no auth.
678
+ * (clob.polymarket.com) exposes price history. The Data API
679
+ * (data-api.polymarket.com) exposes the trades tape, holder lists, and
680
+ * per-wallet positions/activity/PnL. All three are public; no auth.
679
681
  *
680
682
  * What agents typically want from this pack:
681
683
  * - "What does the market think about X?" → polymarket_search
@@ -683,8 +685,22 @@ function collapse(s: string): string {
683
685
  * - "Full detail / resolution criteria for one market" → polymarket_market
684
686
  * - "All markets within one event (e.g., 2028 election)" → polymarket_event
685
687
  * - "How has the Yes probability moved over time?" → polymarket_price_history
688
+ * - "What has this wallet bought/sold, and is it making money?" →
689
+ * polymarket_wallet_positions / polymarket_wallet_activity /
690
+ * polymarket_wallet_performance
691
+ * - "Has this market actually resolved, and when?" → polymarket_resolution_status
686
692
  *
687
693
  * Prices are quoted as probabilities in [0, 1]. outcomePrices[0] is Yes.
694
+ *
695
+ * Data API v1 → v2 migration (fleet #2713, 2026-10-07): Polymarket retires
696
+ * Data API v1 on 2026-10-24 (https://docs.polymarket.com/migrate/data-api-v1-to-v2).
697
+ * /trades and /holders (the two v1 routes this pack called) moved to
698
+ * /v2/trades and /v2/holders: same fields, wrapped in a `{ data, pagination }`
699
+ * envelope, snake_case instead of camelCase, and `market=<conditionId>` renamed
700
+ * to `condition=<conditionId>`. v1 still answers as of 2026-10-07 — confirmed
701
+ * live, byte-identical rows to v2 for the same market — but this pack calls
702
+ * v2 everywhere now rather than carrying a v1 fallback past the route this
703
+ * pack actually used being scheduled for removal.
688
704
  */
689
705
 
690
706
 
@@ -893,6 +909,117 @@ const tools: McpToolExport['tools'] = [
893
909
  required: ['slug_or_id'],
894
910
  },
895
911
  },
912
+ {
913
+ name: 'polymarket_wallet_positions',
914
+ description:
915
+ 'Open and closed positions for one Polymarket wallet — size, entry basis, current mark, and realized/unrealized P&L per position, plus portfolio-level exposure and concentration (how much of the wallet\'s capital sits in its single largest position — a concentration measure, not a measure of trading skill). Wallet addresses are public on-chain identifiers (0x…), looked up directly — no name/ENS resolution. Omit `status` to get OPEN + CLOSED together in one call; pass a specific status to filter. Use for "what is this wallet holding", "how exposed is this wallet to market X", "is this wallet up or down overall".',
916
+ summary: 'One Polymarket wallet\'s open and closed positions, with exposure, entry basis, and P&L per position.',
917
+ inputSchema: {
918
+ type: 'object' as const,
919
+ properties: {
920
+ wallet: { type: 'string', description: 'Polymarket proxy wallet address, 0x-prefixed (40 hex chars). Get one from polymarket_holders or polymarket_trades on a market of interest.' },
921
+ status: { type: 'string', description: 'OPEN | CLOSED | REDEEMABLE | REDEEMABLE_LOST | MERGEABLE — filter to one lifecycle state. Omit to fetch OPEN and CLOSED together (default).' },
922
+ limit: { type: 'number', description: 'Rows per page, 1-500 (default 50).' },
923
+ cursor: { type: 'string', description: 'Opaque pagination cursor from a previous call\'s `next_cursor` — omit for the first page.' },
924
+ },
925
+ required: ['wallet'],
926
+ },
927
+ outputSchema: {
928
+ type: 'object' as const,
929
+ properties: {
930
+ wallet: { type: 'string' },
931
+ position_count: { type: 'number' },
932
+ total_current_value_usdc: { type: ['number', 'null'] },
933
+ total_realized_pnl_usdc: { type: ['number', 'null'] },
934
+ total_unrealized_pnl_usdc: { type: ['number', 'null'] },
935
+ largest_position_share_of_exposure: { type: ['number', 'null'], description: 'Largest single position\'s current_value as a fraction of total current_value — concentration, not skill.' },
936
+ positions: { type: 'array', items: { type: 'object' } },
937
+ next_cursor: { type: ['string', 'null'] },
938
+ data_as_of: { type: 'string', description: 'ISO timestamp when this response was fetched live from Polymarket Data API v2.' },
939
+ },
940
+ },
941
+ },
942
+ {
943
+ name: 'polymarket_wallet_activity',
944
+ description:
945
+ 'Paginated activity feed (trades, redeems, splits, merges, rebates) for one Polymarket wallet over a period — newest first. Use for "what has this wallet done recently", "show me this wallet\'s trade history", "did this wallet trade market X". Distinct from polymarket_wallet_positions (current holdings) and polymarket_wallet_performance (aggregated P&L) — this is the raw event-by-event tape. Follow `next_cursor` to page past the current window.',
946
+ summary: 'Paginated trade/position-activity history for one Polymarket wallet over a period.',
947
+ inputSchema: {
948
+ type: 'object' as const,
949
+ properties: {
950
+ wallet: { type: 'string', description: 'Polymarket proxy wallet address, 0x-prefixed (40 hex chars).' },
951
+ start_date: { type: 'string', description: 'ISO date/datetime — only activity at or after this time (default: no lower bound).' },
952
+ end_date: { type: 'string', description: 'ISO date/datetime — only activity at or before this time (default: no upper bound).' },
953
+ type: { type: 'string', description: 'Filter to one activity type, e.g. TRADE, REDEEM, SPLIT, MERGE, MAKER_REBATE (default: all types).' },
954
+ limit: { type: 'number', description: 'Rows per page, 1-500 (default 50).' },
955
+ cursor: { type: 'string', description: 'Opaque pagination cursor from a previous call\'s `next_cursor` — omit for the first page.' },
956
+ },
957
+ required: ['wallet'],
958
+ },
959
+ outputSchema: {
960
+ type: 'object' as const,
961
+ properties: {
962
+ wallet: { type: 'string' },
963
+ activity_count: { type: 'number' },
964
+ activity: { type: 'array', items: { type: 'object' } },
965
+ next_cursor: { type: ['string', 'null'] },
966
+ data_as_of: { type: 'string', description: 'ISO timestamp when this response was fetched live from Polymarket Data API v2.' },
967
+ },
968
+ },
969
+ },
970
+ {
971
+ name: 'polymarket_wallet_performance',
972
+ description:
973
+ 'Lifetime and time-series P&L, volume, and activity stats for one Polymarket wallet — profile stats (distinct markets traded, biggest single win, profile join date) plus a cumulative P&L history on the requested interval/fidelity grid. States fee treatment explicitly: `realized_pnl` in each point is already NET of taker fees paid; maker rebates and other non-trading income (rewards, referrals) are reported separately in `wallet_income` and are additive on top, not already included in `realized_pnl`. Use for "is this wallet profitable overall", "how has this wallet\'s P&L moved over time", "how active is this wallet". Coverage: Polymarket\'s own PnL ledger starts from when the wallet\'s positions were first tracked — `source_fidelity` on each point says whether it is a native observation or synthesized onto a finer grid.',
974
+ summary: 'A Polymarket wallet\'s lifetime trading stats plus its cumulative P&L history over time, with fees stated explicitly.',
975
+ inputSchema: {
976
+ type: 'object' as const,
977
+ properties: {
978
+ wallet: { type: 'string', description: 'Polymarket proxy wallet address, 0x-prefixed (40 hex chars).' },
979
+ interval: { type: 'string', description: '1d | 1w | 1m | all | max — the P&L history window (default max).' },
980
+ fidelity: { type: 'string', description: 'Grid step for the P&L series: 1h | 3h | 12h | 18h | 1d (default 1d). Finer grids only available for shorter intervals.' },
981
+ },
982
+ required: ['wallet'],
983
+ },
984
+ outputSchema: {
985
+ type: 'object' as const,
986
+ properties: {
987
+ wallet: { type: 'string' },
988
+ profile: { type: 'object', properties: { distinct_markets_traded: { type: 'number' }, biggest_win_usdc: { type: 'number' }, profile_views: { type: 'number' }, joined_at: { type: ['string', 'null'] } } },
989
+ fee_treatment: { type: 'string' },
990
+ pnl_history: { type: 'array', items: { type: 'object' } },
991
+ data_as_of: { type: 'string', description: 'ISO timestamp when this response was fetched live from Polymarket Data API v2.' },
992
+ },
993
+ },
994
+ },
995
+ {
996
+ name: 'polymarket_resolution_status',
997
+ description:
998
+ 'Resolution lifecycle state and timestamps for one Polymarket market: initialized / posed / proposed / challenged / reproposed / disputed / resolved (or active/arbitration), whether it was disputed, the reporter (UMA_OO / Chainlink / EOA), and — once resolved — the exact resolution timestamp and per-outcome payouts. Use for "has this market actually settled yet", "when did market X resolve", "was this resolution disputed" — distinct from polymarket_market\'s `closed`/`active` flags, which reflect trading status, not oracle finality. Pass a market slug or numeric id (same input as polymarket_market).',
999
+ summary: 'A Polymarket market\'s oracle resolution state, dispute history, and (once resolved) settlement timestamp and payouts.',
1000
+ inputSchema: {
1001
+ type: 'object' as const,
1002
+ properties: {
1003
+ slug_or_id: { type: 'string', description: 'Market slug or numeric id — same input as polymarket_market.' },
1004
+ },
1005
+ required: ['slug_or_id'],
1006
+ },
1007
+ outputSchema: {
1008
+ type: 'object' as const,
1009
+ properties: {
1010
+ market_slug: { type: 'string' },
1011
+ question: { type: 'string' },
1012
+ status: { type: 'string' },
1013
+ was_disputed: { type: 'boolean' },
1014
+ extended_review: { type: 'boolean' },
1015
+ resolved_at: { type: ['string', 'null'] },
1016
+ expected_settlement_time: { type: ['string', 'null'] },
1017
+ payouts: { type: ['array', 'null'], items: { type: 'number' } },
1018
+ reporter: { type: ['string', 'null'] },
1019
+ data_as_of: { type: 'string', description: 'ISO timestamp when this response was fetched live from Polymarket Data API v2.' },
1020
+ },
1021
+ },
1022
+ },
896
1023
  ];
897
1024
 
898
1025
  // ── Helpers ────────────────────────────────────────────────────────
@@ -1516,17 +1643,27 @@ async function polymarketEventBooks(args: Record<string, unknown>) {
1516
1643
  };
1517
1644
  }
1518
1645
 
1519
- // data-api.polymarket.com — public, no auth; serves the trades tape and
1520
- // holder lists keyed by conditionId (0x…).
1521
- async function dataGet<T = unknown>(path: string, params: Record<string, string | number>): Promise<T> {
1646
+ // data-api.polymarket.com v2 — public, no auth; serves the trades tape,
1647
+ // holder lists, and per-wallet positions/activity/PnL. Every v2 route wraps
1648
+ // its payload in `{ data, pagination? }` — this helper unwraps it, so callers
1649
+ // get the inner `T` plus the pagination envelope when the route is paginated.
1650
+ // v1 (bare arrays, camelCase, `market=` param) retires 2026-10-24; see the
1651
+ // file-header comment (fleet #2713).
1652
+ interface V2Pagination { has_more?: boolean; limit?: number; next_cursor?: string | null; offset?: number }
1653
+ async function dataGetV2<T = unknown>(
1654
+ path: string,
1655
+ params: Record<string, string | number | boolean | undefined>,
1656
+ ): Promise<{ data: T; pagination?: V2Pagination }> {
1522
1657
  const url = new URL(DATA_API + path);
1523
- for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
1658
+ for (const [k, v] of Object.entries(params)) {
1659
+ if (v !== undefined) url.searchParams.set(k, String(v));
1660
+ }
1524
1661
  const res = await pwFetch(url.toString(), { headers: { Accept: 'application/json' } });
1525
1662
  if (!res.ok) {
1526
1663
  const text = await res.text();
1527
- throw new Error(`Polymarket Data API: ${res.status} ${text.slice(0, 200)}`);
1664
+ throw new Error(`Polymarket Data API v2: ${res.status} ${text.slice(0, 200)}`);
1528
1665
  }
1529
- return parseJson<T>(res, 'Polymarket');
1666
+ return parseJson<{ data: T; pagination?: V2Pagination }>(res, 'Polymarket');
1530
1667
  }
1531
1668
 
1532
1669
  // Map an outcome index (0/1/…) to its label via the market's outcomes array.
@@ -1541,6 +1678,34 @@ function shortWallet(w?: string): string | null {
1541
1678
  return w.length > 12 ? `${w.slice(0, 6)}…${w.slice(-4)}` : w;
1542
1679
  }
1543
1680
 
1681
+ // Polymarket proxy wallets are standard 20-byte EVM addresses. Validate the
1682
+ // shape only — we never resolve an address to a name (ENS or otherwise);
1683
+ // addresses are public on-chain identifiers, not personal data (fleet #2713).
1684
+ function requireWallet(args: Record<string, unknown>): string {
1685
+ const wallet = String(args.wallet ?? '').trim();
1686
+ if (!/^0x[0-9a-fA-F]{40}$/.test(wallet)) {
1687
+ throw new Error('wallet must be a 0x-prefixed 40-hex-char Polymarket proxy wallet address.');
1688
+ }
1689
+ return wallet.toLowerCase();
1690
+ }
1691
+
1692
+ // ISO date/datetime (or a bare epoch-seconds string) → epoch seconds for the
1693
+ // v2 `start`/`end` params. Returns undefined (omit the param) on anything
1694
+ // unparseable rather than silently sending `NaN`.
1695
+ function toEpochSeconds(value: unknown): number | undefined {
1696
+ if (value === undefined || value === null || value === '') return undefined;
1697
+ const str = String(value).trim();
1698
+ if (/^\d+$/.test(str)) return Number(str);
1699
+ const ms = Date.parse(str);
1700
+ return Number.isNaN(ms) ? undefined : Math.floor(ms / 1000);
1701
+ }
1702
+
1703
+ function isoOrNull(epochSeconds: number | string | null | undefined): string | null {
1704
+ if (epochSeconds === null || epochSeconds === undefined || epochSeconds === '') return null;
1705
+ const n = Number(epochSeconds);
1706
+ return Number.isFinite(n) ? new Date(n * 1000).toISOString() : null;
1707
+ }
1708
+
1544
1709
  async function polymarketTrades(args: Record<string, unknown>) {
1545
1710
  const slugOrId = String(args.slug_or_id ?? '').trim();
1546
1711
  if (!slugOrId) throw new Error('slug_or_id is required.');
@@ -1550,10 +1715,10 @@ async function polymarketTrades(args: Record<string, unknown>) {
1550
1715
  if (!market.conditionId) return { error: 'no_condition_id', message: 'Market has no conditionId — trades unavailable.' };
1551
1716
 
1552
1717
  type RawTrade = {
1553
- proxyWallet?: string; name?: string; side?: string; size?: number; price?: number;
1554
- timestamp?: number; outcome?: string; outcomeIndex?: number;
1718
+ proxy_wallet?: string; name?: string; side?: string; size?: number; price?: number;
1719
+ timestamp?: number; outcome?: string; outcome_index?: number;
1555
1720
  };
1556
- const trades = await dataGet<RawTrade[]>('/trades', { market: market.conditionId, limit });
1721
+ const { data: trades } = await dataGetV2<RawTrade[]>('/v2/trades', { condition: market.conditionId, limit });
1557
1722
 
1558
1723
  return {
1559
1724
  market_id: market.id,
@@ -1562,12 +1727,12 @@ async function polymarketTrades(args: Record<string, unknown>) {
1562
1727
  trade_count: trades.length,
1563
1728
  trades: trades.map((t) => ({
1564
1729
  side: t.side ?? null,
1565
- outcome: t.outcome ?? outcomeLabel(market, t.outcomeIndex),
1730
+ outcome: t.outcome ?? outcomeLabel(market, t.outcome_index),
1566
1731
  size: t.size ?? null,
1567
1732
  price: t.price ?? null,
1568
1733
  usd_value: t.size != null && t.price != null ? Math.round(t.size * t.price * 100) / 100 : null,
1569
1734
  timestamp: t.timestamp ? new Date(t.timestamp * 1000).toISOString() : null,
1570
- trader: t.name || shortWallet(t.proxyWallet),
1735
+ trader: t.name || shortWallet(t.proxy_wallet),
1571
1736
  })),
1572
1737
  };
1573
1738
  }
@@ -1580,22 +1745,22 @@ async function polymarketHolders(args: Record<string, unknown>) {
1580
1745
  if (!market) return { error: 'not_found', message: `No market matching "${slugOrId}".` };
1581
1746
  if (!market.conditionId) return { error: 'no_condition_id', message: 'Market has no conditionId — holders unavailable.' };
1582
1747
 
1583
- type RawHolder = { proxyWallet?: string; pseudonym?: string; name?: string; amount?: number; outcomeIndex?: number };
1584
- type RawHolderToken = { token?: string; holders?: RawHolder[] };
1585
- const data = await dataGet<RawHolderToken[]>('/holders', { market: market.conditionId, limit });
1748
+ type RawHolder = { proxy_wallet?: string; pseudonym?: string; name?: string; amount?: number; outcome_index?: number };
1749
+ type RawHolderToken = { token_id?: string; holders?: RawHolder[] };
1750
+ const { data } = await dataGetV2<RawHolderToken[]>('/v2/holders', { condition: market.conditionId, limit });
1586
1751
 
1587
1752
  return {
1588
1753
  market_id: market.id,
1589
1754
  market_slug: market.slug,
1590
1755
  question: market.question,
1591
1756
  outcomes: (data ?? []).map((grp) => {
1592
- const idx = grp.holders?.[0]?.outcomeIndex;
1757
+ const idx = grp.holders?.[0]?.outcome_index;
1593
1758
  return {
1594
- outcome: outcomeLabel(market, idx) ?? `token ${grp.token?.slice(0, 8)}…`,
1595
- token_id: grp.token ?? null,
1759
+ outcome: outcomeLabel(market, idx) ?? `token ${grp.token_id?.slice(0, 8)}…`,
1760
+ token_id: grp.token_id ?? null,
1596
1761
  top_holders: (grp.holders ?? []).slice(0, limit).map((h) => ({
1597
- trader: h.pseudonym || h.name || shortWallet(h.proxyWallet),
1598
- wallet: shortWallet(h.proxyWallet),
1762
+ trader: h.pseudonym || h.name || shortWallet(h.proxy_wallet),
1763
+ wallet: shortWallet(h.proxy_wallet),
1599
1764
  shares: h.amount ?? null,
1600
1765
  })),
1601
1766
  };
@@ -1603,6 +1768,240 @@ async function polymarketHolders(args: Record<string, unknown>) {
1603
1768
  };
1604
1769
  }
1605
1770
 
1771
+ // ── Wallet analytics (fleet #2713) ────────────────────────────────────────
1772
+ // Four tools over Data API v2's per-wallet routes. Wallet addresses are
1773
+ // public on-chain identifiers; we validate the 0x-address shape and pass it
1774
+ // straight through — no ENS/name resolution anywhere in this section.
1775
+
1776
+ type RawPosition = {
1777
+ proxy_wallet: string; token_id: string; condition_id: string; title?: string; slug?: string;
1778
+ event_slug?: string; outcome?: string; outcome_index?: number; current_size?: number;
1779
+ avg_price?: number; entry_cost_usdc?: number; entry_fees_usdc?: number; total_cost_usdc?: number;
1780
+ current_price?: number; current_value?: number; total_size?: number; realized_pnl?: number;
1781
+ unrealized_pnl?: number; total_pnl?: number; percent_pnl?: number; percent_realized_pnl?: number;
1782
+ status?: string; redeemable?: boolean; mergeable?: boolean; negative_risk?: boolean;
1783
+ end_date?: string; last_event_at?: number; first_entry_at?: number;
1784
+ };
1785
+
1786
+ function shapePosition(p: RawPosition) {
1787
+ return {
1788
+ market_slug: p.slug ?? null,
1789
+ event_slug: p.event_slug ?? null,
1790
+ question: p.title ?? null,
1791
+ condition_id: p.condition_id,
1792
+ outcome: p.outcome ?? null,
1793
+ status: p.status ?? null,
1794
+ current_size: p.current_size ?? null,
1795
+ avg_entry_price: p.avg_price ?? null,
1796
+ entry_cost_usdc: p.entry_cost_usdc ?? null,
1797
+ entry_fees_usdc: p.entry_fees_usdc ?? null,
1798
+ current_price: p.current_price ?? null,
1799
+ current_value_usdc: p.current_value ?? null,
1800
+ realized_pnl_usdc: p.realized_pnl ?? null,
1801
+ unrealized_pnl_usdc: p.unrealized_pnl ?? null,
1802
+ total_pnl_usdc: p.total_pnl ?? null,
1803
+ percent_pnl: p.percent_pnl ?? null,
1804
+ redeemable: p.redeemable ?? null,
1805
+ mergeable: p.mergeable ?? null,
1806
+ end_date: p.end_date ?? null,
1807
+ last_activity_at: isoOrNull(p.last_event_at ?? null),
1808
+ first_entry_at: isoOrNull(p.first_entry_at ?? null),
1809
+ };
1810
+ }
1811
+
1812
+ const POSITION_STATUSES = ['OPEN', 'REDEEMABLE', 'REDEEMABLE_LOST', 'MERGEABLE', 'CLOSED'];
1813
+
1814
+ async function polymarketWalletPositions(args: Record<string, unknown>) {
1815
+ const wallet = requireWallet(args);
1816
+ const limit = Math.min(500, Math.max(1, Number(args.limit ?? 50)));
1817
+ const cursor = args.cursor ? String(args.cursor) : undefined;
1818
+ const statusArg = args.status ? String(args.status).trim().toUpperCase() : undefined;
1819
+ if (statusArg && !POSITION_STATUSES.includes(statusArg)) {
1820
+ throw new Error(`Invalid status "${statusArg}". Valid: ${POSITION_STATUSES.join(' | ')}, or omit for OPEN+CLOSED.`);
1821
+ }
1822
+
1823
+ let rows: RawPosition[];
1824
+ let nextCursor: string | null = null;
1825
+ if (statusArg) {
1826
+ const { data, pagination } = await dataGetV2<RawPosition[]>('/v2/positions', { user: wallet, status: statusArg, limit, cursor });
1827
+ rows = data ?? [];
1828
+ nextCursor = pagination?.next_cursor ?? null;
1829
+ } else {
1830
+ // No status filter requested — the API defaults to the OPEN lifecycle
1831
+ // states on an unfiltered call, so fetch CLOSED explicitly and merge, to
1832
+ // actually deliver "open + closed" as one call. Pagination (`cursor`)
1833
+ // only applies cleanly within a single status, so a caller paging needs
1834
+ // to pass `status` explicitly from the second page on.
1835
+ const [openRes, closedRes] = await Promise.all([
1836
+ dataGetV2<RawPosition[]>('/v2/positions', { user: wallet, limit, cursor }),
1837
+ dataGetV2<RawPosition[]>('/v2/positions', { user: wallet, status: 'CLOSED', limit, cursor }),
1838
+ ]);
1839
+ rows = [...(openRes.data ?? []), ...(closedRes.data ?? [])];
1840
+ nextCursor = openRes.pagination?.next_cursor ?? closedRes.pagination?.next_cursor ?? null;
1841
+ }
1842
+
1843
+ const totalCurrentValue = rows.reduce((sum, p) => sum + (p.current_value ?? 0), 0);
1844
+ const totalRealized = rows.reduce((sum, p) => sum + (p.realized_pnl ?? 0), 0);
1845
+ const totalUnrealized = rows.reduce((sum, p) => sum + (p.unrealized_pnl ?? 0), 0);
1846
+ const largestValue = rows.reduce((max, p) => Math.max(max, p.current_value ?? 0), 0);
1847
+
1848
+ return {
1849
+ wallet,
1850
+ position_count: rows.length,
1851
+ total_current_value_usdc: Math.round(totalCurrentValue * 100) / 100,
1852
+ total_realized_pnl_usdc: Math.round(totalRealized * 100) / 100,
1853
+ total_unrealized_pnl_usdc: Math.round(totalUnrealized * 100) / 100,
1854
+ // Concentration, not skill: how much of current exposure sits in the
1855
+ // single largest position. A wallet all-in on one market is highly
1856
+ // concentrated whether that bet is working out or not.
1857
+ largest_position_share_of_exposure: totalCurrentValue > 0 ? Math.round((largestValue / totalCurrentValue) * 10000) / 10000 : null,
1858
+ positions: rows.map(shapePosition),
1859
+ next_cursor: nextCursor,
1860
+ data_as_of: new Date().toISOString(),
1861
+ };
1862
+ }
1863
+
1864
+ type RawActivity = {
1865
+ timestamp?: number; condition_id?: string; type?: string; size?: number; usdc_size?: number;
1866
+ price?: number; side?: string; outcome?: string; outcome_index?: number; title?: string;
1867
+ slug?: string; event_slug?: string; transaction_hash?: string;
1868
+ };
1869
+
1870
+ async function polymarketWalletActivity(args: Record<string, unknown>) {
1871
+ const wallet = requireWallet(args);
1872
+ const limit = Math.min(500, Math.max(1, Number(args.limit ?? 50)));
1873
+ const cursor = args.cursor ? String(args.cursor) : undefined;
1874
+ const type = args.type ? String(args.type).trim().toUpperCase() : undefined;
1875
+ const start = toEpochSeconds(args.start_date);
1876
+ const end = toEpochSeconds(args.end_date);
1877
+
1878
+ const { data, pagination } = await dataGetV2<RawActivity[]>('/v2/activity', {
1879
+ user: wallet, limit, cursor, type, start, end,
1880
+ });
1881
+ const rows = data ?? [];
1882
+
1883
+ return {
1884
+ wallet,
1885
+ activity_count: rows.length,
1886
+ activity: rows.map((a) => ({
1887
+ type: a.type ?? null,
1888
+ side: a.side || null,
1889
+ market_slug: a.slug || null,
1890
+ event_slug: a.event_slug || null,
1891
+ question: a.title || null,
1892
+ outcome: a.outcome || null,
1893
+ size: a.size ?? null,
1894
+ usdc_value: a.usdc_size ?? null,
1895
+ price: a.price ?? null,
1896
+ timestamp: a.timestamp ? new Date(a.timestamp * 1000).toISOString() : null,
1897
+ transaction_hash: a.transaction_hash || null,
1898
+ })),
1899
+ next_cursor: pagination?.next_cursor ?? null,
1900
+ data_as_of: new Date().toISOString(),
1901
+ };
1902
+ }
1903
+
1904
+ type RawUserPnlPoint = {
1905
+ timestamp?: number; realized_pnl?: number; unrealized_pnl?: number; fees?: number; fees_paid?: number;
1906
+ fees_refunded?: number; maker_rebate?: number; taker_rebate?: number; wallet_income?: number;
1907
+ settled_pnl?: number; economic_pnl?: number; trade_pnl?: number; volume?: number; volume_usdc?: number;
1908
+ trade_count?: number;
1909
+ };
1910
+ type RawUserStats = {
1911
+ proxy_wallet?: string; trades?: number; biggest_win?: number; views?: number; join_date?: number | null;
1912
+ all_time_pnl?: RawUserPnlPoint | null;
1913
+ };
1914
+
1915
+ const FEE_TREATMENT =
1916
+ 'realized_pnl at each point is already NET of taker trading fees paid (fees_paid is broken out separately for reference). ' +
1917
+ 'Maker rebates and other non-trading income (rewards, referrals, sponsorships) are reported in wallet_income and are ' +
1918
+ 'ADDITIVE on top — they are included in settled_pnl/economic_pnl but NOT in realized_pnl. unrealized_pnl marks open ' +
1919
+ 'positions to current price and is not yet realized or fee-adjusted.';
1920
+
1921
+ async function polymarketWalletPerformance(args: Record<string, unknown>) {
1922
+ const wallet = requireWallet(args);
1923
+ const interval = String(args.interval ?? 'max');
1924
+ const fidelity = args.fidelity ? String(args.fidelity) : undefined;
1925
+
1926
+ const [pnlRes, statsRes] = await Promise.all([
1927
+ dataGetV2<RawUserPnlPoint[] | { points?: RawUserPnlPoint[] }>('/v2/user-pnl', { user: wallet, interval, fidelity }),
1928
+ dataGetV2<RawUserStats | null>('/v2/user-stats', { user: wallet }),
1929
+ ]);
1930
+
1931
+ // /v2/user-pnl's `data` is the UserPnlSeries object ({ points: [...] }),
1932
+ // not a bare array — handle both shapes defensively in case that changes.
1933
+ const pnlData = pnlRes.data as { points?: RawUserPnlPoint[] } | RawUserPnlPoint[];
1934
+ const points: RawUserPnlPoint[] = Array.isArray(pnlData) ? pnlData : (pnlData?.points ?? []);
1935
+ const stats = statsRes.data;
1936
+
1937
+ return {
1938
+ wallet,
1939
+ profile: stats
1940
+ ? {
1941
+ distinct_markets_traded: stats.trades ?? null,
1942
+ biggest_win_usdc: stats.biggest_win ?? null,
1943
+ profile_views: stats.views ?? null,
1944
+ joined_at: isoOrNull(stats.join_date ?? null),
1945
+ }
1946
+ : null,
1947
+ fee_treatment: FEE_TREATMENT,
1948
+ pnl_history: points.map((p) => ({
1949
+ timestamp: p.timestamp ? new Date(p.timestamp * 1000).toISOString() : null,
1950
+ realized_pnl_usdc: p.realized_pnl ?? null,
1951
+ unrealized_pnl_usdc: p.unrealized_pnl ?? null,
1952
+ fees_paid_usdc: p.fees_paid ?? null,
1953
+ fees_refunded_usdc: p.fees_refunded ?? null,
1954
+ maker_rebate_usdc: p.maker_rebate ?? null,
1955
+ wallet_income_usdc: p.wallet_income ?? null,
1956
+ settled_pnl_usdc: p.settled_pnl ?? null,
1957
+ volume_usdc: p.volume_usdc ?? p.volume ?? null,
1958
+ trade_count: p.trade_count ?? null,
1959
+ })),
1960
+ data_as_of: new Date().toISOString(),
1961
+ };
1962
+ }
1963
+
1964
+ type RawResolution = {
1965
+ status?: string; was_disputed?: boolean; extended_review?: boolean; resolved_at?: string | null;
1966
+ expected_settlement_time?: string | null; payouts?: number[] | null; reporter?: string | null;
1967
+ resolution_source?: string | null; was_arbitrated?: boolean | null;
1968
+ };
1969
+
1970
+ async function polymarketResolutionStatus(args: Record<string, unknown>) {
1971
+ const slugOrId = String(args.slug_or_id ?? '').trim();
1972
+ if (!slugOrId) throw new Error('slug_or_id is required.');
1973
+ const market = await lookupMarket(slugOrId);
1974
+ if (!market) return { error: 'not_found', message: `No market matching "${slugOrId}".` };
1975
+ if (!market.conditionId) return { error: 'no_condition_id', message: 'Market has no conditionId — resolution status unavailable.' };
1976
+
1977
+ const { data } = await dataGetV2<RawResolution[]>('/v2/resolutions', { condition: market.conditionId });
1978
+ const res = data?.[0];
1979
+ if (!res) {
1980
+ return {
1981
+ market_slug: market.slug,
1982
+ question: market.question,
1983
+ status: 'unresolved',
1984
+ message: 'No resolution lifecycle row yet for this market\'s condition.',
1985
+ data_as_of: new Date().toISOString(),
1986
+ };
1987
+ }
1988
+
1989
+ return {
1990
+ market_slug: market.slug,
1991
+ question: market.question,
1992
+ status: res.status ?? null,
1993
+ was_disputed: res.was_disputed ?? false,
1994
+ extended_review: res.extended_review ?? false,
1995
+ resolved_at: res.resolved_at ?? null,
1996
+ expected_settlement_time: res.expected_settlement_time ?? null,
1997
+ payouts: res.payouts ?? null,
1998
+ reporter: res.reporter ?? null,
1999
+ resolution_source: res.resolution_source ?? null,
2000
+ was_arbitrated: res.was_arbitrated ?? null,
2001
+ data_as_of: new Date().toISOString(),
2002
+ };
2003
+ }
2004
+
1606
2005
  async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
1607
2006
  switch (name) {
1608
2007
  case 'polymarket_search':
@@ -1623,6 +2022,14 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
1623
2022
  return polymarketTrades(args);
1624
2023
  case 'polymarket_holders':
1625
2024
  return polymarketHolders(args);
2025
+ case 'polymarket_wallet_positions':
2026
+ return polymarketWalletPositions(args);
2027
+ case 'polymarket_wallet_activity':
2028
+ return polymarketWalletActivity(args);
2029
+ case 'polymarket_wallet_performance':
2030
+ return polymarketWalletPerformance(args);
2031
+ case 'polymarket_resolution_status':
2032
+ return polymarketResolutionStatus(args);
1626
2033
  default:
1627
2034
  throw new Error(`Unknown tool: ${name}`);
1628
2035
  }
package/src/server.ts CHANGED
@@ -9,7 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
9
9
  import pack from './index.js';
10
10
 
11
11
  const server = new Server(
12
- { name: '@pipeworx/mcp-polymarket', version: '0.1.2' },
12
+ { name: '@pipeworx/mcp-polymarket', version: '0.1.3' },
13
13
  { capabilities: { tools: {} } },
14
14
  );
15
15