Skip to main content
Polymarket is the largest prediction market, running on Polygon. Horizon supports the CLOB (Central Limit Order Book) API with EIP-712 signed orders.

Quick Setup

hz.run(
    name="poly_mm",
    exchange=hz.Polymarket(
        private_key="0x...",
        api_key="...",
        api_secret="...",
        api_passphrase="...",
    ),
    mode="live",
    ...
)

Credentials

There are three ways to provide credentials:
When only private_key is set, Horizon automatically derives API credentials from the CLOB /auth/derive-api-key endpoint on startup (requires eth-account package):
exch = hz.Polymarket(private_key="0xYOUR_PRIVATE_KEY")
You can also call the derivation manually before hz.run():
exch = hz.Polymarket(private_key="0xYOUR_PRIVATE_KEY")
exch.derive_api_credentials()  # derives api_key, api_secret, api_passphrase
This requires pip install eth-account. The automatic derivation only happens in live mode. Call derive_api_credentials() manually if you need the credentials earlier.

Polymarket Configuration

@dataclass
class Polymarket:
    private_key: str | None = None
    clob_url: str = "https://clob.polymarket.com"
    gamma_url: str = "https://gamma-api.polymarket.com"
    api_key: str | None = None
    api_secret: str | None = None
    api_passphrase: str | None = None
FieldDefaultDescription
private_keyNoneEthereum private key for EIP-712 signing
clob_urlhttps://clob.polymarket.comCLOB API base URL
gamma_urlhttps://gamma-api.polymarket.comGamma API for market metadata
api_keyNoneCLOB API key
api_secretNoneCLOB API secret (base64-encoded, used as HMAC-SHA256 key)
api_passphraseNoneCLOB API passphrase
The api_secret must be base64-encoded. This is the format returned by the CLOB /auth/derive-api-key endpoint. If you provide a raw hex string, authentication will fail with “invalid api_secret (bad base64)”.

EIP-712 Signing

Polymarket orders require EIP-712 typed data signatures for the CTF Exchange contract on Polygon (chainId 137). Horizon handles this entirely in Rust using k256 (secp256k1). No Python crypto dependencies needed. The signing flow:
  1. Build CtfOrder struct (token_id, maker address, price, size, nonce, etc.)
  2. Generate a cryptographically random salt (UUID v4)
  3. Compute EIP-712 struct hash and domain separator
  4. Sign the digest with the private key (ECDSA on secp256k1)
  5. Submit the signed order to the CLOB API

Order Types & Time-in-Force

Polymarket supports four time-in-force modes for limit orders, plus market orders:
Order TypeTimeInForceBehavior
LimitGTCGood-til-Canceled. Rests on the book until filled or canceled.
LimitGTDGood-til-Date. Expires at a specified time.
LimitFOKFill-or-Kill. Must fill entirely in one match or is rejected.
LimitFAKFill-and-Kill (Immediate-or-Cancel). Fills what it can, cancels the rest.
Market(auto)Uses FOK semantics. Executes immediately at best available price or fails.
# Limit order with GTC (default)
engine.submit_order(market_id, hz.Quote(bid=0.45, ask=0.55, size=10))

# Market orders use FOK under the hood
req = hz.OrderRequest(
    market_id="will-btc-hit-100k",
    order_type=hz.OrderType.Market,
    order_side=hz.OrderSide.Buy,
    size=10.0,
)

Order Management

Submit Orders

Orders are submitted through the pipeline via hz.Quote objects, or directly via the engine:
engine.submit_quotes(market_id, [quote], hz.Side.Yes,
                     token_id=token_id, neg_risk=True)

Cancel Orders

# Cancel a specific order by ID
engine.cancel_order(order_id)

# Cancel all orders for a specific market
engine.cancel_market(market_id)

# Cancel ALL orders across all markets
engine.cancel_all()
In the pipeline loop, hz.run() automatically calls cancel_market() before submitting new quotes each cycle (cancel-before-requote pattern). You typically don’t need to cancel manually.

Amend Orders

engine.amend_order(
    order_id,
    new_price=0.50,
    new_size=20.0,
    token_id=token_id,
    neg_risk=True,
)
Amend validates parameters before canceling the existing order to prevent leaving you with no order if validation fails.

Token ID Routing

Polymarket uses numeric token IDs (not market slugs) to identify outcomes. Each market has a Yes token and a No token:
market = hz.Market(
    id="will-btc-hit-100k",
    yes_token_id="123456789",
    no_token_id="987654321",
    condition_id="0x...",
    neg_risk=True,
)

market.token_id(Side.Yes)  # "123456789"
market.token_id(Side.No)   # "987654321"
In live mode, hz.run() automatically resolves token IDs from the Gamma API. You don’t need to set them manually unless you want to override the resolution. The PolymarketBook feed also auto-resolves slugs to CLOB token IDs for the WebSocket subscription.

Market Resolution

When mode="live", Horizon queries the Gamma API to resolve market metadata:
GET https://gamma-api.polymarket.com/markets?slug=will-btc-hit-100k
This populates:
  • yes_token_id / no_token_id (from the tokens array or clobTokenIds field)
  • condition_id
  • neg_risk
  • exchange = "polymarket"
The resolution uses a 10-second timeout. If the API returns no results or an error, the slug is logged as a warning and left unresolved. If the token ID cannot be extracted from the response, a RuntimeError is raised to prevent trading with an invalid market.

Position Reconciliation

Horizon can fetch your current Polymarket positions for reconciliation:
GET https://clob.polymarket.com/positions (authenticated)
This is called internally and returns Position objects with:
  • market_id (the asset/token ID)
  • side (Yes or No, parsed from the outcome field)
  • size and avg_entry_price
  • exchange = "polymarket"
Positions with zero size are filtered out. Unknown outcome strings default to Yes with a warning.

Fill Polling

Polymarket fills arrive via polling the /trades endpoint. Each cycle, drain_fills() calls poll_fills_async() which:
  1. Queries recent trades for tracked order IDs
  2. Deduplicates against previously seen fill IDs (FIFO eviction, capped at 10k)
  3. Returns new fills with exchange-assigned timestamps
Open order tracking is also capped at 10k entries per market to prevent unbounded memory growth.

Order Scoring & Rewards

Polymarket pays USDC liquidity rewards to qualifying market makers. You can check if a live order qualifies:
scoring = engine.check_order_scoring("order-id-123")
print(f"Qualifies for rewards: {scoring}")  # True or False
This calls the authenticated GET /order-scoring?order_id=... endpoint. Returns False for non-Polymarket exchanges. For full rewards tracking (eligible markets, rebate estimation, pipeline integration), see the Rewards Tracker documentation.

HMAC-SHA256 Authentication

API requests are authenticated with HMAC-SHA256:
  • The api_secret is base64-decoded and used as the HMAC-SHA256 key
  • Each request includes headers: POLY_TIMESTAMP, POLY_API_KEY, POLY_SIGNATURE, and POLY_PASSPHRASE
  • The signature covers the timestamp, HTTP method, request path, and body
HMAC-SHA256(base64_decode(api_secret), timestamp + method + path + body)
The resulting signature is base64-encoded and sent in the POLY_SIGNATURE header.