Initial commit: Polymarket skill (Gamma/Data/CLOB/Bridge/Relayer client + CLI)

Public market data via direct HTTP; authenticated CLOB trading via the official
py-clob-client-v2 SDK. Includes README, endpoint reference, and trading/auth guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
GoldPm 2026-06-22 16:07:29 +08:00
commit 1d4d3a22f4
7 changed files with 1187 additions and 0 deletions

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.eggs/
build/
dist/
# Virtual environments
.venv/
venv/
env/
ENV/
# Secrets — NEVER commit wallet keys or env files
.env
.env.*
*.key
*.pem
secrets*
*private*key*
# OS / editor
.DS_Store
.idea/
.vscode/
*.swp
# Packaged skill artifact
*.skill

123
README.md Normal file
View file

@ -0,0 +1,123 @@
# Polymarket Skill
一个让 Claude / 命令行操作 [Polymarket](https://polymarket.com) 的技能包:查询行情、事件、盘口、价格、持仓、成交、排行榜等公开数据,以及在 CLOB 上真实下单 / 撤单 / 查单。
A skill for driving the Polymarket APIs (Gamma, Data, CLOB, Bridge, Relayer) from
Python — read market data and place/cancel/query real orders.
---
## 安装 Install
从 git 仓库安装(把仓库克隆为技能目录):
```bash
git clone <仓库地址> ~/.claude/skills/polymarket
```
或双击预打包的 `polymarket.skill` 由 Claude 桌面端安装,或把目录放进 `~/.claude/skills/`
依赖:
```bash
pip install requests # 公开数据(行情/盘口/持仓/排行榜),无需账户
pip install py-clob-client-v2 # 仅真实交易时需要(官方 v2 SDK)
```
> macOS/Homebrew 若报 `externally-managed-environment`,用虚拟环境:
> `python3 -m venv .venv && source .venv/bin/activate`,激活后再 `pip install`
---
## 快速开始:查数据(无需任何配置)
```bash
cd scripts
python polymarket.py list-commands # 列出全部命令
python polymarket.py gamma.list_markets --json '{"limit":5,"closed":false}'
python polymarket.py gamma.search --q "world cup"
python polymarket.py clob.midpoint --token_id <TOKEN>
python polymarket.py clob.order_book --token_id <TOKEN>
python polymarket.py data.positions --user 0xADDR --json '{"limit":50}'
python polymarket.py data.leaderboard --json '{"limit":10}'
```
简单参数用 `--key value`;结构化参数(数字/布尔/多参数)用一个 `--json '{...}'`
**ID 概念**:`slug`(市场/事件的 URL 片段)、`condition_id`(标识一个市场)、
`token_id`(某个结果 YES/NO 的资产 id,盘口/价格/历史都按它查)。从
`gamma.list_markets` / `gamma.get_market` 返回的 `clobTokenIds` 拿 token_id。
Python 里用:
```python
import sys; sys.path.insert(0, "scripts")
from polymarket import GammaClient, DataClient, ClobPublicClient
g = GammaClient()
markets = g.list_markets(limit=10, closed=False, order="volume24hr", ascending=False)
book = ClobPublicClient().order_book(token_id="...")
```
---
## 真实交易(动用钱包私钥与真金白银)⚠️
每个人用自己的钱包。配置环境变量后从 Python 调用:
```bash
export PK=0x你的私钥
export POLY_PROXY_ADDRESS=0x你的交易账户地址 # 代理钱包持有 USDC 的地址
export POLY_SIGNATURE_TYPE=... # 0 / 1 / 2 / 3,见下
```
```python
import sys; sys.path.insert(0, "scripts")
from polymarket import TradingClient
t = TradingClient() # 首次构造自动派生 L2 API 凭证
t.get_balance_allowance() # 查 USDC 余额 + 授权
t.create_and_post_order(token_id="...", price=0.42, size=10, side="BUY", order_type="GTC")
t.create_and_post_market_order(token_id="...", amount=5, side="BUY") # amount=USDC金额
t.get_open_orders(asset_id="...")
t.cancel_all()
```
### 签名类型(每个账户不同,务必选对)
| 注册方式 | `signature_type` |
|---|---|
| 自有钱包直接充值的 EOA | `0` |
| 旧版 邮箱/Magic 登录 | `1` |
| 浏览器钱包(MetaMask 等) | `2` |
| 新版 邮箱/Google 内嵌钱包 | `3` |
**关键坑**:类型选错会让 `get_balance_allowance` 静默返回 `balance: 0`(查错了代理)。
若认证成功(凭证 + 签名地址正确)但余额是 0,**逐个试 `signature_type=1/2/3`**;当
`balance` 等于你网页上的现金、且三个 `allowances` 是一个很大的数(≈2^256-1)时即为正确类型。
新账户首次交易需先在网页端成交一笔以部署代理 + 设置授权。
详见 `references/trading.md`
---
## 文件结构
```
polymarket/
├── SKILL.md # 技能说明(给 Claude 读)+ 用法
├── README.md # 本文件
├── scripts/polymarket.py # 客户端 + CLI(78+ 公开命令 + 交易)
└── references/
├── endpoints.md # 每个端点 → 方法/路径/参数 全表
└── trading.md # 认证、签名类型、下单类型、安全
```
---
## 安全须知
- 私钥 = 钱包完全控制权。**只在自己的终端 `export`,绝不写入文件、不贴给任何人、不进 git。**
- 交易是真实、难以撤销的资金操作。先小额测试,量力而行。
- 本工具调用的是 Polymarket 内部 API,可能随其更新而变化;若某端点 404,见
`references/endpoints.md` 核对路径。
- 仅供学习与个人使用;遵守 Polymarket 的服务条款与你所在地区的法律法规。

134
SKILL.md Normal file
View file

@ -0,0 +1,134 @@
---
name: polymarket
description: >-
Interact with the Polymarket prediction-market APIs (Gamma, Data, CLOB,
Bridge, Relayer) from Python: browse and search Polymarket markets and events;
read prices, order books, midpoints, spreads and price history; look up a
wallet's positions, trades, activity, PnL, holders, leaderboard, open interest
and liquidity-mining rewards; and place, cancel or query real CLOB orders.
Trigger whenever the user wants Polymarket market data or wants to trade on
Polymarket, even without naming an endpoint — including Chinese requests like
查询/看 Polymarket 的行情、赔率、订单簿、持仓、成交、盈亏、排行榜、奖励,或下单/
挂单/撤单/交易; answer in Chinese when the user writes in Chinese. Specific to
Polymarket: do NOT use for other venues (Kalshi, DraftKings/sportsbooks,
Binance, Coinbase, Robinhood), for conceptual questions about how prediction
markets work or their legality, or for pure opinion/comparison of platforms.
---
# Polymarket API
A Python client (`scripts/polymarket.py`) covering the full Polymarket REST
surface. Public read endpoints use plain HTTP; authenticated **trading** is
delegated to the official `py_clob_client_v2` (the docs recommend it —
hand-rolling EIP-712 order signing risks real funds).
When the user writes in Chinese, answer in Chinese (tables, labels, and summary
in 中文); keep API field names, token ids, and command snippets as-is.
## The four+1 APIs
| API | Base URL | Auth | What it has |
|-----|----------|------|-------------|
| Gamma | gamma-api.polymarket.com | none | markets, events, tags, series, comments, sports, search, public profiles |
| Data | data-api.polymarket.com | none | positions, trades, activity, holders, value, leaderboard, open interest |
| CLOB (public) | clob.polymarket.com | none | order book, price, midpoint, spread, tick size, price history, markets |
| CLOB (trade) | clob.polymarket.com | L1+L2 | place / cancel / query orders, balances, API keys |
| Bridge / Relayer | bridge / relayer-v2 | varies | deposits, withdrawals, proxy-wallet tx |
## Setup
```bash
pip install requests # public endpoints
pip install py_clob_client_v2 # only needed for authenticated trading (official v2 SDK)
```
## Reading data (no auth) — use the CLI
The fastest path. Every public method is exposed as `<api>.<method>`:
```bash
cd scripts
python polymarket.py list-commands # see everything
python polymarket.py gamma.list_markets --json '{"limit":5,"closed":false}'
python polymarket.py gamma.search --q "election"
python polymarket.py gamma.get_event_by_slug --slug some-event-slug
python polymarket.py clob.order_book --token_id 7193... # full book
python polymarket.py clob.midpoint --token_id 7193...
python polymarket.py clob.prices_history --market 7193... --json '{"interval":"1d","fidelity":60}'
python polymarket.py data.positions --user 0xABC... --json '{"limit":50}'
python polymarket.py data.trades --json '{"user":"0xABC...","limit":20}'
python polymarket.py data.leaderboard --json '{"window":"7d","limit":10}'
```
Pass simple flags as `--key value`; pass anything structured (numbers, bools,
lists, multiple params) as one `--json '{...}'`. Both can be combined.
Key id concepts the user will hand you:
- **slug** — human URL fragment of an event/market.
- **condition_id** — identifies a market (a yes/no question) on-chain.
- **token_id (clob)** — the ERC1155 asset id of *one outcome* (YES or SELL side).
Order book / price / midpoint / price-history all key on token_id.
To go from a market to its token ids: `gamma.list_markets` / `gamma.get_market`
return `clobTokenIds`. Use those with the `clob.*` pricing calls.
## Reading data from Python
```python
import sys; sys.path.insert(0, "scripts")
from polymarket import GammaClient, DataClient, ClobPublicClient
g = GammaClient()
markets = g.list_markets(limit=10, closed=False, order="volume24hr", ascending=False)
book = ClobPublicClient().order_book(token_id="7193...")
pos = DataClient().positions(user="0xABC...", limit=100)
```
## Trading (authenticated — real money)
Trading needs the user's wallet private key, so it runs from Python with env
vars, never through the flat CLI. **Before placing any order, confirm with the
user the token_id, side, price, and size** — these move real funds and are not
easily reversible.
```bash
export PK=0xYOUR_PRIVATE_KEY
export POLY_PROXY_ADDRESS=0xYOUR_PROXY # if using a Polymarket proxy wallet
```
```python
from polymarket import TradingClient
t = TradingClient() # auto-derives L2 API creds on first run
# Limit order: buy 10 shares of outcome token at $0.42
t.create_and_post_order(token_id="7193...", price=0.42, size=10, side="BUY",
order_type="GTC")
# Market order by USDC amount
t.create_and_post_market_order(token_id="7193...", amount=20, side="BUY")
t.get_open_orders() # open orders
t.cancel_order(payload="0x...") # cancel one
t.cancel_all() # cancel everything
t.get_trades() # your fills
t.get_balance_allowance()
# L2-authenticated rewards / rebates (read-only, signed for you)
t.rewards_user_earnings(date="2026-06-22")
t.rebates_current(date="2026-06-22", maker_address="0x...")
```
`TradingClient` wraps the official **v2** SDK (`py_clob_client_v2`): L1 (EIP-712)
order signing and L2 (HMAC-SHA256 `POLY_*` headers) are handled automatically.
The user-scoped rewards/rebates GETs aren't exposed by the SDK, so the skill
signs those L2 requests itself. Set `POLY_SIGNATURE_TYPE` (0 EOA / 1 proxy /
2 gnosis-safe) if you hit balance/allowance errors. See `references/trading.md`.
## When you need an endpoint that isn't obvious
`references/endpoints.md` maps every documented Polymarket endpoint to its
client method and notes the exact query parameters. Read it when the user asks
for something specific (rewards, rebates, combo markets, builder analytics,
maker quotes, accounting snapshots) so you call the right method with the right
params. These tools hit **undocumented-stability** internal APIs and can change;
if a call 404s, check the live path in `references/endpoints.md` and the docs at
https://docs.polymarket.com/api-reference.

138
references/endpoints.md Normal file
View file

@ -0,0 +1,138 @@
# Polymarket endpoint catalog
Every documented endpoint mapped to a client method in `scripts/polymarket.py`.
Source of truth: https://docs.polymarket.com/api-reference (and `/llms.txt`).
Paths marked ⚠ are internal/best-effort — verify against the live docs if a call
fails, and adjust the method body in `polymarket.py`.
## Gamma API — `GammaClient` (gamma-api.polymarket.com)
| Capability | Method | Path | Key params |
|---|---|---|---|
| List markets | `list_markets()` | GET /markets | limit, offset, order, ascending, id, slug, clob_token_ids, condition_ids, tag_id, closed, liquidity_num_min/max, volume_num_min/max, start_date_min/max, end_date_min/max |
| Market by id | `get_market(id)` | GET /markets/{id} | |
| Market tags | `get_market_tags(id)` | GET /markets/{id}/tags | |
| List events | `list_events()` | GET /events | limit, offset, order, ascending, slug, tag_id, closed, active |
| Events (keyset) | `list_events_paginated()` | GET /events/pagination | |
| Event by id | `get_event(id)` | GET /events/{id} | |
| Event by slug | `get_event_by_slug(slug)` | GET /events/slug/{slug} | |
| Event tags | `get_event_tags(id)` | GET /events/{id}/tags | |
| List tags | `list_tags()` | GET /tags | limit, offset |
| Tag by id / slug | `get_tag(id)` / `get_tag_by_slug(slug)` | GET /tags/{id}, /tags/slug/{slug} | |
| Related tags | `get_related_tags_by_id/slug`, `get_tags_related_to_id/slug` | GET /tags/{id}/related-tags[/tags] | |
| List / get series | `list_series()` / `get_series(id)` | GET /series, /series/{id} | |
| List comments | `list_comments()` | GET /comments | limit, offset, parent_entity_type, parent_entity_id |
| Comments by id | `get_comments_by_id(id)` | GET /comments/{id} | |
| Comments by user | `get_comments_by_user(addr)` | GET /comments/user_address/{addr} | |
| Search | `search(q)` | GET /public-search | q, limit_per_type, events_status |
| Sports metadata | `get_sports_metadata()` | GET /sports | |
| Sports market types | `get_sports_market_types()` | GET /sports/market-types | |
| List teams | `list_teams()` | GET /teams | league, name |
| Public profile | `get_public_profile(addr)` | GET /public-profile/{addr} | |
## Data API — `DataClient` (data-api.polymarket.com)
| Capability | Method | Path | Key params |
|---|---|---|---|
| Current positions | `positions(user)` | GET /positions | user, market, eventId, sizeThreshold, redeemable, mergeable, limit(≤500), offset, sortBy, sortDirection, title |
| Closed positions | `closed_positions(user)` | GET /closed-positions | user, limit, offset |
| Positions in a market | `market_positions(market)` | GET /positions | market=conditionIds |
| Total value | `value(user)` | GET /value | user, market |
| User activity | `activity(user)` | GET /activity | user, limit, offset, market, type, side, start, end |
| Combo activity | `combo_activity(user)` | GET /combo-activity | user |
| Combo positions | `combo_positions(user)` | GET /combo-positions | user |
| Trades | `trades()` | GET /trades | user, market, limit, offset, takerOnly, side |
| Top holders | `holders(market)` | GET /holders | market, limit |
| Leaderboard | `leaderboard()` | GET /v1/leaderboard | category, timePeriod, orderBy(Pnl/Vol), limit(≤50), offset, user, userName |
| Markets traded count | `traded(user)` | GET /traded | user |
| Open interest | `open_interest()` | GET /oi | market |
| Live event volume | `live_volume_event(id)` | GET /live-volume | id |
| Builder leaderboard | `builder_leaderboard()` | GET /v1/builders/leaderboard | timePeriod(DAY/WEEK/MONTH/ALL), limit(≤50), offset |
| Builder volume series | `builder_volume()` | GET /v1/builders/volume | timePeriod |
| Accounting snapshot (ZIP) | `accounting_snapshot(user)` | GET /v1/accounting/snapshot | user — saves positions.csv+equity.csv to disk |
## CLOB public — `ClobPublicClient` (clob.polymarket.com)
| Capability | Method | Path |
|---|---|---|
| Order book (single) | `order_book(token_id)` | GET /book |
| Order books (batch) | `order_books([token_ids])` | POST /books |
| Price (one side) | `price(token_id, side)` | GET /price |
| Prices (batch) | `prices([{token_id,side}])` | POST /prices |
| Midpoint / midpoints | `midpoint`, `midpoints` | GET /midpoint, POST /midpoints |
| Spread / spreads | `spread`, `spreads` | GET /spread, POST /spreads |
| Last trade price(s) | `last_trade_price`, `last_trade_prices` | GET /last-trade-price, POST /last-trades-prices |
| Tick size | `tick_size(token_id)` | GET /tick-size |
| Fee rate | `fee_rate_bps()` | GET /fee-rate-bps |
| Price history | `prices_history(market)` | GET /prices-history — params: interval(1m/1h/6h/1d/1w/max), startTs, endTs, fidelity |
| Market by condition id | `get_market(condition_id)` | GET /markets/{condition_id} |
| List markets | `list_markets(next_cursor)` | GET /markets |
| Simplified markets | `simplified_markets(next_cursor)` | GET /simplified-markets |
| Sampling (reward) markets | `sampling_markets`, `sampling_simplified_markets` | GET /sampling-markets, /sampling-simplified-markets |
| Server time | `server_time()` | GET /time |
| Rewards: active configs | `rewards_markets_current()` | GET /rewards/markets/current — sponsored, next_cursor |
| Rewards: markets w/ rewards | `rewards_markets_multi()` | GET /rewards/markets/multi — q, tag_slug, event_id, order_by, min/max_volume_24hr, min/max_spread, min/max_price, page_size |
| Rewards: raw for a market | `rewards_market(condition_id)` | GET /rewards/markets/{condition_id} — sponsored |
| Builder-attributed trades | `builder_trades(builder_code)` | GET /builder/trades — id, market, asset_id, before, after |
## CLOB authenticated trade — `TradingClient` (via py-clob-client)
Backed by the official **v2** SDK `py_clob_client_v2`.
| Capability | Method | Notes |
|---|---|---|
| Create / derive API key | `create_or_derive_api_key(nonce=None)` | L1 EIP-712 |
| Post limit order | `create_and_post_order(token_id, price, size, side, order_type)` | order_type GTC/FOK/FAK |
| Post market order | `create_and_post_market_order(token_id, amount, side, order_type)` | FOK/FAK |
| Cancel one | `cancel_order(payload)` | order hash/id |
| Cancel many/all/market | `cancel_orders(hashes)`, `cancel_all()`, `cancel_market_orders(payload)` | |
| Get order | `get_order(order_id)` | L2 |
| Get open orders | `get_open_orders(market=, asset_id=, id=)` | L2; filters optional, else all pages. Fresh orders may lag indexing |
| Get trades | `get_trades(market=, asset_id=, maker_address=, before=, after=)` | L2 |
| Order scoring | `is_order_scoring(order_id)`, `are_orders_scoring(order_ids)` | reward eligibility |
| Balance/allowance | `get_balance_allowance(params)`, `update_balance_allowance(params)` | |
### L2-authenticated rewards / rebates (signed by the skill, not the SDK)
| Capability | Method | Path |
|---|---|---|
| User earnings by date | `rewards_user_earnings(date)` | GET /rewards/user |
| User total earnings by date | `rewards_user_total(date)` | GET /rewards/user/total |
| User reward percentages | `rewards_user_percentages()` | GET /rewards/user/percentages |
| User earnings + markets config | `rewards_user_markets()` | GET /rewards/user/markets |
| Maker rebated fees | `rebates_current(date, maker_address)` | GET /rebates/current |
All accept `signature_type`, `maker_address`, `sponsored`, `next_cursor` etc. as
kwargs. They use `_l2_get`, which builds the HMAC-SHA256 `POLY_*` headers from
the derived creds.
Maker (RFQ) quote endpoints — submit_quote / cancel_quote / confirm last-look —
live on the underlying `TradingClient().client` when your account is enabled for
the maker program; see the py_clob_client_v2 docs.
## Bridge — `BridgeClient` (bridge.polymarket.com)
`supported_assets()`, `get_quote(body)`, `create_bridge_addresses(body)`,
`create_withdrawal_addresses(body)`, `transaction_status(tx_id)`.
## Relayer — `RelayerClient` (relayer-v2.polymarket.com) ⚠ host/paths internal
`is_deployed(addr)`, `nonce(addr)`, `relayer_address(addr)`,
`transaction(id)`, `recent_transactions(addr)`, `submit(body)`.
## Combo markets — `ComboClient` (combos-rfq-api.polymarket.com)
| Capability | Method | Path | Params |
|---|---|---|---|
| Get combo markets | `combo_markets()` | GET /v1/rfq/combo-markets | limit(1-100), cursor, exclude |
## Anything still missing
These tools hit internal APIs that change. If a path 404s, look it up on the
relevant doc page under https://docs.polymarket.com/api-reference and either fix
the method body in `polymarket.py` or call the generic helper directly:
```python
from polymarket import _request, CLOB, DATA
_request("GET", f"{CLOB}/some/new/path", params={"next_cursor": ""})
```

84
references/trading.md Normal file
View file

@ -0,0 +1,84 @@
# Polymarket trading & authentication
Authenticated CLOB access is two-layered. `TradingClient` (in
`scripts/polymarket.py`) wraps the official **v2** SDK (`py_clob_client_v2`),
which implements both layers correctly. Read this when an order behaves
unexpectedly or you need to choose a signature type.
```bash
pip install py_clob_client_v2
```
## L1 — wallet / EIP-712
Your wallet private key signs an EIP-712 message under the `ClobAuthDomain`
(name `ClobAuthDomain`, version `1`, chainId `137` Polygon). L1 is used to:
- create API credentials (`POST /auth/api-key`)
- derive existing credentials (`GET /auth/derive-api-key`)
- sign each order locally before it is posted
The L1 request headers are `POLY_ADDRESS`, `POLY_SIGNATURE`, `POLY_TIMESTAMP`,
`POLY_NONCE`. You never send the private key anywhere — only signatures.
## L2 — API key / HMAC
Credential creation returns `apiKey`, `secret`, `passphrase`. Every subsequent
trading request (post/cancel/query orders, balances) is authenticated with an
HMAC-SHA256 signature over the request, sent as headers `POLY_ADDRESS`,
`POLY_SIGNATURE`, `POLY_TIMESTAMP`, `POLY_API_KEY`, `POLY_PASSPHRASE`.
`TradingClient` calls `create_or_derive_api_key()` on construction, so the
first run derives (or creates) and caches creds in memory. To reuse fixed creds
across runs, set `CLOB_API_KEY`, `CLOB_SECRET`, `CLOB_PASSPHRASE`.
## Signature types (how your funds are held)
Pass `signature_type` to `TradingClient(signature_type=...)`:
- `0` — EOA: you trade directly from the signing wallet.
- `1` — Polymarket proxy (older email/magic login). Set `funder=` /
`POLY_PROXY_ADDRESS` to the proxy address that holds the USDC.
- `2` — Gnosis-safe proxy (browser-wallet signup). Also needs `funder=`.
- `3` — newer Polymarket embedded-wallet accounts (current email/Google signups).
The signer key (from "Export Private Key" in Settings → 账号 → 私钥) resolves to
a Magic EOA, while the funded **trading account** is a *separate* address shown
in Settings → 个人资料 → 地址 (even though it's labelled "for API use only").
Under type 3 the SDK derives that account from the signer automatically, so
`get_balance_allowance()` shows the balance with no `funder` needed.
Undocumented gotcha that wastes hours: types 0/1/2 will all silently return
`balance: 0` for these newer accounts because they derive the wrong proxy. If
auth succeeds (creds + correct signer address) but balance is 0 under 1/2, **try
`signature_type=3`**. Confirm you've got the right type when `balance` matches
your on-screen 现金 and the three exchange `allowances` are large (≈2^256-1).
If you signed up on polymarket.com with a browser wallet, you are almost always
type `2`; older email/magic is `1`, newer email/Google is often `3`. Trading
from a raw key you funded directly
is type `0`. Getting this wrong yields "not enough balance/allowance" errors
even when funds are visible in the UI.
## Order types
- `GTC` — good-till-cancelled limit order (rests on the book).
- `GTD` — good-till-date; supply `expiration` (unix seconds).
- `FOK` — fill-or-kill (used for market orders by default).
- `FAK` — fill-and-kill (immediate, partial allowed, remainder cancelled).
Prices are in USDC per share, 01. Size is in shares (for limit orders) or USDC
amount (for `create_market_order`). Tick size and min order size vary by market:
check `clob.tick_size(token_id)` first; orders off-tick are rejected.
## Allowances
Before the first trade the exchange contracts need USDC (and CTF) allowances set
on-chain. The official client exposes `update_balance_allowance()` /
`get_balance_allowance()`; the Polymarket web UI sets these automatically on
first deposit. If posting fails with an allowance error and you funded via the
website, you likely have the wrong `signature_type`/`funder` (see above) rather
than a missing allowance.
## Safety
These are real, hard-to-reverse financial actions. Always confirm token_id,
side, price, and size with the user before posting, and never place an order the
user did not explicitly authorize. Do not log the private key.

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
# Public data (markets, prices, order books, positions, leaderboard) — required
requests>=2.28
# Authenticated trading (place/cancel/query orders) — optional, only if you trade
# pip install py-clob-client-v2

673
scripts/polymarket.py Executable file
View file

@ -0,0 +1,673 @@
#!/usr/bin/env python3
"""
Polymarket API client covers the full public + authenticated surface.
APIs wrapped here (see https://docs.polymarket.com/api-reference/introduction):
- Gamma (https://gamma-api.polymarket.com) markets, events, tags, series,
comments, sports, search, public profiles. Public, no auth.
- Data (https://data-api.polymarket.com) positions, trades, activity,
holders, value, leaderboard, open interest. Public, no auth.
- CLOB (https://clob.polymarket.com) order book, prices, midpoints,
spreads, price history (public) + orders/trades (authenticated).
- Bridge (https://bridge.polymarket.com) deposits/withdrawals.
- Relayer(https://relayer-v2.polymarket.com) proxy wallet tx submission.
Read endpoints are plain HTTP (requests). Authenticated TRADING (signing orders,
creating/deriving API keys, posting/cancelling orders, L2 HMAC headers) is
delegated to the official `py-clob-client`, which the Polymarket docs explicitly
recommend over hand-rolled EIP-712 signing. See TradingClient below.
CLI:
python polymarket.py <command> [--json '{...}'] [--key value ...]
python polymarket.py list-commands
"""
from __future__ import annotations
import json
import os
import sys
from typing import Any, Optional
try:
import requests
except ImportError:
sys.stderr.write("Missing dependency: pip install requests\n")
raise
GAMMA = "https://gamma-api.polymarket.com"
DATA = "https://data-api.polymarket.com"
CLOB = "https://clob.polymarket.com"
BRIDGE = "https://bridge.polymarket.com"
RELAYER = "https://relayer-v2.polymarket.com"
DEFAULT_TIMEOUT = 30
class PolymarketError(RuntimeError):
pass
def _clean(params: Optional[dict]) -> Optional[dict]:
"""Drop None values; leave False/0 intact."""
if not params:
return None
return {k: v for k, v in params.items() if v is not None}
def _request(method: str, url: str, *, params=None, json_body=None,
headers=None, timeout=DEFAULT_TIMEOUT) -> Any:
resp = requests.request(method, url, params=_clean(params), json=json_body,
headers=headers, timeout=timeout)
if resp.status_code >= 400:
raise PolymarketError(f"{method} {resp.url} -> {resp.status_code}: {resp.text[:500]}")
ctype = resp.headers.get("content-type", "")
if "application/json" in ctype:
return resp.json()
return resp.text
# --------------------------------------------------------------------------- #
# Gamma API — markets, events, tags, series, comments, search, sports, profiles
# --------------------------------------------------------------------------- #
class GammaClient:
def __init__(self, base=GAMMA):
self.base = base
def _get(self, path, **params):
return _request("GET", f"{self.base}{path}", params=params)
# Markets
def list_markets(self, **params):
return self._get("/markets", **params)
def get_market(self, market_id):
return self._get(f"/markets/{market_id}")
def get_market_tags(self, market_id):
return self._get(f"/markets/{market_id}/tags")
# Events
def list_events(self, **params):
return self._get("/events", **params)
def list_events_paginated(self, **params):
return self._get("/events/pagination", **params)
def get_event(self, event_id):
return self._get(f"/events/{event_id}")
def get_event_by_slug(self, slug):
return self._get(f"/events/slug/{slug}")
def get_event_tags(self, event_id):
return self._get(f"/events/{event_id}/tags")
# Tags
def list_tags(self, **params):
return self._get("/tags", **params)
def get_tag(self, tag_id):
return self._get(f"/tags/{tag_id}")
def get_tag_by_slug(self, slug):
return self._get(f"/tags/slug/{slug}")
def get_related_tags_by_id(self, tag_id):
return self._get(f"/tags/{tag_id}/related-tags")
def get_related_tags_by_slug(self, slug):
return self._get(f"/tags/slug/{slug}/related-tags")
def get_tags_related_to_id(self, tag_id):
return self._get(f"/tags/{tag_id}/related-tags/tags")
def get_tags_related_to_slug(self, slug):
return self._get(f"/tags/slug/{slug}/related-tags/tags")
# Series
def list_series(self, **params):
return self._get("/series", **params)
def get_series(self, series_id):
return self._get(f"/series/{series_id}")
# Comments
def list_comments(self, **params):
return self._get("/comments", **params)
def get_comments_by_id(self, comment_id, **params):
return self._get(f"/comments/{comment_id}", **params)
def get_comments_by_user(self, address, **params):
return self._get(f"/comments/user_address/{address}", **params)
# Search
def search(self, q, **params):
return self._get("/public-search", q=q, **params)
# Sports
def get_sports_metadata(self):
return self._get("/sports")
def get_sports_market_types(self):
return self._get("/sports/market-types")
def list_teams(self, **params):
return self._get("/teams", **params)
# Public profiles
def get_public_profile(self, address):
return self._get(f"/public-profile/{address}")
# --------------------------------------------------------------------------- #
# Data API — positions, trades, activity, holders, value, leaderboard, OI
# --------------------------------------------------------------------------- #
class DataClient:
def __init__(self, base=DATA):
self.base = base
def _get(self, path, **params):
return _request("GET", f"{self.base}{path}", params=params)
def positions(self, user, **params):
return self._get("/positions", user=user, **params)
def closed_positions(self, user, **params):
return self._get("/closed-positions", user=user, **params)
def market_positions(self, market, **params):
return self._get("/positions", market=market, **params)
def value(self, user, **params):
return self._get("/value", user=user, **params)
def activity(self, user, **params):
return self._get("/activity", user=user, **params)
def combo_activity(self, user, **params):
return self._get("/combo-activity", user=user, **params)
def combo_positions(self, user, **params):
return self._get("/combo-positions", user=user, **params)
def trades(self, **params):
return self._get("/trades", **params)
def holders(self, market, **params):
return self._get("/holders", market=market, **params)
def leaderboard(self, **params):
# category, timePeriod, orderBy(PNL/VOLUME), limit(≤50), offset, user, userName
return self._get("/v1/leaderboard", **params)
def traded(self, user):
return self._get("/traded", user=user)
def open_interest(self, **params):
return self._get("/oi", **params)
def live_volume_event(self, event_id):
return self._get("/live-volume", id=event_id)
# Builder analytics (public)
def builder_leaderboard(self, **params):
# timePeriod(DAY/WEEK/MONTH/ALL), limit(≤50), offset
return self._get("/v1/builders/leaderboard", **params)
def builder_volume(self, **params):
# timePeriod(DAY/WEEK/MONTH/ALL)
return self._get("/v1/builders/volume", **params)
def accounting_snapshot(self, user, out_path=None):
"""Download the accounting snapshot ZIP (positions.csv + equity.csv).
Returns the saved file path. ZIP is binary, so this bypasses the JSON
helper and streams to disk.
"""
url = f"{self.base}/v1/accounting/snapshot"
resp = requests.get(url, params={"user": user}, timeout=DEFAULT_TIMEOUT)
if resp.status_code >= 400:
raise PolymarketError(f"GET {resp.url} -> {resp.status_code}: {resp.text[:300]}")
out_path = out_path or f"polymarket_accounting_{user}.zip"
with open(out_path, "wb") as f:
f.write(resp.content)
return {"saved": out_path, "bytes": len(resp.content)}
# --------------------------------------------------------------------------- #
# Combo markets (RFQ) — separate host
# --------------------------------------------------------------------------- #
class ComboClient:
def __init__(self, base="https://combos-rfq-api.polymarket.com"):
self.base = base
def combo_markets(self, **params):
# limit(1-100), cursor, exclude (comma-separated condition ids)
return _request("GET", f"{self.base}/v1/rfq/combo-markets", params=params)
# --------------------------------------------------------------------------- #
# CLOB API — PUBLIC market data (no auth)
# --------------------------------------------------------------------------- #
class ClobPublicClient:
def __init__(self, base=CLOB):
self.base = base
def _get(self, path, **params):
return _request("GET", f"{self.base}{path}", params=params)
def _post(self, path, body):
return _request("POST", f"{self.base}{path}", json_body=body)
# Pricing / book
def order_book(self, token_id):
return self._get("/book", token_id=token_id)
def order_books(self, token_ids):
return self._post("/books", [{"token_id": t} for t in token_ids])
def price(self, token_id, side):
return self._get("/price", token_id=token_id, side=side)
def prices(self, params_list):
# params_list: [{"token_id":..,"side":"BUY"|"SELL"}, ...]
return self._post("/prices", params_list)
def midpoint(self, token_id):
return self._get("/midpoint", token_id=token_id)
def midpoints(self, token_ids):
return self._post("/midpoints", [{"token_id": t} for t in token_ids])
def spread(self, token_id):
return self._get("/spread", token_id=token_id)
def spreads(self, token_ids):
return self._post("/spreads", [{"token_id": t} for t in token_ids])
def last_trade_price(self, token_id):
return self._get("/last-trade-price", token_id=token_id)
def last_trade_prices(self, token_ids):
return self._post("/last-trades-prices", [{"token_id": t} for t in token_ids])
def tick_size(self, token_id):
return self._get("/tick-size", token_id=token_id)
def fee_rate_bps(self, **params):
return self._get("/fee-rate-bps", **params)
# Price history
def prices_history(self, market, **params):
# market = clob token id. interval/startTs/endTs/fidelity
return self._get("/prices-history", market=market, **params)
# Market discovery (CLOB-side)
def get_market(self, condition_id):
return self._get(f"/markets/{condition_id}")
def list_markets(self, next_cursor=""):
return self._get("/markets", next_cursor=next_cursor)
def simplified_markets(self, next_cursor=""):
return self._get("/simplified-markets", next_cursor=next_cursor)
def sampling_markets(self, next_cursor=""):
return self._get("/sampling-markets", next_cursor=next_cursor)
def sampling_simplified_markets(self, next_cursor=""):
return self._get("/sampling-simplified-markets", next_cursor=next_cursor)
def server_time(self):
return self._get("/time")
# Rewards — market configs (public, no auth)
def rewards_markets_current(self, sponsored=False, next_cursor=""):
return self._get("/rewards/markets/current", sponsored=sponsored, next_cursor=next_cursor)
def rewards_markets_multi(self, **params):
# q, tag_slug, event_id, order_by, position, min/max_volume_24hr,
# min/max_spread, min/max_price, next_cursor, page_size
return self._get("/rewards/markets/multi", **params)
def rewards_market(self, condition_id, **params):
return self._get(f"/rewards/markets/{condition_id}", **params)
# Builder-attributed trades (public; needs builder_code)
def builder_trades(self, builder_code, **params):
# id, market, asset_id, before, after, next_cursor
return self._get("/builder/trades", builder_code=builder_code, **params)
# --------------------------------------------------------------------------- #
# Bridge API — deposits / withdrawals
# --------------------------------------------------------------------------- #
class BridgeClient:
def __init__(self, base=BRIDGE):
self.base = base
def supported_assets(self):
return _request("GET", f"{self.base}/assets")
def get_quote(self, body):
return _request("POST", f"{self.base}/quote", json_body=body)
def create_bridge_addresses(self, body):
return _request("POST", f"{self.base}/bridge-addresses", json_body=body)
def create_withdrawal_addresses(self, body):
return _request("POST", f"{self.base}/withdrawal-addresses", json_body=body)
def transaction_status(self, tx_id):
return _request("GET", f"{self.base}/transactions/{tx_id}")
# --------------------------------------------------------------------------- #
# Relayer API — proxy wallet helpers
# --------------------------------------------------------------------------- #
class RelayerClient:
def __init__(self, base=RELAYER):
self.base = base
def is_deployed(self, address):
return _request("GET", f"{self.base}/deployed", params={"address": address})
def nonce(self, address):
return _request("GET", f"{self.base}/nonce", params={"address": address})
def relayer_address(self, address):
return _request("GET", f"{self.base}/relayer-address", params={"address": address})
def transaction(self, tx_id):
return _request("GET", f"{self.base}/transaction", params={"id": tx_id})
def recent_transactions(self, address):
return _request("GET", f"{self.base}/transactions", params={"address": address})
def submit(self, body):
return _request("POST", f"{self.base}/submit", json_body=body)
# --------------------------------------------------------------------------- #
# Authenticated TRADING — delegated to official py-clob-client
# --------------------------------------------------------------------------- #
# The Polymarket docs recommend the official v2 client for EIP-712 L1 signing,
# API-key derivation, and L2 HMAC headers. Reimplementing order signing by hand
# risks losing real funds, so we wrap the maintained library:
#
# pip install py_clob_client_v2
#
# Auth model:
# L1 (wallet private key, EIP-712) -> create/derive API credentials, sign orders
# L2 (apiKey/secret/passphrase, HMAC-SHA256) -> post/cancel/query orders, and the
# user-scoped rewards/rebates GETs (implemented here via _l2_get).
#
# Env vars expected:
# PK wallet private key (0x...)
# POLY_PROXY_ADDRESS your Polymarket proxy/funder address (for proxy wallets)
# POLY_SIGNATURE_TYPE 0=EOA, 1=POLY_PROXY, 2=POLY_GNOSIS_SAFE (optional)
# CLOB_API_KEY / CLOB_SECRET / CLOB_PASSPHRASE (optional; else auto-derived)
class TradingClient:
def __init__(self, private_key=None, chain_id=137, signature_type=None,
funder=None, host=CLOB, creds=None):
try:
from py_clob_client_v2 import ClobClient, ApiCreds
except ImportError as e:
raise PolymarketError(
"Authenticated trading needs the official v2 client: "
"pip install py_clob_client_v2"
) from e
private_key = private_key or os.environ.get("PK")
if not private_key:
raise PolymarketError("Set PK env var (wallet private key) or pass private_key=")
funder = funder or os.environ.get("POLY_PROXY_ADDRESS")
if signature_type is None and os.environ.get("POLY_SIGNATURE_TYPE"):
signature_type = int(os.environ["POLY_SIGNATURE_TYPE"])
self.host = host
kwargs = dict(host=host, key=private_key, chain_id=chain_id)
if signature_type is not None:
kwargs["signature_type"] = signature_type
if funder:
kwargs["funder"] = funder
self._ApiCreds = ApiCreds
self.client = ClobClient(**kwargs)
# Attach L2 creds: explicit -> env -> derive.
self.creds = creds or self._creds_from_env() or self.create_or_derive_api_key()
if hasattr(self.client, "set_api_creds"):
self.client.set_api_creds(self.creds)
def _creds_from_env(self):
k = os.environ.get("CLOB_API_KEY")
s = os.environ.get("CLOB_SECRET")
p = os.environ.get("CLOB_PASSPHRASE")
if k and s and p:
return self._ApiCreds(api_key=k, api_secret=s, api_passphrase=p)
return None
# --- API key lifecycle (L1) ---
def create_or_derive_api_key(self, nonce=None):
# Derive first: existing accounts already have creds, so trying to CREATE
# first just logs a noisy 400 ("Could not create api key"). Fall back to
# create only if derive genuinely fails (brand-new account).
try:
return self.client.derive_api_key(nonce=nonce)
except Exception:
return self.client.create_api_key(nonce=nonce)
# --- Orders (L1 sign + L2 post) ---
def create_and_post_order(self, token_id, price, size, side, order_type="GTC"):
from py_clob_client_v2 import OrderArgs, OrderType
args = OrderArgs(
token_id=token_id,
price=float(price),
size=float(size),
side="BUY" if str(side).upper() == "BUY" else "SELL", # SDK accepts str
)
return self.client.create_and_post_order(args, order_type=getattr(OrderType, order_type))
def create_and_post_market_order(self, token_id, amount, side, order_type="FOK"):
from py_clob_client_v2 import MarketOrderArgs, OrderType
args = MarketOrderArgs(
token_id=token_id,
amount=float(amount),
side="BUY" if str(side).upper() == "BUY" else "SELL",
)
return self.client.create_and_post_market_order(args, order_type=getattr(OrderType, order_type))
# --- Cancel ---
def cancel_order(self, order_id):
# Accept a raw order id/hash string or an OrderPayload.
from py_clob_client_v2 import OrderPayload
payload = order_id if not isinstance(order_id, str) else OrderPayload(orderID=order_id)
return self.client.cancel_order(payload)
def cancel_orders(self, order_hashes):
return self.client.cancel_orders(order_hashes)
def cancel_all(self):
return self.client.cancel_all()
def cancel_market_orders(self, payload):
return self.client.cancel_market_orders(payload)
# --- Query (L2) ---
# These accept plain filter kwargs and build the SDK's param objects for you.
# Pass a ready-made `params` object to bypass that. Note: a freshly placed
# order can take a moment to show up here (indexing lag) even though it's live.
def get_order(self, order_id):
return self.client.get_order(order_id)
def get_open_orders(self, market=None, asset_id=None, id=None, params=None,
only_first_page=False):
from py_clob_client_v2 import OpenOrderParams
if params is None and (market or asset_id or id):
params = OpenOrderParams(market=market, asset_id=asset_id, id=id)
return self.client.get_open_orders(params, only_first_page=only_first_page)
def get_trades(self, market=None, asset_id=None, maker_address=None,
id=None, before=None, after=None, params=None):
from py_clob_client_v2 import TradeParams
if params is None and any([market, asset_id, maker_address, id, before, after]):
params = TradeParams(market=market, asset_id=asset_id,
maker_address=maker_address, id=id,
before=before, after=after)
return self.client.get_trades(params)
def is_order_scoring(self, order_id=None, params=None):
from py_clob_client_v2 import OrderScoringParams
if params is None and order_id:
params = OrderScoringParams(orderId=order_id)
return self.client.is_order_scoring(params)
def are_orders_scoring(self, order_ids=None, params=None):
from py_clob_client_v2 import OrdersScoringParams
if params is None and order_ids:
params = OrdersScoringParams(orderIds=order_ids)
return self.client.are_orders_scoring(params)
def _balance_params(self, asset_type="COLLATERAL", token_id=None):
# COLLATERAL = your USDC; CONDITIONAL = a specific outcome token (needs token_id).
from py_clob_client_v2 import BalanceAllowanceParams, AssetType
at = AssetType.CONDITIONAL if str(asset_type).upper() == "CONDITIONAL" else AssetType.COLLATERAL
return BalanceAllowanceParams(asset_type=at, token_id=token_id)
def get_balance_allowance(self, asset_type="COLLATERAL", token_id=None):
return self.client.get_balance_allowance(self._balance_params(asset_type, token_id))
def update_balance_allowance(self, asset_type="COLLATERAL", token_id=None):
return self.client.update_balance_allowance(self._balance_params(asset_type, token_id))
# --- L2-authenticated rewards / rebates GETs ---------------------------- #
# These read-only endpoints require L2 HMAC headers but the SDK does not
# surface them, so we sign the GET ourselves with the derived creds. This is
# the documented, deterministic L2 scheme (no fund movement, no EIP-712).
def _l2_get(self, path, params=None):
import base64, hashlib, hmac as _hmac, time as _time
from urllib.parse import urlencode
c = self.creds
api_key = getattr(c, "api_key", None) or c["api_key"]
secret = getattr(c, "api_secret", None) or c["api_secret"]
passphrase = getattr(c, "api_passphrase", None) or c["api_passphrase"]
address = self.client.get_address() if hasattr(self.client, "get_address") else os.environ.get("POLY_ADDRESS", "")
request_path = path
qs = urlencode(_clean(params) or {})
if qs:
request_path += "?" + qs
ts = str(int(_time.time()))
# L2 HMAC: base64url(HMAC_SHA256(b64url_decode(secret), ts+method+requestPath))
message = ts + "GET" + request_path
digest = _hmac.new(base64.urlsafe_b64decode(secret),
message.encode(), hashlib.sha256).digest()
sig = base64.urlsafe_b64encode(digest).decode()
headers = {
"POLY_ADDRESS": address,
"POLY_SIGNATURE": sig,
"POLY_TIMESTAMP": ts,
"POLY_API_KEY": api_key,
"POLY_PASSPHRASE": passphrase,
}
return _request("GET", f"{self.host}{request_path}", headers=headers)
def rewards_user_earnings(self, date, **params):
return self._l2_get("/rewards/user", {"date": date, **params})
def rewards_user_total(self, date, **params):
return self._l2_get("/rewards/user/total", {"date": date, **params})
def rewards_user_percentages(self, **params):
return self._l2_get("/rewards/user/percentages", params)
def rewards_user_markets(self, **params):
return self._l2_get("/rewards/user/markets", params)
def rebates_current(self, date, maker_address, **params):
return self._l2_get("/rebates/current",
{"date": date, "maker_address": maker_address, **params})
# --------------------------------------------------------------------------- #
# CLI
# --------------------------------------------------------------------------- #
def _parse_cli(argv):
"""argv after command name -> (kwargs). Supports --key value and --json '{...}'."""
kwargs = {}
i = 0
while i < len(argv):
tok = argv[i]
if tok.startswith("--"):
key = tok[2:]
if key == "json":
kwargs.update(json.loads(argv[i + 1]))
i += 2
continue
val = argv[i + 1] if i + 1 < len(argv) else "true"
# light coercion
if val.lower() in ("true", "false"):
val = val.lower() == "true"
i += 2
kwargs[key] = val
else:
i += 1
return kwargs
# command -> (client_factory, method_name)
def _registry():
g, d, c, b, r = GammaClient(), DataClient(), ClobPublicClient(), BridgeClient(), RelayerClient()
combo = ComboClient()
reg = {}
def add(prefix, obj):
for name in dir(obj):
if name.startswith("_"):
continue
attr = getattr(obj, name)
if callable(attr):
reg[f"{prefix}.{name}"] = attr
add("gamma", g)
add("data", d)
add("clob", c)
add("bridge", b)
add("relayer", r)
add("combo", combo)
return reg
def main(argv):
if not argv or argv[0] in ("-h", "--help", "help"):
print(__doc__)
return 0
cmd = argv[0]
reg = _registry()
if cmd == "list-commands":
for k in sorted(reg):
print(k)
print("\nTrading (needs py-clob-client + PK env): use TradingClient in Python, e.g.")
print(" python -c 'from polymarket import TradingClient; "
"print(TradingClient().create_and_post_order(TOKEN, 0.5, 10, \"BUY\"))'")
return 0
if cmd.startswith("trade.") or cmd.startswith("trading."):
sys.stderr.write(
"Trading commands run in Python (they need your private key in env), "
"not via this flat CLI. See references/trading.md.\n")
return 2
if cmd not in reg:
sys.stderr.write(f"Unknown command: {cmd}\nTry: python polymarket.py list-commands\n")
return 2
kwargs = _parse_cli(argv[1:])
result = reg[cmd](**kwargs)
print(json.dumps(result, indent=2, ensure_ascii=False) if not isinstance(result, str) else result)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))