Redeeming resolved Polymarket positions on-chain
How to redeem winning positions via the CTF contract directly, the relayer vs direct-call tradeoff, and gas math for the redeemer bot.
Why redemption requires a separate step
When a Polymarket binary resolves, winning outcome tokens do not automatically convert to USDC in your wallet. You hold ERC-1155 tokens that are now worth $1.00 each, but you need to call the smart contract to exchange them for USDC.
This is by design: on-chain settlement means no central party can freeze your funds, but it also means you are responsible for claiming.
What you are calling
Positions live in the Conditional Token Framework (CTF) contract on Polygon:
CTF address: 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045
USDC address: 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 (Polygon USDC)
The function you call is redeemPositions:
function redeemPositions(
address collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint256[] calldata indexSets
) externalFor a top-level binary market, parentCollectionId is bytes32(0). For YES outcome, indexSets = [1]. For NO outcome, indexSets = [2].
Direct redemption in JavaScript
const { ethers } = require('ethers')
const CTF_ABI = [
'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets) external',
]
const CTF_ADDRESS = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'
const USDC_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'
async function redeemPosition(wallet, conditionId, outcome) {
const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, wallet)
const indexSet = outcome === 'YES' ? [1] : [2]
const tx = await ctf.redeemPositions(
USDC_ADDRESS,
ethers.constants.HashZero,
conditionId,
indexSet,
{ gasLimit: 150_000 }
)
const receipt = await tx.wait()
console.log(`Redeemed in tx ${receipt.transactionHash}`)
return receipt
}Gas math
On Polygon, a single redemption costs approximately 80,000-120,000 gas. At 50 gwei gas price (normal on Polygon), that is:
100,000 gas * 50 gwei = 5,000,000 gwei = 0.005 MATIC
At $0.40/MATIC, that is $0.002 per redemption. A batch of 10 redemptions via a multicall would cost roughly $0.02.
Always keep at least 0.1 MATIC on the wallet to cover gas. The redeemer bot checks MATIC balance on startup and warns if it drops below 0.05.
async function checkGasBalance(provider, walletAddress) {
const balance = await provider.getBalance(walletAddress)
const matic = parseFloat(ethers.utils.formatEther(balance))
if (matic < 0.05) {
console.warn(`Low MATIC balance: ${matic.toFixed(4)} MATIC — top up to continue redemptions`)
}
return matic
}Relayer vs direct call
The Polymarket CLOB also offers a relayer path: submit a signed redemption request via API, and Polymarket's relayer submits the on-chain transaction on your behalf. This means you do not need MATIC.
Trade-offs:
| | Direct | Relayer | |---|---|---| | MATIC needed | Yes (~0.005/tx) | No | | Speed | You control | Relayer queue | | Dependency | None | Polymarket uptime | | Privacy | Full | Polymarket sees intent |
For the redeemer bot we use direct calls: no relayer dependency, faster, and MATIC on Polygon is cheap.
Polling for resolved markets
Before calling the contract, confirm resolution via the Gamma API:
async function findResolvedPositions(client, walletAddress) {
// Get all positions with non-zero balance
const positions = await client.getPositions(walletAddress)
const resolved = []
for (const pos of positions) {
if (parseFloat(pos.size) === 0) continue
const market = await fetch(
`https://gamma-api.polymarket.com/markets/${pos.condition_id}`
).then((r) => r.json())
if (market.closed) {
const winningToken = market.tokens.find(
(t) => parseFloat(t.price) >= 0.99
)
if (winningToken && winningToken.token_id === pos.asset_id) {
resolved.push({ conditionId: pos.condition_id, size: pos.size })
}
}
}
return resolved
}The redeemer bot loop
async function redeemerLoop(wallet, client, intervalMs = 5 * 60 * 1000) {
while (true) {
await checkGasBalance(wallet.provider, wallet.address)
const resolved = await findResolvedPositions(client, wallet.address)
console.log(`Found ${resolved.length} positions to redeem`)
for (const pos of resolved) {
try {
await redeemPosition(wallet, pos.conditionId, pos.outcome)
await sleep(2000) // brief pause between txs
} catch (e) {
console.error(`Redemption failed for ${pos.conditionId}: ${e.message}`)
}
}
await sleep(intervalMs)
}
}Handling neg-risk markets
If the market uses the neg-risk adapter (check market.neg_risk in the Gamma response), follow the two-step process described in the neg-risk adapter article before calling the CTF contract. The redeemer bot detects this automatically from the Gamma API flag.
Summary
- Call
redeemPositionson the CTF contract at0x4D97DCd97...with the correct condition ID and index set (1 for YES, 2 for NO). - Gas cost: ~$0.002 per redemption on Polygon. Keep 0.1 MATIC in the wallet.
- Confirm resolution via Gamma API before calling the contract.
- Check
neg_riskflag -- neg-risk markets need the adapter conversion step first. - Direct call is preferred over relayer: faster and no external dependency.