@c0pilot/mcp-polymarket 1.0.0
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 +159 -0
- package/build/client.d.ts +15 -0
- package/build/client.js +79 -0
- package/build/config.d.ts +24 -0
- package/build/config.js +55 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +26 -0
- package/build/tools/account.d.ts +3 -0
- package/build/tools/account.js +110 -0
- package/build/tools/index.d.ts +3 -0
- package/build/tools/index.js +15 -0
- package/build/tools/markets.d.ts +3 -0
- package/build/tools/markets.js +128 -0
- package/build/tools/orderbook.d.ts +3 -0
- package/build/tools/orderbook.js +49 -0
- package/build/tools/trading.d.ts +3 -0
- package/build/tools/trading.js +173 -0
- package/build/types.d.ts +51 -0
- package/build/types.js +1 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# mcp-polymarket
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server and client library for [Polymarket](https://polymarket.com) prediction markets.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/mcp-polymarket)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **MCP Server**: Run as a standalone MCP server for AI agents
|
|
10
|
+
- **Client Library**: Import and use in your own projects
|
|
11
|
+
- Browse and search prediction markets
|
|
12
|
+
- View order books and market prices
|
|
13
|
+
- Check wallet balance and positions
|
|
14
|
+
- Place and cancel orders
|
|
15
|
+
- Full integration with Polymarket's CLOB API
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install mcp-polymarket
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### As MCP Server
|
|
26
|
+
|
|
27
|
+
#### With Claude Desktop
|
|
28
|
+
|
|
29
|
+
Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"polymarket": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["mcp-polymarket"],
|
|
37
|
+
"env": {
|
|
38
|
+
"POLYMARKET_PRIVATE_KEY": "0x...",
|
|
39
|
+
"POLYMARKET_FUNDER": "0x..."
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Standalone
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export POLYMARKET_PRIVATE_KEY="0x..."
|
|
50
|
+
export POLYMARKET_FUNDER="0x..."
|
|
51
|
+
npx mcp-polymarket
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### As Library
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { ClobClientWrapper } from 'mcp-polymarket/client';
|
|
58
|
+
import { createConfig } from 'mcp-polymarket/config';
|
|
59
|
+
|
|
60
|
+
// Create config
|
|
61
|
+
const config = createConfig({
|
|
62
|
+
privateKey: '0x...',
|
|
63
|
+
funder: '0x...', // optional
|
|
64
|
+
readonly: false, // optional
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Initialize client
|
|
68
|
+
const client = new ClobClientWrapper(config);
|
|
69
|
+
await client.initialize();
|
|
70
|
+
|
|
71
|
+
// Use the client
|
|
72
|
+
const clobClient = client.getClient();
|
|
73
|
+
const orderbook = await clobClient.getOrderBook(tokenId);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
### Environment Variables
|
|
79
|
+
|
|
80
|
+
| Variable | Required | Default | Description |
|
|
81
|
+
|----------|----------|---------|-------------|
|
|
82
|
+
| `POLYMARKET_PRIVATE_KEY` | Yes | - | Wallet private key for signing |
|
|
83
|
+
| `POLYMARKET_FUNDER` | No | derived | Proxy wallet address |
|
|
84
|
+
| `POLYMARKET_API_KEY` | No | derived | API key (auto-derived if not set) |
|
|
85
|
+
| `POLYMARKET_API_SECRET` | No | derived | API secret (auto-derived if not set) |
|
|
86
|
+
| `POLYMARKET_PASSPHRASE` | No | derived | API passphrase (auto-derived if not set) |
|
|
87
|
+
| `POLYMARKET_CHAIN_ID` | No | 137 | Polygon mainnet |
|
|
88
|
+
| `POLYMARKET_READONLY` | No | false | Disable trading tools |
|
|
89
|
+
|
|
90
|
+
### Finding Your Funder Address
|
|
91
|
+
|
|
92
|
+
Your "funder" is your Polymarket proxy wallet - the address shown on polymarket.com when logged in. If you deposited through Polymarket's UI, funds are in this proxy wallet.
|
|
93
|
+
|
|
94
|
+
## Available MCP Tools
|
|
95
|
+
|
|
96
|
+
### Read-Only
|
|
97
|
+
|
|
98
|
+
| Tool | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `polymarket_get_markets` | List active prediction markets |
|
|
101
|
+
| `polymarket_get_market` | Get details for a specific market |
|
|
102
|
+
| `polymarket_get_orderbook` | View order book for a token |
|
|
103
|
+
| `polymarket_get_balance` | Check wallet USDC balance |
|
|
104
|
+
| `polymarket_get_positions` | View open orders and positions |
|
|
105
|
+
| `polymarket_get_trades` | Get recent trade history |
|
|
106
|
+
|
|
107
|
+
### Trading
|
|
108
|
+
|
|
109
|
+
| Tool | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `polymarket_place_order` | Place a limit order (BUY/SELL) |
|
|
112
|
+
| `polymarket_cancel_order` | Cancel an open order |
|
|
113
|
+
|
|
114
|
+
## API Exports
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Main MCP server entry
|
|
118
|
+
import mcp from 'mcp-polymarket';
|
|
119
|
+
|
|
120
|
+
// Client wrapper for Polymarket CLOB
|
|
121
|
+
import { ClobClientWrapper } from 'mcp-polymarket/client';
|
|
122
|
+
|
|
123
|
+
// Configuration utilities
|
|
124
|
+
import { createConfig, loadConfig, Config } from 'mcp-polymarket/config';
|
|
125
|
+
|
|
126
|
+
// Type definitions
|
|
127
|
+
import { MarketInfo, OrderbookInfo, Position } from 'mcp-polymarket/types';
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Security
|
|
131
|
+
|
|
132
|
+
- Private keys are never logged
|
|
133
|
+
- Use `POLYMARKET_READONLY=true` for safe exploration
|
|
134
|
+
- API credentials auto-derived from private key
|
|
135
|
+
- Input validation on all parameters
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Install dependencies
|
|
141
|
+
npm install
|
|
142
|
+
|
|
143
|
+
# Build
|
|
144
|
+
npm run build
|
|
145
|
+
|
|
146
|
+
# Run tests
|
|
147
|
+
npm test
|
|
148
|
+
|
|
149
|
+
# Run E2E tests (requires env vars)
|
|
150
|
+
npm run test:e2e
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Related
|
|
154
|
+
|
|
155
|
+
- [@openclaw/polymarket](https://github.com/unsanction/openclaw-polymarket) - OpenClaw plugin using this library
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ClobClient } from "@polymarket/clob-client";
|
|
2
|
+
import { Config } from "./config.js";
|
|
3
|
+
export declare class ClobClientWrapper {
|
|
4
|
+
private client;
|
|
5
|
+
private config;
|
|
6
|
+
constructor(config?: Config);
|
|
7
|
+
initialize(): Promise<void>;
|
|
8
|
+
getClient(): ClobClient;
|
|
9
|
+
isReadonly(): boolean;
|
|
10
|
+
ensureWriteAccess(): void;
|
|
11
|
+
getGammaApiUrl(): string;
|
|
12
|
+
getClobApiUrl(): string;
|
|
13
|
+
getFunder(): string;
|
|
14
|
+
}
|
|
15
|
+
export declare function getClientWrapper(): Promise<ClobClientWrapper>;
|
package/build/client.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ClobClient } from "@polymarket/clob-client";
|
|
2
|
+
import { Wallet } from "ethers";
|
|
3
|
+
import { getConfig } from "./config.js";
|
|
4
|
+
const CLOB_API_URL = "https://clob.polymarket.com";
|
|
5
|
+
const GAMMA_API_URL = "https://gamma-api.polymarket.com";
|
|
6
|
+
export class ClobClientWrapper {
|
|
7
|
+
client = null;
|
|
8
|
+
config;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config || getConfig();
|
|
11
|
+
}
|
|
12
|
+
async initialize() {
|
|
13
|
+
// Skip if already initialized
|
|
14
|
+
if (this.client) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Cast to any to handle ethers version mismatch between our ethers and @polymarket/clob-client's ethers
|
|
18
|
+
const wallet = new Wallet(this.config.privateKey);
|
|
19
|
+
const funder = this.config.funder;
|
|
20
|
+
// Signature type depends on wallet setup:
|
|
21
|
+
// 0 = Direct EOA (when funder == signer)
|
|
22
|
+
// 1 = Magic/Privy (email wallet)
|
|
23
|
+
// 2 = Browser wallet proxy/GnosisSafe (when funder != signer)
|
|
24
|
+
const signatureType = funder.toLowerCase() === wallet.address.toLowerCase() ? 0 : 2;
|
|
25
|
+
if (this.config.apiKey && this.config.apiSecret && this.config.passphrase) {
|
|
26
|
+
// Use provided API credentials
|
|
27
|
+
this.client = new ClobClient(CLOB_API_URL, this.config.chainId, wallet, {
|
|
28
|
+
key: this.config.apiKey,
|
|
29
|
+
secret: this.config.apiSecret,
|
|
30
|
+
passphrase: this.config.passphrase,
|
|
31
|
+
}, signatureType, funder);
|
|
32
|
+
// Initialized with provided API credentials
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Create client with funder to properly derive API credentials
|
|
36
|
+
const tempClient = new ClobClient(CLOB_API_URL, this.config.chainId, wallet, undefined, signatureType, funder);
|
|
37
|
+
try {
|
|
38
|
+
// Try to derive or create API credentials
|
|
39
|
+
const creds = await tempClient.createOrDeriveApiKey();
|
|
40
|
+
this.client = new ClobClient(CLOB_API_URL, this.config.chainId, wallet, creds, signatureType, funder);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Failed to derive API credentials, using unauthenticated client
|
|
44
|
+
this.client = tempClient;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
getClient() {
|
|
49
|
+
if (!this.client) {
|
|
50
|
+
throw new Error("Client not initialized. Call initialize() first.");
|
|
51
|
+
}
|
|
52
|
+
return this.client;
|
|
53
|
+
}
|
|
54
|
+
isReadonly() {
|
|
55
|
+
return this.config.readonly;
|
|
56
|
+
}
|
|
57
|
+
ensureWriteAccess() {
|
|
58
|
+
if (this.config.readonly) {
|
|
59
|
+
throw new Error("Trading is disabled in readonly mode. Set POLYMARKET_READONLY=false to enable trading.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
getGammaApiUrl() {
|
|
63
|
+
return GAMMA_API_URL;
|
|
64
|
+
}
|
|
65
|
+
getClobApiUrl() {
|
|
66
|
+
return CLOB_API_URL;
|
|
67
|
+
}
|
|
68
|
+
getFunder() {
|
|
69
|
+
return this.config.funder;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let clientWrapper = null;
|
|
73
|
+
export async function getClientWrapper() {
|
|
74
|
+
if (!clientWrapper) {
|
|
75
|
+
clientWrapper = new ClobClientWrapper();
|
|
76
|
+
await clientWrapper.initialize();
|
|
77
|
+
}
|
|
78
|
+
return clientWrapper;
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
privateKey: string;
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
apiSecret?: string;
|
|
5
|
+
passphrase?: string;
|
|
6
|
+
funder: string;
|
|
7
|
+
chainId: number;
|
|
8
|
+
readonly: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function deriveAddressFromPrivateKey(privateKey: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Create a Config from an object (useful for plugins that pass config directly)
|
|
13
|
+
*/
|
|
14
|
+
export declare function createConfig(options: {
|
|
15
|
+
privateKey: string;
|
|
16
|
+
funder?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
apiSecret?: string;
|
|
19
|
+
passphrase?: string;
|
|
20
|
+
chainId?: number;
|
|
21
|
+
readonly?: boolean;
|
|
22
|
+
}): Config;
|
|
23
|
+
export declare function loadConfig(): Config;
|
|
24
|
+
export declare function getConfig(): Config;
|
package/build/config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Wallet } from "ethers";
|
|
2
|
+
function getEnvVar(name, required = false) {
|
|
3
|
+
const value = process.env[name];
|
|
4
|
+
if (required && !value) {
|
|
5
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
export function deriveAddressFromPrivateKey(privateKey) {
|
|
10
|
+
const wallet = new Wallet(privateKey);
|
|
11
|
+
return wallet.address;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a Config from an object (useful for plugins that pass config directly)
|
|
15
|
+
*/
|
|
16
|
+
export function createConfig(options) {
|
|
17
|
+
const privateKey = options.privateKey;
|
|
18
|
+
if (!privateKey) {
|
|
19
|
+
throw new Error("privateKey is required");
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
privateKey,
|
|
23
|
+
funder: options.funder || deriveAddressFromPrivateKey(privateKey),
|
|
24
|
+
apiKey: options.apiKey,
|
|
25
|
+
apiSecret: options.apiSecret,
|
|
26
|
+
passphrase: options.passphrase,
|
|
27
|
+
chainId: options.chainId || 137,
|
|
28
|
+
readonly: options.readonly || false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
const privateKey = getEnvVar("POLYMARKET_PRIVATE_KEY", true);
|
|
33
|
+
const funder = getEnvVar("POLYMARKET_FUNDER") || deriveAddressFromPrivateKey(privateKey);
|
|
34
|
+
const chainId = parseInt(getEnvVar("POLYMARKET_CHAIN_ID") || "137", 10);
|
|
35
|
+
const readonly = getEnvVar("POLYMARKET_READONLY")?.toLowerCase() === "true";
|
|
36
|
+
const apiKey = getEnvVar("POLYMARKET_API_KEY");
|
|
37
|
+
const apiSecret = getEnvVar("POLYMARKET_API_SECRET");
|
|
38
|
+
const passphrase = getEnvVar("POLYMARKET_PASSPHRASE");
|
|
39
|
+
return {
|
|
40
|
+
privateKey,
|
|
41
|
+
apiKey,
|
|
42
|
+
apiSecret,
|
|
43
|
+
passphrase,
|
|
44
|
+
funder,
|
|
45
|
+
chainId,
|
|
46
|
+
readonly,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
let cachedConfig = null;
|
|
50
|
+
export function getConfig() {
|
|
51
|
+
if (!cachedConfig) {
|
|
52
|
+
cachedConfig = loadConfig();
|
|
53
|
+
}
|
|
54
|
+
return cachedConfig;
|
|
55
|
+
}
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { getClientWrapper } from "./client.js";
|
|
5
|
+
import { registerAllTools } from "./tools/index.js";
|
|
6
|
+
async function main() {
|
|
7
|
+
console.error("Starting Polymarket MCP Server...");
|
|
8
|
+
// Initialize the CLOB client
|
|
9
|
+
const clientWrapper = await getClientWrapper();
|
|
10
|
+
console.error("CLOB client initialized");
|
|
11
|
+
// Create MCP server
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "polymarket",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
});
|
|
16
|
+
// Register all tools
|
|
17
|
+
registerAllTools(server, clientWrapper);
|
|
18
|
+
// Connect via stdio transport
|
|
19
|
+
const transport = new StdioServerTransport();
|
|
20
|
+
await server.connect(transport);
|
|
21
|
+
console.error("Polymarket MCP Server running on stdio");
|
|
22
|
+
}
|
|
23
|
+
main().catch((error) => {
|
|
24
|
+
console.error("Fatal error:", error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AssetType } from "@polymarket/clob-client";
|
|
3
|
+
const GetBalanceSchema = z.object({});
|
|
4
|
+
const GetPositionsSchema = z.object({});
|
|
5
|
+
export function registerAccountTools(server, clientWrapper) {
|
|
6
|
+
server.tool("polymarket_get_balance", "Get the USDC balance and allowance for the configured wallet on Polymarket.", GetBalanceSchema.shape, async () => {
|
|
7
|
+
try {
|
|
8
|
+
const client = clientWrapper.getClient();
|
|
9
|
+
const funder = clientWrapper.getFunder();
|
|
10
|
+
// Get balance allowance for USDC collateral
|
|
11
|
+
const balanceData = await client.getBalanceAllowance({
|
|
12
|
+
asset_type: AssetType.COLLATERAL,
|
|
13
|
+
});
|
|
14
|
+
const result = {
|
|
15
|
+
balance: balanceData.balance || "0",
|
|
16
|
+
allowance: balanceData.allowance || "0",
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: "text",
|
|
22
|
+
text: JSON.stringify({
|
|
23
|
+
address: funder,
|
|
24
|
+
...result,
|
|
25
|
+
}, null, 2),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: `Error fetching balance: ${message}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
server.tool("polymarket_get_positions", "Get all open orders and positions for the configured wallet, including P&L calculations.", GetPositionsSchema.shape, async () => {
|
|
44
|
+
try {
|
|
45
|
+
const client = clientWrapper.getClient();
|
|
46
|
+
// Get open orders to derive positions
|
|
47
|
+
const openOrders = (await client.getOpenOrders());
|
|
48
|
+
if (!openOrders || openOrders.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: JSON.stringify({ positions: [], open_orders: [] }, null, 2),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Group orders by token to calculate positions
|
|
59
|
+
const positionMap = new Map();
|
|
60
|
+
for (const order of openOrders) {
|
|
61
|
+
const tokenId = order.asset_id;
|
|
62
|
+
const existing = positionMap.get(tokenId);
|
|
63
|
+
if (!existing) {
|
|
64
|
+
positionMap.set(tokenId, {
|
|
65
|
+
token_id: tokenId,
|
|
66
|
+
market: order.market || "Unknown",
|
|
67
|
+
outcome: order.outcome || "Unknown",
|
|
68
|
+
size: order.original_size,
|
|
69
|
+
avg_price: order.price,
|
|
70
|
+
current_price: order.price,
|
|
71
|
+
pnl: "0",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const positions = Array.from(positionMap.values());
|
|
76
|
+
// Also return raw open orders for transparency
|
|
77
|
+
const formattedOrders = openOrders.map((o) => ({
|
|
78
|
+
id: o.id,
|
|
79
|
+
token_id: o.asset_id,
|
|
80
|
+
side: o.side,
|
|
81
|
+
price: o.price,
|
|
82
|
+
size: o.original_size,
|
|
83
|
+
filled: o.size_matched,
|
|
84
|
+
}));
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: JSON.stringify({
|
|
90
|
+
positions,
|
|
91
|
+
open_orders: formattedOrders,
|
|
92
|
+
}, null, 2),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: `Error fetching positions: ${message}`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registerMarketTools } from "./markets.js";
|
|
2
|
+
import { registerOrderbookTools } from "./orderbook.js";
|
|
3
|
+
import { registerAccountTools } from "./account.js";
|
|
4
|
+
import { registerTradingTools } from "./trading.js";
|
|
5
|
+
export function registerAllTools(server, clientWrapper) {
|
|
6
|
+
const isReadonly = clientWrapper.isReadonly();
|
|
7
|
+
console.error(`Registering tools (readonly: ${isReadonly})`);
|
|
8
|
+
// Register read-only tools
|
|
9
|
+
registerMarketTools(server, clientWrapper);
|
|
10
|
+
registerOrderbookTools(server, clientWrapper);
|
|
11
|
+
registerAccountTools(server, clientWrapper);
|
|
12
|
+
// Register trading tools (write tools are conditionally included)
|
|
13
|
+
registerTradingTools(server, clientWrapper, !isReadonly);
|
|
14
|
+
console.error("All tools registered successfully");
|
|
15
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const GetMarketsSchema = z.object({
|
|
3
|
+
limit: z.number().min(1).max(100).optional().default(10),
|
|
4
|
+
offset: z.number().min(0).optional().default(0),
|
|
5
|
+
search: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
const GetMarketSchema = z.object({
|
|
8
|
+
condition_id: z.string().min(1),
|
|
9
|
+
});
|
|
10
|
+
async function fetchGammaMarkets(clientWrapper, limit, offset, search) {
|
|
11
|
+
const baseUrl = clientWrapper.getGammaApiUrl();
|
|
12
|
+
const params = new URLSearchParams({
|
|
13
|
+
limit: limit.toString(),
|
|
14
|
+
offset: offset.toString(),
|
|
15
|
+
active: "true",
|
|
16
|
+
});
|
|
17
|
+
if (search) {
|
|
18
|
+
params.set("slug_contains", search.toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
const url = `${baseUrl}/markets?${params.toString()}`;
|
|
21
|
+
const response = await fetch(url);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Failed to fetch markets: ${response.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
return Array.isArray(data) ? data : [];
|
|
27
|
+
}
|
|
28
|
+
async function fetchGammaMarket(clientWrapper, conditionId) {
|
|
29
|
+
const baseUrl = clientWrapper.getGammaApiUrl();
|
|
30
|
+
const url = `${baseUrl}/markets/${conditionId}`;
|
|
31
|
+
const response = await fetch(url);
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
if (response.status === 404) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Failed to fetch market: ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
return await response.json();
|
|
39
|
+
}
|
|
40
|
+
function formatMarket(market) {
|
|
41
|
+
const tokens = [];
|
|
42
|
+
const outcomes = market.outcomes || ["Yes", "No"];
|
|
43
|
+
const prices = market.outcomePrices || [];
|
|
44
|
+
const tokenIds = market.clobTokenIds || [];
|
|
45
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
46
|
+
tokens.push({
|
|
47
|
+
token_id: tokenIds[i] || "",
|
|
48
|
+
outcome: outcomes[i],
|
|
49
|
+
price: prices[i] ? parseFloat(prices[i]) : 0,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
condition_id: market.conditionId,
|
|
54
|
+
question: market.question,
|
|
55
|
+
tokens,
|
|
56
|
+
volume: market.volume || "0",
|
|
57
|
+
end_date: market.endDate || "",
|
|
58
|
+
active: market.active,
|
|
59
|
+
closed: market.closed,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function registerMarketTools(server, clientWrapper) {
|
|
63
|
+
server.tool("polymarket_get_markets", "List available prediction markets on Polymarket. Returns market question, current prices for Yes/No outcomes, and trading volume.", GetMarketsSchema.shape, async (args) => {
|
|
64
|
+
try {
|
|
65
|
+
const { limit, offset, search } = GetMarketsSchema.parse(args);
|
|
66
|
+
const markets = await fetchGammaMarkets(clientWrapper, limit, offset, search);
|
|
67
|
+
const formatted = markets.map(formatMarket);
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify(formatted, null, 2),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `Error fetching markets: ${message}`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
server.tool("polymarket_get_market", "Get detailed information about a specific prediction market including token IDs, current prices, and market status.", GetMarketSchema.shape, async (args) => {
|
|
91
|
+
try {
|
|
92
|
+
const { condition_id } = GetMarketSchema.parse(args);
|
|
93
|
+
const market = await fetchGammaMarket(clientWrapper, condition_id);
|
|
94
|
+
if (!market) {
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: `Market not found: ${condition_id}`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const formatted = formatMarket(market);
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: JSON.stringify(formatted, null, 2),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `Error fetching market: ${message}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
isError: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const GetOrderbookSchema = z.object({
|
|
3
|
+
token_id: z.string().min(1),
|
|
4
|
+
});
|
|
5
|
+
function formatOrderbook(tokenId, rawOrderbook) {
|
|
6
|
+
const formatEntries = (entries) => {
|
|
7
|
+
if (!entries)
|
|
8
|
+
return [];
|
|
9
|
+
return entries.map((e) => ({
|
|
10
|
+
price: e.price,
|
|
11
|
+
size: e.size,
|
|
12
|
+
}));
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
token_id: tokenId,
|
|
16
|
+
bids: formatEntries(rawOrderbook.bids),
|
|
17
|
+
asks: formatEntries(rawOrderbook.asks),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function registerOrderbookTools(server, clientWrapper) {
|
|
21
|
+
server.tool("polymarket_get_orderbook", "Get the order book for a specific token showing current bids and asks with prices and sizes.", GetOrderbookSchema.shape, async (args) => {
|
|
22
|
+
try {
|
|
23
|
+
const { token_id } = GetOrderbookSchema.parse(args);
|
|
24
|
+
const client = clientWrapper.getClient();
|
|
25
|
+
const orderbook = await client.getOrderBook(token_id);
|
|
26
|
+
const formatted = formatOrderbook(token_id, orderbook);
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: JSON.stringify(formatted, null, 2),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: `Error fetching orderbook: ${message}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Side as ClobSide } from "@polymarket/clob-client";
|
|
3
|
+
const PlaceOrderSchema = z.object({
|
|
4
|
+
token_id: z.string().min(1),
|
|
5
|
+
side: z.enum(["BUY", "SELL"]),
|
|
6
|
+
size: z.string().refine((val) => {
|
|
7
|
+
const num = parseFloat(val);
|
|
8
|
+
return !isNaN(num) && num > 0;
|
|
9
|
+
}, "Size must be a positive number"),
|
|
10
|
+
price: z.string().refine((val) => {
|
|
11
|
+
const num = parseFloat(val);
|
|
12
|
+
return !isNaN(num) && num > 0 && num < 1;
|
|
13
|
+
}, "Price must be between 0 and 1 (exclusive)"),
|
|
14
|
+
});
|
|
15
|
+
const CancelOrderSchema = z.object({
|
|
16
|
+
order_id: z.string().min(1),
|
|
17
|
+
});
|
|
18
|
+
const GetTradesSchema = z.object({
|
|
19
|
+
limit: z.number().min(1).max(100).optional().default(20),
|
|
20
|
+
});
|
|
21
|
+
async function getMarketInfoForToken(clientWrapper, tokenId) {
|
|
22
|
+
const client = clientWrapper.getClient();
|
|
23
|
+
try {
|
|
24
|
+
// The CLOB client should have a method to get market info
|
|
25
|
+
const marketInfo = (await client.getMarket(tokenId));
|
|
26
|
+
return {
|
|
27
|
+
tickSize: marketInfo.minimum_tick_size || 0.01,
|
|
28
|
+
negRisk: marketInfo.neg_risk || false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Default values if we can't fetch market info
|
|
33
|
+
return {
|
|
34
|
+
tickSize: 0.01,
|
|
35
|
+
negRisk: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function mapSide(side) {
|
|
40
|
+
return side === "BUY" ? ClobSide.BUY : ClobSide.SELL;
|
|
41
|
+
}
|
|
42
|
+
export function registerTradingTools(server, clientWrapper, includeWriteTools) {
|
|
43
|
+
// Get trades is always available (read-only)
|
|
44
|
+
server.tool("polymarket_get_trades", "Get recent executed trades for the configured wallet.", GetTradesSchema.shape, async (args) => {
|
|
45
|
+
try {
|
|
46
|
+
const { limit } = GetTradesSchema.parse(args);
|
|
47
|
+
const client = clientWrapper.getClient();
|
|
48
|
+
const allTrades = (await client.getTrades({}));
|
|
49
|
+
const trades = (allTrades || []).slice(0, limit);
|
|
50
|
+
const formatted = trades.map((t) => ({
|
|
51
|
+
id: t.id,
|
|
52
|
+
token_id: t.asset_id,
|
|
53
|
+
side: t.side,
|
|
54
|
+
price: t.price,
|
|
55
|
+
size: t.size,
|
|
56
|
+
timestamp: t.timestamp || t.match_time || "",
|
|
57
|
+
status: t.status,
|
|
58
|
+
}));
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: JSON.stringify(formatted, null, 2),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: `Error fetching trades: ${message}`,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// Only register write tools if not in readonly mode
|
|
82
|
+
if (!includeWriteTools) {
|
|
83
|
+
console.error("Readonly mode: trading tools (place_order, cancel_order) disabled");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
server.tool("polymarket_place_order", "Place a limit order on Polymarket. CAUTION: This executes a real trade with real funds. Price must be between 0 and 1, size in shares.", PlaceOrderSchema.shape, async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
clientWrapper.ensureWriteAccess();
|
|
89
|
+
const { token_id, side, size, price } = PlaceOrderSchema.parse(args);
|
|
90
|
+
const client = clientWrapper.getClient();
|
|
91
|
+
// Get market info for tick size and neg risk
|
|
92
|
+
const { tickSize, negRisk } = await getMarketInfoForToken(clientWrapper, token_id);
|
|
93
|
+
// Create the order
|
|
94
|
+
const orderArgs = {
|
|
95
|
+
tokenID: token_id,
|
|
96
|
+
side: mapSide(side),
|
|
97
|
+
size: parseFloat(size),
|
|
98
|
+
price: parseFloat(price),
|
|
99
|
+
feeRateBps: 0,
|
|
100
|
+
};
|
|
101
|
+
const signedOrder = await client.createOrder(orderArgs);
|
|
102
|
+
// Post the order
|
|
103
|
+
const response = (await client.postOrder(signedOrder));
|
|
104
|
+
const result = {
|
|
105
|
+
order_id: response.orderID || "",
|
|
106
|
+
status: response.status || "unknown",
|
|
107
|
+
message: response.errorMsg,
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: JSON.stringify({
|
|
114
|
+
...result,
|
|
115
|
+
order_details: {
|
|
116
|
+
token_id,
|
|
117
|
+
side,
|
|
118
|
+
size,
|
|
119
|
+
price,
|
|
120
|
+
tick_size: tickSize,
|
|
121
|
+
neg_risk: negRisk,
|
|
122
|
+
},
|
|
123
|
+
}, null, 2),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: `Error placing order: ${message}`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
server.tool("polymarket_cancel_order", "Cancel an existing order on Polymarket.", CancelOrderSchema.shape, async (args) => {
|
|
142
|
+
try {
|
|
143
|
+
clientWrapper.ensureWriteAccess();
|
|
144
|
+
const { order_id } = CancelOrderSchema.parse(args);
|
|
145
|
+
const client = clientWrapper.getClient();
|
|
146
|
+
const response = await client.cancelOrder({ orderID: order_id });
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: JSON.stringify({
|
|
152
|
+
order_id,
|
|
153
|
+
status: "cancelled",
|
|
154
|
+
response,
|
|
155
|
+
}, null, 2),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `Error cancelling order: ${message}`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
isError: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface TokenInfo {
|
|
2
|
+
token_id: string;
|
|
3
|
+
outcome: string;
|
|
4
|
+
price: number;
|
|
5
|
+
}
|
|
6
|
+
export interface MarketInfo {
|
|
7
|
+
condition_id: string;
|
|
8
|
+
question: string;
|
|
9
|
+
tokens: TokenInfo[];
|
|
10
|
+
volume: string;
|
|
11
|
+
end_date: string;
|
|
12
|
+
active: boolean;
|
|
13
|
+
closed: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface OrderbookEntry {
|
|
16
|
+
price: string;
|
|
17
|
+
size: string;
|
|
18
|
+
}
|
|
19
|
+
export interface OrderbookInfo {
|
|
20
|
+
bids: OrderbookEntry[];
|
|
21
|
+
asks: OrderbookEntry[];
|
|
22
|
+
token_id: string;
|
|
23
|
+
}
|
|
24
|
+
export interface Position {
|
|
25
|
+
token_id: string;
|
|
26
|
+
market: string;
|
|
27
|
+
outcome: string;
|
|
28
|
+
size: string;
|
|
29
|
+
avg_price: string;
|
|
30
|
+
current_price: string;
|
|
31
|
+
pnl: string;
|
|
32
|
+
}
|
|
33
|
+
export interface TradeInfo {
|
|
34
|
+
id: string;
|
|
35
|
+
token_id: string;
|
|
36
|
+
side: string;
|
|
37
|
+
price: string;
|
|
38
|
+
size: string;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
status: string;
|
|
41
|
+
}
|
|
42
|
+
export interface BalanceInfo {
|
|
43
|
+
balance: string;
|
|
44
|
+
allowance: string;
|
|
45
|
+
}
|
|
46
|
+
export interface OrderResult {
|
|
47
|
+
order_id: string;
|
|
48
|
+
status: string;
|
|
49
|
+
message?: string;
|
|
50
|
+
}
|
|
51
|
+
export type Side = "BUY" | "SELL";
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c0pilot/mcp-polymarket",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server and client library for Polymarket prediction markets - trade, browse markets, manage positions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./build/index.js",
|
|
7
|
+
"types": "./build/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./build/index.d.ts",
|
|
11
|
+
"default": "./build/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./build/client.d.ts",
|
|
15
|
+
"default": "./build/client.js"
|
|
16
|
+
},
|
|
17
|
+
"./config": {
|
|
18
|
+
"types": "./build/config.d.ts",
|
|
19
|
+
"default": "./build/config.js"
|
|
20
|
+
},
|
|
21
|
+
"./types": {
|
|
22
|
+
"types": "./build/types.d.ts",
|
|
23
|
+
"default": "./build/types.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"mcp-polymarket": "./build/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"build",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc && chmod 755 build/index.js",
|
|
35
|
+
"start": "node build/index.js",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:e2e": "vitest run e2e",
|
|
38
|
+
"clean": "rm -rf build",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"mcp",
|
|
43
|
+
"polymarket",
|
|
44
|
+
"prediction-markets",
|
|
45
|
+
"trading",
|
|
46
|
+
"crypto",
|
|
47
|
+
"clob",
|
|
48
|
+
"model-context-protocol"
|
|
49
|
+
],
|
|
50
|
+
"author": "unsanction",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/unsanction/mcp-polymarket"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/unsanction/mcp-polymarket#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/unsanction/mcp-polymarket/issues"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18.0.0"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
65
|
+
"@polymarket/clob-client": "^5.2.1",
|
|
66
|
+
"ethers": "^6.16.0",
|
|
67
|
+
"zod": "^3.22.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "^20.0.0",
|
|
71
|
+
"dotenv": "^17.2.3",
|
|
72
|
+
"typescript": "^5.0.0",
|
|
73
|
+
"vitest": "^4.0.18"
|
|
74
|
+
}
|
|
75
|
+
}
|