Nightly docs
Polymarket
Founded in 2020, Polymarket is a decentralized prediction market platform that enables traders to speculate on event outcomes by buying and selling outcome tokens.
NautilusTrader provides a venue integration for data and execution via Polymarket's Central Limit Order Book (CLOB) API.
The adapter is implemented in Rust and exposed to Python at
nautilus_trader.adapters.polymarket; data, execution, signing, and WebSocket
operations therefore have the same behavior from Rust and Python.
The adapter handles order preparation and signing for several wallet configurations. This guide covers market data, trade execution, session key administration, and Deposit Wallet position operations.
Installation
The Python package includes the Polymarket adapter; no adapter-specific extra is required.
To install the latest pre-release build:
uv pip install --pre nautilus_trader --extra-index-url=https://packages.nautechsystems.io/simpleTo build the Python package from source, run from the repository root:
make build-debugFor development wheels and source-build prerequisites, see the installation guide.
Examples
The maintained examples are available in
crates/adapters/polymarket/examples
for Rust. For Python, use the Rust-native data tester,
execution tester,
or Up/Down smoke tester.
The exec tester configurations apply the
close precision needed for Polymarket market SELL orders.
Binary options
A binary option is a type of financial exotic
option contract in which traders bet on the outcome of a yes-or-no proposition. If the
prediction is correct, the trader receives a fixed payout; otherwise, they receive nothing.
NautilusTrader represents Polymarket outcome tokens as BinaryOption instruments.
Polymarket uses pUSD as the collateral token for trading.
Polymarket documentation
Polymarket offers resources for different audiences:
- Polymarket Learn: Educational content and guides for users to understand the platform and how to engage with it.
- Polymarket CLOB API: Technical documentation for developers interacting with the Polymarket CLOB API.
Overview
This guide assumes a trader is setting up for both live market data feeds and trade execution. The Rust implementation includes multiple components, which can be used together or separately depending on the use case.
PolymarketWebSocketClient: Low-level WebSocket API connectivity built on the Nautilus RustWebSocketClient.PolymarketInstrumentProvider: Instrument parsing and loading functionality forBinaryOptioninstruments.PolymarketDataClient: A market data feed manager.PolymarketExecutionClient: A trade execution gateway.PolymarketDataClientFactory: Factory for Polymarket data clients (used by the live node builder).PolymarketExecutionClientFactory: Factory for Polymarket execution clients (used by the live node builder).PolymarketPositionClient: Deposit Wallet split, merge, and redeem operations.PolymarketSessionKeyClient: Owner-operated session authorization, listing, and revocation.
Python users configure live nodes through the exported configuration and factory classes, and call
position operations through PolymarketPositionClient and session administration through
PolymarketSessionKeyClient. The direct WebSocket, provider, data client, and execution client types
are Rust-only implementation components.
pUSD
pUSD is the collateral token used for trading on Polymarket. It is a standard ERC-20 token on Polygon, backed by USDC.
The proxy contract address is 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB on Polygon. Direct on-chain funding wraps Polygon USDC.e (bridged USDC) into pUSD through the CollateralOnramp. The Bridge API can also deposit supported assets from other chains and credit pUSD after conversion.
Wallets and accounts
To trade on Polymarket via NautilusTrader, use a Polygon-compatible wallet, such as MetaMask.
Signature types
Polymarket supports multiple signature types for order signing and verification:
| Signature Type | Wallet Type | Description | Use Case |
|---|---|---|---|
0 | EOA (Externally Owned Account) | Standard EIP712 signatures from wallets with direct private key control. | Adapter default. Allowlisted EOA trading where the funder and signer are the same address. |
1 | Proxy Wallet | Legacy smart contract wallet created through email or social login. | Requires the Proxy Wallet funder address. |
2 | Safe Wallet | Legacy Gnosis Safe wallet created with an external browser wallet. | Requires the Safe Wallet funder address. |
3 | Deposit Wallet | ERC-1271 smart wallet used for new Polymarket account wallets. | Requires the Deposit Wallet funder; API credentials stay bound to the signer. |
Polymarket uses Deposit Wallets for account wallets deployed on or after May 4, 2026. Direct EOA trading requires an allowlisted EOA. See the Polymarket wallet and authentication guide for the account types and setup flows.
NautilusTrader defaults to signature type 0 (EOA). Set signature_type to use another supported
wallet type. Proxy signature clients fail during construction unless funder is present and differs
from the signing address.
A single wallet address is supported per trader instance when using environment variables, or multiple wallets can be configured through multiple execution client instances.
Fund your wallet with pUSD before submitting orders. An unfunded wallet produces the "not enough balance or allowance" API error.
Setting EOA allowances
The adapter includes a direct on-chain allowance command for EOA accounts. Use it only when the
funding wallet is the signer (PolymarketSignatureType::Eoa). Fund the EOA with POL for gas, set
POLYMARKET_PK, and run:
cargo run -p nautilus-polymarket --bin polymarket-set-allowancesThe command grants maximum pUSD and CTF approvals to the CTF Exchange, Neg Risk CTF Exchange, and
NegRiskCtfCollateralAdapter. It uses https://polygon.drpc.org by default; set
POLYGON_RPC_URL to use another Polygon RPC endpoint. Run it again if Polymarket changes the
required contracts.
The command grants approvals only; it does not revoke approvals for contracts that are no longer targets. Treat revocation as a separate on-chain operation and confirm that no remaining redemption or settlement flow depends on the legacy approval before submitting it.
Setting smart-wallet allowances
Do not run the EOA command for a proxy, Safe, or Deposit Wallet funder. It signs transactions from the EOA key and cannot grant approvals from a smart contract wallet.
Use Polymarket's wallet and authentication flow
to submit the approvals from the account wallet. Deposit Wallet approvals use an ordered WALLET
batch authorized by the signer and submitted through the Relayer. Safe and Proxy Wallet approvals
need their wallet-specific SDK payloads.
Refreshing and verifying allowances
After the approval transaction confirms, refresh the CLOB cache. Rust callers can use
PolymarketClobHttpClient::update_balance_allowance with AssetType::Collateral for pUSD. Use
AssetType::Conditional with a conditional token ID for a conditional-token allowance. Both forms
also need the account's signature type. The authenticated request maps to
GET /balance-allowance/update. Use PolymarketSignatureType::Poly1271 for a Deposit Wallet.
The balance-allowance endpoint has two decoding paths:
| Path | Used for | Allowance handling | Meaning of success |
|---|---|---|---|
PolymarketClobHttpClient::get_balance_allowance | Reading spender allowance evidence. | Requires the plural allowances map; rejects a missing map, a non-null legacy singular value, malformed or non-canonical keys, and semantic duplicates such as case variants. | Balance plus an unambiguous map; required targets and amounts still need checking. |
| Internal balance-only projection | Account state refresh and market-buy fee adjustment. | Ignores allowance fields; its return type cannot expose or grant approval authority. | Balance only; required CLOB spender approvals remain unproven. |
Use the strict path whenever a decision depends on allowance evidence so ambiguous wire data cannot become approval authority.
Account balances
Balance calculation
Balance refreshes use the venue-reported pUSD total and derive locked collateral from the adapter's local cache of open BUY order reservations for the execution client's account. Each reservation uses the cached limit price and remaining quantity. Submitted orders awaiting acceptance and orders absent from the execution cache are not included. SELL orders reserve outcome tokens rather than pUSD and do not contribute to locked collateral. Locked collateral is capped at the reported total, with free balance equal to total minus locked, following the balance model.
Refresh timing
The adapter seeds this cache from the execution cache at connect and updates it as the core processes order events, including reconciled events. The initial balance refresh precedes startup reconciliation; orders discovered during reconciliation affect balances on the next refresh. Refreshes occur at connect, on account queries, after finalized trade updates, and on user WebSocket reconnect. Account balances do not change on each order event. Balance refreshes do not request open orders.
These balances are estimates: HTTP balance responses and order events can reflect different points in time. They do not guarantee funds are reserved before submission or account for spending on trades awaiting settlement.
API keys
CLOB credentials
The execution client requires CLOB L2 credentials. Create or derive them with Polymarket's
API authentication flow. The
adapter provides a command that reads POLYMARKET_PK and prints the created or derived credentials:
cargo run -p nautilus-polymarket --bin polymarket-create-api-keySet the returned values as:
POLYMARKET_API_KEYPOLYMARKET_API_SECRETPOLYMARKET_PASSPHRASE
The credentials authenticate the private-key signer, not a proxy or Deposit Wallet funder. The public data client does not require these credentials.
Relayer credentials
Deposit Wallet split, merge, and redeem operations also require a Relayer API key. Create one under Settings > API Keys > Relayer API Keys, as described in Connect your account, then set:
POLYMARKET_RELAYER_API_KEYPOLYMARKET_RELAYER_SIGNER_ADDRESS
POLYMARKET_RELAYER_SIGNER_ADDRESS is the signer address shown when the Relayer key is created.
The position client also reads POLYMARKET_PK and POLYMARKET_FUNDER.
Builder credentials
Session authorization and revocation require Builder credentials approved for the session-key API. These are separate from the Relayer API key used for position operations. Follow Polymarket's session-key requirements, and keep these values in the owner's administration environment:
POLYMARKET_BUILDER_API_KEYPOLYMARKET_BUILDER_API_SECRETPOLYMARKET_BUILDER_PASSPHRASE
Deposit Wallet verification
Construction fails when the funder equals the signing address. Before signing, the position client:
- Checks the current and legacy Deposit Wallet addresses predicted by the canonical Polygon factory.
- Requires deployed wallet code and verifies that the signer owns the wallet.
- Reads the signed nonce from the wallet's on-chain counter.
Polygon RPC defaults to https://polygon.drpc.org. Pass base_url_rpc to the
PolymarketPositionClient constructor to use another trusted Polygon endpoint.
Session keys
Session keys let a separate signer trade for a Deposit Wallet without giving the trading runtime its owner's private key. The adapter supports CLOB-scoped authorization. See Polymarket's session-key documentation for venue eligibility and Builder approval requirements.
Administer access
Run PolymarketSessionKeyClient in the owner's administration environment, using a single
administration process per wallet. Before authorizing a session:
- Supply the owner private key, owner CLOB credentials, Builder credentials, and Deposit Wallet address explicitly. The client does not read environment variables itself.
- Generate and securely store the session keypair separately. Pass only its public address to the authorization method.
The Python example reads credentials from the administration process's environment and passes them into the client:
import os
from nautilus_trader.adapters.polymarket import PolymarketSessionKeyClient
from nautilus_trader.adapters.polymarket import PolymarketSessionKeyClientConfig
admin = PolymarketSessionKeyClient(
PolymarketSessionKeyClientConfig(
private_key=os.environ["POLYMARKET_PK"],
api_key=os.environ["POLYMARKET_API_KEY"],
api_secret=os.environ["POLYMARKET_API_SECRET"],
passphrase=os.environ["POLYMARKET_PASSPHRASE"],
builder_api_key=os.environ["POLYMARKET_BUILDER_API_KEY"],
builder_api_secret=os.environ["POLYMARKET_BUILDER_API_SECRET"],
builder_passphrase=os.environ["POLYMARKET_BUILDER_PASSPHRASE"],
funder=os.environ["POLYMARKET_FUNDER"],
),
)
# Use the public address derived from the separately stored session private key.
session_address = os.environ["SESSION_ADDRESS"]
# Run these calls from an async function in the administration process.
key = await admin.authorize_session_key(session_address)
keys = await admin.list_session_keys()Optional configuration:
base_url_httpoverrides the production CLOB endpoint.base_url_relayeroverrides the production Relayer endpoint.proxy_urlconfigures an HTTP or HTTPS proxy for both clients.
The Rust example provides the same authorization and listing operations:
cargo run -p nautilus-polymarket --example polymarket-session-keys -- authorize "$SESSION_ADDRESS"
cargo run -p nautilus-polymarket --example polymarket-session-keys -- listEach administration method has a specific completion condition:
| Method | Successful return requires |
|---|---|
authorize_session_key | On-chain confirmation; registry matches the address, CLOB scope, and expiration |
list_session_keys | Validated active, unexpired registry entries |
revoke_session_key | On-chain confirmation; key absent from the active registry |
Expiration
Authorizations expire after 4,315 hours, matching the official SDK's wire value. Polymarket describes
this period as 180 days, but its raw example and SDK use a value five hours shorter. The returned
valid_until is the exact expiration in Unix seconds.
Recover an interrupted operation
After an authorization or revocation times out, encounters a transport failure, or is cancelled, retry with the same operation, same address, and same client. The client retains the signed request and idempotency key and rejects a different mutation until the pending operation resolves.
Keep the client alive until the operation resolves: pending requests are held in memory. Unresolved-outcome errors include the idempotency key and any known transaction ID for diagnosis.
Signed batches have a 600-second deadline. If an unaccepted request expires, retrying cannot create a fresh batch, and the client remains blocked. Reconcile the registry and Relayer outcome before creating a new client. The same reconciliation is required after a process restart.
Configure session trading
Create or derive CLOB credentials with the session private key, using the existing CLOB credential workflow. Session mode requires:
- Explicit session credentials and
funder; it does not fall back to owner credentials from the environment. PolymarketSignatureType.Poly1271; other wallet signature types are rejected.
Keep owner and Builder credentials outside the trading runtime.
In this example, session_private_key is the separately stored session key, and
session_api_key, session_api_secret, and session_passphrase are its returned CLOB credentials:
from nautilus_trader.adapters.polymarket import PolymarketExecutionClientConfig
from nautilus_trader.adapters.polymarket import PolymarketSignatureType
from nautilus_trader.adapters.polymarket import PolymarketSignerType
execution = PolymarketExecutionClientConfig(
signer_type=PolymarketSignerType.Session,
signature_type=PolymarketSignatureType.Poly1271,
private_key=session_private_key,
api_key=session_api_key,
api_secret=session_api_secret,
passphrase=session_passphrase,
funder=deposit_wallet_address,
)Existing configurations default to PolymarketSignerType.Owner.
Reconciliation and restarts
Orders, trades, and notifications are scoped to the session's activity. Reconciliation requires the session API key to match order ownership; a shared wallet address alone does not establish ownership.
Session cancel-all cancels known order IDs rather than using wallet-wide market cancellation. Mass status contains session orders and fills but omits wallet-wide positions. Direct position queries return an error because wallet holdings cannot establish a session's position.
Configure the node accordingly:
- Position checks: Set
LiveExecutionEngineConfig.position_check_interval_secs=Noneto disable periodic position checks for the node. - History across restarts: Configure cache database persistence
and keep
load_cache=Trueto retain order and fill history.
Revoke or rotate access
Rotate keys from the administration environment. Coordinate outstanding orders and reconciliation before switching credentials: a new session does not inherit visibility into an old session's orders.
To revoke a key, use the administration client created above, from an async function:
await admin.revoke_session_key(session_address)Or run the Rust example:
cargo run -p nautilus-polymarket --example polymarket-session-keys -- revoke "$SESSION_ADDRESS"Expired or revoked permissions cause venue rejections. A live transport connection does not prove that authorization remains active.
Position operations
PolymarketPositionClient splits pUSD into complete outcome-token sets, merges complete sets back
to pUSD, and redeems resolved positions. The client supports Deposit Wallet (PolymarketSignatureType::Poly1271)
only. Safe, Proxy, and EOA paths are not available. The client does not deploy wallets, batch
unrelated calls, size a merge to "max", or redeem positions automatically.
Inputs and approvals
Each operation looks up the market on the public CLOB by condition ID and errors if neg_risk is
absent. It then selects the canonical pUSD token and the standard or negative-risk collateral
adapter. Callers pass a 0x-prefixed 32-byte condition ID and do not supply contract addresses.
- Split and merge amounts: Positive pUSD decimals exactly representable at six decimal places,
with no rounding. Amounts use pUSD units, never base units;
"max"is not accepted. - Redemption: No amount argument; redeems both binary index sets
[1, 2].
Approvals are not submitted automatically. Grant them from the Deposit Wallet before submitting:
| Operation | Required approval for the market's collateral adapter |
|---|---|
| Split | Spend the wallet's pUSD |
| Merge or redeem | Act as a Conditional Tokens operator |
The standard collateral adapter is absent from the approval plan in polymarket-set-allowances; the negative-risk adapter is included. That command signs approvals
from the EOA, so it cannot grant either approval for a Deposit Wallet. Use Polymarket's
wallet and authentication flow to grant the
position-operation approvals from the Deposit Wallet.
Submit and wait
The following example shows a split submission. Replace the illustrative condition ID with the market's actual condition ID before running it.
from decimal import Decimal
from nautilus_trader.adapters.polymarket import PolymarketPositionClient
condition_id = "0x" + "11" * 32
client = PolymarketPositionClient()
transaction = await client.split_position(condition_id, Decimal("1"))
outcome = await transaction.wait()split_position and merge_positions take the condition ID and a positive pUSD Decimal.
redeem_positions takes only the condition ID. Each method returns a
PolymarketPositionTransaction after Relayer submission. Call wait() once to poll until a
PolymarketPositionOutcome is available. A second wait() on the same Python handle raises. The
handle retains its transaction_id after waiting starts, including after cancellation or an error.
Outcomes and errors
PolymarketPositionOutcome.status | Meaning |
|---|---|
confirmed | Relayer reported STATE_CONFIRMED. |
failed | Relayer reported STATE_FAILED. |
invalid | Relayer reported STATE_INVALID. |
Every outcome exposes transaction_id. Confirmed and failed outcomes expose transaction_hash when the
Relayer supplies one; invalid outcomes always return None. Failed and invalid outcomes also expose
error_msg when the Relayer supplies one.
Submit errors
An HTTP rejection raises before a transaction handle exists. A timed-out submit or a success response
with no transaction_id also raises; the on-chain outcome is unknown.
Submit is not retried.
Wait timeout
The default is 120 seconds, checked between polling requests. An in-flight request or polling delay can extend the elapsed time. A timeout raises an error naming the Relayer transaction ID; the terminal state remains unknown. Python does not expose the Rust wait-timeout or poll-interval setters.
Response validation
Relayer redirects are rejected, and polling rejects a response whose transaction ID differs from the submitted ID.
Submission coordination
Clients in the same process share submission state for each wallet. A later operation requires a matching terminal Relayer result and an advanced wallet nonce. A failed or invalid operation that does not advance the nonce remains blocked.
After signing, the client records a reservation before sending the batch. An HTTP rejection, timeout, cancellation, or response without a transaction ID then blocks further operations for that wallet, even if the client is recreated. Failures before that reservation, such as invalid amounts or failed wallet verification, do not create a new block. An HTTP status alone does not prove that a signed batch cannot execute.
Do not restart and blindly retry an unknown submission. A process restart clears local submission state but does not cancel a signed transaction. Use a single submitting process per wallet; coordination does not extend across processes or external wallet tools.
Recover an unknown submission
Retain the submission record
Enable and retain INFO logs before submitting position operations. Before each submit request, the
client emits a Deposit Wallet submission record containing:
- Wallet address, reserved nonce, and signed deadline (Unix seconds).
- Operation, target contract, and value.
- Unsigned calldata identifying the condition and, for split or merge, the amount.
The record excludes signatures and credentials, but contains trading intent; restrict access to retained logs. Keep the transaction ID from the returned handle when available.
Logging is not a durable transaction journal. Disabled logging or a process crash can leave no retained record, preventing safe recovery without further evidence.
Reconcile before retrying
- Stop submissions. Include other processes and external wallet tools using the wallet.
- Establish whether the operation executed. Find the submission record and any Relayer transaction ID. Match the intended wallet, target, and calldata against finalized on-chain transaction receipts and effects. A successful HTTP response, a Relayer failure, or an advanced nonce alone does not establish whether the intended operation executed. If it executed, do not repeat it.
- Keep the wallet blocked while execution remains unknown. To reconcile by expiry, perform all the expiry checks below. If the nonce advanced, inspect the transaction that consumed it instead of assuming failure.
- Prove that retrying is safe before restarting. Establish both that the intended operation did not execute and that the original signed batch can no longer execute. If the record is missing, the RPC cannot provide consistent finalized state, or contract behavior is unverified, resolve the uncertainty with Polymarket before retrying.
Expiry checks
The signed deadline is 1,800 seconds after signing by default. Rust callers can change this with
with_deadline_secs; longer deadlines delay expiry-based recovery. A wait() timeout does not
shorten the signed deadline or cancel the batch.
First verify that the deployed wallet contract enforces the signed deadline and consumes its nonce
when a batch executes. Then read the wallet's nonce() with eth_call at a finalized Polygon block
and obtain that same block's timestamp. Require both:
- The nonce is unchanged from the reserved nonce.
- The block timestamp is strictly after the signed deadline.
Elapsed local time is insufficient. An advanced nonce requires inspection of the transaction that consumed it; it does not establish failure.
The client does not perform these recovery checks or release a reservation automatically. Consult the Polymarket contract registry when identifying the deployed contracts and the JSON-RPC reference for block-specific calls.
Configuration
Configure signing and authentication through these parameters or their environment-variable fallbacks:
| Parameter | Environment variable | Purpose |
|---|---|---|
private_key | POLYMARKET_PK | Wallet key used to sign orders according to signature_type |
funder | POLYMARKET_FUNDER | pUSD funding wallet address |
api_key | POLYMARKET_API_KEY | CLOB L2 API key |
api_secret | POLYMARKET_API_SECRET | CLOB L2 API secret |
passphrase | POLYMARKET_PASSPHRASE | CLOB L2 API passphrase |
In owner mode, when a parameter is not supplied explicitly, the client reads its environment variable.
Session mode requires explicit credentials and a funder. CLOB L2 credentials authenticate the
private-key signer. For POLY_1271, the Deposit Wallet remains the
funder; it is not the L2 authentication address.
Use environment variables to supply credentials without embedding them in client configuration code.
Instrument loading also has two common controls:
auto_load_missing_instruments(defaultTrue): Subscribe and request commands for uncached instruments trigger an ad-hoc Gamma API load. When disabled, subscribing to an uncached instrument returns an error. See Runtime instrument loading.auto_load_debounce_ms(default100): Window in milliseconds for coalescing concurrent auto-load requests into a single batched Gamma call.
Data capability
Polymarket supports live L2_MBP order book deltas, quotes, trades, and resolution
InstrumentStatus/InstrumentClose events. Instrument definitions are published by bootstrap,
configured refreshes, on-demand loading, single-instrument requests, new-market discovery, and
tick-size changes.
Orders capability
Polymarket operates as a prediction market with a more limited set of order types and instructions compared to traditional exchanges.
For Polymarket live execution, set both the disconnection timeout and post-stop delay to 30
seconds with with_timeout_disconnection_secs(30) and with_delay_post_stop_secs(30). The delay
allows residual order and cancellation events to arrive before disconnection, while the timeout
gives each client time to shut down cleanly.
Order types
| Order Type | Binary Options | Notes |
|---|---|---|
MARKET | ✓ | BUY orders require quote quantity, SELL orders require base quantity. |
LIMIT | ✓ | BUY orders accept base or quote quantity; SELL orders require base. |
STOP_MARKET | - | Not supported by Polymarket. |
STOP_LIMIT | - | Not supported by Polymarket. |
MARKET_IF_TOUCHED | - | Not supported by Polymarket. |
LIMIT_IF_TOUCHED | - | Not supported by Polymarket. |
TRAILING_STOP_MARKET | - | Not supported by Polymarket. |
Quantity semantics
Polymarket interprets order quantities differently depending on the order type, side, and
quote_quantity setting:
- Limit orders with
quote_quantity=Falseinterpretquantityas the number of conditional tokens (base units). - Limit BUY orders with
quote_quantity=Trueinterpretquantityas pUSD collateral. - Market BUY orders interpret
quantityas quote notional in pUSD. - Market SELL orders use base-unit quantities.
Quote-sized limit SELL orders are not supported. The adapter denies them before submission. It also denies any limit order whose base or quote quantity truncates to zero at the two-decimal signing boundary.
Limit BUY example
For limit BUY orders with quote_quantity=True:
- The adapter truncates collateral to cents, signs it as the maker amount, and derives shares at the market's amount precision.
- It updates the local order to the signed share quantity before processing the venue response.
- The signed amounts must preserve the limit price exactly. Otherwise, the adapter rejects the order before HTTP. For example, 10.00 pUSD at 0.33 is not representable, while 9.90 pUSD at 0.33 produces exactly 30 shares.
To cap a limit BUY by collateral, set quote_quantity=True:
# Limit BUY with quote quantity (spend $10 pUSD at a limit price of 0.50)
order = strategy.order_factory.limit(
instrument_id=instrument_id,
order_side=OrderSide.BUY,
quantity=instrument.make_qty(10.0),
price=instrument.make_price(0.50),
time_in_force=TimeInForce.GTC,
quote_quantity=True,
)
strategy.submit_order(order)Market BUY example
When submitting market BUY orders, set quote_quantity=True on the order. The adapter converts
the quote amount (pUSD) to the signed base-unit share amount before posting to the CLOB. The
Polymarket execution client denies base-denominated market buys to
prevent unintended fills. A market BUY submitted with a base-denominated quantity can execute far
more size than intended.
# Market BUY with quote quantity (spend $10 pUSD)
order = strategy.order_factory.market(
instrument_id=instrument_id,
order_side=OrderSide.BUY,
quantity=instrument.make_qty(10.0),
time_in_force=TimeInForce.IOC, # Maps to Polymarket FAK
quote_quantity=True, # Interpret as pUSD notional
)
strategy.submit_order(order)Execution instructions
| Instruction | Binary Options | Notes |
|---|---|---|
post_only | ✓ | Supported for limit orders with GTC or GTD only. |
reduce_only | - | Not supported by Polymarket. |
Time-in-force options
Polymarket calls the POST /order field orderType. In NautilusTrader, this maps to
TimeInForce. The valid combinations depend on the Nautilus order type:
| Nautilus TIF | Polymarket orderType | Nautilus order scope | Notes |
|---|---|---|---|
GTC | GTC | LIMIT only | Good-Til-Cancelled; rests on the book. |
GTD | GTD | LIMIT only | Good-Til-Date; rests until expiration, fill, or cancel. |
FOK | FOK | LIMIT or MARKET | Fill the full size immediately or cancel the whole order. |
IOC | FAK | LIMIT or MARKET | Fill available size immediately and cancel the remainder. |
Polymarket uses FAK (Fill-And-Kill) for the semantics NautilusTrader calls
IOC (Immediate or Cancel). Polymarket docs classify FOK and FAK as market
order types, while GTC and GTD are limit order types. For Nautilus MARKET
orders, the adapter accepts only IOC and FOK; GTC and GTD are valid for
resting LIMIT orders only.
GTD expiry
Set GTD expiry at least three minutes after submission. The adapter denies a shorter expiry
before signing. It uses whole Unix seconds and accepts the exact three-minute boundary.
Polymarket reports both a user cancel and a GTD expiry as CANCELED. It expires the order one
minute before the supplied expiration, so the minimum effective lifetime is about two minutes. To
request an effective lifetime of N seconds, supply now + 60 + N, still subject to the
three-minute minimum.
On the user channel, a CANCELED event at or after that one-minute mark becomes OrderExpired.
An earlier cancel stays OrderCanceled. A REST-recovered cancel also stays OrderCanceled. The
open-order payload has an expiration, but no cancel time.
Leave manage_gtd_expiry false. The strategy timer fires at the stated expiration and submits a
cancel. It does not emit OrderExpired. A user-channel expiry already clears that timer. If the
expiry message was missed, the late cancel does not close the order. Reconciliation still has to.
See GTD orders.
Minimum order size
Read each market's min_order_size from its order book; active markets commonly report five
shares. Marketable orders can also be rejected below 1 pUSD in notional value with
invalid amount for a marketable BUY order … min size: $1. The adapter leaves instrument
min_quantity unset because quote-sized BUY quantities use pUSD while base-sized orders use
shares.
Advanced order features
| Feature | Binary Options | Notes |
|---|---|---|
| Order modification | Yes | Adapter-managed cancel-replace for open LIMIT orders. |
| Bracket/OCO orders | - | Not supported by Polymarket. |
| Iceberg orders | - | Not supported by Polymarket. |
Order modification
Polymarket has no in-place modify endpoint. The execution client cancels the current venue order,
reconciles its final confirmed fills, and signs a replacement for the remaining quantity. The
ModifyOrder.quantity value is the absolute target for the logical order, not the replacement leg.
The replacement keeps the ClientOrderId and receives a new VenueOrderId. The resulting logical
quantity reflects the exact signed base quantity after venue precision normalization, so it can be
slightly lower than the requested target.
The adapter submits no replacement unless the cancel response, canceled order state, and confirmed
trade totals agree. An ambiguous cancel emits OrderModifyRejected. An ambiguous replacement stays
in flight under its deterministic signed order hash so a later order update, fill, or order
reconciliation can establish the replacement without emitting a second OrderAccepted. Later
modify and cancel commands remain blocked until that happens. This recovery state is not persisted
across an execution-client process restart.
Batch operations
| Operation | Binary Options | Notes |
|---|---|---|
| Batch Submit | ✓ | The adapter uses POST /orders for independent limit-order batches (max 15 orders per request). See Batch submit. |
| Batch Modify | - | Not supported by Polymarket. |
| Batch Cancel | ✓ | The adapter uses DELETE /orders. See Batch cancel. |
Batch submit
SubmitOrderList commands are routed to Polymarket's POST /orders endpoint. The endpoint
accepts at most 15 orders per request (BATCH_ORDER_LIMIT); larger lists are split into
sequential 15-order chunks.
- Only
LIMITorders are batched.MARKETorders inside the list are routed to the single-order path, which signs a marketable order and submits it withFAKorFOKbased on Nautilustime_in_force. reduce_onlyorders, quote-sized SELL orders, andpost_onlywith market TIF (IOCorFOK) are denied before submission.- A single eligible order falls through to
POST /orderso it keeps the single-order retry semantics; the batch path deliberately disables retry because the venue does not expose an idempotency key. - If the batch response omits a leg, that order stays submitted for reconciliation. The adapter registers the signed order's expected hash so later WebSocket events and cancels still resolve to the local order. An omitted response cannot prove that the venue rejected the order.
Batch cancel
BatchCancelOrders commands with resolved venue order IDs use Polymarket's
DELETE /orders
endpoint. The adapter sends sequential chunks and chooses each new chunk from the smaller of the
endpoint's 1,000-ID limit and the signer's current cancellation burst. A signer starts with the
Standard 120-token burst, and a tier reported by one response applies to the next new chunk.
Each chunk retries independently with the same order IDs unless a lower reported tier requires smaller chunks before the retry. The adapter merges the completed responses and processes each requested order once after every chunk succeeds. If a later chunk exhausts its retries, earlier chunks may already have changed venue state, but the adapter emits no partial per-order results; reconciliation resolves the unknown overall outcome.
In owner mode, without a side filter, CancelAllOrders applies to the selected outcome token for the authenticated
execution account, across strategies, even when the local order cache has no matches. The adapter
sends the instrument's raw token ID as asset_id to
DELETE /cancel-market-orders.
With a Buy or Sell filter, or in session mode regardless of the side filter, the adapter
selects matching open orders from the local cache and sends their venue order IDs through
the same chunked DELETE /orders path. A matching order that is still awaiting its venue order ID
retains a pending cancellation, which is sent after submission resolves. The ID-based path sends
no cancellation request when the cache has no matching orders.
Submit response handling
Polymarket's public documentation describes successful
POST /order responses
with success, orderID, status, and errorMsg, and documents
API errors as structured error responses.
It does not document statusless client exceptions or transport failures as venue rejections.
Successful responses
For a successful response with a non-empty orderID, the adapter uses status to choose the
initial Nautilus state and whether an order with FOK time-in-force needs the five-second REST
check. The venue meanings follow Polymarket's
order lifecycle.
Submit status | Venue meaning | Initial Nautilus state | FOK REST check |
|---|---|---|---|
live | Resting on the book | Accepted | Kept |
matched | Matched immediately | Accepted | Skipped |
delayed | Matching delay in progress | Submitted until WebSocket or REST activity proves acceptance | Kept |
unmatched | Delay completed without a match; now resting | Accepted | Kept |
| Absent or empty | No status supplied | Accepted unless a proven FOK/FAK error rejects it | Kept unless rejected |
These meanings apply to the submit response. The adapter treats delayed as a submit outcome, not
as a market configuration signal. A matched response skips the REST check because the submit
already confirms an immediate match. An absent or empty status emits OrderAccepted for
compatibility and keeps the REST check unless a proven unfilled FOK or no-match FAK response
causes immediate rejection. For a successful response with a non-empty orderID, an explicit
status takes precedence over an unfilled FOK or no-match FAK error string.
Delayed responses
A delayed response:
- Registers the venue order identity and fill tracking immediately and retains them independently of
bounded replay caches. Later order queries, WebSocket events, and reconciliation reports can then
resolve the local
ClientOrderId. - Leaves the order
Submitteduntil a fill, order update, or REST result proves acceptance. - Emits
OrderAcceptedbefore any fill, cancellation, expiry, or filled status that proves acceptance. - Resolves an unfilled
FOKdirectly asOrderRejectedwhen REST returnsUNMATCHED.
Definitive and ambiguous outcomes
Polymarket applies the shared command outcome policy and the adapter guide's diagnostic and strategy reason boundary at its execution boundary.
Ambiguous failures include:
- Transport failures and timeouts.
- Retry exhaustion after an attempt with an unknown outcome.
- Response serialization or decoding failures.
- Local I/O failures.
- Server-side failures.
- HTTP 425 responses.
- HTTP 429 responses that lack CLOB signer-limiter headers.
| Outcome | Nautilus result | Reason |
|---|---|---|
success=false, a documented processing error, or another non-retryable client/API error | OrderRejected | The response proves rejection. |
Single or batch FOK: success=true, no status, and the unfilled error | Immediate OrderRejected | The venue proves it killed the order. |
Single or batch IOC/FAK: success=true, no status, and the exact no-match error | Immediate OrderRejected | The venue proves no quantity matched. |
Batch leg: success=true, empty orderID, and a populated errorMsg | OrderRejected with reason | The venue proves it rejected that leg. |
No orderID and no reason | Remains Submitted | The response does not prove rejection. |
| Any ambiguous failure | Remains Submitted | The adapter cannot determine the outcome. |
| Definitive retry error after an earlier ambiguous attempt | Remains Submitted | The earlier attempt may have succeeded. |
Failure before POST /order, such as a failed pUSD balance lookup | OrderDenied | The adapter did not submit the order. |
Local denials format the strategy-facing reason from OrderDeniedReason. The leading token is the
stable code, such as VALIDATION_FAILED or UNSUPPORTED_ORDER_TYPE.
The proven unfilled FOK and no-match FAK responses resolve as immediate rejections. The FOK
response skips the REST check. After an ambiguous single-order attempt, a later HTTP error or
decoded rejection does not prove that the first attempt failed. An accepted response carrying the
matching valid order ID confirms the deterministic signed order; a rejection does not, even with a
matching ID.
Error reasons
Diagnostic errors retain the HTTP status and transport or rate-limit context. For venue HTTP status,
rate-limit, and exchange errors, strategy-facing rejection events use the venue reason; other
failures use the bounded error description. The adapter reads the first non-blank string from
error, then errorMsg, and collapses whitespace and control characters. An empty body becomes
empty response body. A plain-text or malformed response uses the same bounded fallback. Invalid
UTF-8 is decoded lossily before that handling. An HTML response uses its title when available, or its
visible text otherwise. Reasons are limited to 512 characters, including the literal
... [truncated] truncation marker and its preceding space.
On single and batch submit responses, the exact normalized reason order_version_mismatch becomes
Polymarket CLOB order version mismatch; adapter supports V2 only. Other submit response reasons
remain unchanged after normalization.
The venue reports a post-only crossing as invalid post-only order: order crosses book. Only that
exact normalized reason sets OrderRejected.due_post_only=true; other post-only errors remain
ordinary rejections.
Retry classification
Retry-managed single-order submit and cancel requests retry HTTP 425, 429, and 5xx responses with the configured backoff. After retries are exhausted, submit classification is:
| HTTP status | Retried | Submit result | Notes |
|---|---|---|---|
| 425 | Yes | Remains Submitted | Too Early does not prove rejection. |
| 429 with CLOB signer-limiter headers | Yes | OrderRejected | Requires Poly-RateLimit-Remaining, Poly-RateLimit-Reset, or Poly-RateLimit-Tier. An earlier unknown attempt stays Submitted. |
| 429 without those headers | Yes | Remains Submitted | Cloudflare or another hop may have seen the request. |
| 5xx | Yes | Remains Submitted | The command may already have been applied. |
| 400, 401, 403, 404 | No | OrderRejected | Non-retryable client or API error. |
A malformed successful submit response also remains unknown and enters reconciliation instead of becoming a terminal rejection.
Cancel classification uses the same evidence classes. A non-retryable client or API error after the
cancel is sent, or a local failure that proves the cancel was never transmitted, emits
OrderCancelRejected. HTTP 425, headerless 429, and 5xx leave the cancel in flight.
Unknown-outcome reconciliation
For an unknown outcome, the adapter:
- Derives the expected Polymarket order hash from the signed EIP-712 order when possible and caches
it as the
VenueOrderId. Later WebSocket events and reconciliation reports attach to the localClientOrderIdinstead of becoming external orders. - Applies the signed quote-to-base quantity update for a quote-quantity market BUY.
- Defers a pending cancel until the expected venue order ID is known.
- Registers fill tracking under that venue order ID.
Order and trade reads
The adapter reads the order and its trades from REST, retrying with backoff capped at 30 seconds until the venue state is known. Terminal trades apply through the settlement records first, then the order status accepts the order and releases its fills. The status waits while any trade is still provisional or while confirmed trades do not yet cover the venue's matched quantity.
QueryOrder commands made while a signed submission awaits acknowledgement use the same settlement path.
They apply venue trade IDs directly, so buffered WebSocket trades and the later submit response do
not repeat acceptance or count the same fill twice.
Report gating
Reports that cover the order or its instrument fail until that read applies or reading stops, so reconciliation does not infer fills from partial venue state.
Read timeout
Reading stops after 10 minutes without applying the venue state. The adapter logs a warning, lifts
the report gate, and leaves the order Submitted.
If the local order closes before recovery completes, the adapter cancels any venue order still reported as live or delayed. Recovery then requires a terminal order read and trade evidence covering its matched quantity. Cancellation alone does not establish that no fill occurred. Failed or empty reads and the 10-minute timeout do not end this recovery, even if a late fill reopens the local order. If the venue keeps returning an empty order lookup, the report gate remains closed indefinitely for that order, its instrument, and account-wide reports. Fills that the engine cannot apply remain visible through the settlement report gate.
Position management
| Feature | Binary Options | Notes |
|---|---|---|
| Query positions | ✓ | Data API user positions, excluding resolved balances. |
| Split, merge, redeem | ✓ | Deposit Wallet operations; see Position operations. |
| Position mode | - | Binary outcome positions only. |
| Leverage control | - | No leverage available. |
| Margin mode | - | No margin trading. |
Order querying
| Feature | Binary Options | Notes |
|---|---|---|
| Query open orders | ✓ | Active orders only. |
| Query order history | ✓ | Limited historical data. |
| Order status updates | ✓ | Real-time order state changes. |
| Trade history | ✓ | Execution and fill reports. |
Contingent orders
| Feature | Binary Options | Notes |
|---|---|---|
| Order lists | - | Independent order batches exist, but linked contingency semantics do not. |
| OCO orders | - | Not supported by Polymarket. |
| Bracket orders | - | Not supported by Polymarket. |
| Conditional orders | - | Not supported by Polymarket. |
Precision limits
Polymarket enforces different precision constraints based on tick size and orderType.
Every Polymarket BinaryOption uses a canonical price_precision of 4, independent of its active
tick size. The instrument's price_increment carries the active tick, and order signing derives
the venue tick decimals from that increment.
Binary option instruments typically support up to six decimal places for amounts with a 0.0001 tick size. The signing rules also depend on the venue order type.
Market order types: FAK and FOK
The direct maker amount is limited to two decimal places. The computed taker amount uses the market
tick decimals plus two size decimals. A limit order submitted with FAK or FOK must also satisfy this
stricter market-order amount validation; the venue rejects values that are valid for a resting order but
not for that market-order type.
Base-sized limit BUY orders
quantity is the nominal share quantity at the limit price. With FAK or FOK, Polymarket spends
a pUSD maker budget, so price improvement can return more shares.
When quantity * price is not an exact cent amount, the adapter truncates that budget to two
decimal places. It signs the share quantity derived from that budget, rounded up to the market
amount precision, so the signed ratio does not exceed the limit price. It updates the local order
to that signed quantity before posting. This applies to single and batch submissions.
A later fill can still raise the quantity to the actual matched size. A budget that truncates to zero is denied before signing.
Collateral-sized limit BUY orders
The adapter truncates the direct maker amount to cents. The computed share amount uses the market tick decimals plus two size decimals. The signed integer amounts must preserve the requested limit price exactly after this quantization.
Resting limit order types: GTC and GTD
Resting orders allow more flexible precision based on market tick size.
Tick size precision hierarchy
| Tick size | Tick decimals | Size decimals | Amount decimals |
|---|---|---|---|
| 0.1 | 1 | 2 | 3 |
| 0.01 | 2 | 2 | 4 |
| 0.005 | 3 | 2 | 5 |
| 0.0025 | 4 | 2 | 6 |
| 0.001 | 3 | 2 | 5 |
| 0.0001 | 4 | 2 | 6 |
Tick validation
- The adapter validates tick size before signing. A base-sized limit
FAKorFOKBUY whose cent budget truncates to zero is denied before signing. See Base-sized limit BUY orders. - The adapter requires instrument tick sizes to be exactly representable at four decimals. It rejects instrument definitions and tick-size events that do not meet this requirement; a rejected event leaves the current tick active.
- Tick decimals control signing and amount precision. They do not change the instrument's canonical four-decimal price precision.
- Base-sized resting
GTCandGTDlimit orders and all SELL orders keep their tick-derived amount precision. Collateral-sized limit BUYs use cents for the direct maker amount and tick-derived precision for the computed share amount. - The adapter rejects limit prices outside the current market's
[tick_size, 1 - tick_size]interval before signing. - The published
BinaryOptionadvertisesmin_priceandmax_priceequal totick_sizeand1 - tick_size, so consumers that clamp to the instrument bounds stay within that accepted range. - Market-order precision limits include two decimals for the sell size plus tick-derived bounds for the computed amount.
- Tick sizes can change dynamically during market conditions, particularly when markets become one-sided.
Tick size change handling
When a market's tick size changes (tick_size_change WebSocket event), old
book levels can be invalid on the new grid (for example 0.505 fits a 0.001
tick but not a 0.01 tick). To keep old-grid prices out of the new epoch, the
adapter treats the change as a book epoch transition:
- Publish the updated
BinaryOptionwith the newprice_increment, canonical four-decimalprice_precision, and tick-relativemin_price/max_pricebounds. - Drop the local order book for the instrument.
- Gate incremental
price_changebook deltas on a fresh snapshot and request recovery. - Resubscribe the market until the venue replays a snapshot.
- Reseed the book from the snapshot and resume normal processing.
Trade ticks and the instrument update flow through unchanged. Quote handling
follows drop_quotes_missing_side: when enabled, quote ticks require both bid
and ask prices; when disabled, missing sides use Polymarket boundary prices with
zero size. The adapter can keep quotes flowing during the gap by reading best_bid
and best_ask from each price_change.
Gamma instrument data supplies the active tick until the first tick_size_change for that token.
The WebSocket tick then remains authoritative across later Gamma refreshes and instrument requests.
The adapter ignores older tick-size events by venue timestamp. A data-client reset starts a new
generation and allows Gamma to supply each token's tick again.
Trades
Trades on Polymarket can have the following statuses:
MATCHED_NOT_BROADCASTED: The orders matched before an on-chain transaction was broadcast.MATCHED: Trade has been matched and sent to the executor service. The executor submits it as a transaction to the Exchange contract.MINED: Trade is observed to be mined into the chain, and no finality threshold is established.CONFIRMED: Trade has achieved strong probabilistic finality and was successful.RETRYING: Trade transaction has failed (revert or reorg) and is being retried/resubmitted by the operator.FAILED: Trade has failed and is not being retried.
CONFIRMED and FAILED are terminal. The other statuses are provisional: the trade can still
succeed or fail.
Settlement updates
Once a trade is initially matched, subsequent status updates arrive through the user WebSocket. The execution adapter tracks each trade's venue settlement separately from whether the engine has applied each of the account's fills, and it emits each fill and each correction at most once.
For an order this client submitted in the current WebSocket session, the adapter emits one
OrderFilled at the first provisional status it receives, usually MATCHED. It treats later
provisional statuses as settlement updates without emitting another fill. CONFIRMED records
finality and refreshes the account. Matched WebSocket fills retain the raw trade fields in the
info field of the OrderFilled event.
Failed trades and REST resolution
A WebSocket FAILED update never voids a fill by itself. The adapter quarantines a trade and reads
it by ID from the authenticated REST trades endpoint (GET /data/trades) when:
- The WebSocket reports
FAILED. - WebSocket evidence contradicts a fill the engine has not applied, such as changed fill values or a missing owned order.
- A trade message fails validation.
The first read starts immediately. Until REST returns a terminal status, the adapter retries after
500 ms, doubling the delay up to 30 seconds, so a long RETRYING period leaves the trade quarantined.
The first terminal REST result is final. A later WebSocket status that contradicts it, or new or changed evidence for fills the engine has not applied, triggers another REST read but never reverses the first result.
REST FAILED result
The adapter emits one OrderFillVoided for each fill the engine applied, including a fill the engine
applies after the result arrives, and refreshes the account. Fills the adapter had not yet emitted
are never emitted. The correction does not relist the failed quantity, but it preserves any
maker-order remainder that was already working. An execution-complete order becomes VOIDED.
REST CONFIRMED result
The adapter emits any of the account's fills that the engine has not applied, using the REST trade values, and refreshes the account.
Reconnects and restarts
A provisional status applies a fill only when the trade and all of the account's orders in it belong to the current uninterrupted WebSocket session. The stream does not replay updates missed while disconnected, so after a reconnect the adapter reads from REST:
- New trades on existing orders: the adapter quarantines them until REST reports
CONFIRMEDorFAILED. This also applies after a restart to orders restored from the cache, so fills on a resting order can lag the venue until on-chain confirmation. - Provisionally applied trades: the adapter reads each by ID.
CONFIRMEDkeeps the fill andFAILEDvoids it. Reports touching these trades fail until the read returns. - Trades matched while disconnected: the adapter reads each open order it submitted or restored from the cache, and each order whose submit was in flight and succeeds after the reconnect. It reads the order and the account's trades in its market, applies each trade it has not seen exactly once with the REST values, and leaves the order status to the WebSocket and reconciliation. Orders adopted from the venue are not read; their fills arrive through reconciliation.
The adapter reads at most 10 orders per resolution pass, retrying each with backoff capped at 30 seconds. Reports touching an order fail until none of its trades is provisional and confirmed trades cover the venue's matched quantity. Reading stops after 10 minutes without that evidence, which also lifts the report gate.
On connect, the adapter rebuilds applied fills and voids from the cached order events, so a replayed trade does not produce a second fill. Those rebuilt fills are not read again.
Settlement faults
A trade enters a hard fault when its venue outcome and the engine's state cannot be reconciled:
- A later terminal REST result differs from the first one.
- The engine declines a fill the adapter emitted, unless REST already reported
FAILED. - The engine declines a correction void, so the applied fill cannot be reversed.
- REST
CONFIRMEDvalues contradict an applied fill, or the terminal REST result omits one of the account's fills that the engine applied.
A hard-faulted trade admits no further fills or voids, except the one void owed for a fill applied
after REST reported FAILED. The adapter logs the fault at error level, refreshes the account, and
blocks reconciliation for the trade (see settlement precedence) until the
node restarts.
The adapter keeps settlement records for the lifetime of the execution client. If more than 100,000 records accumulate after connect, the client faults closed: it reports disconnected and refuses new commands until restart. Records rebuilt from the cache on connect do not count toward this limit.
Trade ID derivation
Polymarket does not publish a trade ID on last_trade_price market-data events.
The adapter derives a deterministic TradeId from the asset ID, side, price,
size, and timestamp via the Rust determine_trade_id function using FNV-1a.
For execution fills, taker reports use the venue's trade id in both REST reconciliation and the
user WebSocket, so the same fill deduplicates across sources. A maker trade can fill more than one
of the user's resting orders, so maker reports combine the venue trade ID with the maker venue
order ID. The same venue event yields the same trade ID across replays.
For historical Data API trades, the loader uses
{transaction_hash[-24:]}-{token_id[-4:]}-{seq:06d} to distinguish fills in one transaction.
Instrument metadata
BinaryOption.event_id contains the Gamma parent event ID. The provider's
event-based discovery and PolymarketDataLoader.from_event_slug use the enclosing
event; direct market loading uses the unique ID in the market's events array. Missing or ambiguous
relationships leave event_id unset. Both outcome instruments share the same event ID.
Live metadata
Live instruments retain the complete received Gamma market JSON string in info["gamma_market"], including
unknown fields and nested events, tags, and series when returned by Gamma. Event-based discovery also
retains the enclosing response in info["gamma_event"], including its full markets array. This event
snapshot is repeated for each outcome instrument, so events with many markets increase metadata size.
The existing normalized metadata keys remain available, including token_id, condition_id, market_id,
and event_id when known.
These strings contain the received JSON objects before enrichment. Tick-size updates preserve them; use the instrument's typed price increment for the current tick size. The adapter does not fetch additional related resources solely to populate metadata. Storing the original JSON strings preserves unknown fields and numeric precision across serialization formats. Decode them when needed:
import json
from decimal import Decimal
market = json.loads(instrument.info["gamma_market"], parse_float=Decimal)Historical metadata
The historical PolymarketDataLoader retains these JSON strings under resolution_metadata["gamma_market"]
and, for event loading, resolution_metadata["gamma_event"] instead of instrument.info, because fetched
responses can contain terminal outcomes that were not known during the historical period. Decode these
strings with json.loads as above. They precede CLOB enrichment, so the raw token IDs and outcome labels
can differ from the constructed instrument.
Numeric precision
Financial wire values are decoded directly as decimals. Values outside the supported decimal or domain-type range fail HTTP decoding or report construction. WebSocket and RTDS handlers log and skip invalid updates. Report construction does not substitute zero for an invalid price or quantity.
Fees
The adapter reads each instrument's fee_schedule and applies its rate and exponent as:
platform fee = shares * rate * (price * (1 - price)) ^ exponentThe current public schedule uses exponent 1, which is Polymarket's published
C * feeRate * p * (1 - p) formula. Platform fees peak at p = 0.50, decrease
symmetrically toward the extremes, and apply only to taker fills.
| Category | Taker feeRate | Maker feeRate | Maker rebate |
|---|---|---|---|
| Crypto | 0.07 | 0 | 20% |
| Sports | 0.05 | 0 | 15% |
| Finance | 0.04 | 0 | 25% |
| Politics | 0.04 | 0 | 25% |
| Economics | 0.05 | 0 | 25% |
| Culture | 0.05 | 0 | 25% |
| Weather | 0.05 | 0 | 25% |
| Other / General | 0.05 | 0 | 25% |
| Mentions | 0.04 | 0 | 25% |
| Tech | 0.04 | 0 | 25% |
| Geopolitics | 0 | 0 | - |
Every order signed by the adapter carries the hard-coded Nautilus builder code. Its builder fee rate is fixed at zero and is not configurable.
Fill commission handling
Instrument fee_schedule metadata stores decimal parameters as strings; readers also accept legacy
numeric metadata.
The live fee curve uses exact decimal arithmetic, so the exponent must be a whole number. Negative rates, fractional or negative exponents, and arithmetic overflow return errors.
FillReport.commission is denominated in pUSD and floors the platform fee to five decimal places,
matching the venue charge. The venue charges a BUY taker fee in pUSD on top of the fill and deducts a
SELL taker fee from the pUSD proceeds, so the position quantity equals the shares filled.
If the exact result cannot be represented as Money, the adapter returns an error instead of using
zero or a generic commission. See the
commission failure contract.
A commission construction error fails a direct fill report request, terminal trade-history recovery, or complete mass status. Startup returns a mass-status error without applying that client's reports. When an active order's trade-history request fails, the adapter logs the error and caps matched quantity to fills already applied in core. Reconciliation then defers any unsupported residual quantity. Commission construction and settlement validation errors instead fail the report request. The adapter does not drop a failed fill while returning an order or position report that could recreate its quantity without the Polymarket commission.
For the latest public schedule, see Polymarket's Fees documentation.
Backtest fee model
Use PolymarketFeeModel for backtests that include taker fees and maker rebates. The model reads
rate, rebateRate, exponent, and takerOnly from each binary option instrument's
fee_schedule. It requires a maker or taker liquidity side, a fill price in [0, 1], and a
taker-only schedule with exponent 1. Unsupported instruments and invalid inputs return an error;
an instrument without a fee schedule produces zero commission.
use nautilus_execution::models::fee::FeeModelHandle;
use nautilus_polymarket::models::PolymarketFeeModel;
let fee_model = FeeModelHandle::new(PolymarketFeeModel);Pass the Rust handle through
nautilus_backtest::config::SimulatedVenueConfig::builder().fee_model(...). In Python, pass the model
to BacktestEngine.add_venue as fee_model or set it on BacktestVenueConfig.fee_model.
Maker rebate approximation
For maker fills, fee_equivalent is the platform fee formula above using the schedule's taker
rate. The model credits fee_equivalent * rebateRate as negative commission. This approximates
Polymarket's daily pool allocation because a backtest does not know the total fee equivalent from
other makers in that market.
Live maker fills have zero commission; Polymarket pays the actual pUSD rebate separately each day. The model does not represent that payment as a separate event, and it does not model competition between makers, daily aggregation, or the minimum payout threshold. See Polymarket's Maker Rebates Program for the venue formula.
Reconciliation
The Polymarket API returns either all active (open) orders or specific orders when queried by
the Polymarket order ID (venue_order_id). The execution reconciliation procedure for Polymarket
is as follows:
- Generate order reports for all instruments with active (open) orders, as reported by Polymarket.
- Generate filled order reports from confirmed trades for orders that closed before startup and are not in the cache, so their fills apply with the venue quantity and commission. For an instrument with a position report, the fills must explain that position; see report precision. Without a lookback window, only those instruments qualify, because fills miss balance changes such as redemption; see missing reports.
- In owner mode, generate position reports from current user positions reported by Polymarket's Data API. Session mode omits these wallet-wide positions; see session keys.
- Compare these reports with Nautilus execution state.
- Generate missing orders to bring Nautilus execution state in line with positions reported by Polymarket.
Position reports
Report precision
The Data API reports position size and average price to four decimal places. When the confirmed
fills in a mass status build one long position from zero without returning to flat, and the
resulting quantity differs from the reported size by less than 0.0001, the position report takes
the quantity and average entry price of those fills. Startup reconciliation then applies the fills
without a synthetic adjustment for the rounding. A buy and a sell with the same match time do not
qualify, because their order is ambiguous.
Otherwise, including when the cache retains an open position that other trades built, the report
keeps the Data API values, and the fills of closed orders in that instrument are not reported.
These reports set avg_px_open_precision to 4, so a retained position whose fills fell outside a
bounded lookback still passes the startup entry-price check against the truncated average. See reported entry averages.
Resolved balances
Position reports omit resolved balances:
- A balance in an instrument that Nautilus settled from an
InstrumentCloseis always omitted, so reconciliation cannot reopen settled exposure. - A balance that the Data API marks
redeemableis omitted when the account has no open Nautilus position in that instrument. While an open position still holds it, the balance stays reported until settlement closes the position.
The adapter drops these balances before instrument mapping, so an expired instrument that is no longer loaded does not fail reconciliation. The outcome tokens stay in the wallet until redeemed.
Missing reports
A missing position report is not evidence of a flat position. Redemption removes a balance from the Data API without a trade, and Polymarket can redeem winning tokens automatically shortly after resolution. Continuous position checks therefore never close a position that the Data API no longer reports; open positions close through fills or settlement.
Settlement precedence
Order status, fill, position status, and mass-status reports fail instead of returning coverage that reconciliation could use to infer fills while:
- A trade is quarantined, hard-faulted, awaiting a REST read after a reconnect, or waiting for the engine to apply a fill or void.
- The adapter reads a submitted order with an unknown outcome, or the trades of an order that was live during a WebSocket disconnect.
- The adapter rebuilds its settlement records on connect.
Mass status checks the whole account; the other reports check the requested instrument or order. A trade quarantined because its message failed validation blocks every report when it has no earlier admitted legs; otherwise the order and instrument scope of those legs applies. See settlement updates for how trades resolve.
QueryOrder checks settlement before and after its venue reads. If settlement evidence for the
order remains unresolved, the query emits no status report and leaves the local order unchanged.
Queries for an unacknowledged submission still use
unknown-outcome reconciliation.
Fill-report generation checks retained settlement outcomes and rejects a CONFIRMED trade row that:
- Belongs to a trade already settled as
FAILED. - Contradicts a retained terminal leg.
- Adds a leg to a retained terminal trade.
Rejection fails the report request without creating a fill or changing the retained outcome. Scoped report evidence cannot establish a terminal outcome for the complete trade; only the targeted terminal REST read can do that. Applied fill values take precedence over non-terminal REST copies.
Order reports cap filled_qty using fills already applied in core and fill reports that pass
settlement validation, counting each fill once. This cap applies with or without a lookback window.
Missing orders and API lag
Open-order checks
Periodic open-order checks are disabled by default (open_check_interval_secs=None). Configure
these checks on the node's LiveExecutionEngineConfig, separately from the Polymarket client.
Startup reconciliation and WebSocket processing do not depend on enabling this timer.
With the default open_check_open_only=True, absence from an open-order response does not
advance the missing-order retry counter or close a cached order. An open-only response can omit
an order that has just filled or been canceled. Enabling the interval alone therefore does not
resolve every venue-side cancellation missed by the WebSocket.
With open_check_open_only=False, the missing-order path works as follows:
- Defer action during recent local order activity.
open_check_threshold_ms=5000is a settling window since local activity, not simply a minimum age since submission. - Count consecutive eligible misses from successful reports covering the responsible client. A positive order report resets the counter. Orders outside a configured lookback and incomplete client coverage do not establish that an order is missing.
- At
open_check_missing_retries=5, schedule a targeted order-status query, subject to per-cycle query limits and throttling. This is the fifth eligible miss, not five additional queries. - Reconcile a returned report with its fills. A failed or incomplete targeted query defers
resolution. If complete targeted coverage returns no report, the engine retains unacknowledged
Polymarket submissions. It resolves
ACCEPTEDasREJECTED, andPARTIALLY_FILLEDasCANCELED. Pending update or cancel states remain in flight.
Polymarket first attempts single-order recovery from trades
when its order lookup is empty. For a previously accepted cached order, empty trade history produces
a CANCELED report with ORDER_NOT_FOUND_AT_VENUE. An order that was never accepted remains
unresolved; pending trades instead preserve a non-terminal state.
A successful empty order lookup plus empty trade history cannot distinguish venue cancellation from replica lag. The fallback can therefore close a previously accepted local order during sustained lag. The settling window and retry count reduce this risk; they do not prove that an order no longer exists.
Position checks
Position checks have their own position_check_interval_secs, also disabled by default. Open-order
checks do not poll wallet positions. Owner-mode position reports come from the Data API and can
reflect a different point in time from CLOB orders and trades; session mode omits wallet-wide
position reports. Treat an apparent position mismatch as requiring reconciliation, not as proof
that a particular fill is false. A position the Data API no longer reports stays open until a
fill or settlement closes it; see missing reports.
Mass-status reconciliation
Mass-status reconciliation pairs each order report with its venue fill reports. It applies the real fills first to preserve trade IDs and commissions, then infers only any residual quantity needed to reach the venue-reported status.
Mass status caps REST matched quantity using fills already applied in core and authenticated
CONFIRMED fill reports that pass settlement validation, counting each fill once. This prevents
pending settlement from creating an inferred fill, with or without a lookback window. Applied fills
outside the lookback window still contribute to the cap.
Runtime order checks fetch confirmed trade history when the venue's matched quantity exceeds the local order's applied fill quantity. Unpaired fill reports retain the normal fill-only path.
A commission construction error fails the complete REST report request. Startup returns the error without applying a mass status; periodic and targeted reconciliation defer the affected work. The adapter does not drop the failed fill because an order or position report could then recreate its quantity without the Polymarket commission.
Single-order recovery from trades
/data/order/{id} can return live or terminal orders. When it returns no order for a known ID,
generate_order_status_report falls back to /data/trades and filters the returned trades by the venue
order ID. This recovers a cached order whose terminal WebSocket update was missed, and avoids the engine
resolving a local ACCEPTED order as REJECTED, which would discard fills that already happened at the
venue. Only CONFIRMED trades contribute to recovered fills; pending and failed settlement states do
not.
The cached order is resolved via client_order_id, falling back to the cache's venue_order_id index
when only the venue ID is known. When the request supplies or resolves to a client_order_id, the cached
order must be a base-denominated LIMIT order; otherwise the request returns an error. An unassociated
venue-order request without a cached order defers to the engine rather than synthesizing an external
order from trade history alone:
- Cached order + recovered fills covering the cached quantity: returns
Filled. Non-IOC orders also returnFilledwhen the positive remainder is less than 0.01 shares. - Cached IOC/FAK order + any positive remainder, or another order with a remainder of at least
0.01 shares:
returns
Canceledwith the recoveredfilled_qty. Targeted reconciliation applies the associated fill reports before closing the remainder. If terminal quantity is still unaccounted for, the engine defers the terminal transition rather than discarding the missing fills. - Previously accepted cached order, no trades: returns
Canceledwithcancel_reason="ORDER_NOT_FOUND_AT_VENUE". - Cached order that was never accepted, no trades: returns
None, preserving its unresolved state. - Cached order with any
MATCHED,MINED, orRETRYINGtrade: a singular order query preserves the locally applied matched quantity while terminal REST recovery waits forCONFIRMEDorFAILED. - No cached order and no known client association (regardless of trades): returns
None; the engine's not-found-at-venue path resolves the local entry.
The bulk open-order response does not itself perform this per-order recovery. With
open_check_open_only=False, the engine requests it after the missing-order retry threshold. With the
default True, absent orders remain open for later reconciliation; see missing orders and API
lag.
Ghost fills and cumulative quantities
Duplicate delivery, failed settlement, and delayed REST snapshots require different handling.
Duplicate fills
The adapter tracks each venue trade ID and emits each of the account's fills at most once, so a replayed trade message does not emit again. Individual execution fills use the venue trade ID for takers and a composite of trade ID and maker order ID for makers. REST and WebSocket use the same fill identifiers, so replaying the same fill does not add its quantity again. See trade ID derivation and fill recovery and deduplication.
Failed settlement
For orders submitted in the current WebSocket session, MATCHED emits a fill before final
settlement. MINED and RETRYING do not emit another fill. CONFIRMED can recover a fill whose
earlier update was missed. A WebSocket FAILED quarantines the trade; only a targeted REST FAILED
result voids locally applied fills and suppresses buffered fills for that trade.
Exposure can therefore change before finality: for current-session orders the adapter does not wait for confirmation. After a reconnect or restart, fills on existing orders wait for a terminal REST result. See settlement updates.
Cumulative reports
filled_qty is an order total, not a position delta. Reconciliation applies unseen fills and, where
permitted, infers only the remaining positive difference. For example, a total of 7 against 5 already
applied contributes at most the missing 2, not 7.
A lower continuous order report does not by itself reverse applied fills. Fill void events and startup snapshot reconciliation provide correction paths, so the local total is not an irreversible floor.
REST evidence and cache state
For runtime order checks, the adapter caps REST matched quantity at
min(venue_matched, max(local_applied, settlement_validated_quantity)).
The validated quantity combines effective fills in cached order history with fill reports that pass settlement validation:
- Each venue fill ID counts once across applied fills and report rows.
- Inferred core fills provide a floor rather than additional venue evidence. A later venue report for the same quantity cannot inflate the total.
- Cumulative fill voids remove only the corrected quantity. An older report cannot restore that quantity through the cap.
If core still retains quantity for a leg whose targeted REST settlement is FAILED, report generation
fails closed rather than preserving that failed exposure through the cap.
WebSocket fills awaiting core processing do not raise the local applied-fill floor. This prevents an
unsupported increase in REST size_matched from becoming an inferred fill while preserving applied fills.
Mass status uses the same validated quantity with or without a lookback window. See
mass-status reconciliation.
The cache retains order identity, applied fills, and correction history used for replay handling. It is not an unconditional override of venue state, and missing or lagging venue evidence cannot establish settlement finality.
Fill quantity normalization
Polymarket wire amounts use six-decimal fixed-point mantissas. Market SELL signing truncates the
share-denominated makerAmount to two decimal places, while market BUY quote conversion can leave
a few microshares of drift between the registered and filled quantities. Every fill keeps the venue
quantity. Truncation is fixed in absolute share terms, so for underfill the adapter uses
DUST_SNAP_THRESHOLD_DEC = 0.01 shares; a shortfall at or above that threshold remains a real
partial fill.
| Direction | Source | Adapter behavior |
|---|---|---|
| Overfill | BUY filled below its limit, or quote drift | Raise the BUY order quantity to the fill |
| Underfill | Signed or venue quantity truncation (< 0.01) | Normalize atomic FOK; cancel a FAK remainder |
See BUY overfills for how a BUY can receive more shares than it signed.
BUY overfills
A Polymarket BUY is sized by the pUSD it spends, so it can receive more shares than it signed. The adapter keeps every fill at the venue quantity and raises the order quantity to match. A SELL is sized in shares and never fills past its signed quantity.
A BUY order's quantity can increase after submission, through an OrderUpdated event. Treat its
filled quantity, or the position quantity, as the shares held.
Why a BUY receives extra shares
The signed order sets makerAmount (pUSD to spend) and takerAmount (shares to receive). The
exchange guarantees at least that ratio of shares per pUSD for whatever part executes, then credits
the shares actually delivered. A partial execution spends less and receives proportionally fewer
shares. A full execution receives more than takerAmount in two cases:
- Price improvement: a limit BUY of 9 shares at 0.58 commits 5.22 pUSD. Filled entirely at 0.56, it receives 9.321429 shares.
- Signing precision: a market BUY signs shares truncated to the tick's decimal places plus two, while settlement uses six. A 5 pUSD market BUY at 0.66 signs 7.5757 shares and receives 7.575758.
How the adapter raises the order quantity
Nautilus orders are sized in shares, and the execution engine rejects a fill past the order quantity
by default. The quantity therefore rises before the fill applies, through an OrderUpdated event
recorded in the order's history like any other amendment:
- WebSocket fills: the adapter emits
OrderUpdatedwith the cumulative filled quantity, thenOrderFilled, so the order reachesFilled. - REST reports: a
FilledBUY status report carries its evidence-capped filled quantity as its quantity. Reconciliation sees that it differs from the cached order and applies a reconciliationOrderUpdatedbefore the fills. Status checks accept the raised quantity. - Modified orders: the raised quantity covers the whole order, including fills on earlier venue orders.
Commission is computed on the venue fill quantity.
Recovery limitations
Two REST recovery paths apply a recovered BUY overfill without raising the order quantity first, so
the engine rejects the fill unless LiveExecutionEngineConfig.allow_overfills is enabled:
- The periodic position check applies recovered fills as standalone reports. A rejected fill holds
back position reconciliation until
position_check_threshold_mspasses. A later check then synthesizes a correcting fill, without the venue commission, whengenerate_missing_ordersis enabled. - Reconciliation of an order with a pending cancel or modify skips the quantity update for a
Filledreport, so its fills apply against the signed quantity.
Both paths apply only when the user stream misses the fill and stream-gap trade discovery does not recover it.
Terminal order handling
Terminal quantity normalization triggers from the MATCHED order update for resting maker
orders, or directly on the confirming taker trade for atomic FOK orders. It emits a reconciliation
OrderUpdated which lowers the order quantity to the cumulative venue fill. It does not emit a
fill and does not change positions, balances, or commissions.
IOC maps to venue FAK. Once a taker trade confirms, every positive difference between
original_size and size_matched is an unfilled remainder which the venue has killed. The adapter
therefore emits OrderCanceled after the real fills instead of normalizing quantity or leaving the
order partially filled. REST reports apply the same rule when a MATCHED FAK has
size_matched < original_size. The same terminal handling runs after buffered fills drain when a
confirmed trade arrives before the submit response. A buffered Canceled, Expired, or
Rejected report takes precedence.
Commissions and tracking scope
FillReport.commission is computed from the venue-reported fill size, the same quantity the fill
carries.
The fill tracker is keyed by venue_order_id. It registers orders on accept and restores cached
open orders on startup, so the WebSocket overfill raise applies only to orders it tracks.
DUST_SNAP_THRESHOLD_DEC is not configurable per-strategy; it lives in
nautilus_polymarket::common::consts.
Order message size denomination
The user channel reports original_size on an order message as the signed makerAmount. For a
market order type (FAK or FOK) BUY that amount is the pUSD budget rather than a share count, so
a BUY of 100 shares at 0.01 reports 1. The adapter divides by the order price when it must express
that venue amount as shares in an order status report. Locally submitted quote-sized limit BUYs use
the share quantity derived during signing as their authoritative fill-tracker quantity.
A SELL signs shares as its maker amount and needs no conversion. Resting types (GTC and GTD)
pass through unchanged: their denomination is unconfirmed, and converting a share-denominated size
would misreport every externally-managed resting order.
Exec tester close residuals
close_positions_qty_precision is an ExecTesterConfig option. It defaults to None, which
submits the full position quantity. The Rust and Python Polymarket examples set it to 2 because
market order maker amounts allow two decimals. The examples also set
close_positions_time_in_force=IOC; custom
configurations must use IOC or FOK because Polymarket rejects GTC market orders.
On stop, the tester truncates only the submitted market SELL quantity to the configured decimal precision and logs the exact difference at WARN level. It does not round the position state or create a synthetic fill.
A 5 pUSD BUY that fills 5.1975 shares therefore submits a 5.19-share close. After the venue fills that order, the position remains open at exactly 0.0075 shares. If the whole position is below 0.01 shares, the tester warns and submits no zero-quantity order. Treat close-on-stop as best-effort and check the position and warning before assuming the account is flat. A non-zero close must also satisfy the venue's applicable order constraints; rejection leaves the full position open. See the position reporting limitation for sub-0.01-share venue reports.
WebSockets
PolymarketWebSocketClient uses the Nautilus Rust WebSocketClient.
Data
The data adapter opens market subscriptions dynamically as instruments are requested. It spreads
those subscriptions across a pool of market WebSocket connections so that no single connection
carries more than ws_max_subscriptions assets. The pool grows lazily (a universe below the cap
stays on one connection) and closes a secondary connection once it owns no assets.
The pool does not open a connection when the data client connects, unless subscribe_new_markets
is set. That setting opens the primary connection for new-market discovery. Otherwise the first
asset subscription opens a connection.
Each connection replays only its own assets on reconnect. A shard reconnect also drops that shard's local books and
gates its book deltas (and book-derived best_bid_ask tops) until fresh snapshots arrive; a
one-shot monitor starts recovery if a snapshot is still missing after book_snapshot_timeout_secs.
The venue sends a book snapshot when an asset is first subscribed and ignores a duplicate
subscribe. When a book delta subscription joins an asset that a quote, trade, or resolution
subscription already holds, and the book has no accepted snapshot, the adapter starts book
recovery unless recovery or a post-reconnect snapshot wait already covers the book. Recovery cycles
the asset's subscription until a valid snapshot arrives.
A single price_change payload can contain interleaved updates for several assets. The adapter
groups updates by instrument and publishes one atomic order book delta batch per instrument, while
quote processing remains in the venue payload order.
Quote ticks
The adapter exposes one quote tick subscription type. It does not expose separate
subscriptions for snapshot-derived, price-change-derived, and best_bid_ask quotes. Quote, book
delta, and trade subscriptions for the same instrument share one asset-scoped market WebSocket
subscription. A book delta subscription alone does not emit quote ticks; quote output remains gated
by an active quote subscription.
| Venue message | Trigger | Price source | Size source |
|---|---|---|---|
book | Book snapshot | Snapshot best bid and ask | Snapshot best-level sizes |
price_change | Subscribed level update | Message best_bid and best_ask | Changed best-level size; previous quote or zero otherwise |
best_bid_ask | Top move with subscribe_new_markets = true | Direct message best bid and ask | Maintained-book top or prior quote, depending on book state |
All three venue message paths converge on the same quote tick stream. Deduplication compares prices and sizes with the last emitted quote regardless of which message type produced it.
best_bid_ask handling
With subscribe_new_markets enabled, the venue also sends best_bid_ask events when an asset's top
of book moves. Every market connection requests these asset-scoped events; only the primary
connection forwards global new-market and resolution events. The payload carries prices only, so the
adapter selects each side's size as follows:
- With effective deltas, an active book delta subscription, and book updates not gated pending a valid snapshot, a side takes its size from the maintained local book when its top price matches. Before the first snapshot, or when the top does not match, its size is zero.
- Without effective deltas, or while book updates are gated pending a valid snapshot, a side keeps the previous quote size when its top price matches. A moved or unknown side has zero size.
The adapter ignores events older than the last emitted quote or, with effective deltas, the local book. It also rejects locked, crossed, out-of-range, and off-grid events.
An empty price, a bid at or below zero, or an ask at or above one is a missing side. By default,
drop_quotes_missing_side drops the event. When missing sides are allowed, the missing price uses
the current tick-relative venue bound and its size is zero.
Book snapshot validation
When a book snapshot includes a hash and its full preimage, the adapter reproduces it from the
exact wire values and level order. It logs and rejects a mismatch before the snapshot can update
local book state, emit snapshot-derived deltas or quotes, or resume gated book deltas. For
book-delta subscribers, a mismatch also triggers book recovery: the adapter resubscribes the
market until a valid snapshot arrives and drops incremental price_change deltas in the meantime.
A mismatch during recovery fails the current attempt, so the next resubscribe follows without
waiting for the snapshot deadline. After its retry budget, recovery retries at an interval that
doubles from one minute to fifteen minutes.
Polymarket also sends hashed book updates that omit fields included in the server's hash preimage,
such as tick_size and last_trade_price. The adapter accepts these updates without hash
verification because their exact hash preimage is unavailable. Snapshots without a hash remain
compatible.
Price change bursts
Polymarket reports a match as a burst of price_change messages that share one timestamp. When the
taker order rests a remainder, that remainder arrives first, followed by one removal for each
opposite-side level it consumed, and a book event closes the burst. Each message's best_bid and
best_ask already reflect the completed match, so applying the messages one at a time can cross the
book until the consumed levels are removed.
A book delta subscription keeps a local book from its first accepted snapshot. When a
price_change batch leaves that book crossed, with a bid above an ask, the adapter appends deletes
for the bids above that asset's best_bid and the asks below its best_ask. The emitted batch then
leaves the book uncrossed, and the venue's later removals of those levels delete nothing. Batches
that leave the book uncrossed, including a book locked at one price, pass through unchanged.
A missing best_bid or best_ask leaves its side unpruned, and an invalid one skips pruning. The
adapter logs a warning whenever an emitted batch leaves the book crossed.
Live recovery validation
The polymarket-book-stress harness is a development tool for changes to book synchronization and
recovery. It uses Polymarket public market data, submits no orders, and subscribes one outcome token
from each of the six open, order-accepting markets with the highest 24-hour volume. Each book has
its own connection (ws_max_subscriptions is 1).
The harness checks every emitted book against the book stream contract and an independent
reconstruction of the venue feed's best 20 levels. Polymarket books carry no sequence, so the
reconstruction aligns snapshots with book events and updates with price_change events by
timestamp, and skips batches it cannot align. A session also fails if a book emits no incremental
updates after the venue sent it at least 10 price_change events.
Run it from a network location Polymarket serves. From the repository root, run:
CARGO_BUILD_JOBS=16 bash scripts/strip-adapter-env.bash \
cargo test -p nautilus-polymarket --features examples --test polymarket-book-stress -- --timeout 10 --rounds 12--scenario selects the run:
churn(default): one phase per round: a broken snapshot hash, a dropped initial snapshot, a reconnect while a recovering book is held, dropped snapshots after a reconnect, and a restart during recovery.boundaries: holds a recovering book through the retry budget, then checks the retry ceiling, a reconnect at the ceiling, unsubscribe during recovery, and shutdown during a reconnect.
--timeout sets the snapshot timeout in seconds, where 0 disables snapshot deadlines, and
--rounds sets the number of rounds (12 by default). --tokens takes comma-separated outcome
token IDs from six distinct open, order-accepting markets to test instead of the most traded
markets, since a quiet book can miss the recovery waits.
Two venue behaviors limit what the harness can force:
- Polymarket never rejects a subscription, and an unverifiable
bookevent can complete recovery before a corrupted replacement arrives, so the harness withholds replacement snapshots instead. - Reconnect replay also resubscribes a recovering book, so the ceiling reconnect check shows prompt recovery without isolating the ceiling wake.
The harness requires the market WebSocket channel, the Gamma markets API, and the CLOB API. See Stress harnesses for the shared flags and output format.
Effective deltas
compute_effective_deltas defaults to false. Enable it to trade extra processing for smaller
snapshot batches (see Data client options):
- A full book snapshot with prior local state emits only net level changes:
ADDfor new levels,UPDATEfor resized levels, andDELETEwith the last known size for removed levels. No-op snapshots emit nothing, and the final record carriesF_LAST. - Without prior state, such as after a tick size change, the snapshot passes through unchanged to seed the new book epoch.
- Incremental
price_changebatches follow price change bursts handling and update the local comparison state. - When book deltas are subscribed, the maintained comparison book can supply matching sizes to
best_bid_askquote ticks. This can change those quote sizes and their unchanged-quote suppression, and the carried sizes can affect laterprice_changequotes. Trades are unchanged.
RTDS custom data
The data client also supports Polymarket's real-time data (RTDS) crypto, crypto TWAP, and equity
topics. Subscribe through generic custom data with a required, non-empty symbol metadata value.
TWAP subscriptions also require window_seconds equal to 30 or 60:
from nautilus_trader.adapters.polymarket import POLYMARKET_CLIENT_ID
from nautilus_trader.adapters.polymarket import PolymarketRtdsCryptoPrice
from nautilus_trader.adapters.polymarket import PolymarketRtdsCryptoTwap
from nautilus_trader.adapters.polymarket import PolymarketRtdsEquityPrice
from nautilus_trader.model import DataType
crypto_type = DataType(
PolymarketRtdsCryptoPrice.__name__,
metadata={"symbol": "btcusdt"},
)
equity_type = DataType(
PolymarketRtdsEquityPrice.__name__,
metadata={"symbol": "AAPL"},
)
twap_type = DataType(
PolymarketRtdsCryptoTwap.__name__,
metadata={"symbol": "BTC/USD", "window_seconds": 60},
)
strategy.subscribe_data(crypto_type, client_id=POLYMARKET_CLIENT_ID)
strategy.subscribe_data(equity_type, client_id=POLYMARKET_CLIENT_ID)
strategy.subscribe_data(twap_type, client_id=POLYMARKET_CLIENT_ID)Symbols and values
Symbol matching is case-insensitive, and published symbols are lowercase. Crypto RTDS uses the
crypto_prices topic; equity RTDS uses equity_prices. Equity updates prefer
full_accuracy_value when the venue supplies it and fall back to value for snapshots or updates
that omit it. Crypto TWAP uses crypto_prices_twap_thirty or
crypto_prices_twap_sixty, requires the frame's window_s to match the subscription, and exposes
the exact signed-E18 full_accuracy_value as a Rust Decimal. Python receives the exact decimal
string, which can be converted with decimal.Decimal; the display-only value is required and
decimal-like for wire conformance but is never published.
TWAP reconnects and replay
Polymarket TWAP subscriptions start with the next update and provide no snapshot, history, or replay after a disconnect. The adapter restores subscriptions after reconnect and resumes with the next update, so the disconnect interval remains a data gap. The replay guard survives reconnect, so a redelivery of the last observation remains suppressed. The adapter also suppresses older observations. A different value for the same observation timestamp is not emitted; it is logged at error level with the topic, symbol, timestamp, prior value, and received value. The stream continues with the prior observation authoritative, and emission resumes at the next newer observation timestamp.
Runtime instrument loading
Polymarket lists thousands of active markets and new markets appear throughout the day, so preloading the full universe at startup is rarely practical. The data adapter auto-loads missing instruments on demand so that strategies can subscribe to markets that are not in the cache:
- When a strategy issues
subscribe_quotes,subscribe_trades,subscribe_book_deltas,subscribe_instrument_status,subscribe_instrument_close, orrequest_instrumentfor an instrument that is not cached, the adapter registers the request and waitsauto_load_debounce_ms(default 100 ms) so that concurrent requests coalesce. - It then issues a single batched Gamma API call. Batches larger than the Gamma
condition_idsquery ceiling (about 100) are split across multiple calls and merged. - Once the instruments are loaded, they are published to the data engine (populating the cache) and the deferred subscriptions open their WebSocket subscriptions atomically. A strategy that unsubscribes while the auto-load is in flight does not see a spurious subscription opened.
Loading configuration
The feature is enabled by default. Disable it by setting auto_load_missing_instruments=False on
PolymarketDataClientConfig. To preload a known set of markets at startup instead, supply any of
these on PolymarketInstrumentProviderConfig:
load_idsfiltersevent_slugsmarket_slugsevent_slug_builderseries_ids
These scopes compose rather than override each other: filter-driven queries run alongside any
explicit slug or series scope, and load_ids loads additively on top. Only the unfiltered
full-universe fetch is suppressed once an explicit scope is present. The same composition applies
to the periodic refresh driven by update_instruments_interval_mins, so a scope configured at
startup keeps refreshing for the life of the client, and the bootstrap and refresh universes
match.
Markets awaiting CLOB metadata
Gamma can report a newly listed market before usable clob_token_ids are available, or omit it from a
lookup. Auto-load retries these Gamma results and fetch failures with bounded exponential backoff plus
jitter. It does not query CLOB GET /markets/{cid} to classify hydration.
Tune the cadence with auto_load_max_retries (default 12), auto_load_retry_delay_initial_secs (default
5.0), and auto_load_retry_delay_max_secs (default 15.0). The default retry delays total approximately
three minutes; HTTP requests and rate-limit waits add to elapsed time. Set auto_load_max_retries=0 to
disable retry.
5-minute markets (e.g. updown crypto) can expire before the venue finishes hydrating, so budget for that or raise the cap. After the retry budget is exhausted, a condition still missing on Gamma is logged as a terminal miss and the caller must resubscribe after the market becomes available.
Market resolution events
The Rust data client tracks Polymarket exposure at condition_id level so both YES and NO legs
close together when the venue resolves the market. Position events add open Polymarket binary
option instruments to an internal watchlist. Data clients can also watch an instrument without a
position by subscribing to InstrumentStatus, InstrumentClose, or both. These subscriptions are
independent: a status subscription emits only the status close, while a close subscription emits
only the settlement price. Unsubscribing from one does not remove the other.
Subscription ownership and pending instruments
Cached instruments establish a watch when the subscription is accepted. Missing instruments first pass through auto-loading and the configured instrument filters. Unsubscribing removes only that data owner; open positions retain their independent ownership. If loading cannot produce usable metadata, no automatic watch is created. An accepted unresolved intent can still be checked with an explicit manual resolution selector.
If an outcome arrives while a subscribed instrument is still loading, the client retains that outcome until its metadata passes the configured filters. Already admitted data and position owners settle immediately; a pending sibling does not delay them. Completing the pending subscription emits only its requested events and does not reopen ordinary market-data streams. Unsubscribing its last event type or rejecting its instrument filter discards the retained outcome.
Automatic resolution paths
Once a watched condition expires, the data client waits resolve_poll_grace_secs, then polls Gamma
every resolve_poll_interval_secs until the condition resolves or
resolve_poll_max_wait_secs elapses.
| Delivery path | Configuration and eligibility | Release |
|---|---|---|
| Auto-load outcome. | A strict outcome in a fetched Gamma payload, regardless of polling. | Applied immediately; the auto-load task completes. |
| Gamma/CLOB polling. | resolve_poll_enabled=true, within the expiration-based poll window. | Resolution, last owner removal, timeout, or shutdown. |
| Resolution WebSocket. | subscribe_new_markets=true, with an active, unpaused data watch. | Resolution, last data owner removal, timeout, or shutdown. |
| Manual request. | Any configuration, using explicit selectors or the watchlist rules. | Request completion; successful resolution removes the watch. |
The shipped defaults use polling, without a resolution-only WebSocket subscription. Disabling
polling does not enable WebSocket resolution: that path requires subscribe_new_markets=true.
With both disabled, later recovery requires a manual request.
These WebSocket ownership rules apply to the data subscription's token, not the independently configured venue-wide discovery feed. Releasing the token does not disconnect that feed. Valid resolutions received there still use the shared apply path for existing data and position owners.
Winner inference and settlement
Resolution uses strict winner inference:
- Gamma must return a closed binary market with exactly two token IDs, two outcomes, and a binary
outcomePricesshape. - If Gamma does not provide a strict result for the condition, the client falls back to CLOB
GET /markets/{condition_id}and usestokens[].winner. - Non-binary, ambiguous, malformed, or still-unresolved payloads are skipped. They remain on the watchlist until the poll window times out or a manual request resolves them.
Auto-loading applies a strict outcome from either its normal lookup or positive closure probe immediately. An expiration that is future, stale, or missing does not discard an outcome already obtained. Without a strict outcome, existing expiration deadlines still apply: late subscriptions do not receive a fresh polling window, and missing expiration does not cause indefinite polling.
When the client applies a resolution, position-owned legs emit one InstrumentStatus close and one
InstrumentClose. Data-only legs emit whichever event types have active subscriptions. The winner
leg closes at 1, and the losing leg closes at 0. The close type is
InstrumentCloseType.CONTRACT_EXPIRED. In a live node, the execution engine settles each open
position in the leg at that price and emits one PositionClosed without an order or fill; the
first close applied is authoritative (see
Settlement at contract expiration).
Settlement does not redeem tokens or claim funds on-chain. The pUSD balance includes the payout only after redemption, so account balances exclude unredeemed winnings until then. Deposit Wallet users can redeem winning tokens with Position operations.
Closure and subscription release
Gamma's positive closed=true evidence stops normal quote, trade, and book-delta streams for both
outcome siblings, even when the payload cannot produce usable instruments. Closure alone does not
establish a winner or emit settlement events. Existing resolution owners remain available for
polling or manual recovery; an enabled, unpaused resolution WebSocket may remain until resolution
or timeout. Later live subscriptions cannot reopen the closed condition.
The same apply path handles auto-load outcomes, WebSocket market_resolved events, automatic
polling, and manual requests. Successful resolution emits each admitted owner's event types once,
removes those owners and the condition's watch, and releases its WebSocket subscriptions. Existing
pending data intents retain their outcome until admission or cancellation; new subscriptions cannot
re-enroll the resolved condition. Automatic delivery never bypasses instrument-filter admission.
Timeout, reconnect, and reset
After resolve_poll_max_wait_secs, the watch pauses and releases resolution-only WebSocket
ownership, including when polling is disabled. An open market's independent quote, trade, or book
subscriptions are unaffected by this pause. The client retains settlement metadata and ownership
for manual recovery; a manual request does not restart the automatic deadline. Disconnect stops
network work, and reconnect resumes unfinished loading and replays cached, active resolution
WebSocket subscriptions. Reset discards retained ownership and outcomes and requires fresh
subscriptions.
Manual resolution requests
Use request_data() with data type PolymarketResolveRequest to force a resolution check. The
request accepts any of these params:
| Param | Type | Description |
|---|---|---|
condition_id | str | Resolve one Polymarket condition. |
condition_ids | str or list[str] | Resolve one or more Polymarket conditions. |
instrument_ids | str or list[str] | Resolve Polymarket instrument IDs; other venues are ignored. |
If a request omits all selectors, the client uses the watchlist. With automatic polling enabled, the fallback selects paused or timed-out entries. With automatic polling disabled, it selects all expired eligible entries, so operators can run the recovery flow manually.
The response payload is custom data with this dictionary shape:
| Key | Meaning |
|---|---|
requested_condition_ids | Deduplicated condition IDs checked by the request. |
fetched_markets | Gamma markets returned across the batched lookup. |
resolved_markets | Conditions with a strict Gamma result or successful CLOB fallback result. |
skipped_non_binary_markets | Gamma markets skipped for non-binary or ambiguous resolution shape. |
clob_fallback_successes | Conditions resolved through the CLOB fallback path. |
emitted_condition_ids | Conditions that emitted at least one InstrumentClose. |
failed_condition_ids | Conditions where both Gamma and CLOB lookup failed. |
used_watchlist_fallback | Whether the request selected conditions from the watchlist. |
timed_out_watchlist | Timed-out watchlist entries seen during fallback selection. |
error | First summary error, if one occurred. |
Redemption is a separate account or execution workflow. Do not extend the data client resolution path to claim funds; it only publishes market-outcome close events into Nautilus.
Purging instruments at runtime
Polymarket auto-loads instruments on demand, so a long-running session keeps growing the cache as
markets resolve, new markets appear, and strategies cycle through events. Use cache.purge_instrument
to drop markets the strategy no longer tracks. The call removes the instrument record and every
cache-owned map keyed by it (order book, quotes, trades, bars).
class PolymarketHousekeeping(Strategy):
def on_position_closed(self, event: PositionClosed) -> None:
# Drop the market once the position is closed and you have no further interest.
instrument_id = event.instrument_id
self.unsubscribe_quotes(instrument_id)
self.unsubscribe_book_deltas(instrument_id)
self.cache.purge_instrument(instrument_id)Common triggers on Polymarket:
- A market resolves and produces no further trades.
- An event ends and the strategy rotates off its markets.
- The strategy rotates a fixed-size watchlist and drops the oldest entry.
The purge skips any instrument that still has non-terminal orders (initialized, submitted, accepted, emulated, released, or inflight) or non-closed positions, so it is safe to call without coordinating with the execution client. Active WebSocket subscriptions belong to the data engine. Unsubscribe before purging if you no longer want updates.
The cache also exposes purge_order, purge_position, purge_closed_orders,
purge_closed_positions, and purge_account_events for trimming closed execution state.
For long-running Polymarket nodes, schedule the bulk purges from LiveExecutionEngineConfig
(15 min interval, 60 min buffer is a sensible default). See
Cache: purging cached data for the full set.
The caller decides when an instrument is no longer needed. Purging an instrument that another actor, strategy, or engine still relies on causes missing instrument lookups and loses market-data history.
Execution
Before starting its WebSocket or initializing account state, the execution client queries
unauthenticated GET /version. Startup continues only when the venue reports numeric version 2.
Any other version stops startup with an unsupported-version error; a missing, malformed, or errored
response stops startup with a version-query failure.
The execution adapter subscribes once to an account-wide user channel for order and trade events.
It does not open market-channel subscriptions for instruments seen during trading.
The shared WebSocket client logs a peer close code and reason before reconnecting. Malformed payload warnings and venue rejection reasons use the same bounded text handling as HTTP responses. Order rejections received through WebSocket or reconciliation use the same exact post-only classification as submit responses.
Fill recovery and deduplication
Matched WebSocket fills and their corrections are restored from cached order history and deduplicated across reconnects. If a trade arrives before its instrument is available, the adapter leaves it out of the dedup state. A redelivered event or later REST reconciliation can apply it after instrument loading completes.
The adapter also validates every owned leg of a trade, including its commission, before emitting any fill for it. If validation fails, it emits no fill for that trade and quarantines it, and a targeted terminal REST read settles it as described in Failed trades and REST resolution. Replayed WebSocket updates do not retry a quarantined trade.
Terminal quantity normalization
For a fully matched order, terminal quantity normalization waits for every trade ID in the order's
associate_trades list to confirm before lowering the order quantity to its actual fills. If a
confirmed trade is recovered through REST after a WebSocket gap, reconciliation applies the same
order-only normalization. If a MATCHED WebSocket update omits associate_trades, the adapter does
not infer that settlement is final; the next REST reconciliation recovers the residual after the
trade reaches CONFIRMED.
Subscription limits
Polymarket does not publish a WebSocket subscription cap in its current rate-limit documentation.
ws_max_subscriptions (default 200) is therefore a conservative, self-chosen per-connection
reliability bound rather than a venue-enforced limit: high per-connection subscription counts have
been observed to silently stall a connection. The adapter enforces the bound by sharding asset
subscriptions across a pool of market connections, opening a new connection only when the existing
ones are full and closing a secondary connection once it owns no assets.
Rate limiting
Polymarket applies Cloudflare IP limits to its APIs and separate per-signer token buckets to CLOB order and cancellation requests. The adapter enforces the signer limits in process. All clients for one signer use the same limiter, which has independent order and cancellation buckets.
Per-signer CLOB trading limits
The adapter starts each signer at the Standard tier. Polymarket determines tier eligibility from
the maker wallet's cumulative 30-day trading volume, even when the maker differs from the signer,
and refreshes assignments every three hours. The adapter does not calculate eligibility: a
recognized Poly-RateLimit-Tier response header selects one of these encoded profiles and updates
both buckets, while an unknown tier is logged and ignored.
| Tier | 30-day maker volume | Order rate (tokens/s) | Order burst | Cancel rate (tokens/s) | Cancel burst | Negative cancel balance |
|---|---|---|---|---|---|---|
| Standard | - | 40 | 60 | 80 | 120 | Yes |
| Copper | $30,000+ | 60 | 90 | 120 | 180 | Yes |
| Bronze | $50,000+ | 80 | 120 | 160 | 240 | Yes |
| Silver | $100,000+ | 200 | 300 | 400 | 600 | Yes |
| Gold | $500,000+ | 400 | 600 | 800 | 1,200 | Yes |
| Platinum | $2.5M+ | 450 | 675 | 900 | 1,350 | No |
| Diamond | $5M+ | 525 | 787 | 1,050 | 1,575 | No |
| Elite | $10M+ | 600 | 900 | 1,200 | 1,800 | No |
Request token costs
Covered requests consume:
| Bucket | Request | Token cost |
|---|---|---|
| Order | POST /order | 1 |
| Order | POST /orders | Number of orders |
| Cancellation | DELETE /order | 1 |
| Cancellation | DELETE /orders | Number of submitted order IDs |
| Cancellation | DELETE /cancel-all | 1 plus successful cancellations |
| Cancellation | DELETE /cancel-market-orders | 1 plus successful matching cancellations |
A request waits for its full token cost and is rejected locally only when that cost exceeds the
current tier's burst. Before each new DELETE /orders chunk, the adapter recomputes its cap from the
smaller of the endpoint's 1,000-ID limit and that burst. Cancel-all and cancel-market requests debit
one token before the request, then debit each successful cancellation after the response. Standard
through Gold tiers can enter cancellation debt; Platinum through Elite tiers floor the balance at
zero.
Rate-limit responses
Poly-RateLimit-Remaining can lower the local balance, and Poly-RateLimit-Reset extends a rejected
or indebted bucket's wait. The adapter logs Poly-RateLimit-Warning responses with the endpoint,
token cost, tier, remaining balance, and reset time.
A 429 Too Many Requests response with Retry-After blocks the applicable bucket for at least that
delay before retry. Without Retry-After, the retry manager uses its configured exponential
backoff. Submit classification of 425 and 429 is in
Definitive and ambiguous outcomes.
Selected IP-based REST limits
Polymarket changes these quotas over time. As of 2026-08-04, the official limits are:
| Endpoint | Burst (10s) | Sustained (10 min) | Notes |
|---|---|---|---|
| General rate limiting | 15,000 | - | Global documented rate limit. |
Health check (/ok) | 100 | - | Health endpoint. |
| CLOB general | 9,000 | - | Aggregate across CLOB endpoints. |
CLOB POST /order | 5,000 | 120,000 | Single-order submit. |
CLOB POST /orders | 2,000 | 21,000 | Batch submit (up to 15 orders per request). |
CLOB DELETE /order | 5,000 | 120,000 | Single-order cancel. |
CLOB DELETE /orders | 2,000 | 15,000 | Batch cancel. |
CLOB DELETE /cancel-all | 250 | 6,000 | Cancel all orders. |
CLOB DELETE /cancel-market-orders | 1,500 | 21,000 | Cancel orders for one market. |
CLOB GET /balance-allowance | 200 | - | Balance and allowance queries. |
| CLOB API key endpoints | 100 | - | Key management. |
| Gamma general | 4,000 | - | Aggregate across Gamma endpoints. |
Gamma /markets | 300 | - | Market metadata. |
Gamma /events | 500 | - | Event metadata. |
| Data general | 1,000 | - | Aggregate across Data API endpoints. |
Data /trades | 200 | - | Trade history. |
Data /positions | 150 | - | Current positions. |
WebSocket limits
The WebSocket quotas are not part of the published REST rate-limits table. The adapter enforces
ws_max_subscriptions (default 200) by sharding subscriptions across a pool of market connections.
Exceeding the IP-based limits triggers Cloudflare throttling. Requests are queued using sliding windows rather than rejected immediately, but sustained overshoot can result in HTTP 429 responses or temporary blocking.
For the latest limits, see the official Polymarket CLOB trading rate limits and general rate limits.
Limitations and considerations
- Reduce-only orders are not supported.
- Batch submit (
POST /orders) accepts at most 15 orders per request; the adapter splits largerSubmitOrderListcommands into sequential 15-order chunks. - Batch cancel (
DELETE /orders) accepts at most 1,000 order IDs per request; the adapter also limits each new chunk to the signer's current cancellation burst and recomputes that limit before the chunk. - Position reports omit balances below 0.01 shares. Do not treat an omitted report as proof that a dust position is flat; a sub-minimum residual cannot be exited through the market's minimum order size, which active markets commonly report as five shares. Position reconciliation therefore tolerates differences through 0.009999 shares and reconciles differences of 0.01 shares or more.
Client configuration
Rust structs and Python classes expose the same client configuration. The only Rust-only fields
are the programmatic filters and new_market_filter trait objects on
PolymarketDataClientConfig.
Data client options
Class/struct: PolymarketDataClientConfig.
| Option | Default | Description |
|---|---|---|
instrument_config | None | Bootstrap scope, passed as PolymarketInstrumentProviderConfig. |
filters | [] | Rust-only instrument filters applied during loading and discovery. |
base_url_http, base_url_ws | None | Override the CLOB HTTP or WebSocket endpoint. |
base_url_gamma, base_url_data_api | None | Override the Gamma or Data API endpoint. |
base_url_rtds | None | Override the RTDS endpoint. |
proxy_url | None | HTTP or HTTPS proxy for every data transport. |
http_timeout_secs, ws_timeout_secs | 60, 30 | HTTP and WebSocket timeout in seconds. |
ws_max_subscriptions | 200 | Per-connection subscription cap; the market pool shards across connections at this bound. |
update_instruments_interval_mins | 60 | Instrument catalog refresh interval; pass None to disable it. |
subscribe_new_markets | false | Subscribe to discovery and resolution events; also enables best_bid_ask quote ticks. |
new_market_filter | None | Rust-only filter applied to newly discovered markets before instrument emission. |
new_market_fetch_max_concurrency | 8 | Bound concurrent market fetches from discovery events. |
drop_quotes_missing_side | true | Drop quotes that do not contain both a bid and an ask. |
compute_effective_deltas | false | Emit net snapshot changes when prior book state exists. |
auto_load_missing_instruments | true | Load unknown instruments for supported requests and subscriptions. |
auto_load_debounce_ms | 100 | Coalesce concurrent auto-load requests. |
auto_load_max_retries | 12 | Retry transient CLOB hydration misses; 0 disables retry. |
auto_load_retry_delay_initial_secs | 5.0 | Initial auto-load retry delay. |
auto_load_retry_delay_max_secs | 15.0 | Maximum auto-load retry delay. |
resolve_poll_enabled | true | Poll expired watched conditions for resolution. |
resolve_poll_interval_secs | 30 | Resolution polling interval. |
resolve_poll_grace_secs | 10 | Delay after expiry before polling begins. |
resolve_poll_max_wait_secs | 1,800 | Pause automatic polling after this wait. |
transport_backend | Sockudo | WebSocket transport implementation. |
book_snapshot_timeout_secs | 10 | Max wait for a post-reconnect or recovery book snapshot. |
book_stale_check_interval_secs | 5 | Book feed staleness check interval. |
book_stale_threshold_secs | 0 | Max book feed silence before reporting stale; 0 disables the monitor. |
Execution client options
Class/struct: PolymarketExecutionClientConfig.
| Option | Default | Description |
|---|---|---|
account_id | POLYMARKET-001 | Account identifier for this execution client. |
private_key | POLYMARKET_PK | EIP-712 signing key. |
api_key, api_secret, passphrase | environment variables | CLOB L2 authentication credentials. |
funder | POLYMARKET_FUNDER | Funding wallet; proxy and deposit-wallet signatures require it to differ from the signing address. |
signature_type | Eoa | Eoa, PolyProxy, PolyGnosisSafe, or Poly1271. |
signer_type | Owner | Owner or Session; sessions require explicit credentials and Poly1271. |
base_url_http, base_url_ws, base_url_data_api | None | Override the respective production endpoint. |
proxy_url | None | HTTP or HTTPS proxy for every execution transport. |
http_timeout_secs | 60 | HTTP timeout in seconds. |
max_retries | 3 | Retries for single-order submit/cancel requests and for each batch-cancel chunk. |
retry_delay_initial_ms | 1,000 | Initial retry delay. |
retry_delay_max_ms | 10,000 | Maximum retry delay. |
heartbeat_enabled | false | Send an authenticated order-safety heartbeat immediately after execution readiness and every five seconds thereafter. |
transport_backend | Sockudo | WebSocket transport implementation. |
instrument_config | None | Same PolymarketInstrumentProviderConfig as the data client. Unmapped records use its load_ids. |
Order-safety heartbeats
Enable heartbeat_enabled for a dedicated automated execution process only when every order owned
by its CLOB API credentials should be canceled if the process stops responding. Use dedicated
credentials for each heartbeat-owning process.
A normal disconnect stops heartbeats and causes cancellation after the venue timeout. Leave
heartbeat_enabled disabled when orders must survive client shutdown or another process uses the
same CLOB API credentials.
Enabling this option starts Polymarket's order-safety heartbeat contract for those credentials. Polymarket cancels their open orders when it does not receive a heartbeat within 10 seconds, with an additional 5-second buffer.
The adapter sends the first empty heartbeat ID, chains each returned ID, and uses a replacement ID from an HTTP 400 response to resynchronize. The execution client reports as disconnected until the first heartbeat is acknowledged. It also reports as disconnected after any of these failures:
- Authentication or venue rejection.
- Two consecutive retryable request failures.
- A request or retry delay that cannot finish with a one-second margin before the 10-second safety deadline.
After such a failure, explicitly disconnect and reconnect the client to restore heartbeats.
Proxy routing
Set proxy_url to apply one HTTP or HTTPS proxy to every transport owned by that client. The data
client routes CLOB HTTP, Gamma HTTP, Data API HTTP, the market WebSocket pool, and RTDS through the
proxy. The execution client routes authenticated CLOB HTTP, Data API HTTP, and the authenticated
user WebSocket through it. Configure the same value on both clients when running data and execution
together.
SOCKS URLs and malformed URLs fail configuration validation. When proxy_url is None, the adapter
does not configure an explicit proxy: HTTP uses environment proxy settings and
WebSockets connect directly. Treat credential-bearing proxy URLs as secrets because serialized
configs contain the supplied URL. Python exposes only has_proxy_url; configuration Debug output
and transport diagnostics redact proxy credentials.
Instrument provider options
Pass the same PolymarketInstrumentProviderConfig as instrument_config on the data client
config and the execution client config.
load_ids is the only reconciliation scope. When that set is non-empty, unmapped records
outside it are expected absences. When load_ids is unset or empty, every unmapped open
order and position is in scope and fails the report request. event_slugs, market_slugs,
series_ids, filters, and event_slug_builder discover instruments; they do not classify
unmapped records. A node that scopes discovery with those fields and still wants scoped
reconciliation must also set load_ids.
| Option | Default | Description |
|---|---|---|
load_all | false | Load the full venue catalog at startup. |
load_ids | None | Load exact Nautilus instrument IDs. |
filters | None | Validated Gamma market keyset filters. |
event_slugs | None | Resolve all markets for the listed events at bootstrap. |
market_slugs | None | Load the listed Gamma market slugs at bootstrap. |
event_slug_builder | None | Rust-backed Up/Down event-slug generator. |
series_ids | None | Load markets for the listed Gamma series at bootstrap. |
log_warnings | true | Emit provider warnings. |
use_gamma_markets | false | Reserved compatibility field with no additional effect. |
Gamma query filters
The adapter uses the Gamma market and event keyset endpoints. It validates filters before
the first HTTP request, follows next_cursor, and applies the endpoint page ceilings of 100 markets
and 500 events.
Market keyset fields
| Class | Fields |
|---|---|
| Scalar | limit, order, ascending, closed, decimalized, liquidity_num_min, liquidity_num_max, volume_num_min, volume_num_max, start_date_min, start_date_max, end_date_min, end_date_max, related_tags, tag_match, cyom, rfq_enabled, uma_resolution_status, game_id, include_tag, locale |
| Repeated | id, slug, clob_token_ids, condition_ids, question_ids, market_maker_address, tag_id, sports_market_types |
| Compatibility | active, archived |
| Alias | is_active |
| Client only | offset, max_markets |
The provider filters dictionary accepts only market fields. Rust callers configure event
discovery with EventParamsFilter and GetGammaEventsParams; event-only fields such as live or
tag_slug are not valid provider dictionary keys.
Event keyset fields
| Class | Fields |
|---|---|
| Scalar | limit, order, ascending, closed, live, featured, cyom, title_search, liquidity_min, liquidity_max, volume_min, volume_max, start_date_min, start_date_max, end_date_min, end_date_max, start_time_min, start_time_max, tag_slug, related_tags, tag_match, event_date, event_week, featured_order, recurrence, parent_event_id, include_children, partner_slug, include_chat, include_template, include_best_lines, locale |
| Repeated | id, slug, tag_id, exclude_tag_id, series_id, game_id, created_by |
| Compatibility | active, archived |
| Client only | offset, max_events |
Filter values and validation
Repeated fields are sent as repeated query keys. offset is applied across returned keyset pages
and is never sent to Gamma. max_markets caps markets locally, with each binary market normally
producing two instruments. max_events caps events locally; each event can contain many markets.
condition_ids accepts at most 100 values, and event tag_id values cannot overlap exclude_tag_id
values.
The provider filters dictionary accepts strings in the native Rust config and also accepts Python
bool, int, finite float, string, or lists of those scalar values when converting a
mapping-shaped Python config. The Python conversion ignores None entries; native config entries
must be strings. is_active=true supplies active=true, archived=false, and closed=false;
explicit values override those defaults. Unknown keys, malformed values, empty lists, invalid date
or numeric bounds, and invalid combinations raise ValueError during Python config conversion.
See the official market keyset and event keyset references for the venue contract.
Filter scopes
Filters come in two forms: the filters map on PolymarketInstrumentProviderConfig, and Rust
InstrumentFilters registered on the client. Registered filters take precedence: when both are
present the filters map is ignored and the provider logs a warning.
A filter that sources markets is a complete bootstrap scope on its own and does not need
load_all or a slug or series scope alongside it. A registered filter sources markets when it
supplies any of:
- Market or event slugs
- Gamma market query params
- Gamma event params
- Search params
A non-empty filters map qualifies on the same basis. A filter that only accepts or rejects
instruments, such as PredicateFilter, refines another source's results and still needs one of
those alongside it.
Event slug builder
The adapter treats Python as a configuration, factory, and user strategy boundary.
Provider, data, and execution operations run in Rust. event_slug_builder therefore accepts a
Rust-backed PolymarketUpDownEventSlugConfig; it does not accept Python callable paths.
Use this for predictable Polymarket Up/Down event slugs without downloading the full venue
catalog. The builder emits slugs with the pattern
{asset}-updown-{interval_mins}m-{unix_timestamp} for the configured window of aligned periods.
from nautilus_trader.adapters.polymarket import PolymarketInstrumentProviderConfig
from nautilus_trader.adapters.polymarket import PolymarketUpDownEventSlugConfig
instrument_config = PolymarketInstrumentProviderConfig(
event_slug_builder=PolymarketUpDownEventSlugConfig(
assets=["btc"],
interval_mins=5,
periods=3,
start_offset_periods=0,
),
)For custom event patterns, pass explicit event_slugs, pass direct market_slugs, scope by
series_ids, or add a Rust filter or builder. The adapter rejects Python callable
event_slug_builder values so adapter operations do not cross into Python during live trading.
Series IDs
A Gamma series groups a recurring market family, such as the 5-minute Up/Down crypto intervals or
a daily weather market. Scoping by series_ids loads the markets of every active, unresolved event
in those series, which avoids reconstructing slugs client-side as each interval rolls over:
from nautilus_trader.adapters.polymarket import PolymarketInstrumentProviderConfig
instrument_config = PolymarketInstrumentProviderConfig(
series_ids=[10684, 10192],
)The provider resolves each series through the Gamma events endpoint with active=true and
closed=false, then loads the markets of the matching events. Because the query is re-evaluated on
every refresh, pairing series_ids with update_instruments_interval_mins on the data client keeps
a rolling family of markets current without any slug arithmetic.
Find the series ID for a market family in the series field of its Gamma event payload.
Python discovery and historical data
The Python package exports a Rust-backed PolymarketDataLoader for public discovery,
instrument construction, and historical trades. It uses the Rust Gamma, CLOB, and Data API clients,
so it does not require trading credentials or run networking in Python.
Market loading
All network methods are asynchronous. Build a loader from a market slug and select its outcome token by index:
from nautilus_trader.adapters.polymarket import PolymarketDataLoader
loader = await PolymarketDataLoader.from_market_slug(
"will-jd-vance-win-the-2028-us-presidential-election",
token_index=0,
)
instrument = loader.instrument
token_id = loader.token_id
condition_id = loader.condition_idResolution metadata
instrument is a normalized BinaryOption. When the source fields are available, resolution data
is retained as follows:
| Data | instrument.info | resolution_metadata |
|---|---|---|
| Market description | description | - |
| Event start | event_start_time | - |
| Market end | end_date | - |
| Resolution source | resolution_source | resolutionSource |
| Crypto resolution config | crypto_market_config | - |
| Raw Gamma market JSON | - | gamma_market |
| Raw Gamma event JSON | - | gamma_event (event loading) |
| Closed state | - | closed |
| Closure time | - | closedTime |
| UMA resolution status | - | umaResolutionStatus |
| Token outcome/winner state | - | tokens with outcome and winner |
Read resolution_metadata after a backtest or simulation to inspect the lifecycle snapshot:
metadata = loader.resolution_metadata
winner = next(
(token["outcome"] for token in metadata["tokens"] if token["winner"]),
None,
)Event loading
An event factory returns one loader for each market in the event:
loaders = await PolymarketDataLoader.from_event_slug(
"how-many-fed-rate-cuts-in-2026",
token_index=1,
)A negative token index or an index outside a market's token list raises ValueError. Construction
also fails clearly when Gamma has no matching slug or CLOB has not populated usable token IDs.
Public discovery
Static query methods return stable Python mappings and lists while Rust owns validation and pagination. JSON values map to Python as follows:
| JSON value | Python value | Scope |
|---|---|---|
| Fractional number | decimal.Decimal | Includes nested event markets, fee schedules, and CLOB rewards |
| Integer | int | A financial field can be int or Decimal, depending on its JSON token |
| String | str | JSON-encoded strings such as outcomePrices are not parsed further |
| Null | None | Preserves absence |
Use decimal operands when calculating with these values; Python does not mix Decimal and float
arithmetic. The Gamma competitiveness score is returned as Decimal after an approximate Rust
floating-point conversion.
market = await PolymarketDataLoader.query_market_by_slug("some-market")
details = await PolymarketDataLoader.query_market_details(market["conditionId"])
event = await PolymarketDataLoader.query_event_by_slug("some-event")
markets = await PolymarketDataLoader.query_markets(
filters={
"is_active": True,
"tag_id": [21, 42],
"order": "volume",
"max_markets": 200,
},
)
events = await PolymarketDataLoader.query_events(
filters={
"active": True,
"closed": False,
"max_events": 100,
},
)
tags = await PolymarketDataLoader.query_tags()
results = await PolymarketDataLoader.query_search(
"bitcoin",
events_status="active",
limit_per_type=20,
)Market and event filter dictionaries use the fields listed under
Gamma query filters. The provider config accepts only the market fields,
while query_events accepts the event fields. Unknown or malformed filters raise ValueError
before any request.
Historical trades
load_trades returns normalized TradeTick objects in chronological order:
from datetime import UTC, datetime, timedelta
end = datetime.now(UTC)
start = end - timedelta(days=1)
trades = await loader.load_trades(
start=start,
end=end,
limit=1_000,
)Time window and pagination
The window is inclusive. The Data API records trade timestamps in whole seconds, so Rust keeps all
trades in the start and end boundary seconds. The v2 condition feed serves a
fixed three-year window and ignores
start/end bounds, so the adapter never sends them and filters the window locally instead. The
feed serves pages newest-first; with a start bound the walk continues until a whole page precedes
start:
| Request | Meaning of limit | Walk termination |
|---|---|---|
With start | Earliest matching trades in the window | A whole page precedes start, or the cursor exhausts |
Without start | Most recent matching trades | The matching-trade count reaches limit; without limit, the 10,000-row walk cap; or the cursor exhausts |
Retention and request limits
When the cursor exhausts and start predates the approximate three-year retention horizon,
Rust logs a warning that results may be incomplete. History outside the venue's retention window
cannot be recovered through this feed. A request with neither start nor limit stops after a page brings the retained count to
at least 10,000 window-matching rows and returns the newest partial results with a logged warning.
The final page can add up to 999 rows beyond that threshold.
An end-only request traverses all pages newer than end before collecting matching trades.
Its duration therefore grows with the volume newer than end: the cap bounds retained history,
not the number of requests.
Closed market cleanup
Gamma endDate is a scheduled end, not proof that trading stopped. The client keeps cached
instruments while Gamma reports closed=false and removes live state after a positive closed=true.
The closure check runs on every resolve-poll tick for expired cached instruments still reported open. It retries failed requests on the next tick, so request failures or delayed venue data can delay retirement beyond one cycle. A failed condition ID batch does not discard the closures confirmed by the other batches. If both Gamma lookups omit a market, the client keeps it because closure was not observed.
Only live instruments carry this state. The historical data loader reports terminal state through
resolution_metadata instead, so a backtest cannot see a market's current closure through
instrument.info.
Contributing
To contribute features or fixes to the Polymarket adapter, see the contributing guide.
OKX
Founded in 2017, OKX is a cryptocurrency exchange that offers spot, margin, perpetual swap, futures, options, spread, and event contract trading. This...
Tardis
Tardis provides granular cryptocurrency market data, including tick-by-tick order book snapshots and updates, trades, open interest, funding rates, option...