Gas-free Split, Merge, and Redeem operations for Polymarket CLOB tokens using the Builder Relayer.
Repository: github.com/AleSZanello/poly-examples
For CLOB tokens, you MUST use the CTF contract directly with parentCollectionId = bytes32(0)
| Approach | Target Contract | Works for CLOB Tokens? |
|---|---|---|
| NegRisk Adapter | 0xd91E80cF... |
❌ NO - Different token IDs |
| CTF Direct | 0x4d97dcd9... |
✅ YES - Use parent=0 |
The NegRisk Adapter computes different internal token IDs, causing "SafeMath: subtraction overflow" errors when trying to merge/redeem CLOB tokens.
All operations tested and verified on Polygon Mainnet:
| Operation | TX Hash | Result |
|---|---|---|
| Split | 0x2de1ba6f... | 1 USDC → 1 YES + 1 NO ✅ |
| Merge | 0x24c98b90... | 1 YES + 1 NO → 1 USDC ✅ |
| Redeem | 0x0b115de5... | After resolution → 1 USDC ✅ |
- Python 3.9+
- Polymarket account with Builder API credentials
- USDC in your Polymarket wallet
- Clone this repository:
git clone https://github.com/AleSZanello/poly-examples.git
cd poly-examples- Create virtual environment:
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate- Install dependencies:
pip install -r requirements.txt- Configure environment:
cp .env.example .env
# Edit .env with your credentialspython test_split_merge_redeem.py --wallet proxy --check-only# Split 1 USDC into 1 YES + 1 NO tokens
python test_split_merge_redeem.py --wallet proxy --split-only --amount 1# Merge 1 YES + 1 NO back to 1 USDC
python test_split_merge_redeem.py --wallet proxy --merge-only --amount 1 -c <CONDITION_ID># Redeem tokens after market resolves
python test_split_merge_redeem.py --wallet proxy --redeem -c <CONDITION_ID># Run complete split -> merge cycle
python test_split_merge_redeem.py --wallet proxy --amount 1# Simulate without executing (validates everything works)
python test_split_merge_redeem.py --wallet proxy --dry-run --amount 1The script supports two wallet types:
| Wallet | Flag | Description |
|---|---|---|
| PROXY | --wallet proxy |
Polymarket Proxy wallet (default for website trading) |
| SAFE | --wallet safe |
Gnosis Safe wallet (Builder Relayer default) |
Most users should use --wallet proxy since that's where your USDC and tokens are when trading on Polymarket.
| Contract | Address |
|---|---|
| CTF (Conditional Tokens) | 0x4d97dcd97ec945f40cf65f87097ace5ea0476045 |
| USDC | 0x2791bca1f2de4661ed88a30c99a7a9449aa84174 |
| NegRisk Adapter | 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296 |
| CTF Exchange | 0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e |
// Split USDC into YES + NO tokens
function splitPosition(
address collateralToken, // USDC
bytes32 parentCollectionId, // bytes32(0) for CLOB tokens!
bytes32 conditionId, // Market condition ID
uint256[] partition, // [1, 2] for YES and NO
uint256 amount // Amount in 6 decimals
) external;
// Merge YES + NO tokens back to USDC
function mergePositions(
address collateralToken, // USDC
bytes32 parentCollectionId, // bytes32(0) for CLOB tokens!
bytes32 conditionId, // Market condition ID
uint256[] partition, // [1, 2] for YES and NO
uint256 amount // Amount in 6 decimals
) external;
// Redeem tokens after resolution
function redeemPositions(
address collateralToken, // USDC
bytes32 parentCollectionId, // bytes32(0) for CLOB tokens!
bytes32 conditionId, // Market condition ID
uint256[] indexSets // [1, 2] for both outcomes
) external;The critical parameter is parentCollectionId = bytes32(0). This tells the CTF contract to use the root collection, which matches the CLOB token IDs.
def build_merge_data(condition_id: str, amount: int) -> str:
params = encode(
['address', 'bytes32', 'bytes32', 'uint256[]', 'uint256'],
[
USDC_ADDRESS,
bytes(32), # parentCollectionId = 0 (KEY!)
condition_id_bytes,
[1, 2],
amount
]
)
return "0x" + (selector + params).hex()When you need to find a market's conditionId:
| Endpoint | Works? | Notes |
|---|---|---|
GET /markets/{conditionId} |
❌ | Returns "id is invalid" |
GET /markets?conditionId=X |
❌ | Returns wrong market! |
GET /markets?clob_token_ids=X |
✅ | Best method |
GET /book?token_id=X (CLOB API) |
✅ | Returns conditionId as market |
import aiohttp
import json
async def get_condition_id(token_id: str) -> str:
url = f"https://gamma-api.polymarket.com/markets?clob_token_ids={token_id}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
markets = await resp.json()
return markets[0]['conditionId']Cause: Using NegRisk Adapter instead of CTF contract directly.
Solution: Ensure you're targeting 0x4d97dcd97ec945f40cf65f87097ace5ea0476045 with parent=0.
Cause: The Gamma API doesn't support direct conditionId lookup.
Solution: Use token_id lookup instead: GET /markets?clob_token_ids={token_id}
Cause: Not enough USDC or tokens in the wallet.
Solution: Check balances with --check-only and ensure you're using the correct wallet type.
Cause: Usually indicates the transaction would revert. Solution: Verify token balances, approvals, and that the market hasn't resolved yet (for merge) or has resolved (for redeem).
- Never commit your
.envfile with real credentials - The private key controls your Polymarket wallets - keep it secure
- Builder API credentials should be treated as sensitive
- Test with small amounts first
py-builder-relayer-client
py-builder-signing-sdk
python-dotenv
eth-account
web3
eth-abi
aiohttp
requests
MIT License
Pull requests welcome! Please test thoroughly before submitting.
- Polymarket for the Builder Relayer API
- Gnosis Conditional Tokens Framework