
Trading Polymarket
- 29 installs
- 610 repo stars
- Updated June 26, 2026
- alsk1992/cloddsbot
trading-polymarket is a skill that executes Polymarket CLOB trades via the official py_clob_client library.
About
This skill executes trades on Polymarket's Central Limit Order Book using the official py_clob_client library. It reads market data and orderbooks, creates and posts orders in GTC, FOK, GTD, and FAK types, cancels orders, and queries positions. A developer uses it to trade Polymarket prediction markets programmatically from the clodds bot.
- Executes trades on Polymarket's CLOB via py_clob_client
- Full order lifecycle: market data, GTC/FOK/GTD/FAK orders, positions
- Documents three authentication levels and proxy/EOA signature types
Trading Polymarket by the numbers
- 29 all-time installs (skills.sh)
- Ranked #667 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
trading-polymarket capabilities & compatibility
Requires an Ethereum PRIVATE_KEY plus Polymarket API creds; USDC and gas on Polygon apply.
- Capabilities
- prediction market trading · clob orders · orderbook data · position tracking
- Use cases
- trading
- Runs
- Runs locally
- Pricing
- Bring your own API key
What trading-polymarket says it does
Full access to Polymarket's CLOB (Central Limit Order Book) via the official `py_clob_client` library.
60+ methods documented. This is the complete reference.
npx skills add https://github.com/alsk1992/cloddsbot --skill trading-polymarketAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 610 |
| Last updated | June 26, 2026 |
| Repository | alsk1992/cloddsbot ↗ |
What it does
Trade Polymarket CLOB markets with full order-type support via py_clob_client.
Who is it for?
Programmatic trading of Polymarket prediction markets with full order-type control.
Skip if: Crypto DEX swaps or centralized-exchange futures.
When should I use this skill?
You need to read Polymarket markets or create, post, and cancel CLOB orders.
What you get
Placed and managed Polymarket CLOB orders with position queries.
- Placed CLOB orders
- Market data and position queries
By the numbers
- 60+ methods documented
- 4 order types (GTC, FOK, GTD, FAK)
- 3 authentication levels
Files
Polymarket Trading - Complete API Reference
Full access to Polymarket's CLOB (Central Limit Order Book) via the official py_clob_client library.
60+ methods documented. This is the complete reference.
Required Environment Variables
PRIVATE_KEY=0x... # Ethereum private key for signing
POLY_FUNDER_ADDRESS=0x... # Your wallet address on Polygon
POLY_API_KEY=... # From Polymarket API
POLY_API_SECRET=... # Base64 encoded secret
POLY_API_PASSPHRASE=... # API passphraseInstallation
pip install py-clob-client requests---
Authentication Levels
| Level | Requirements | Capabilities |
|---|---|---|
| L0 | None | Read-only: orderbooks, prices, markets |
| L1 | Private key | Create & sign orders (not post) |
| L2 | Private key + API creds | Full trading: post orders, cancel, query |
Signature Types
| Type | Use Case |
|---|---|
0 | Standard EOA (MetaMask, hardware wallets) |
1 | Magic/email wallets (delegated signing) |
2 | Proxy wallets (Gnosis Safe, browser proxy) |
---
ClobClient - Complete API (60+ Methods)
Initialization
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import (
OrderArgs, MarketOrderArgs, ApiCreds, OrderType,
BookParams, TradeParams, OpenOrderParams, BalanceAllowanceParams,
AssetType, OrderScoringParams, OrdersScoringParams, DropNotificationParams
)
from py_clob_client.order_builder.constants import BUY, SELL
from py_clob_client.constants import POLYGON # 137
# Level 2 Auth (full trading access)
client = ClobClient(
host="https://clob.polymarket.com",
key=os.getenv("PRIVATE_KEY"), # Private key for signing
chain_id=POLYGON, # 137 for mainnet, 80002 for Amoy testnet
funder=os.getenv("POLY_FUNDER_ADDRESS"), # Wallet address (for proxy wallets)
signature_type=2 # 0=EOA, 1=MagicLink, 2=Proxy
)
# Set API credentials for authenticated endpoints
client.set_api_creds(ApiCreds(
api_key=os.getenv("POLY_API_KEY"),
api_secret=os.getenv("POLY_API_SECRET"),
api_passphrase=os.getenv("POLY_API_PASSPHRASE")
))Health & Configuration
client.get_ok() # Check if server is up
client.get_server_time() # Get server timestamp
client.get_address() # Your signer's public address
client.get_collateral_address() # USDC contract address
client.get_conditional_address() # CTF contract address
client.get_exchange_address() # Exchange contract (neg_risk=False default)Market Data - Single Token
# Get prices and spreads
client.get_midpoint(token_id) # Mid market price
client.get_price(token_id, side="BUY") # Best price for side
client.get_spread(token_id) # Current spread
client.get_last_trade_price(token_id) # Last executed trade price
# Get full orderbook
orderbook = client.get_order_book(token_id)
# Returns: OrderBookSummary with bids, asks, tick_size, neg_risk, timestamp, hashMarket Data - Batch (Multiple Tokens)
params = [
BookParams(token_id="TOKEN1", side="BUY"),
BookParams(token_id="TOKEN2", side="SELL")
]
client.get_midpoints(params) # Multiple midpoints
client.get_prices(params) # Multiple prices
client.get_spreads(params) # Multiple spreads
client.get_order_books(params) # Multiple orderbooks
client.get_last_trades_prices(params) # Multiple last pricesMarket Metadata
client.get_tick_size(token_id) # Returns: "0.1", "0.01", "0.001", or "0.0001"
client.get_neg_risk(token_id) # Returns: True/False (negative risk market)
client.get_fee_rate_bps(token_id) # Returns: fee rate in basis points (0 or 1000)---
Order Types
from py_clob_client.clob_types import OrderType
OrderType.GTC # Good Till Cancelled - stays open until filled/cancelled
OrderType.FOK # Fill Or Kill - fill entirely immediately or cancel
OrderType.GTD # Good Till Date - expires at timestamp (min 60 seconds)
OrderType.FAK # Fill And Kill - fill what's possible, cancel restWhen to Use Each Order Type
| Type | Use Case | Example |
|---|---|---|
| GTC | Entries - wait for fill | Place buy at 0.45, wait for dip |
| FOK | Exits - need immediate fill | Market sell entire position NOW |
| GTD | Time-limited orders | Offer expires in 5 minutes |
| FAK | Partial fills OK | Get as much as possible now |
OrderArgs - Limit Orders
OrderArgs(
token_id: str, # Token ID (outcome to trade)
price: float, # Price 0.01-0.99
size: float, # Number of shares
side: str, # "BUY" or "SELL" (or use BUY/SELL constants)
fee_rate_bps: int = 0, # Optional: fee rate in bps (0 or check market)
nonce: int = 0, # Optional: unique nonce for cancellation
expiration: int = 0, # Optional: expiry timestamp (0 = GTC, use timestamp for GTD)
taker: str = ZERO_ADDRESS # Optional: specific taker (ZERO_ADDRESS = anyone)
)MarketOrderArgs - Market Orders
MarketOrderArgs(
token_id: str, # Token ID
amount: float, # Total USDC amount to spend (BUY) or shares (SELL)
side: str, # "BUY" or "SELL"
price: float = 0, # Optional: worst acceptable price (slippage protection)
fee_rate_bps: int = 0, # Optional: fee rate
nonce: int = 0, # Optional: nonce
taker: str = ZERO_ADDRESS, # Optional: taker address
order_type: OrderType = FOK # Optional: FOK (default) or FAK
)Complete Order Examples
from py_clob_client.order_builder.constants import BUY, SELL
# 1. LIMIT BUY (GTC) - sits on book until filled
order = client.create_and_post_order(
OrderArgs(token_id=TOKEN_ID, price=0.45, size=100.0, side=BUY)
)
# 2. LIMIT SELL (GTC)
order = client.create_and_post_order(
OrderArgs(token_id=TOKEN_ID, price=0.55, size=50.0, side=SELL)
)
# 3. MARKET BUY - spend $100 USDC at current prices (FOK)
signed = client.create_market_order(
MarketOrderArgs(token_id=TOKEN_ID, amount=100.0, side=BUY)
)
result = client.post_order(signed, orderType=OrderType.FOK)
# 4. MARKET SELL - sell all shares immediately (FOK)
signed = client.create_market_order(
MarketOrderArgs(token_id=TOKEN_ID, amount=my_shares, side=SELL)
)
result = client.post_order(signed, orderType=OrderType.FOK)
# 5. POST-ONLY MAKER ORDER (avoid taker fees, earn rebates)
signed = client.create_order(
OrderArgs(token_id=TOKEN_ID, price=0.44, size=100.0, side=BUY)
)
result = client.post_order(signed, orderType=OrderType.GTC, post_only=True)
# If order would cross spread, it gets REJECTED instead of taking
# 6. GOOD TIL DATE (GTD) - expires after timestamp
import time
expiry = int(time.time()) + 300 # 5 minutes from now
signed = client.create_order(
OrderArgs(token_id=TOKEN_ID, price=0.50, size=100.0, side=BUY, expiration=expiry)
)
result = client.post_order(signed, orderType=OrderType.GTD)
# 7. FILL AND KILL (FAK) - fill what you can, cancel rest
signed = client.create_market_order(
MarketOrderArgs(token_id=TOKEN_ID, amount=1000.0, side=BUY)
)
result = client.post_order(signed, orderType=OrderType.FAK)---
Order Operations
Create and Post Orders
# SIMPLE: Create and post in one call (recommended)
result = client.create_and_post_order(
OrderArgs(
token_id="123456789012345678901234567890",
price=0.45,
size=10.0,
side="BUY"
)
)
# Returns: {"orderID": "...", "status": "...", ...}
# ADVANCED: Separate create and post
order = client.create_order(OrderArgs(...)) # Returns SignedOrder
result = client.post_order(order, orderType=OrderType.GTC, post_only=False)
# Market order (calculates price automatically)
result = client.create_market_order(
MarketOrderArgs(
token_id="...",
amount=100.0, # Spend $100 USDC
side="BUY"
)
)
# Calculate expected fill price before market order
price = client.calculate_market_price(
token_id="...",
side="BUY",
amount=100.0,
order_type=OrderType.FOK
)Cancel Orders
client.cancel(order_id="ORDER_ID") # Cancel specific order
client.cancel_orders(["ID1", "ID2", "ID3"]) # Cancel multiple
client.cancel_all() # Cancel ALL open orders
client.cancel_market_orders( # Cancel by market/asset
market="CONDITION_ID",
asset_id="TOKEN_ID"
)Query Orders
# Get all open orders
orders = client.get_orders(
params=OpenOrderParams(
id="ORDER_ID", # Optional: specific order
market="COND_ID", # Optional: filter by market
asset_id="TOKEN" # Optional: filter by token
),
next_cursor="MA==" # For pagination
)
# Get specific order
order = client.get_order(order_id="ORDER_ID")Trade History
trades = client.get_trades(
params=TradeParams(
id="TRADE_ID", # Optional: specific trade
maker_address="0x...", # Optional: filter by maker
market="CONDITION_ID", # Optional: filter by market
asset_id="TOKEN_ID", # Optional: filter by token
before="2024-01-01", # Optional: before date
after="2023-01-01" # Optional: after date
),
next_cursor="MA=="
)Balance & Allowance
# Check balance and allowances
balance = client.get_balance_allowance(
params=BalanceAllowanceParams(
asset_type=AssetType.COLLATERAL, # USDC balance
# or AssetType.CONDITIONAL # Token balance
token_id="TOKEN_ID" # For conditional tokens
)
)
# Update/refresh allowance cache
client.update_balance_allowance(params=...)---
Market Discovery
Get Markets from CLOB
# All active markets
markets = client.get_markets(next_cursor="MA==")
simplified = client.get_simplified_markets()
# Specific market
market = client.get_market(condition_id="CONDITION_ID")
# Market trade events
events = client.get_market_trades_events(condition_id="CONDITION_ID")
# Sampling/featured markets
client.get_sampling_markets()
client.get_sampling_simplified_markets()Get Markets from Gamma API (More Details)
import requests
def search_markets(query: str, limit: int = 10):
"""Search Polymarket markets by keyword"""
url = "https://gamma-api.polymarket.com/markets"
params = {
"_q": query,
"active": "true",
"closed": "false",
"_limit": limit
}
r = requests.get(url, params=params)
return r.json()
# Get market details
markets = search_markets("bitcoin")
for m in markets:
print(f"Question: {m['question']}")
print(f"Condition ID: {m['condition_id']}")
print(f"Volume: ${m.get('volume', 0):,.2f}")
for token in m.get('tokens', []):
print(f" {token['outcome']}: {token['token_id']}")
print(f" Price: {float(token['price']):.2f}")---
On-Chain Operations
Check Token Balance (Position Size)
import requests
CTF_CONTRACT = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
RPC_URL = "https://polygon-rpc.com/"
def get_token_balance(wallet: str, token_id: str) -> float:
"""Get balance of a specific outcome token in shares"""
token_int = int(token_id)
# ERC-1155 balanceOf(address,uint256)
data = f"0x00fdd58e000000000000000000000000{wallet[2:].lower()}{token_int:064x}"
r = requests.post(RPC_URL, json={
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{"to": CTF_CONTRACT, "data": data}, "latest"],
"id": 1
})
result = r.json().get("result", "0x0")
balance = int(result, 16) / 1e6 # Convert from raw to shares
return balance
# Usage
balance = get_token_balance(
wallet="0x7c2211103e7Fbb257Ac6fa59f972cfd8bc9D4795",
token_id="12345678901234567890"
)
print(f"Position: {balance} shares")Check USDC Balance
USDC_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
def get_usdc_balance(wallet: str) -> float:
"""Get USDC balance on Polygon"""
# ERC-20 balanceOf(address)
data = f"0x70a08231000000000000000000000000{wallet[2:].lower()}"
r = requests.post(RPC_URL, json={
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{"to": USDC_CONTRACT, "data": data}, "latest"],
"id": 1
})
result = r.json().get("result", "0x0")
return int(result, 16) / 1e6 # USDC has 6 decimals---
API Key Management
# Create new API key
creds = client.create_api_key(nonce=0)
# Derive existing API key (if you lost creds but have private key)
creds = client.derive_api_key(nonce=0)
# Create or derive (tries both)
creds = client.create_or_derive_api_creds(nonce=0)
# Get all your API keys
keys = client.get_api_keys()
# Delete current API key
client.delete_api_key()
# Readonly API keys (for monitoring only)
readonly = client.create_readonly_api_key()
client.get_readonly_api_keys()
client.delete_readonly_api_key(key="...")
client.validate_readonly_api_key(address="0x...", key="...")---
Advanced Features
Order Heartbeat (Keep Orders Alive)
# Start heartbeat - if not sent within 10s, all orders cancelled
heartbeat_id = client.post_heartbeat(heartbeat_id=None)
# Continue sending heartbeats
while trading:
client.post_heartbeat(heartbeat_id=heartbeat_id)
time.sleep(5)Order Scoring
# Check if order is scoring (earning rewards)
is_scoring = client.is_order_scoring(
params=OrderScoringParams(order_id="...")
)
# Check multiple orders
scores = client.are_orders_scoring(
params=OrdersScoringParams(order_ids=["ID1", "ID2"])
)Notifications
notifications = client.get_notifications()
client.drop_notifications(params=DropNotificationParams(...))---
Fee Structure
IMPORTANT: Most Polymarket markets have ZERO fees (0% maker, 0% taker).
15-min Crypto Markets (Exception)
Only 15-minute BTC/ETH/SOL/XRP price prediction markets have fees:
fee = shares × 0.25 × (price × (1 - price))²| Entry Price | Fee % (per side) |
|---|---|
| 0.50 | ~1.56% |
| 0.60 or 0.40 | ~1.44% |
| 0.70 or 0.30 | ~1.10% |
| 0.80 or 0.20 | ~0.64% |
| 0.90 or 0.10 | ~0.20% |
TAKER = crosses spread = PAYS fee MAKER = adds liquidity = NO fee + earns rebates
To be a maker: Post orders that don't immediately fill (inside the spread).
---
Decimal Precision Rules
| Order Side | Price Decimals | Size Decimals |
|---|---|---|
| BUY | 2 | 4 |
| SELL | 2 | 2 |
Min order size: $1 per side
---
Complete Trading Example
#!/usr/bin/env python3
"""
Production-ready Polymarket trading script
"""
import os
import time
import requests
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs, ApiCreds, OrderType
# Initialize
client = ClobClient(
"https://clob.polymarket.com",
key=os.getenv("PRIVATE_KEY"),
chain_id=137,
funder=os.getenv("POLY_FUNDER_ADDRESS"),
signature_type=2
)
client.set_api_creds(ApiCreds(
api_key=os.getenv("POLY_API_KEY"),
api_secret=os.getenv("POLY_API_SECRET"),
api_passphrase=os.getenv("POLY_API_PASSPHRASE")
))
TOKEN_ID = "YOUR_TOKEN_ID"
WALLET = os.getenv("POLY_FUNDER_ADDRESS")
CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
def get_balance(token_id):
"""Get position size"""
token_int = int(token_id)
data = f"0x00fdd58e000000000000000000000000{WALLET[2:].lower()}{token_int:064x}"
r = requests.post("https://polygon-rpc.com/", json={
"jsonrpc": "2.0", "method": "eth_call",
"params": [{"to": CTF, "data": data}, "latest"], "id": 1
})
return int(r.json().get("result", "0x0"), 16) / 1e6
def get_orderbook(token_id):
"""Get current bid/ask"""
book = client.get_order_book(token_id)
return {
"best_bid": float(book.bids[0].price) if book.bids else 0,
"best_ask": float(book.asks[0].price) if book.asks else 1
}
# Check position
position = get_balance(TOKEN_ID)
print(f"Current position: {position} shares")
# Get market
book = get_orderbook(TOKEN_ID)
print(f"Bid: {book['best_bid']:.2f}, Ask: {book['best_ask']:.2f}")
# Place a buy order (maker - inside spread)
buy_price = book['best_bid'] + 0.01 # 1 cent above bid
if buy_price < book['best_ask']: # Ensure we're maker
result = client.create_and_post_order(OrderArgs(
token_id=TOKEN_ID,
price=buy_price,
size=10.0,
side="BUY"
))
print(f"Buy order placed: {result}")
# Place a sell order (market sell via FOK)
if position > 0:
result = client.create_and_post_order(OrderArgs(
token_id=TOKEN_ID,
price=0.01, # Lowest price = immediate fill
size=position,
side="SELL"
))
print(f"Sold position: {result}")---
Error Handling
from py_clob_client.exceptions import PolyApiException
try:
result = client.create_and_post_order(OrderArgs(...))
except PolyApiException as e:
print(f"API Error: {e}")
except Exception as e:
print(f"Error: {e}")Common errors:
"insufficient balance"- Not enough USDC/tokens"invalid price"- Price outside 0.01-0.99 or wrong decimals"order too small"- Below minimum order size"market closed"- Market not accepting orders
---
CLI Commands (/poly)
Access Polymarket trading directly from Claude Code:
Market Data
/poly search <query> # Search markets
/poly market <condition-id> # Market details
/poly book <token-id> # View orderbookTrading
/poly buy <token-id> <size> <price> # Buy shares (limit)
/poly sell <token-id> <size> <price> # Sell shares (limit)
/poly orders # Open orders
/poly cancel <order-id> # Cancel order
/poly cancel all # Cancel all orders
/poly trades [limit] # Recent trade history
/poly balance # USDC + positionsAdvanced Orders
/poly twap <buy|sell> <token> <total> <price> [slices] [interval-sec]
/poly bracket <token> <size> <tp> <sl> # TP + SL bracket
/poly trigger buy <token> <size> <price> # Buy when price dropsNote: TWAP and bracket orders are persisted to the database and will automatically resume after restarts.
Auto-Redeem (Resolved Positions)
/poly redeem # One-time redeem all resolved positions
/poly redeem start # Start auto-polling (default: every 60s)
/poly redeem stop # Stop auto-polling
/poly redeem status # Check auto-redeemer status
/poly redeem pending # List positions pending redemption
/poly redeem <conditionId> <tokenId> # Redeem specific positionEnv vars:
POLY_REDEEM_INTERVAL_MS- Polling interval in ms (default: 60000 = 1 minute)
Real-Time Fills (WebSocket)
/poly fills # Connect fills WebSocket
/poly fills status # Show connection + recent fills
/poly fills stop # Disconnect fills WebSocket
/poly fills clear # Clear tracked fillsOrder Heartbeat
/poly heartbeat # Start heartbeat (keeps orders alive)
/poly heartbeat status # Check heartbeat status
/poly heartbeat stop # Stop heartbeat (orders cancelled in 10s)Account & Settlements
/poly settlements # View pending settlements from resolved markets
/poly allowance # Check USDC approval status
/poly orderbooks <token1> [token2] ... # Batch fetch orderbooks---
Complete ClobClient Method Reference
Health & Config (L0 - No Auth)
| Method | Parameters | Returns | Description |
|---|---|---|---|
get_ok() | - | dict | Health check |
get_server_time() | - | dict | Server timestamp |
get_address() | - | str | Your signer address |
get_collateral_address() | - | str | USDC contract |
get_conditional_address() | - | str | CTF contract |
get_exchange_address(neg_risk) | bool | str | Exchange contract |
Market Data (L0 - No Auth)
| Method | Parameters | Returns | Description |
|---|---|---|---|
get_midpoint(token_id) | str | dict | Mid price |
get_midpoints(params) | list[BookParams] | dict | Multiple mid prices |
get_price(token_id, side) | str, str | dict | Best price for side |
get_prices(params) | list[BookParams] | dict | Multiple prices |
get_spread(token_id) | str | dict | Bid-ask spread |
get_spreads(params) | list[BookParams] | dict | Multiple spreads |
get_order_book(token_id) | str | OrderBookSummary | Full orderbook |
get_order_books(params) | list[BookParams] | list | Multiple orderbooks |
get_last_trade_price(token_id) | str | dict | Last trade |
get_last_trades_prices(params) | list[BookParams] | dict | Multiple last trades |
get_tick_size(token_id) | str | TickSize | "0.1"/"0.01"/"0.001"/"0.0001" |
get_neg_risk(token_id) | str | bool | Is neg_risk market |
get_fee_rate_bps(token_id) | str | int | Fee in basis points |
Market Discovery (L0 - No Auth)
| Method | Parameters | Returns | Description |
|---|---|---|---|
get_markets(next_cursor) | str | dict | Paginated markets |
get_simplified_markets(next_cursor) | str | dict | Simplified markets |
get_sampling_markets(next_cursor) | str | dict | Featured markets |
get_market(condition_id) | str | dict | Single market |
get_market_trades_events(condition_id) | str | dict | Trade events |
Order Creation (L1 - Needs Private Key)
| Method | Parameters | Returns | Description |
|---|---|---|---|
create_order(order_args, options) | OrderArgs, CreateOrderOptions | dict | Sign limit order |
create_market_order(order_args, options) | MarketOrderArgs, CreateOrderOptions | dict | Sign market order |
calculate_market_price(token_id, side, amount, order_type) | str, str, float, OrderType | float | Expected fill price |
Order Posting (L2 - Needs API Creds)
| Method | Parameters | Returns | Description |
|---|---|---|---|
post_order(order, orderType, post_only) | SignedOrder, OrderType, bool | dict | Post single order |
post_orders(args) | list[PostOrdersArgs] | dict | Post batch orders |
create_and_post_order(order_args, options) | OrderArgs, PartialCreateOrderOptions | dict | Create + post (recommended) |
Order Cancellation (L2 - Needs API Creds)
| Method | Parameters | Returns | Description |
|---|---|---|---|
cancel(order_id) | str | dict | Cancel one order |
cancel_orders(order_ids) | list[str] | dict | Cancel multiple |
cancel_all() | - | dict | Cancel ALL orders |
cancel_market_orders(market, asset_id) | str, str | dict | Cancel by market |
Order Queries (L2 - Needs API Creds)
| Method | Parameters | Returns | Description |
|---|---|---|---|
get_orders(params, next_cursor) | OpenOrderParams, str | list | Get open orders |
get_order(order_id) | str | dict | Get specific order |
get_trades(params, next_cursor) | TradeParams, str | list | Trade history |
API Key Management (L2 - Needs API Creds)
| Method | Parameters | Returns | Description |
|---|---|---|---|
create_api_key(nonce) | int | ApiCreds | Create new key |
derive_api_key(nonce) | int | ApiCreds | Derive existing key |
create_or_derive_api_creds(nonce) | int | ApiCreds | Create or derive |
set_api_creds(creds) | ApiCreds | - | Set credentials |
get_api_keys() | - | dict | List your keys |
delete_api_key() | - | dict | Delete current key |
create_readonly_api_key() | - | ReadonlyApiKeyResponse | Readonly key |
get_readonly_api_keys() | - | list[str] | List readonly keys |
delete_readonly_api_key(key) | str | bool | Delete readonly key |
Balance & Allowance (L2 - Needs API Creds)
| Method | Parameters | Returns | Description |
|---|---|---|---|
get_balance_allowance(params) | BalanceAllowanceParams | dict | Check balance |
update_balance_allowance(params) | BalanceAllowanceParams | dict | Refresh allowance |
Advanced Features (L2 - Needs API Creds)
| Method | Parameters | Returns | Description |
|---|---|---|---|
post_heartbeat(heartbeat_id) | str | dict | Keep orders alive (10s timeout) |
is_order_scoring(params) | OrderScoringParams | dict | Check if earning rewards |
are_orders_scoring(params) | OrdersScoringParams | dict | Check multiple orders |
get_notifications() | - | dict | Get notifications |
drop_notifications(params) | DropNotificationParams | dict | Delete notifications |
get_closed_only_mode() | - | dict | Check closed-only status |
---
Contract Addresses (Polygon Mainnet)
USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" # Collateral token
CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" # Conditional tokens (ERC-1155)
EXCHANGE = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E" # Regular exchange
NEG_RISK = "0xC5d563A36AE78145C45a50134d48A1215220f80a" # Neg risk exchange (crypto)---
Quick Reference: Common Patterns
Check Position → Sell All
balance = get_token_balance(wallet, token_id)
if balance > 0:
client.create_and_post_order(OrderArgs(
token_id=token_id, price=0.01, size=balance, side="SELL"
))Maker Entry (No Fees)
book = client.get_order_book(token_id)
best_bid = float(book.bids[0].price) if book.bids else 0
maker_price = best_bid + 0.01 # 1 cent above bid
signed = client.create_order(OrderArgs(token_id=token_id, price=maker_price, size=100, side="BUY"))
client.post_order(signed, orderType=OrderType.GTC, post_only=True)Market Buy $50 Worth
signed = client.create_market_order(MarketOrderArgs(token_id=token_id, amount=50.0, side="BUY"))
client.post_order(signed, orderType=OrderType.FOK)Cancel Everything
client.cancel_all()---
WebSocket Channels (Real-Time Updates)
Polymarket provides WebSocket channels for real-time updates. No RPC needed - everything goes through CLOB.
WebSocket URLs
MARKET_WS = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
USER_WS = "wss://ws-subscriptions-clob.polymarket.com/ws/user"Market Channel (Public - No Auth)
Subscribe to orderbook updates, price changes, trades for any market.
import websocket
import json
def on_message(ws, message):
data = json.loads(message)
event_type = data.get("event_type")
if event_type == "book":
# Full orderbook snapshot (on subscribe + after trades)
print(f"Book update: {data['bids']}, {data['asks']}")
elif event_type == "price_change":
# Order placed/cancelled affecting price level
print(f"Price change: {data}")
elif event_type == "last_trade_price":
# Trade executed
print(f"Trade: {data['price']} x {data['size']}")
elif event_type == "tick_size_change":
# Tick size changed (price went extreme)
print(f"Tick size: {data['old_tick_size']} -> {data['new_tick_size']}")
def on_open(ws):
# Subscribe to specific token
ws.send(json.dumps({
"type": "subscribe",
"channel": "market",
"assets_ids": [TOKEN_ID] # List of token IDs
}))
ws = websocket.WebSocketApp(
"wss://ws-subscriptions-clob.polymarket.com/ws/market",
on_message=on_message,
on_open=on_open
)
ws.run_forever()User Channel (Authenticated - For Fills)
Subscribe to YOUR order updates, fills, trades. This is how you get fill notifications.
import websocket
import json
import hmac
import hashlib
import time
import base64
API_KEY = os.getenv("POLY_API_KEY")
API_SECRET = os.getenv("POLY_API_SECRET")
API_PASSPHRASE = os.getenv("POLY_API_PASSPHRASE")
def get_auth_headers():
"""Generate HMAC auth for WebSocket"""
timestamp = str(int(time.time()))
message = f"GET\n{timestamp}\n/ws/user"
signature = hmac.new(
base64.b64decode(API_SECRET),
message.encode(),
hashlib.sha256
).digest()
return {
"POLY-ADDRESS": WALLET_ADDRESS,
"POLY-SIGNATURE": base64.b64encode(signature).decode(),
"POLY-TIMESTAMP": timestamp,
"POLY-API-KEY": API_KEY,
"POLY-PASSPHRASE": API_PASSPHRASE
}
def on_message(ws, message):
data = json.loads(message)
event_type = data.get("event_type")
if event_type == "trade":
# YOUR FILL - order matched!
status = data.get("status") # MATCHED, MINED, CONFIRMED, FAILED
print(f"FILL: {data['side']} {data['size']} @ {data['price']}")
print(f" Status: {status}")
print(f" Trade ID: {data['id']}")
print(f" Market: {data['market']}")
if status == "CONFIRMED":
print(" ✓ Trade confirmed on-chain!")
elif status == "FAILED":
print(" ✗ Trade failed - check logs")
elif event_type == "order":
order_type = data.get("type") # PLACEMENT, UPDATE, CANCELLATION
print(f"ORDER {order_type}: {data['side']} {data['original_size']} @ {data['price']}")
print(f" Filled: {data.get('size_matched', 0)}")
if order_type == "CANCELLATION":
print(" Order cancelled")
def on_open(ws):
# Subscribe with auth
ws.send(json.dumps({
"type": "subscribe",
"channel": "user",
"auth": get_auth_headers(),
# Optional: filter to specific market
# "markets": [CONDITION_ID]
}))
ws = websocket.WebSocketApp(
"wss://ws-subscriptions-clob.polymarket.com/ws/user",
on_message=on_message,
on_open=on_open
)
ws.run_forever()Message Types Summary
Market Channel:
| Event | Trigger | Key Fields |
|---|---|---|
book | Subscribe, trades affect book | bids, asks, timestamp |
price_change | Order placed/cancelled | price, size, side |
last_trade_price | Trade executed | price, size, side |
tick_size_change | Price extreme (>0.96 or <0.04) | old_tick_size, new_tick_size |
User Channel:
| Event | Trigger | Key Fields |
|---|---|---|
trade | Your order filled | status, side, size, price, market |
order | Order placed/updated/cancelled | type (PLACEMENT/UPDATE/CANCELLATION), size_matched |
Trade Status Flow
MATCHED → MINED → CONFIRMED (success)
→ RETRYING → CONFIRMED/FAILEDKeepalive
Send PING every 10 seconds to keep connection alive:
import threading
def send_ping():
while True:
ws.send(json.dumps({"type": "ping"}))
time.sleep(10)
threading.Thread(target=send_ping, daemon=True).start()Real-Time Data Client (Alternative)
Polymarket also provides @polymarket/real-time-data-client for TypeScript:
import { RealTimeDataClient } from "@polymarket/real-time-data-client";
const client = new RealTimeDataClient({
onMessage: (msg) => console.log(msg),
onConnect: (c) => {
// Subscribe to user fills with auth
c.subscribe({
subscriptions: [{
topic: "clob_user",
type: "*",
clob_auth: {
key: API_KEY,
secret: API_SECRET,
passphrase: API_PASSPHRASE
}
}]
});
}
});
client.connect();---
No RPC Needed - CLOB Handles Everything
| Operation | Method |
|---|---|
| Get positions | GET /data/positions?user={address} via Gamma API |
| Get balance | client.get_balance_allowance() |
| Place orders | client.create_and_post_order() |
| Get fills | User WebSocket channel |
| Get prices | client.get_order_book() or Market WebSocket |
| Cancel orders | client.cancel() |
The only time you might use RPC is to check on-chain token balances independently, but even that's available via Gamma API positions endpoint.
/**
* Trading Polymarket CLI Skill
*
* Wired to:
* - src/feeds/polymarket (createPolymarketFeed - WebSocket, market search, orderbook)
* - src/execution (createExecutionService - CLOB order placement/cancellation)
*
* Commands:
* /poly search <query> - Search markets
* /poly market <condition-id> - Market details
* /poly book <token-id> - View orderbook
* /poly buy <token-id> <size> [price] - Buy shares
* /poly sell <token-id> <size> [price] - Sell shares
* /poly positions - View open orders
* /poly orders - View open orders
* /poly cancel <order-id|all> - Cancel orders
* /poly balance - USDC balance
* /poly whales - Whale activity monitoring
*/
import type { PolymarketFeed } from '../../../feeds/polymarket';
import type { ExecutionService } from '../../../execution';
import type { TwapOrder, BracketOrder, TriggerOrderManager, AutoRedeemer } from '../../../execution';
import { logger } from '../../../utils/logger';
// =============================================================================
// HELPERS
// =============================================================================
function formatNumber(n: number, decimals = 2): string {
if (isNaN(n)) return '0.00';
if (Math.abs(n) >= 1e9) return (n / 1e9).toFixed(decimals) + 'B';
if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(decimals) + 'M';
if (Math.abs(n) >= 1e3) return (n / 1e3).toFixed(decimals) + 'K';
return n.toFixed(decimals);
}
let feedInstance: PolymarketFeed | null = null;
let execInstance: ExecutionService | null = null;
async function getCircuitBreaker() {
const { getGlobalCircuitBreaker } = await import('../../../execution/circuit-breaker');
return getGlobalCircuitBreaker();
}
// Advanced order state
const activeTwaps = new Map<string, TwapOrder>();
const activeBrackets = new Map<string, BracketOrder>();
let triggerManager: TriggerOrderManager | null = null;
let autoRedeemer: AutoRedeemer | null = null;
let nextOrderId = 1;
async function getFeed(): Promise<PolymarketFeed> {
if (!feedInstance) {
const { createPolymarketFeed } = await import('../../../feeds/polymarket');
feedInstance = await createPolymarketFeed();
}
return feedInstance;
}
function getExecution(): ExecutionService | null {
if (!execInstance) {
const apiKey = process.env.POLY_API_KEY;
const apiSecret = process.env.POLY_API_SECRET;
const passphrase = process.env.POLY_API_PASSPHRASE;
const funderAddress = process.env.POLY_FUNDER_ADDRESS || '';
if (!apiKey || !apiSecret || !passphrase) return null;
try {
const { createExecutionService } = require('../../../execution');
execInstance = createExecutionService({
polymarket: {
address: funderAddress,
apiKey,
apiSecret,
apiPassphrase: passphrase,
privateKey: process.env.POLY_PRIVATE_KEY,
funderAddress,
signatureType: (() => { const n = process.env.POLY_SIGNATURE_TYPE ? Number(process.env.POLY_SIGNATURE_TYPE) : undefined; return n !== undefined && Number.isNaN(n) ? undefined : n; })(),
},
dryRun: process.env.DRY_RUN === 'true',
});
} catch {
return null;
}
}
return execInstance;
}
// =============================================================================
// HELP TEXT
// =============================================================================
function helpText(): string {
return [
'**Polymarket Trading Commands**',
'',
'**Market Data:**',
' /poly search <query> - Search markets',
' /poly market <condition-id> - Market details',
' /poly book <token-id> - View orderbook',
'',
'**Trading:**',
' /poly buy <token-id> <size> <price> - Buy shares (limit)',
' /poly sell <token-id> <size> <price> - Sell shares (limit)',
' /poly orders - Open orders',
' /poly cancel <order-id> - Cancel order',
' /poly cancel all - Cancel all orders',
' /poly trades [limit] - Recent trade history',
' /poly balance - USDC + positions',
'',
'**Advanced Orders:**',
' /poly redeem - Redeem all resolved positions',
' /poly redeem <cond-id> <token-id> - Redeem specific position',
' /poly twap <buy|sell> <token> <total> <price> [slices] [interval-sec]',
' /poly twap status - Active TWAP progress',
' /poly twap cancel <id> - Cancel a TWAP',
' /poly bracket <token> <size> <tp> <sl> - TP + SL bracket',
' /poly bracket status - Active brackets',
' /poly bracket cancel <id> - Cancel a bracket',
' /poly trigger buy <token> <size> <price> [limit] - Buy when price drops',
' /poly trigger sell <token> <size> <price> [limit] - Sell when price rises',
' /poly trigger cancel <id> - Cancel a trigger',
' /poly triggers - List active triggers',
'',
'**Cross-Platform:**',
' /poly route <token> <buy|sell> <size> - Compare prices across platforms',
'',
'**Real-Time Fills:**',
' /poly fills - Connect fills WebSocket',
' /poly fills status - Show connection + recent fills',
' /poly fills stop - Disconnect fills WebSocket',
' /poly fills clear - Clear tracked fills',
'',
'**Order Heartbeat:**',
' /poly heartbeat - Start heartbeat (keeps orders alive)',
' /poly heartbeat status - Check heartbeat status',
' /poly heartbeat stop - Stop heartbeat (orders cancelled in 10s)',
'',
'**Account & Settlements:**',
' /poly settlements - View pending settlements from resolved markets',
' /poly allowance - Check USDC approval status',
' /poly orderbooks <token1> [token2] ... - Batch fetch orderbooks',
'',
'**Env vars:** POLY_API_KEY, POLY_API_SECRET, POLY_API_PASSPHRASE',
' Optional: POLY_PRIVATE_KEY, POLY_FUNDER_ADDRESS',
'',
'**Examples:**',
' /poly search bitcoin',
' /poly buy 1234567890 100 0.65',
' /poly sell 1234567890 50 0.70',
' /poly book 1234567890',
].join('\n');
}
// =============================================================================
// MARKET DATA HANDLERS
// =============================================================================
async function handleSearch(query: string): Promise<string> {
if (!query) return 'Usage: /poly search <query>';
try {
const feed = await getFeed();
const markets = await feed.searchMarkets(query);
if (markets.length === 0) {
return `No Polymarket markets found for "${query}"`;
}
const lines = ['**Polymarket Markets**', ''];
for (const m of markets.slice(0, 15)) {
lines.push(` [${m.id}] ${m.question}`);
const outcomeStrs = m.outcomes.slice(0, 4).map(o => {
const tokenSuffix = o.tokenId ? ` (${o.tokenId.slice(0, 8)}...)` : '';
return `${o.name}: ${(o.price * 100).toFixed(0)}c${tokenSuffix}`;
});
lines.push(` ${outcomeStrs.join(' | ')} | Vol: $${formatNumber(m.volume24h)}`);
}
if (markets.length > 15) {
lines.push('', `...and ${markets.length - 15} more`);
}
return lines.join('\n');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error searching: ${message}`;
}
}
async function handleMarket(marketId: string): Promise<string> {
if (!marketId) return 'Usage: /poly market <condition-id>';
try {
const feed = await getFeed();
const market = await feed.getMarket('polymarket', marketId);
if (!market) {
return `Market ${marketId} not found`;
}
const lines = [
`**${market.question}**`,
'',
`Condition ID: ${market.id}`,
`Slug: ${market.slug}`,
`Platform: Polymarket`,
market.description ? `Description: ${typeof market.description === 'string' ? market.description.slice(0, 200) : ''}` : '',
'',
'**Outcomes:**',
];
for (const o of market.outcomes) {
const tokenId = o.tokenId || o.id;
lines.push(` ${o.name}: ${(o.price * 100).toFixed(1)}c`);
lines.push(` Token: ${tokenId}`);
}
lines.push(
'',
`Volume: $${formatNumber(market.volume24h)}`,
`Liquidity: $${formatNumber(market.liquidity)}`,
market.endDate ? `End Date: ${market.endDate.toLocaleDateString()}` : '',
`Resolved: ${market.resolved ? 'Yes' : 'No'}`,
'',
`URL: ${market.url}`,
);
return lines.filter(l => l !== '').join('\n');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
async function handleOrderbook(tokenId: string): Promise<string> {
if (!tokenId) return 'Usage: /poly book <token-id>';
try {
const feed = await getFeed();
const orderbook = await feed.getOrderbook('polymarket', tokenId);
if (!orderbook) {
return `No orderbook found for token ${tokenId}`;
}
const lines = [
`**Orderbook: ${tokenId.slice(0, 20)}...**`,
'',
`Mid: ${(orderbook.midPrice * 100).toFixed(1)}c | Spread: ${(orderbook.spread * 100).toFixed(2)}c`,
'',
'**Bids:**',
];
for (const [price, size] of orderbook.bids.slice(0, 5)) {
lines.push(` ${(price * 100).toFixed(1)}c - ${formatNumber(size)} shares`);
}
lines.push('', '**Asks:**');
for (const [price, size] of orderbook.asks.slice(0, 5)) {
lines.push(` ${(price * 100).toFixed(1)}c - ${formatNumber(size)} shares`);
}
// Also show imbalance if enough data
if (orderbook.bids.length > 0 && orderbook.asks.length > 0) {
try {
const { calculateOrderbookImbalance } = await import('../../../execution');
const imbalance = calculateOrderbookImbalance({
bids: orderbook.bids,
asks: orderbook.asks,
midPrice: orderbook.midPrice,
});
lines.push(
'',
'**Imbalance:**',
` Signal: ${imbalance.signal.toUpperCase()} (${(imbalance.confidence * 100).toFixed(0)}% confidence)`,
` Bid/Ask Ratio: ${imbalance.bidAskRatio.toFixed(2)}`,
` Score: ${imbalance.imbalanceScore.toFixed(3)}`,
);
} catch {
// Imbalance calculation not available, skip
}
}
return lines.join('\n');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
// =============================================================================
// TRADING HANDLERS
// =============================================================================
async function handleBuy(tokenId: string, sizeStr: string, priceStr: string): Promise<string> {
const exec = getExecution();
if (!exec) {
return 'Set POLY_API_KEY, POLY_API_SECRET, and POLY_API_PASSPHRASE to trade on Polymarket.';
}
if (!tokenId || !sizeStr) {
return 'Usage: /poly buy <token-id> <size> <price>\nExample: /poly buy 1234567890 100 0.65';
}
const size = parseFloat(sizeStr);
if (isNaN(size) || size <= 0) {
return 'Invalid size. Must be a positive number.';
}
// Circuit breaker pre-check
const cb = await getCircuitBreaker();
if (!cb.canTrade()) {
const state = cb.getState();
return `**Trade blocked** — Circuit breaker tripped: ${state.tripReason || 'unknown'}\nUse \`/risk reset\` to re-arm.`;
}
// If no price, try to use market price with slippage protection
if (!priceStr) {
try {
const result = await exec.protectedBuy({
platform: 'polymarket',
marketId: tokenId,
tokenId,
price: 0.99, // Will be adjusted by protectedBuy
size,
});
cb.recordTrade({
pnlUsd: 0,
success: result.success,
sizeUsd: size * 0.50,
error: result.error,
});
if (result.success) {
return `BUY ${size} shares (market order, slippage-protected) (Order: ${result.orderId})`;
}
return `Order failed: ${result.error}`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
const price = parseFloat(priceStr);
if (isNaN(price) || price < 0.01 || price > 0.99) {
return 'Invalid price. Must be between 0.01 and 0.99 (e.g., 0.65 for 65c).';
}
try {
// Auto-detect neg_risk for crypto markets
let negRisk: boolean | undefined;
try {
const { checkPolymarketNegRisk } = await import('../../../execution');
negRisk = await checkPolymarketNegRisk(tokenId);
} catch {
// Neg risk check not critical, proceed without
}
const result = await exec.buyLimit({
platform: 'polymarket',
marketId: tokenId,
tokenId,
price,
size,
negRisk,
});
cb.recordTrade({
pnlUsd: 0,
success: result.success,
sizeUsd: size * price,
error: result.error,
});
if (result.success) {
try {
const { getGlobalPositionManager } = await import('../../../execution/position-manager');
const pm = getGlobalPositionManager();
pm.updatePosition({
platform: 'polymarket',
marketId: tokenId,
tokenId,
outcomeName: 'Yes',
side: 'long',
size,
entryPrice: result.avgFillPrice || price,
currentPrice: result.avgFillPrice || price,
openedAt: new Date(),
});
} catch { /* position tracking non-critical */ }
return [
`BUY ${size} shares @ ${(price * 100).toFixed(0)}c`,
`Token: ${tokenId.slice(0, 20)}...`,
`Order: ${result.orderId}`,
result.transactionHash ? `Tx: ${result.transactionHash}` : '',
negRisk ? '(neg-risk market)' : '',
].filter(Boolean).join('\n');
}
return `Order failed: ${result.error}`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
async function handleSell(tokenId: string, sizeStr: string, priceStr: string): Promise<string> {
const exec = getExecution();
if (!exec) {
return 'Set POLY_API_KEY, POLY_API_SECRET, and POLY_API_PASSPHRASE to trade on Polymarket.';
}
if (!tokenId || !sizeStr) {
return 'Usage: /poly sell <token-id> <size> <price>\nExample: /poly sell 1234567890 50 0.70';
}
const size = parseFloat(sizeStr);
if (isNaN(size) || size <= 0) {
return 'Invalid size. Must be a positive number.';
}
// Circuit breaker pre-check
const cb = await getCircuitBreaker();
if (!cb.canTrade()) {
const state = cb.getState();
return `**Trade blocked** — Circuit breaker tripped: ${state.tripReason || 'unknown'}\nUse \`/risk reset\` to re-arm.`;
}
if (!priceStr) {
try {
const result = await exec.protectedSell({
platform: 'polymarket',
marketId: tokenId,
tokenId,
price: 0.01, // Will be adjusted by protectedSell
size,
});
cb.recordTrade({
pnlUsd: 0,
success: result.success,
sizeUsd: size * 0.50,
error: result.error,
});
if (result.success) {
return `SELL ${size} shares (market order, slippage-protected) (Order: ${result.orderId})`;
}
return `Order failed: ${result.error}`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
const price = parseFloat(priceStr);
if (isNaN(price) || price < 0.01 || price > 0.99) {
return 'Invalid price. Must be between 0.01 and 0.99.';
}
try {
let negRisk: boolean | undefined;
try {
const { checkPolymarketNegRisk } = await import('../../../execution');
negRisk = await checkPolymarketNegRisk(tokenId);
} catch {
// Neg risk check not critical
}
const result = await exec.sellLimit({
platform: 'polymarket',
marketId: tokenId,
tokenId,
price,
size,
negRisk,
});
cb.recordTrade({
pnlUsd: 0,
success: result.success,
sizeUsd: size * price,
error: result.error,
});
if (result.success) {
try {
const { getGlobalPositionManager } = await import('../../../execution/position-manager');
const pm = getGlobalPositionManager();
const existing = pm.getPositionsByPlatform('polymarket')
.find(p => p.tokenId === tokenId && p.status === 'open');
if (existing) {
pm.closePosition(existing.id, result.avgFillPrice || price, 'manual');
}
} catch { /* position tracking non-critical */ }
return [
`SELL ${size} shares @ ${(price * 100).toFixed(0)}c`,
`Token: ${tokenId.slice(0, 20)}...`,
`Order: ${result.orderId}`,
result.transactionHash ? `Tx: ${result.transactionHash}` : '',
negRisk ? '(neg-risk market)' : '',
].filter(Boolean).join('\n');
}
return `Order failed: ${result.error}`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
async function handleOrders(): Promise<string> {
const exec = getExecution();
if (!exec) {
return 'Set POLY_API_KEY, POLY_API_SECRET, and POLY_API_PASSPHRASE to view orders.';
}
try {
const orders = await exec.getOpenOrders('polymarket');
if (orders.length === 0) {
return 'No open Polymarket orders';
}
const lines = ['**Polymarket Open Orders**', ''];
for (const o of orders) {
const tokenDisplay = o.tokenId ? o.tokenId.slice(0, 12) + '...' : o.marketId;
lines.push(
` [${o.orderId.slice(0, 10)}...] ${o.side.toUpperCase()} @ ${(o.price * 100).toFixed(0)}c x ${o.remainingSize}/${o.originalSize}`
);
lines.push(` Token: ${tokenDisplay} | Filled: ${o.filledSize}`);
}
return lines.join('\n');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
async function handleCancel(orderId: string): Promise<string> {
const exec = getExecution();
if (!exec) {
return 'Set POLY_API_KEY, POLY_API_SECRET, and POLY_API_PASSPHRASE to cancel orders.';
}
if (!orderId) {
return 'Usage: /poly cancel <order-id|all>';
}
try {
if (orderId.toLowerCase() === 'all') {
const count = await exec.cancelAllOrders('polymarket');
return `Cancelled ${count} Polymarket order(s)`;
}
const success = await exec.cancelOrder('polymarket', orderId);
return success ? `Order ${orderId} cancelled` : `Failed to cancel order ${orderId}`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
async function handleBalance(): Promise<string> {
const funderAddress = process.env.POLY_FUNDER_ADDRESS;
if (!funderAddress) {
return 'Set POLY_FUNDER_ADDRESS to check USDC balance.';
}
try {
// Try CLOB API first
const { getPolymarketBalance, getPolymarketPositions } = await import('../../../execution/index');
const apiKey = process.env.POLY_API_KEY;
const apiSecret = process.env.POLY_API_SECRET;
const apiPassphrase = process.env.POLY_API_PASSPHRASE;
if (apiKey && apiSecret && apiPassphrase) {
const auth = { apiKey, apiSecret, apiPassphrase, address: funderAddress };
const [balanceData, positions] = await Promise.all([
getPolymarketBalance(auth, funderAddress),
getPolymarketPositions(auth, funderAddress),
]);
let output = [
'**Polymarket Balance**',
'',
`Wallet: ${funderAddress.slice(0, 6)}...${funderAddress.slice(-4)}`,
`USDC: $${formatNumber(balanceData.balance)}`,
`Allowance: $${formatNumber(balanceData.allowance)}`,
];
if (positions.length > 0) {
output.push('', '**Positions:**');
let totalValue = 0;
let totalPnl = 0;
for (const p of positions.slice(0, 10)) {
const value = p.size * p.currentPrice;
totalValue += value;
totalPnl += p.unrealizedPnl;
const pnlStr = p.unrealizedPnl >= 0 ? `+$${p.unrealizedPnl.toFixed(2)}` : `-$${Math.abs(p.unrealizedPnl).toFixed(2)}`;
output.push(` ${p.tokenId.slice(0, 10)}... ${p.size.toFixed(0)} @ ${(p.currentPrice * 100).toFixed(0)}c = $${value.toFixed(2)} (${pnlStr})`);
}
if (positions.length > 10) {
output.push(` ... and ${positions.length - 10} more`);
}
output.push('', `**Total Position Value:** $${formatNumber(totalValue)}`);
output.push(`**Unrealized PnL:** ${totalPnl >= 0 ? '+' : ''}$${formatNumber(totalPnl)}`);
}
return output.join('\n');
}
// Fallback: Query USDC balance on Polygon via public RPC
const USDC_CONTRACT = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'; // USDC on Polygon
const balanceData = `0x70a08231000000000000000000000000${funderAddress.slice(2).toLowerCase()}`;
const controller = new AbortController();
const rpcTimeout = setTimeout(() => controller.abort(), 10_000);
let result: { result?: string };
try {
const response = await fetch('https://polygon-rpc.com/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_call',
params: [{ to: USDC_CONTRACT, data: balanceData }, 'latest'],
id: 1,
}),
signal: controller.signal,
});
if (!response.ok) throw new Error(`RPC error: ${response.status}`);
result = await response.json() as { result?: string };
} finally {
clearTimeout(rpcTimeout);
}
const rawBalance = parseInt(result.result || '0x0', 16);
const balance = rawBalance / 1e6; // USDC has 6 decimals
return [
'**Polymarket Balance**',
'',
`Wallet: ${funderAddress.slice(0, 6)}...${funderAddress.slice(-4)}`,
`USDC: $${formatNumber(balance)}`,
'',
'_Set POLY_API_KEY, POLY_API_SECRET, POLY_API_PASSPHRASE to see positions_',
].join('\n');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error fetching balance: ${message}`;
}
}
async function handleWhales(): Promise<string> {
try {
// Use the whale tracker module if available
const { createWhaleTracker } = await import('../../../feeds/polymarket/whale-tracker');
const tracker = createWhaleTracker();
// Start the tracker to collect trades
if (!tracker.isRunning()) {
await tracker.start();
}
// Get recent whale trades
const trades = tracker.getRecentTrades(10);
if (!trades || trades.length === 0) {
// Fall back to top whales
const topWhales = tracker.getTopWhales(5);
if (topWhales.length === 0) {
return 'No whale activity detected yet. The tracker is now running and will collect data.';
}
const lines = ['**Top Whales**', ''];
for (const w of topWhales) {
lines.push(` ${w.address.slice(0, 10)}... | $${formatNumber(w.totalValue)} | WR: ${w.winRate.toFixed(0)}%`);
lines.push(` Positions: ${w.positions.length} | Last active: ${w.lastActive.toLocaleTimeString()}`);
}
return lines.join('\n');
}
const lines = ['**Recent Whale Trades**', ''];
for (const t of trades) {
lines.push(` ${t.side.toUpperCase()} $${formatNumber(t.usdValue)} @ ${(t.price * 100).toFixed(0)}c`);
lines.push(` ${t.outcome} on ${t.marketQuestion || t.marketId.slice(0, 20) + '...'}`);
lines.push(` Maker: ${t.maker.slice(0, 10)}... | ${new Date(t.timestamp).toLocaleTimeString()}`);
}
return lines.join('\n');
} catch {
return 'Whale tracking not available. The whale-tracker module may not be configured.';
}
}
// =============================================================================
// ADVANCED ORDER HANDLERS
// =============================================================================
let autoRedeemerStarted = false;
async function ensureAutoRedeemer(): Promise<string | null> {
const privateKey = process.env.POLY_PRIVATE_KEY;
const funderAddress = process.env.POLY_FUNDER_ADDRESS;
const apiKey = process.env.POLY_API_KEY;
const apiSecret = process.env.POLY_API_SECRET;
const passphrase = process.env.POLY_API_PASSPHRASE;
if (!privateKey || !funderAddress || !apiKey || !apiSecret || !passphrase) {
return 'Set POLY_PRIVATE_KEY, POLY_FUNDER_ADDRESS, POLY_API_KEY, POLY_API_SECRET, POLY_API_PASSPHRASE to redeem.';
}
if (!autoRedeemer) {
const { createAutoRedeemer } = await import('../../../execution');
autoRedeemer = createAutoRedeemer({
polymarketAuth: { address: funderAddress, apiKey, apiSecret, apiPassphrase: passphrase },
privateKey,
funderAddress,
pollIntervalMs: parseInt(process.env.POLY_REDEEM_INTERVAL_MS || '60000', 10),
dryRun: process.env.DRY_RUN === 'true',
});
// Set up event listeners for logging
autoRedeemer.on('redemption_success', (result) => {
logger.info({ conditionId: result.conditionId, usdc: result.usdcRedeemed }, 'Auto-redemption successful');
});
autoRedeemer.on('redemption_failed', (result) => {
logger.warn({ conditionId: result.conditionId, error: result.error }, 'Auto-redemption failed');
});
autoRedeemer.on('position_expired', (data) => {
logger.info({ conditionId: data.conditionId, outcome: data.outcome }, 'Position expired (losing side)');
});
}
return null;
}
async function handleRedeem(subCmd?: string, arg2?: string): Promise<string> {
// Subcommands: start, stop, status, pending, or conditionId/tokenId for manual redeem
if (subCmd === 'start') {
const error = await ensureAutoRedeemer();
if (error) return error;
if (autoRedeemerStarted) {
return 'Auto-redeemer already running. Use `/poly redeem status` to check.';
}
autoRedeemer!.start();
autoRedeemerStarted = true;
const interval = parseInt(process.env.POLY_REDEEM_INTERVAL_MS || '60000', 10) / 1000;
return `**Auto-redeemer started**\n\nPolling every ${interval}s for resolved positions.\nUse \`/poly redeem stop\` to stop.`;
}
if (subCmd === 'stop') {
if (!autoRedeemer || !autoRedeemerStarted) {
return 'Auto-redeemer is not running.';
}
autoRedeemer.stop();
autoRedeemerStarted = false;
return 'Auto-redeemer stopped.';
}
if (subCmd === 'status') {
const error = await ensureAutoRedeemer();
if (error) return error;
const pending = autoRedeemer!.getPendingRedemptions();
const lines = [
'**Auto-Redeemer Status**',
'',
`Running: ${autoRedeemerStarted ? 'Yes' : 'No'}`,
`Pending redemptions: ${pending.length}`,
];
if (pending.length > 0) {
lines.push('', '**Pending:**');
for (const p of pending) {
lines.push(` ${p.conditionId.slice(0, 12)}... | ${p.shares} shares | ${p.outcome || 'unknown'}`);
if (p.marketQuestion) lines.push(` ${p.marketQuestion.slice(0, 60)}...`);
}
}
return lines.join('\n');
}
if (subCmd === 'pending') {
const error = await ensureAutoRedeemer();
if (error) return error;
const pending = autoRedeemer!.getPendingRedemptions();
if (pending.length === 0) return 'No pending redemptions.';
const lines = ['**Pending Redemptions**', ''];
for (const p of pending) {
lines.push(` Condition: ${p.conditionId}`);
lines.push(` Token: ${p.tokenId}`);
lines.push(` Shares: ${p.shares}`);
if (p.outcome) lines.push(` Outcome: ${p.outcome}`);
if (p.marketQuestion) lines.push(` Market: ${p.marketQuestion}`);
lines.push('');
}
return lines.join('\n');
}
// Manual redeem: /poly redeem [conditionId] [tokenId] or /poly redeem (all)
const error = await ensureAutoRedeemer();
if (error) return error;
try {
// If both provided, redeem specific position
if (subCmd && arg2) {
const result = await autoRedeemer!.redeemPosition(subCmd, arg2);
if (result.success) {
return [
'**Redemption Successful**',
'',
`Condition: ${result.conditionId}`,
`Token: ${result.tokenId}`,
`Shares: ${result.shares}`,
`USDC: $${result.usdcRedeemed.toFixed(2)}`,
result.txHash ? `Tx: ${result.txHash}` : '',
].filter(Boolean).join('\n');
}
return `Redemption failed: ${result.error}`;
}
// Otherwise, redeem all resolved positions
const results = await autoRedeemer!.redeemAll();
if (results.length === 0) {
return 'No resolved positions to redeem.';
}
const lines = ['**Redemption Results**', ''];
for (const r of results) {
const status = r.success ? 'OK' : 'FAIL';
lines.push(` [${status}] ${r.conditionId.slice(0, 12)}... | ${r.shares} shares | $${r.usdcRedeemed.toFixed(2)} USDC`);
if (r.txHash) lines.push(` Tx: ${r.txHash}`);
if (r.error) lines.push(` Error: ${r.error}`);
}
const successes = results.filter(r => r.success);
const totalUsdc = successes.reduce((s, r) => s + r.usdcRedeemed, 0);
lines.push('', `Total: ${successes.length}/${results.length} redeemed, $${totalUsdc.toFixed(2)} USDC`);
return lines.join('\n');
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return `Error: ${message}`;
}
}
async function handleTwap(subCmdOrSide: string, tokenIdOrId?: string, totalStr?: string, priceStr?: string, slicesStr?: string, intervalStr?: string): Promise<string> {
// Sub-commands: status, cancel
if (subCmdOrSide === 'status') {
if (activeTwaps.size === 0) return 'No active TWAP orders.';
const lines = ['**Active TWAP Orders**', ''];
for (const [id, twap] of activeTwaps) {
const p = twap.getProgress();
const pct = p.totalSize > 0 ? ((p.filledSize / p.totalSize) * 100).toFixed(0) : '0';
lines.push(` [${id}] ${pct}% filled | ${p.filledSize}/${p.totalSize} | ${p.slicesCompleted}/${p.slicesTotal} slices | avg ${(p.avgFillPrice * 100).toFixed(1)}c | ${p.status}`);
}
return lines.join('\n');
}
if (subCmdOrSide === 'cancel') {
if (!tokenIdOrId) return 'Usage: /poly twap cancel <id>';
const twap = activeTwaps.get(tokenIdOrId);
if (!twap) return `TWAP order ${tokenIdOrId} not found. Active: ${[...activeTwaps.keys()].join(', ') || 'none'}`;
await twap.cancel();
activeTwaps.delete(tokenIdOrId);
return `TWAP ${tokenIdOrId} cancelled.`;
}
// Create new TWAP: twap <buy|sell> <token> <total> <price> [slices] [interval-sec]
const side = subCmdOrSide?.toLowerCase();
if (side !== 'buy' && side !== 'sell') {
return 'Usage: /poly twap <buy|sell> <token> <total> <price> [slices] [interval-sec]\n /poly twap status\n /poly twap cancel <id>';
}
const exec = getExecution();
if (!exec) {
return 'Set POLY_API_KEY, POLY_API_SECRET, and POLY_API_PASSPHRASE to trade.';
}
const tokenId = tokenIdOrId;
if (!tokenId || !totalStr || !priceStr) {
return 'Usage: /poly twap <buy|sell> <token> <total> <price> [slices] [interval-sec]';
}
const totalSize = parseFloat(totalStr);
const price = parseFloat(priceStr);
const slices = slicesStr ? parseInt(slicesStr, 10) : 5;
const intervalSec = intervalStr ? parseInt(intervalStr, 10) : 30;
if (isNaN(totalSize) || totalSize <= 0) return 'Invalid total size.';
if (isNaN(price) || price < 0.01 || price > 0.99) return 'Invalid price (0.01-0.99).';
if (isNaN(slices) || slices < 1) return 'Invalid slices count.';
if (isNaN(intervalSec) || intervalSec < 1) return 'Invalid interval.';
try {
let negRisk: boolean | undefined;
try {
const { checkPolymarketNegRisk } = await import('../../../execution');
negRisk = await checkPolymarketNegRisk(tokenId);
} catch { /* non-critical */ }
const { createTwapOrder } = await import('../../../execution');
const id = `twap_${nextOrderId++}`;
const sliceSize = totalSize / slices;
const twap = createTwapOrder(
exec,
{ platform: 'polymarket', marketId: tokenId, tokenId, side: side as 'buy' | 'sell', price, negRisk },
{ totalSize, sliceSize, intervalMs: intervalSec * 1000 }
);
activeTwaps.set(id, twap);
twap.on('completed', () => { activeTwaps.delete(id); });
twap.on('cancelled', () => { activeTwaps.delete(id); });
twap.start();
return `TWAP started: ${side.toUpperCase()} ${totalSize} shares @ ${(price * 100).toFixed(0)}c in ${slices} slices every ${intervalSec}s (ID: ${id})`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
async function handleBracket(subCmdOrToken: string, sizeStrOrId?: string, tpPriceStr?: string, slPriceStr?: string): Promise<string> {
// Sub-commands: status, cancel
if (subCmdOrToken === 'status') {
if (activeBrackets.size === 0) return 'No active bracket orders.';
const lines = ['**Active Bracket Orders**', ''];
for (const [id, bracket] of activeBrackets) {
const s = bracket.getStatus();
lines.push(` [${id}] TP: ${s.takeProfitOrderId?.slice(0, 10) || '—'}... | SL: ${s.stopLossOrderId?.slice(0, 10) || '—'}... | ${s.status}`);
if (s.filledSide) lines.push(` Filled: ${s.filledSide} @ ${s.fillPrice ? (s.fillPrice * 100).toFixed(1) + 'c' : '—'}`);
}
return lines.join('\n');
}
if (subCmdOrToken === 'cancel') {
if (!sizeStrOrId) return 'Usage: /poly bracket cancel <id>';
const bracket = activeBrackets.get(sizeStrOrId);
if (!bracket) return `Bracket ${sizeStrOrId} not found. Active: ${[...activeBrackets.keys()].join(', ') || 'none'}`;
await bracket.cancel();
activeBrackets.delete(sizeStrOrId);
return `Bracket ${sizeStrOrId} cancelled.`;
}
// Create new bracket: bracket <token> <size> <tp> <sl>
const exec = getExecution();
if (!exec) {
return 'Set POLY_API_KEY, POLY_API_SECRET, and POLY_API_PASSPHRASE to trade.';
}
const tokenId = subCmdOrToken;
if (!tokenId || !sizeStrOrId || !tpPriceStr || !slPriceStr) {
return 'Usage: /poly bracket <token> <size> <tp-price> <sl-price>\n /poly bracket status\n /poly bracket cancel <id>';
}
const size = parseFloat(sizeStrOrId);
const tpPrice = parseFloat(tpPriceStr);
const slPrice = parseFloat(slPriceStr);
if (isNaN(size) || size <= 0) return 'Invalid size.';
if (isNaN(tpPrice) || tpPrice < 0.01 || tpPrice > 0.99) return 'Invalid take-profit price (0.01-0.99).';
if (isNaN(slPrice) || slPrice < 0.01 || slPrice > 0.99) return 'Invalid stop-loss price (0.01-0.99).';
try {
let negRisk: boolean | undefined;
try {
const { checkPolymarketNegRisk } = await import('../../../execution');
negRisk = await checkPolymarketNegRisk(tokenId);
} catch { /* non-critical */ }
const { createBracketOrder } = await import('../../../execution');
const id = `bracket_${nextOrderId++}`;
const bracket = createBracketOrder(exec, {
platform: 'polymarket',
marketId: tokenId,
tokenId,
size,
side: 'long',
takeProfitPrice: tpPrice,
stopLossPrice: slPrice,
negRisk,
});
activeBrackets.set(id, bracket);
bracket.on('take_profit_hit', () => { activeBrackets.delete(id); });
bracket.on('stop_loss_hit', () => { activeBrackets.delete(id); });
bracket.on('cancelled', () => { activeBrackets.delete(id); });
await bracket.start();
return `Bracket set: TP @ ${(tpPrice * 100).toFixed(0)}c / SL @ ${(slPrice * 100).toFixed(0)}c for ${size} shares (ID: ${id})`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
async function handleTrigger(subCmd: string, args: string[]): Promise<string> {
// List triggers
if (subCmd === 'list' || !subCmd) {
if (!triggerManager) return 'No trigger orders. Use /poly trigger buy or /poly trigger sell to create one.';
const triggers = triggerManager.getTriggers();
if (triggers.length === 0) return 'No trigger orders.';
const lines = ['**Trigger Orders**', ''];
for (const t of triggers) {
const cond = t.config.condition;
const condStr = cond.type === 'price_below' ? `<= ${(cond.price * 100).toFixed(0)}c`
: cond.type === 'price_above' ? `>= ${(cond.price * 100).toFixed(0)}c`
: cond.type === 'price_cross' ? `cross ${(cond.price * 100).toFixed(0)}c ${cond.direction}`
: `spread < ${cond.maxSpread}`;
const { order } = t.config;
lines.push(` [${t.id}] ${order.side.toUpperCase()} ${order.size} when ${condStr} | ${t.status}`);
lines.push(` Token: ${(t.config.tokenId || t.config.marketId).slice(0, 20)}...`);
if (t.triggeredAt) lines.push(` Triggered: ${t.triggeredAt.toLocaleTimeString()}`);
}
return lines.join('\n');
}
// Cancel trigger
if (subCmd === 'cancel') {
if (!args[0]) return 'Usage: /poly trigger cancel <trigger-id>';
if (!triggerManager) return 'No active trigger manager.';
triggerManager.cancelTrigger(args[0]);
return `Trigger ${args[0]} cancelled.`;
}
// Create trigger: buy/sell <token> <size> <trigger-price> [limit-price]
const side = subCmd.toLowerCase();
if (side !== 'buy' && side !== 'sell') {
return [
'Usage:',
' /poly trigger buy <token> <size> <trigger-price> [limit-price]',
' /poly trigger sell <token> <size> <trigger-price> [limit-price]',
' /poly trigger cancel <trigger-id>',
' /poly triggers (or /poly trigger list)',
].join('\n');
}
const exec = getExecution();
if (!exec) {
return 'Set POLY_API_KEY, POLY_API_SECRET, and POLY_API_PASSPHRASE to trade.';
}
const [tokenId, sizeStr, triggerPriceStr, limitPriceStr] = args;
if (!tokenId || !sizeStr || !triggerPriceStr) {
return `Usage: /poly trigger ${side} <token> <size> <trigger-price> [limit-price]`;
}
const size = parseFloat(sizeStr);
const triggerPrice = parseFloat(triggerPriceStr);
const limitPrice = limitPriceStr ? parseFloat(limitPriceStr) : undefined;
if (isNaN(size) || size <= 0) return 'Invalid size.';
if (isNaN(triggerPrice) || triggerPrice < 0.01 || triggerPrice > 0.99) return 'Invalid trigger price (0.01-0.99).';
if (limitPrice !== undefined && (isNaN(limitPrice) || limitPrice < 0.01 || limitPrice > 0.99)) return 'Invalid limit price (0.01-0.99).';
try {
if (!triggerManager) {
const feed = await getFeed();
const { createTriggerOrderManager } = await import('../../../execution');
triggerManager = createTriggerOrderManager(exec, feed);
triggerManager.start();
}
const conditionType = side === 'buy' ? 'price_below' : 'price_above';
const triggerId = triggerManager.addTrigger({
platform: 'polymarket',
marketId: tokenId,
tokenId,
condition: { type: conditionType, price: triggerPrice },
order: {
side: side as 'buy' | 'sell',
size,
price: limitPrice,
},
});
const condDesc = side === 'buy'
? `price <= ${(triggerPrice * 100).toFixed(0)}c`
: `price >= ${(triggerPrice * 100).toFixed(0)}c`;
const limitDesc = limitPrice ? ` @ ${(limitPrice * 100).toFixed(0)}c limit` : ' (market)';
return `Trigger set: ${side.toUpperCase()} ${size} shares when ${condDesc}${limitDesc} (ID: ${triggerId})`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
}
// =============================================================================
// MAIN HANDLER
// =============================================================================
async function execute(args: string): Promise<string> {
const parts = args.trim().split(/\s+/);
const cmd = parts[0]?.toLowerCase() || 'help';
try {
switch (cmd) {
case 'search':
case 's':
return handleSearch(parts.slice(1).join(' '));
case 'market':
case 'm':
return handleMarket(parts[1]);
case 'book':
case 'orderbook':
case 'ob':
return handleOrderbook(parts[1]);
case 'buy':
case 'b':
return handleBuy(parts[1], parts[2], parts[3]);
case 'sell':
return handleSell(parts[1], parts[2], parts[3]);
case 'positions':
case 'pos':
case 'orders':
case 'o':
return handleOrders();
case 'cancel':
return handleCancel(parts[1]);
case 'balance':
case 'bal':
return handleBalance();
case 'whales':
case 'whale':
return handleWhales();
case 'redeem':
return handleRedeem(parts[1], parts[2]);
case 'twap':
return handleTwap(parts[1], parts[2], parts[3], parts[4], parts[5], parts[6]);
case 'bracket':
return handleBracket(parts[1], parts[2], parts[3], parts[4]);
case 'trigger':
return handleTrigger(parts[1], parts.slice(2));
case 'triggers':
return handleTrigger('list', []);
case 'route':
case 'compare': {
if (!parts[1] || !parts[2] || !parts[3]) {
return 'Usage: /poly route <token-id> <buy|sell> <size>';
}
const routeMarketId = parts[1];
const routeSide = parts[2] as 'buy' | 'sell';
const routeSize = parseFloat(parts[3]);
if (routeSide !== 'buy' && routeSide !== 'sell') return 'Side must be buy or sell.';
if (isNaN(routeSize) || routeSize <= 0) return 'Invalid size.';
try {
const { createSmartRouter } = await import('../../../execution/smart-router');
const { createFeedManager } = await import('../../../feeds/index');
const feeds = await createFeedManager({
polymarket: { enabled: true },
kalshi: { enabled: true },
manifold: { enabled: false },
metaculus: { enabled: false },
drift: { enabled: false },
news: { enabled: false },
} as any);
const router = createSmartRouter(feeds, { mode: 'balanced' });
const routeResult = await router.findBestRoute({ marketId: routeMarketId, side: routeSide, size: routeSize });
let output = `**Route: ${routeSide.toUpperCase()} ${routeSize} on ${routeMarketId.slice(0, 20)}...**\n\n`;
output += `Best: ${routeResult.bestRoute.platform} @ ${(routeResult.bestRoute.netPrice * 100).toFixed(1)}c\n`;
output += `Fees: $${routeResult.bestRoute.estimatedFees.toFixed(4)}\n`;
output += `Slippage: ${routeResult.bestRoute.slippage.toFixed(2)}%\n\n`;
if (routeResult.allRoutes.length > 1) {
output += `**All Platforms:**\n`;
for (const r of routeResult.allRoutes) {
output += ` ${r.platform}: ${(r.netPrice * 100).toFixed(1)}c (fees: $${r.estimatedFees.toFixed(4)})\n`;
}
}
output += `\n${routeResult.recommendation}`;
return output;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Route error: ${message}`;
}
}
case 'circuit': {
const cb = await getCircuitBreaker();
const state = cb.getState();
return `**Circuit Breaker**\n\n` +
`Status: ${state.isTripped ? 'TRIPPED' : 'Armed'}\n` +
`Session PnL: $${state.sessionPnL.toFixed(2)}\n` +
`Daily trades: ${state.dailyTrades}\n` +
`Consecutive losses: ${state.consecutiveLosses}\n` +
`Error rate: ${(state.errorRate * 100).toFixed(0)}%\n` +
(state.tripReason ? `Trip reason: ${state.tripReason}\n` : '') +
`\nUse \`/risk trip\` / \`/risk reset\` to manually control.`;
}
case 'fills': {
// /poly fills [status|stop|clear]
const subcommand = parts[1]?.toLowerCase();
const exec = getExecution();
if (!exec) {
return 'Polymarket trading not configured. Set env vars and restart.';
}
if (subcommand === 'status') {
const connected = exec.isFillsWebSocketConnected();
const fills = exec.getTrackedFills();
const recentFills = fills.slice(-5);
let output = `**Fills WebSocket Status**\n`;
output += `Connection: ${connected ? 'Connected' : 'Disconnected'}\n`;
output += `Tracked fills: ${fills.length}\n\n`;
if (recentFills.length > 0) {
output += `**Recent Fills:**\n`;
for (const fill of recentFills) {
output += `- ${fill.orderId.slice(0, 8)}... ${fill.side.toUpperCase()} ${fill.size}@${fill.price} [${fill.status}]`;
if (fill.transactionHash) {
output += ` tx:${fill.transactionHash.slice(0, 10)}...`;
}
output += '\n';
}
}
return output;
}
if (subcommand === 'stop') {
exec.disconnectFillsWebSocket();
return 'Fills WebSocket disconnected.';
}
if (subcommand === 'clear') {
const cleared = exec.clearOldFills(0); // Clear all
return `Cleared ${cleared} tracked fills.`;
}
// Default: connect and show status
try {
await exec.connectFillsWebSocket();
// Set up fill logging
exec.onFill((fill) => {
logger.info(
{ fill },
`FILL: ${fill.side.toUpperCase()} ${fill.size}@${fill.price} [${fill.status}]`
);
});
return `Fills WebSocket connected!\n\n` +
`Real-time fill notifications are now active.\n` +
`Fill events will be logged as orders are matched, mined, and confirmed.\n\n` +
`Commands:\n` +
`- \`/poly fills status\` - Show connection status and recent fills\n` +
`- \`/poly fills stop\` - Disconnect WebSocket\n` +
`- \`/poly fills clear\` - Clear tracked fills`;
} catch (err) {
return `Failed to connect fills WebSocket: ${err instanceof Error ? err.message : String(err)}`;
}
}
case 'trades':
case 'history': {
// /poly trades [limit]
const limit = parseInt(parts[1], 10) || 20;
const apiKey = process.env.POLY_API_KEY;
const apiSecret = process.env.POLY_API_SECRET;
const apiPassphrase = process.env.POLY_API_PASSPHRASE;
if (!apiKey || !apiSecret || !apiPassphrase) {
return 'Set POLY_API_KEY, POLY_API_SECRET, POLY_API_PASSPHRASE to view trades.';
}
try {
const { getPolymarketTrades } = await import('../../../execution/index');
const auth = { apiKey, apiSecret, apiPassphrase, address: process.env.POLY_FUNDER_ADDRESS || '' };
const trades = await getPolymarketTrades(auth, limit);
if (trades.length === 0) {
return 'No recent trades found.';
}
let output = `**Recent Trades (${trades.length})**\n\n`;
for (const t of trades) {
const time = t.timestamp.toLocaleTimeString();
output += `${time} ${t.side} ${t.size.toFixed(0)}@${(t.price * 100).toFixed(0)}c`;
if (t.transactionHash) {
output += ` [${t.transactionHash.slice(0, 8)}...]`;
}
output += '\n';
}
return output;
} catch (error) {
return `Error fetching trades: ${error instanceof Error ? error.message : String(error)}`;
}
}
case 'heartbeat':
case 'hb': {
// /poly heartbeat [start|stop|status]
const subcommand = parts[1]?.toLowerCase() || 'start';
const exec = getExecution();
if (!exec) {
return 'Polymarket trading not configured. Set env vars and restart.';
}
if (subcommand === 'status') {
const active = exec.isHeartbeatActive();
return `**Heartbeat Status**\n` +
`Active: ${active ? 'Yes - orders will stay alive' : 'No - orders may be cancelled after 10s'}\n\n` +
`Commands:\n` +
`- \`/poly heartbeat start\` - Start heartbeat\n` +
`- \`/poly heartbeat stop\` - Stop heartbeat`;
}
if (subcommand === 'stop') {
exec.stopHeartbeat();
return 'Heartbeat stopped. Open orders will be cancelled within 10 seconds.';
}
// Default: start
try {
const hbId = await exec.startHeartbeat();
return `Heartbeat started!\n\n` +
`ID: ${hbId}\n` +
`Your orders will now stay alive. Heartbeat is sent automatically every 8 seconds.\n\n` +
`**Important:** Run \`/poly heartbeat stop\` when done trading, or orders will persist.`;
} catch (err) {
return `Failed to start heartbeat: ${err instanceof Error ? err.message : String(err)}`;
}
}
case 'settlements':
case 'settle': {
// /poly settlements - Show pending settlements for resolved markets
const exec = getExecution();
if (!exec) {
return 'Polymarket trading not configured. Set env vars and restart.';
}
const settlements = await exec.getPendingSettlements();
if (settlements.length === 0) {
return '**No Pending Settlements**\n\nYou have no claimable settlements from resolved markets.';
}
let output = '**Pending Settlements**\n\n';
let totalClaimable = 0;
for (const s of settlements) {
output += `• ${s.outcome.toUpperCase()} @ ${s.marketId.slice(0, 12)}...\n`;
output += ` Size: ${s.size.toFixed(2)} | Claimable: $${s.claimable.toFixed(2)}\n`;
totalClaimable += s.claimable;
}
output += `\n**Total Claimable: $${totalClaimable.toFixed(2)}**`;
output += '\n\n_Use Polymarket UI to claim settlements._';
return output;
}
case 'allowance':
case 'approval': {
// /poly allowance - Check USDC approval status for trading
const exec = getExecution();
if (!exec) {
return 'Polymarket trading not configured. Set env vars and restart.';
}
const allowance = await exec.getUSDCAllowance();
const isApproved = allowance > 1000000; // > $1M effectively unlimited
return '**USDC Allowance Status**\n\n' +
`Current Allowance: ${isApproved ? '✅ Unlimited' : `$${allowance.toFixed(2)}`}\n` +
(isApproved
? 'Your wallet is approved for trading.'
: 'You may need to approve USDC spending via the Polymarket UI before trading.');
}
case 'orderbooks':
case 'obs': {
// /poly orderbooks <tokenId1> [tokenId2] ... - Batch fetch orderbooks
const tokenIds = parts.slice(1).filter(t => t.length > 10);
if (tokenIds.length === 0) {
return 'Usage: `/poly orderbooks <tokenId1> [tokenId2] ...`\n\nFetch orderbooks for multiple tokens in one call.';
}
const exec = getExecution();
if (!exec) {
return 'Polymarket trading not configured. Set env vars and restart.';
}
const orderbooks = await exec.getOrderbooksBatch(tokenIds);
let output = `**Orderbooks (${orderbooks.size} tokens)**\n\n`;
for (const [tokenId, ob] of orderbooks) {
if (!ob) {
output += `• ${tokenId.slice(0, 12)}...: _Failed to fetch_\n`;
continue;
}
const bestBid = ob.bids[0]?.[0] || 0;
const bestAsk = ob.asks[0]?.[0] || 1;
const spread = ((bestAsk - bestBid) * 100).toFixed(1);
output += `• ${tokenId.slice(0, 12)}...: Bid ${bestBid.toFixed(2)} / Ask ${bestAsk.toFixed(2)} (${spread}% spread)\n`;
}
return output;
}
case 'help':
default:
return helpText();
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error({ error: message, args }, 'Polymarket command failed');
return `Error: ${message}`;
}
}
export default {
name: 'trading-polymarket',
description: 'Polymarket trading - CLOB orders, positions, orderbooks',
commands: ['/poly', '/trading-polymarket'],
handle: execute,
};
Related skills
FAQ
What library does it use?
The official py_clob_client library against the Polymarket CLOB host.
What order types are supported?
GTC, FOK, GTD, and FAK order types.