@pipeworx/mcp-polymarket 0.1.1 → 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 1679+ 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 1679+ 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.1",
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-320d03ee5d1e634a807856531e2b51142c2e64ac535d4c41ff7b57af8708b81f",
30
- "sourceCommit": "00c836ddeaecbdd581dcfb0fb1842efea104006e"
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.1",
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
@@ -452,7 +452,42 @@ async function fetchWithTimeout(
452
452
  ),
453
453
  );
454
454
  }
455
- throw err;
455
+ // Fleet #2382. Everything that isn't a timeout/abort here is a genuine
456
+ // NETWORK-LEVEL failure — DNS resolution, connection refused, TLS handshake,
457
+ // Cloudflare's own "Network connection lost." — meaning `fetch()` itself
458
+ // threw and no HTTP response of any kind was ever received. Until this fix
459
+ // that raw exception was rethrown VERBATIM: a bare `TypeError: fetch failed`
460
+ // (or the Workers-runtime equivalent) names no upstream, carries no class
461
+ // token, and reads exactly like a defect in OUR code — because it says
462
+ // nothing about the call at all. It landed in `error`, the tier that means
463
+ // "Pipeworx has a defect", for every one of the (at the time of writing)
464
+ // ~470 packs that call this helper directly with no wrapper of their own.
465
+ //
466
+ // `dexscreener` hit this independently (fleet #1579) and fixed it with a
467
+ // bespoke per-pack try/catch around `fetchWithTimeout`. That fix is correct
468
+ // but only covers one pack; every other caller of this shared helper still
469
+ // leaked the raw exception. Moving the same fix HERE — the one place that
470
+ // already carries the timeout case — covers every pack that uses
471
+ // `fetchWithTimeout` without a wrapper, for free, and without widening
472
+ // `classifyToolError`'s regex list: the fix is giving the message a proper
473
+ // `upstream_down:` token at the point the two facts (no response was ever
474
+ // received, and which host we were trying to reach) are actually in hand,
475
+ // not teaching the classifier to guess from prose after the fact.
476
+ //
477
+ // Safe on the same grounds as the timeout branch above: no argument a
478
+ // caller passes can make `fetch()` itself throw a connection-level error,
479
+ // so this is always an availability failure, never a caller mistake. Same
480
+ // `markInternalOrigin` treatment — an origin we run that never answered is
481
+ // still ours, not a third party's outage.
482
+ const raw = err instanceof Error ? err.message : String(err);
483
+ throw new Error(
484
+ markInternalOrigin(
485
+ `upstream_down: could not reach ${name} at all (${raw.slice(0, 160)}). ` +
486
+ `No request reached ${name}, so this says NOTHING about whether the arguments you passed ` +
487
+ 'are valid — do not re-check them on the strength of this error. Retry shortly.',
488
+ url,
489
+ ),
490
+ );
456
491
  }
457
492
  }
458
493
 
@@ -636,11 +671,13 @@ function collapse(s: string): string {
636
671
  return s.replace(/\s+/g, ' ').trim();
637
672
  }
638
673
  /**
639
- * Polymarket MCP — prediction-market data via Gamma + CLOB public APIs.
674
+ * Polymarket MCP — prediction-market data via Gamma + CLOB + Data public APIs.
640
675
  *
641
676
  * Polymarket runs binary-outcome prediction markets on Polygon. The Gamma API
642
677
  * (gamma-api.polymarket.com) exposes market and event metadata. The CLOB API
643
- * (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.
644
681
  *
645
682
  * What agents typically want from this pack:
646
683
  * - "What does the market think about X?" → polymarket_search
@@ -648,8 +685,22 @@ function collapse(s: string): string {
648
685
  * - "Full detail / resolution criteria for one market" → polymarket_market
649
686
  * - "All markets within one event (e.g., 2028 election)" → polymarket_event
650
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
651
692
  *
652
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.
653
704
  */
654
705
 
655
706
 
@@ -858,6 +909,117 @@ const tools: McpToolExport['tools'] = [
858
909
  required: ['slug_or_id'],
859
910
  },
860
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
+ },
861
1023
  ];
862
1024
 
863
1025
  // ── Helpers ────────────────────────────────────────────────────────
@@ -1481,17 +1643,27 @@ async function polymarketEventBooks(args: Record<string, unknown>) {
1481
1643
  };
1482
1644
  }
1483
1645
 
1484
- // data-api.polymarket.com — public, no auth; serves the trades tape and
1485
- // holder lists keyed by conditionId (0x…).
1486
- 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 }> {
1487
1657
  const url = new URL(DATA_API + path);
1488
- 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
+ }
1489
1661
  const res = await pwFetch(url.toString(), { headers: { Accept: 'application/json' } });
