---
title: "Horizon + Polymarket"
description: "Trade on Polymarket with Horizon, the largest prediction market on Polygon."
icon: "/images/exchanges/polymarket.svg"
---

[Polymarket](https://polymarket.com) is the largest prediction market, running on Polygon. Horizon supports the CLOB (Central Limit Order Book) API with EIP-712 signed orders.

## Quick Setup

```python
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:

<Tabs>
  <Tab title="Private key only">
    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):

    ```python
    exch = hz.Polymarket(private_key="0xYOUR_PRIVATE_KEY")
    ```

    You can also call the derivation manually before `hz.run()`:

    ```python
    exch = hz.Polymarket(private_key="0xYOUR_PRIVATE_KEY")
    exch.derive_api_credentials()  # derives api_key, api_secret, api_passphrase
    ```

    <Note>
      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.
    </Note>
  </Tab>
  <Tab title="Full credentials">
    ```python
    hz.Polymarket(
        private_key="0x...",
        api_key="...",
        api_secret="...",       # must be base64-encoded
        api_passphrase="...",
    )
    ```
  </Tab>
  <Tab title="Environment variables">
    ```bash
    export POLYMARKET_PRIVATE_KEY="0x..."
    export POLYMARKET_API_KEY="..."
    export POLYMARKET_API_SECRET="..."
    export POLYMARKET_API_PASSPHRASE="..."
    ```

    ```python
    hz.Polymarket()  # reads from env
    ```
  </Tab>
</Tabs>

## Polymarket Configuration

```python
@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
```

| Field | Default | Description |
|-------|---------|-------------|
| `private_key` | `None` | Ethereum private key for EIP-712 signing |
| `clob_url` | `https://clob.polymarket.com` | CLOB API base URL |
| `gamma_url` | `https://gamma-api.polymarket.com` | Gamma API for market metadata |
| `api_key` | `None` | CLOB API key |
| `api_secret` | `None` | CLOB API secret (base64-encoded, used as HMAC-SHA256 key) |
| `api_passphrase` | `None` | CLOB API passphrase |

<Warning>
  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)".
</Warning>

## 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 Type | TimeInForce | Behavior |
|------------|-------------|----------|
| Limit | `GTC` | Good-til-Canceled. Rests on the book until filled or canceled. |
| Limit | `GTD` | Good-til-Date. Expires at a specified time. |
| Limit | `FOK` | Fill-or-Kill. Must fill entirely in one match or is rejected. |
| Limit | `FAK` | Fill-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. |

```python
# 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:

```python
engine.submit_quotes(market_id, [quote], hz.Side.Yes,
                     token_id=token_id, neg_risk=True)
```

### Cancel Orders

```python
# 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()
```

<Note>
  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.
</Note>

### Amend Orders

```python
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:

```python
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"
```

<Tip>
  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.
</Tip>

### 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:

```python
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](/rewards) 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.
