Skip to content

Redeeming Winning Positions

Polymarket V2 deposit-wallet redeem has one supported production path: the owner signs an EIP-712 WALLET batch, the relayer submits it through the deposit-wallet factory, and the wallet call targets a collateral adapter. Deposit wallets do not redeem pUSD-native positions by calling ConditionalTokens directly.

State Model

Matched, winning, and redeemable are different states:

StateMeaning
matchedThe CLOB filled the order and shares moved into the deposit wallet.
winningThe resolved outcome matches the held token.
redeemableThe Data API reports the held winning shares can be redeemed.

Do not infer winning status from a matched order. Use Data API positions.

CLOB vs Data API: which API answers which question

CLOB tells us order/fill status. Data API tells us outcome/PnL/redeemable state. Mixing the two is the most common runbook bug; route each question to the right endpoint.

QuestionAPIEndpoint
Did my order get accepted? Is it open or canceled?CLOBGET /data/orders
Did my order fill? At what price and size?CLOBGET /data/trades
What positions does my deposit wallet hold? Are any redeemable, mergeable, or negRisk?DataGET /positions
What was my realized/unrealized PnL on a closed market?Data/closed-positions (same family)

A STATE_MATCHED from /data/trades does not mean the market won. The authoritative readiness signal is redeemable=true on the Data API position row. Polymarket’s own redeem flow inspects the Data API, not the CLOB. Polygolem’s settlement.FindRedeemable enforces this — it queries /positions for the deposit wallet and filters redeemable=true; CLOB order/fill state is never used as a redeem trigger.

Detection

Query the deposit wallet, not the EOA:

Terminal window
polygolem data positions --user 0xDEPOSIT_WALLET --limit 50 --json

The important fields are:

FieldPurpose
assetHeld CTF token ID
conditionIdBytes32 condition ID used by redeem calldata
redeemableRedemption readiness signal
mergeableFuture merge-readiness signal
negativeRiskChooses the negative-risk collateral adapter
outcome, outcomeIndexHeld outcome metadata
oppositeOutcome, oppositeAssetOperator context and idempotence checks
endDateResolution window context

There is no separate resolved boolean in the current position schema.

Adapter Selection

PositionTarget
negativeRisk=falseCtfCollateralAdapter
negativeRisk=trueNegRiskCtfCollateralAdapter

Addresses:

CtfCollateralAdapter 0xAdA100Db00Ca00073811820692005400218FcE1f
NegRiskCtfCollateralAdapter 0xadA2005600Dec949baf300f4C6120000bDB6eAab

These addresses come from Polymarket’s current contracts reference. Verify them before treating a relayer allowlist rejection as an upstream blocker. The 2026-05-09 live settlement incident was caused by stale adapter constants, not by a permanent relayer limitation.

The adapter keeps the legacy CTF redeem ABI:

redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)

It uses conditionId; it ignores the collateral address, parent collection, and caller-supplied indexSets. The adapter reads the wallet’s CTF balances, redeems the winning side, wraps proceeds back into pUSD, and sends pUSD to the deposit wallet.

Approval Requirement

Trading approvals are not enough. A deposit wallet that only approved CTFExchangeV2, NegRiskExchangeV2, and NegRiskAdapterV2 can place and settle orders, but it cannot redeem through the collateral adapters.

The adapter-readiness batch is four calls:

  1. pUSD approve(CtfCollateralAdapter, MaxUint256)
  2. CTF setApprovalForAll(CtfCollateralAdapter, true)
  3. pUSD approve(NegRiskCtfCollateralAdapter, MaxUint256)
  4. CTF setApprovalForAll(NegRiskCtfCollateralAdapter, true)

Redeem itself needs the CTF approval leg. The pUSD approval leg is included so the same one-time batch also supports future split flows.

CLI Flow

Terminal window
# Read-only live gate: wallet code, relayer creds, Data API, adapter approvals.
polygolem deposit-wallet settlement-status --json
# One-time migration for existing wallets.
polygolem deposit-wallet approve-adapters --submit --confirm APPROVE_ADAPTERS --json
# Read-only detection.
polygolem deposit-wallet redeemable --json
# Inspect the redeem calls before signing.
polygolem deposit-wallet redeem --json
# Submit only after operator approval.
polygolem deposit-wallet redeem --submit --confirm REDEEM_WINNERS --json

