Skip to main content
Glama
README.md
# Polymarket Agent MCP

MCP server that lets a local AI client read Polymarket markets and, when you turn live mode on, place orders with your wallet. Version 1.7.0. Node.js 20 or newer. MIT license.

The process starts in **preview**. Preview writes simulated trades to a local SQLite file and does not sign or send orders. **Live** mode signs CLOB orders with `POLY_PRIVATE_KEY` and posts them to `https://clob.polymarket.com`. One process, one database, one wallet. It is a single-operator tool.

Stdio is the transport for Claude Code, Cursor, and other local MCP clients. HTTP is optional and should stay on a private network.

Capability details: [PERMISSIONS.md](PERMISSIONS.md) and [SECURITY.md](SECURITY.md).

<p align="center">
  <img src="./assets/agent-mcp-flow.png" width="100%" alt="Flow of the Polymarket Agent MCP: local client, 48 tools, market data, safety limits, preview mode, live CLOB orders, the copy loop, and private-deployment limits" />
</p>

## What a call does

1. The client invokes a tool. Inputs are checked with Zod.
2. Read tools call the public Polymarket APIs, or ESPN when the tool is `wta.discover`.
3. Trade tools run the safety checks, then the daily budget check.
4. In preview, the trade row is stored as `simulated`.
5. In live, `TradeExecutor` builds a CLOB client, derives API credentials from the signing key, and calls `createAndPostOrder`. The position row is stored as `executed` only after the CLOB accepts the order.

`config.go_live` flips the running process to live after `confirm: true`, and only if the live credentials are set. A restart returns to `COPY_MODE` from the environment.

## Install

```bash
npm install -g polymarket-agent-mcp
```

From this repository:

```bash
git clone https://github.com/polytutor-ai/polymarket-mcp-agent.git
cd polymarket-agent-mcp
npm install
npm run build
npm test
```

Claude Code, in `~/.claude/settings.json`:

```json
{
  "mcpServers": {
    "polymarket": {
      "command": "npx",
      "args": ["polymarket-agent-mcp"]
    }
  }
}
```

Copy `.env.example` to `.env` beside the package when you want a budget other than the defaults, or when you want live trading. The loader reads `.env` from the package root, not from the process working directory.

## Modes

| Mode | How it starts | Orders |
|------|----------------|--------|
| Preview | Default. `COPY_MODE=preview` | Simulated rows in SQLite |
| Live | `COPY_MODE=live`, or `config.go_live` with `confirm: true` | Signed CLOB orders |

Live startup requires all four of these to be non-empty:

```bash
POLY_PRIVATE_KEY=
POLY_API_KEY=
POLY_API_SECRET=
POLY_API_PASSPHRASE=
POLY_FUNDER_ADDRESS=
COPY_MODE=live
```

The executor signs with `POLY_PRIVATE_KEY` and then calls `createOrDeriveApiKey()`. The CLOB client is constructed with signature type `1` and `POLY_FUNDER_ADDRESS` as the funder, which matches a Polymarket proxy wallet. `CHAIN_ID` defaults to `137` (Polygon).

Keys stay in the process config. They are not written to SQLite. Error text that looks like a long hex key is redacted before it is logged. The designated order host is `clob.polymarket.com`.

## Copy monitor

`monitor.start` polls watched wallets on an interval (default 30 seconds, allowed range 10–300). Each tick reads recent BUY activity from `data-api.polymarket.com`. A trade is eligible when it is newer than the monitor’s max age and its USDC size is at least `MIN_CONVICTION` (default $3). The copy size is then limited by the remaining daily budget (`DAILY_BUDGET`, default $20).

`monitor.stop` clears that loop. The watchlist survives in SQLite.

`watchlist.rebalance` drops watched addresses that fail the performance rules in that tool. `traders.backtest` replays a wallet’s past trades locally and does not send orders.

## Safety limits

`config.safety_limits` stores limits in the `config` table. Defaults until you change them:

| Limit | Default |
|-------|---------|
| `max_order_size` | $50 |
| `max_exposure` | $200 |
| `max_per_market` | $100 |
| `min_liquidity` | $50 |
| `max_spread` | 0.10 |