1490
1662
  if (!res.ok) {
1491
1663
  const text = await res.text();
1492
- 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)}`);
1493
1665
  }
1494
- return parseJson<T>(res, 'Polymarket');
1666
+ return parseJson<{ data: T; pagination?: V2Pagination }>(res, 'Polymarket');
1495
1667
  }
1496
1668
 
1497
1669
  // Map an outcome index (0/1/…) to its label via the market's outcomes array.
@@ -1506,6 +1678,34 @@ function shortWallet(w?: string): string | null {
1506
1678
  return w.length > 12 ? `${w.slice(0, 6)}…${w.slice(-4)}` : w;
1507
1679
  }
1508
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
+
1509
1709
  async function polymarketTrades(args: Record<string, unknown>) {
1510
1710
  const slugOrId = String(args.slug_or_id ?? '').trim();
1511
1711
  if (!slugOrId) throw new Error('slug_or_id is required.');
@@ -1515,10 +1715,10 @@ async function polymarketTrades(args: Record<string, unknown>) {
1515
1715
  if (!market.conditionId) return { error: 'no_condition_id', message: 'Market has no conditionId — trades unavailable.' };
1516
1716
 
1517
1717
  type RawTrade = {
1518
- proxyWallet?: string; name?: string; side?: string; size?: number; price?: number;
1519
- 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;
1520
1720
  };
1521
- const trades = await dataGet<RawTrade[]>('/trades', { market: market.conditionId, limit });
1721
+ const { data: trades } = await dataGetV2<RawTrade[]>('/v2/trades', { condition: market.conditionId, limit });
1522
1722
 
1523
1723
  return {
1524
1724
  market_id: market.id,
@@ -1527,12 +1727,12 @@ async function polymarketTrades(args: Record<string, unknown>) {
1527
1727
  trade_count: trades.length,
1528
1728
  trades: trades.map((t) => ({
1529
1729
  side: t.side ?? null,
1530
- outcome: t.outcome ?? outcomeLabel(market, t.outcomeIndex),
1730
+ outcome: t.outcome ?? outcomeLabel(market, t.outcome_index),
1531
1731
  size: t.size ?? null,
1532
1732
  price: t.price ?? null,
1533
1733
  usd_value: t.size != null && t.price != null ? Math.round(t.size * t.price * 100) / 100 : null,
1534
1734
  timestamp: t.timestamp ? new Date(t.timestamp * 1000).toISOString() : null,
1535
- trader: t.name || shortWallet(t.proxyWallet),
1735
+ trader: t.name || shortWallet(t.proxy_wallet),
1536
1736
  })),
1537
1737
  };
1538
1738
  }
@@ -1545,22 +1745,22 @@ async function polymarketHolders(args: Record<string, unknown>) {
1545
1745
  if (!market) return { error: 'not_found', message: `No market matching "${slugOrId}".` };
1546
1746
  if (!market.conditionId) return { error: 'no_condition_id', message: 'Market has no conditionId — holders unavailable.' };
1547
1747
 
1548
- type RawHolder = { proxyWallet?: string; pseudonym?: string; name?: string; amount?: number; outcomeIndex?: number };
1549
- type RawHolderToken = { token?: string; holders?: RawHolder[] };
1550
- 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 });
1551
1751
 
1552
1752
  return {
1553
1753
  market_id: market.id,
1554
1754
  market_slug: market.slug,
1555
1755
  question: market.question,
1556
1756
  outcomes: (data ?? []).map((grp) => {
1557
- const idx = grp.holders?.[0]?.outcomeIndex;
1757
+ const idx = grp.holders?.[0]?.outcome_index;
1558
1758
  return {
1559
- outcome: outcomeLabel(market, idx) ?? `token ${grp.token?.slice(0, 8)}…`,
1560
- token_id: grp.token ?? null,
1759
+ outcome: outcomeLabel(market, idx) ?? `token ${grp.token_id?.slice(0, 8)}…`,
1760
+ token_id: grp.token_id ?? null,
1561
1761
  top_holders: (grp.holders ?? []).slice(0, limit).map((h) => ({
1562
- trader: h.pseudonym || h.name || shortWallet(h.proxyWallet),
1563
- wallet: shortWallet(h.proxyWallet),
1762
+ trader: h.pseudonym || h.name || shortWallet(h.proxy_wallet),
1763
+ wallet: shortWallet(h.proxy_wallet),
1564
1764
  shares: h.amount ?? null,
1565
1765
  })),
1566
1766
  };
@@ -1568,6 +1768,240 @@ async function polymarketHolders(args: Record<string, unknown>) {
1568
1768
  };
1569
1769
  }
1570
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
+
1571
2005
  async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
1572
2006
  switch (name) {
1573
2007
  case 'polymarket_search':
@@ -1588,6 +2022,14 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
1588
2022
  return polymarketTrades(args);
1589
2023
  case 'polymarket_holders':
1590
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);
1591
2033
  default:
1592
2034
  throw new Error(`Unknown tool: ${name}`);
1593
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.1' },
12
+ { name: '@pipeworx/mcp-polymarket', version: '0.1.3' },
13
13
  { capabilities: { tools: {} } },
14
14
  );
15
15