The submit path fails closed when CTF.isApprovedForAll(wallet, adapter) is false; the JSON output lists the missing adapter addresses and points the operator back to approve-adapters. The relayer never sees /submit when the pre-check fails.

settlement-status performs the same structural checks without signing or submitting. Live bots should refuse new buys unless it reports ready=true.

If the relayer rejects adapter approval or redeem calls as “not in the allowed list”, first verify the adapter constants against Polymarket’s current contracts reference. The 2026-05-09 live recovery proved that stale adapter addresses produce this exact symptom. If the addresses are current, stop. The production factory deploy() and proxy() entrypoints are onlyOperator, so the owner EOA cannot bypass the relayer, and raw ConditionalTokens redemption is not a deposit-wallet fallback. SAFE/PROXY relayer examples use different wallet types and do not apply to deposit-wallet positions.

Allowlist Rejection Triage

Both approve-adapters --submit and redeem --submit detect three relayer allowlist rejection markers — not in the allowed list, are not permitted, and call blocked — and emit a structured stop response:

{
"ok": false,
"depositWallet": "0x21999a07...",
"command": "approve-adapters",
"error": {
"code": "RELAYER_ALLOWLIST_BLOCKED",
"message": "relayer: allowlist block ...",
"action": "stop",
"reason": "Polymarket relayer rejected the WALLET batch via its allowlist policy. Verify the local V2 adapter constants against Polymarket's current contract reference; if they match, stop. The V2 deposit wallet path has no EOA bypass, raw CTF fallback, or SAFE/PROXY shortcut.",
"upstream": {
"state": "allowlist-rejected",
"verify": "https://docs.polymarket.com/resources/contracts"
}
}
}

Operator runbooks must treat RELAYER_ALLOWLIST_BLOCKED as a stop-and-check event:

  1. Compare local adapter constants to Polymarket’s contracts reference.
  2. If local constants are stale, update Polygolem and retry only after tests pass.
  3. If local constants are current, escalate as an upstream allowlist block.

Do not retry on a different wallet path. The CLI exits 0 with the structured envelope so wrapper scripts can match on data.error.code.

SDK consumers detect the same condition with errors.Is:

import (
"errors"
"github.com/TrebuchetDynamics/polygolem/pkg/relayer"
"github.com/TrebuchetDynamics/polygolem/pkg/settlement"
)
result, err := settlement.SubmitRedeem(ctx, rc, key, positions, settlement.DefaultBatchLimit)
if errors.Is(err, relayer.ErrRelayerAllowlistBlocked) {
return fmt.Errorf("verify adapter registry, then stop: %w", err)
}

SDK Flow

go-bot consumes the SDK directly. The CLI is for humans and runbooks; production integration should not shell out to Polygolem.

import "github.com/TrebuchetDynamics/polygolem/pkg/settlement"
readiness, err := settlement.CheckReadiness(ctx, dataClient, owner, depositWallet, settlement.ReadinessOptions{
RPCURL: polygonRPCURL,
RelayerConfigured: true,
})
if err != nil {
return err
}
if !readiness.Ready {
return fmt.Errorf("settlement gate blocked: %s: %s", readiness.Status, readiness.NextAction)
}
positions, err := settlement.FindRedeemable(ctx, dataClient, depositWallet)
if err != nil {
return err
}
if len(positions) == 0 {
return nil
}
result, err := settlement.SubmitRedeem(ctx, relayerClient, privateKey, positions, settlement.DefaultBatchLimit)
if err != nil {
if errors.Is(err, relayer.ErrRelayerAllowlistBlocked) {
return fmt.Errorf("verify adapter registry, then stop: %w", err)
}
return err
}
log.Printf("redeem tx=%s state=%s redeemed=%d", result.TransactionID, result.State, result.CallCount)

settlement.SubmitRedeem dedupes by conditionId (collapsing YES/NO splits) and caps each WALLET batch at limit calls (default 10). The contract-level no-op behavior keeps it idempotent: re-running on a wallet whose CTF balance for a condition is already zero pays nothing.

Safety Checks

Before live submission:

  1. Dry-run output lists the expected condition IDs.
  2. Required adapter approvals are present on-chain.
  3. Batch size is capped; default target is 10 calls.
  4. The private key and relayer credentials are loaded from ignored env files.
  5. No market-window mismatch exists for the position lineage.