> ## Documentation Index
> Fetch the complete documentation index at: https://mathematicalcompany.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Horizon + Polymarket

> Trade on Polymarket with Horizon, the largest prediction market on Polygon.

[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.

<Warning>
  **Persistence is required for live trading.** Polymarket fill polling uses
  in-memory dedup state. Without `db_path` set, restarts will re-process all
  recent fills and double-count them in your positions. Always pass a `db_path`
  to `hz.run()` (or `Engine()`) when running Polymarket strategies in live mode:

  ```python theme={null}
  hz.run(name="poly_mm", db_path="./my_strategy.db", mode="live", ...)
  ```

  This is also why we recommend running `hz.run()` single-threaded — concurrent
  cancel/submit from multiple Python threads has a TOCTOU race window between
  the HTTP submit and the local order tracker insert.
</Warning>

## Quick Setup

```python theme={null}
hz.run(
    name="poly_mm",
    exchange=hz.Polymarket(
        private_key="0x...",
        api_key="...",
        api_secret="...",
        api_passphrase="...",
    ),
    db_path="./poly_mm.db",  # REQUIRED for live trading
    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 theme={null}
    exch = hz.Polymarket(private_key="0xYOUR_PRIVATE_KEY")
    ```

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

    ```python theme={null}
    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 theme={null}
    hz.Polymarket(
        private_key="0x...",
        api_key="...",
        api_secret="...",       # must be base64-encoded
        api_passphrase="...",
    )
    ```
  </Tab>

  <Tab title="Environment variables">
    ```bash theme={null}
    export POLYMARKET_PRIVATE_KEY="0x..."
    export POLYMARKET_API_KEY="..."
    export POLYMARKET_API_SECRET="..."
    export POLYMARKET_API_PASSPHRASE="..."
    # Optional: for Gnosis Safe / contract wallet
    export POLYMARKET_FUNDER="0xYourSafeAddress"
    export POLYMARKET_SIGNATURE_TYPE="2"
    ```

    ```python theme={null}
    hz.Polymarket()  # reads from env
    ```
  </Tab>
</Tabs>

## Polymarket Configuration

```python theme={null}
@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
    funder: str | None = None
    signature_type: int = 0
```

| 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                                                                      |
| `funder`         | `None`                             | Gnosis Safe or proxy wallet address that holds funds (required for `signature_type` > 0) |
| `signature_type` | `0`                                | Signing mode: `0` = EOA, `1` = Polymarket Proxy, `2` = Gnosis Safe                       |

<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

## Gnosis Safe / Contract Wallet Support

If you connected to Polymarket via MetaMask or WalletConnect through the web interface, your account likely uses a **Gnosis Safe proxy wallet**. In this setup, your EOA (the private key you control) is an owner of a Safe contract that actually holds your USDC and conditional tokens.

To trade with this configuration, set `signature_type=2` and provide the Safe address as `funder`:

```python theme={null}
hz.run(
    name="safe_strategy",
    exchange=hz.Polymarket(
        private_key="0xYOUR_EOA_PRIVATE_KEY",
        funder="0xYOUR_GNOSIS_SAFE_ADDRESS",
        signature_type=2,
        api_key="...",
        api_secret="...",
        api_passphrase="...",
    ),
    mode="live",
    ...
)
```

Or via environment variables:

```bash theme={null}
export POLYMARKET_PRIVATE_KEY="0x..."
export POLYMARKET_FUNDER="0xYourSafeAddress"
export POLYMARKET_SIGNATURE_TYPE="2"
```

### How It Works

| Field              | EOA (type 0)    | Gnosis Safe (type 2)    |
| ------------------ | --------------- | ----------------------- |
| `maker` (EIP-712)  | EOA address     | Safe address (`funder`) |
| `signer` (EIP-712) | EOA address     | EOA address             |
| `signatureType`    | 0               | 2                       |
| Funds held by      | EOA directly    | Safe contract           |
| ECDSA signer       | EOA private key | Same EOA private key    |

The signature is always ECDSA from the EOA. The on-chain contract verifies that `getSafeAddress(signer) == maker` using CREATE2, confirming the EOA is an owner of the Safe.

### Supported Signature Types

| Type | Name               | Description                                                                  |
| ---- | ------------------ | ---------------------------------------------------------------------------- |
| `0`  | `EOA`              | Direct wallet signing (default). Maker and signer are the same address.      |
| `1`  | `POLY_PROXY`       | Polymarket Proxy wallet. For accounts created via Polymarket's proxy system. |
| `2`  | `POLY_GNOSIS_SAFE` | Gnosis Safe wallet. For accounts connected via MetaMask/WalletConnect.       |

### Finding Your Safe Address

Your Gnosis Safe address is the address shown on polymarket.com under your profile. It is distinct from the EOA address that appears in MetaMask. You can also find it in the Polymarket account settings or on [PolygonScan](https://polygonscan.com) by looking at the Safe deployment transaction from your EOA.

<Warning>
  Using the wrong `funder` address or mismatched `signature_type` will result in "invalid signature" errors from the CLOB API. Ensure the funder matches the Safe address that holds your Polymarket funds.
</Warning>

## 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 theme={null}
# 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 theme={null}
engine.submit_quotes(market_id, [quote], hz.Side.Yes,
                     token_id=token_id, neg_risk=True)
```

### Cancel Orders

```python theme={null}
# Cancel a specific order by ID
engine.cancel(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 theme={null}
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 theme={null}
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 theme={null}
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.