`orders.buy` rejects a ticket that would exceed max order size, total recorded exposure, or the per-market total. Market-quality checks used by buy and the monitor look at spread, depth, and price range separately. Stop-loss and take-profit levels are stored with `positions.set_exit_rules` and evaluated when something calls `positions.check_exits`. They are not a background loop of their own.

## Network

| Host | When |
|------|------|
| `gamma-api.polymarket.com` | Market search, metadata, resolution |
| `data-api.polymarket.com` | Leaderboard, positions, wallet activity |
| `clob.polymarket.com` | Books, prices, and live order writes |
| `wss://ws-subscriptions-clob.polymarket.com/ws/market` | Public prices after `markets.watch_price` |
| `site.api.espn.com` | Today’s WTA scoreboard inside `wta.discover` |

HTTP reads use a 10 second timeout and up to two retries. The websocket sends a public market subscription. It does not send the wallet key. There is no analytics or update-check client in this package.

## HTTP transport

HTTP starts when the process is given `--http` or when `PORT` is set. The Docker image does both: `CMD` is `node dist/index.js --http` and `PORT=3000`.

```bash
node dist/index.js --http
PORT=8080 node dist/index.js
```

| Path | Role |
|------|------|
| `POST /mcp` | Streamable HTTP MCP |
| `GET /health` | `{ status, version, db }` |
| `GET /.well-known/mcp/server-card.json` | Discovery card |
| `GET /` | Name, version, and the paths above |

`MCP_API_KEY`, when set, requires `Authorization: Bearer <token>` on `/mcp`. When it is unset, `/mcp` accepts any caller who can open the port. The listen socket is all interfaces. Keep HTTP on localhost, or set `MCP_API_KEY` and do not publish the port.

```bash
docker build -t polymarket-mcp .
docker run -p 127.0.0.1:3000:3000 \
  -v mcp-data:/app/data \
  -e COPY_MODE=preview \
  -e DAILY_BUDGET=20 \
  -e MCP_API_KEY=replace-me \
  polymarket-mcp
```

A release image is published as `ghcr.io/demwick/polymarket-agent-mcp:1.7.0`.

`docker compose up` builds the local Dockerfile, mounts `./data` at `/app/data`, and sets preview mode. The Compose file does not publish port 3000. The container still runs the HTTP command from the Dockerfile. Add a `ports` mapping yourself if you want to reach it from the host.

SQLite path: `DB_PATH`, default `./copytrader.db`, or `/app/data/copytrader.db` in the image. The file holds the watchlist, trades, daily budget, safety config, exit rules, and agent-cycle notes. It does not hold the signing key.

## Configuration

| Variable | Default | Role |
|----------|---------|------|
| `COPY_MODE` | `preview` | `preview` or `live` |
| `DAILY_BUDGET` | `20` | USDC ceiling for the UTC day |
| `MIN_CONVICTION` | `3` | Smallest source trade, in USDC, that the monitor will copy |
| `POLY_PRIVATE_KEY` | empty | Signer. Required for live |
| `POLY_API_KEY` | empty | Required to pass the live credential check |
| `POLY_API_SECRET` | empty | Required to pass the live credential check |
| `POLY_API_PASSPHRASE` | empty | Required to pass the live credential check |
| `POLY_FUNDER_ADDRESS` | empty | Proxy funder on the CLOB client |
| `CHAIN_ID` | `137` | Chain id passed to the CLOB client |
| `MCP_API_KEY` | empty | Bearer token for `/mcp` |
| `DB_PATH` | `./copytrader.db` | SQLite file |
| `PORT` | unset | Setting it enables HTTP on that port |

`config.set` can change `daily_budget` and `min_conviction` in the database while the process is running. It cannot change the signing key or `COPY_MODE`.

## Tools

Forty-eight tools are registered in `src/index.ts`. Names below are the MCP tool ids.

### Markets

