@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 +7 -3
- package/package.json +4 -4
- package/server.json +2 -2
- package/src/index.ts +428 -21
- package/src/server.ts +1 -1
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
|
|
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
|
|
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.
|
|
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-
|
|
30
|
-
"sourceCommit": "
|
|
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.
|
|
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.
|
|
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
|
|
1520
|
-
// holder lists
|
|
1521
|
-
|
|
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))
|
|
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
|
-
|
|
1554
|
-
timestamp?: number; outcome?: string;
|
|
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
|
|
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.
|
|
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.
|
|
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 = {
|
|
1584
|
-
type RawHolderToken = {
|
|
1585
|
-
const data = await
|
|
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]?.
|
|
1757
|
+
const idx = grp.holders?.[0]?.outcome_index;
|
|
1593
1758
|
return {
|
|
1594
|
-
outcome: outcomeLabel(market, idx) ?? `token ${grp.
|
|
1595
|
-
token_id: grp.
|
|
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.
|
|
1598
|
-
wallet: shortWallet(h.
|
|
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.
|
|
12
|
+
{ name: '@pipeworx/mcp-polymarket', version: '0.1.3' },
|
|
13
13
|
{ capabilities: { tools: {} } },
|
|
14
14
|
);
|
|
15
15
|
|