CTF (On-Chain Operations)¶
On-chain merge, split, and redeem via Polymarket's Conditional Token Framework contracts on Polygon.
Requirements:
- Install with the
[ctf]extra:pip install "polymarket-pandas[ctf]" private_keymust be set (for signing transactions)- Amounts are in USDC.e base units (6 decimals):
1_000_000= 1.00 USDC - For proxy wallet users (most Polymarket accounts): builder API credentials are required (
POLYMARKET_BUILDER_API_KEY,POLYMARKET_BUILDER_API_SECRET,POLYMARKET_BUILDER_API_PASSPHRASE)
Contract addresses (Polygon mainnet):
| Contract | Address |
|---|---|
| ConditionalTokens | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 |
| NegRiskAdapter | 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296 |
| USDC.e (collateral) | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 |
Proxy Wallet vs EOA¶
Most Polymarket users have a proxy wallet (a Gnosis Safe or custom proxy) that holds their tokens. The client auto-detects this: when address differs from the EOA derived from private_key, CTF operations route through Polymarket's GSN relayer instead of sending direct on-chain transactions.
- Direct (EOA): The EOA holds tokens and pays gas in MATIC.
- Relayed (proxy): Tokens live in the proxy wallet. The relayer submits the transaction through the proxy's GSN relay hub (gasless for the user). Requires builder API credentials for HMAC authentication.
Endpoints¶
estimate_ctf_tx(tx_data) -> GasEstimate¶
Estimate gas cost for a CTF transaction without sending it. Also available via estimate=True on all CTF methods.
est = client.merge_positions(
condition_id="0x4aee6d11...",
amount_usdc=10.0,
estimate=True,
)
print(f"Gas: {est['gas']:,} units @ {est['gasPrice'] / 1e9:.1f} gwei")
print(f"Cost: {est['costMatic']:.6f} MATIC")
print(f"EOA balance: {est['eoaBalance'] / 1e18:.6f} MATIC")
Returns a GasEstimate TypedDict:
| Field | Type | Description |
|---|---|---|
gas |
int | Estimated gas units |
gasPrice |
int | Current gas price (wei) |
costWei |
int | gas × gasPrice |
costMatic |
float | Cost in MATIC |
eoaBalance |
int | EOA's MATIC balance (wei) |
split_position(condition_id, amount, ...) -> TransactionReceipt¶
Split USDC.e collateral into Yes + No outcome tokens.
result = client.split_position(
condition_id="0x4aee6d11...",
amount_usdc=1.0, # or amount=1_000_000 (base units)
neg_risk=False,
auto_approve=True, # approve USDC.e spending if needed
)
merge_positions(condition_id, amount, ...) -> TransactionReceipt¶
Merge equal amounts of Yes + No tokens back into USDC.e.
# Estimate first
est = client.merge_positions(
condition_id="0x4aee6d11...",
amount_usdc=10.0,
estimate=True,
)
# Then merge
result = client.merge_positions(
condition_id="0x4aee6d11...",
amount_usdc=10.0,
auto_approve=True,
)
redeem_positions(condition_id, index_sets=None, neg_risk=False, amounts=None, ...) -> TransactionReceipt¶
Redeem winning outcome tokens for USDC.e after market resolution.
# Standard binary market
result = client.redeem_positions(condition_id="0x4aee6d11...")
# Neg-risk market — must provide amounts as [yes_amount, no_amount]
# in base units (6 decimals). The losing side should be 0.
result = client.redeem_positions(
condition_id="0x4aee6d11...",
neg_risk=True,
amounts=[10_000_000, 0], # 10 winning Yes tokens
)
See examples/redeem_positions.py for a streamlined workflow that
auto-detects market type and fills in amounts from the positions API.
approve_collateral(spender=None, amount=None, ...) -> TransactionReceipt¶
Approve a CTF contract to spend USDC.e. Called automatically when auto_approve=True is passed to split_position or merge_positions.
# Manual approval (if not using auto_approve)
client.approve_collateral() # defaults to ConditionalTokens, unlimited
Common Parameters¶
All four CTF methods (split_position, merge_positions, redeem_positions, approve_collateral) accept:
| Parameter | Default | Description |
|---|---|---|
estimate |
False |
Return GasEstimate instead of sending |
wait |
True |
Block until mined (returns status, blockNumber, gasUsed) |
timeout |
120 |
Seconds to wait for receipt |
split_position and merge_positions additionally accept:
| Parameter | Default | Description |
|---|---|---|
amount |
— | Token amount in base units (6 decimals) |
amount_usdc |
— | Convenience float (e.g. 1.0 = 1 USDC). Mutually exclusive with amount |
neg_risk |
False |
True for neg-risk markets (uses NegRiskAdapter) |
auto_approve |
False |
Check on-chain allowance and approve if needed before the operation |
Batch Inventory Operations¶
batch_ctf_ops(ops, *, auto_approve=False, estimate=False) -> SubmitTransactionResponse | GasEstimate¶
Bundle multiple split / merge / redeem operations into a single proxy-relayed transaction. Useful for market makers rebalancing across many conditions: one signature, one GSN relay hop, one base fee.
Proxy-wallet only. Polymarket does not publish an EOA multicall target; EOA callers get a PolymarketAuthError directing them to the individual methods.
Each op is a dict (or row in a DataFrame):
| Key | Applies to | Description |
|---|---|---|
op |
all | "split", "merge", or "redeem" |
condition_id |
all | Market condition ID (hex string) |
amount or amount_usdc |
split, merge | Amount in base units or USDC |
neg_risk |
all | True for NegRiskAdapter, default False |
index_sets |
redeem (standard) | Defaults to [1, 2] |
amounts |
redeem (neg-risk) | Required: [yes_amount, no_amount] |
import pandas as pd
ops = [
{"op": "merge", "condition_id": "0x4aee...", "amount_usdc": 10.0},
{"op": "redeem", "condition_id": "0xabcd...", "neg_risk": True, "amounts": [5_000_000, 0]},
{"op": "split", "condition_id": "0x1234...", "amount_usdc": 2.5},
]
# Dry-run to see gas cost of the bundled proxy call
est = client.batch_ctf_ops(ops, estimate=True)
print(f"Gas: {est['gas']:,} Cost: {est['costMatic']:.6f} MATIC")
# auto_approve=True coalesces allowance checks — one approval per spender,
# for the sum of all split/merge amounts targeting that spender.
resp = client.batch_ctf_ops(ops, auto_approve=True)
print(resp["transactionHash"])
# DataFrame input works too
df = pd.DataFrame(ops)
client.batch_ctf_ops(df)
Why batch: - 21k base fee charged once per bundled transaction, not per op - One ECDSA recovery / nonce bump / GSN relay round-trip - The underlying CTF logic still runs per op — this is a dispatch optimization, not a contract-level change
See: https://docs.polymarket.com/market-makers/inventory#batch-operations
Notes¶
- Proxy wallet detection is automatic. When
address(proxy) differs from the EOA, all CTF operations route through the GSN relayer. - Gas estimation for proxy wallets uses
state_overrideto simulate from the proxy (which may have 0 MATIC). web3is lazily imported — users who never call CTF methods do not needweb3installed.neg_risk=Trueroutes split/merge through NegRiskAdapter (2-param ABI);Falseuses ConditionalTokens (5-param ABI).- See
examples/merge_positions.pyfor a complete working example with dry-run and gas estimation.