| Tool | Behavior |
|------|----------|
| `markets.search` | Keyword search |
| `markets.discover` | Markets by end date and category |
| `markets.trending` | Ranked by 24h, 7d, or 30d volume |
| `markets.featured` | Liquid markets in a category such as politics, sports, or crypto |
| `markets.related` | Markets related to a topic or condition |
| `markets.events` | Markets that belong to one event |
| `markets.price` | Bid, ask, mid, spread |
| `markets.price_history` | History and a text sparkline, from 1 hour to 1 month |
| `markets.check` | Spread, depth, and price-range quality |
| `markets.holders` | Largest holders |
| `markets.compare` | Two to five markets side by side |
| `markets.analyze` | Heuristic BUY, SELL, or HOLD from price, quality, and recent momentum. No external LLM |
| `markets.arbitrage` | YES plus NO prices that fail to sum near 1 |
| `markets.watch` | Saved markets and price-alert levels |
| `markets.watch_price` | Live CLOB websocket subscription |
| `wta.discover` | Today’s WTA matches from ESPN, matched to Polymarket slugs, with a suggested discount bid |
| `wta.bid` | Limit bid at that discount. Preview unless the process is live |
| `flow.discover` | Markets where several leaderboard wallets are buying |

### Traders and copy

| Tool | Behavior |
|------|----------|
| `traders.discover` | Leaderboard by PnL, volume, and ROI. Optional `auto_watch` |
| `traders.analyze` | Profile, win rate, and PnL for one address |
| `traders.score` | 0–100 score across the dimensions implemented in `score-trader` |
| `traders.positions` | Open positions for one address |
| `traders.backtest` | Local replay of copying that address |
| `watchlist.add` | Add or remove an address |
| `watchlist.list` | Current watchlist |
| `watchlist.rebalance` | Remove addresses that fail the rebalance rules |
| `monitor.start` | Start the copy loop |
| `monitor.stop` | Stop the copy loop |

### Orders and positions

| Tool | Behavior |
|------|----------|
| `orders.buy` | Buy outcome shares for a condition id. Minimum amount $0.50 |
| `orders.sell` | Sell an open position |
| `orders.batch` | Up to 10 orders in one call |
| `orders.cancel` | Cancel all open CLOB orders. Requires a live client |
| `orders.list` | Open orders |
| `orders.status` | One order by id |
| `positions.list` | Open and closed rows |
| `positions.close` | Close one local position |
| `positions.set_exit_rules` | Store stop-loss and take-profit prices |
| `positions.check_exits` | Evaluate those rules and market resolution |
| `config.go_live` | Switch this process to live. `confirm` must be true |

### Portfolio and config

| Tool | Behavior |
|------|----------|
| `portfolio.balance` | Collateral view, budget, and PnL |
| `portfolio.get` | Open positions grouped by watched wallet |
| `portfolio.risk` | Concentration and limit warnings from recorded trades |
| `portfolio.optimize` | Suggestions for `conservative`, `balanced`, or `aggressive` thresholds. Does not send orders |
| `dashboard.get` | Text dashboard |
| `trades.history` | Filtered trade log |
| `config.set` | `daily_budget` or `min_conviction` |
| `config.safety_limits` | Read or update the limits above |
| `agent.log_cycle` | Append a note to `agent_cycles` for the dashboard |

Two prompts are registered for clients that support them: `daily-trading-cycle` and `evaluate-trader`. They return a checklist of tool calls. They do not place orders by themselves.

## Layout

```
src/
  index.ts           # tool registration, stdio, HTTP
  constants.ts       # Polymarket base URLs
  tools/             # one module per tool
  services/          # executor, monitor, books, backtest, WTA
  db/                # SQLite schema and queries
  utils/             # config, safety, fetch, logging
```

Logs go to stderr so stdio MCP can keep stdout for JSON-RPC. The last 500 lines are also kept in memory and can be forwarded to the MCP client as logging messages.

## Scripts

```bash
npm run build          # tsc
npm start              # node dist/index.js
npm run start:http     # node dist/index.js --http
npm test               # vitest
npx tsc --noEmit       # typecheck; CI runs this before publish
```

`package.json` engines require Node.js `>=20`. `prepublishOnly` runs the TypeScript build. The only install scripts in the lockfile are `better-sqlite3` (native module) and optional macOS `fsevents`.