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:
commit
1d4d3a22f4
7 changed files with 1187 additions and 0 deletions
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
123
README.md
Normal 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
134
SKILL.md
Normal 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
138
references/endpoints.md
Normal 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
84
references/trading.md
Normal 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, 0–1. 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
5
requirements.txt
Normal 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
673
scripts/polymarket.py
Executable 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:]))
|
||||
Loading…
Add table
Reference in a new issue