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:
| State | Meaning |
|---|---|
matched | The CLOB filled the order and shares moved into the deposit wallet. |
winning | The resolved outcome matches the held token. |
redeemable | The 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.
| Question | API | Endpoint |
|---|---|---|
| Did my order get accepted? Is it open or canceled? | CLOB | GET /data/orders |
| Did my order fill? At what price and size? | CLOB | GET /data/trades |
| What positions does my deposit wallet hold? Are any redeemable, mergeable, or negRisk? | Data | GET /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:
polygolem data positions --user 0xDEPOSIT_WALLET --limit 50 --jsonThe important fields are:
| Field | Purpose |
|---|---|
asset | Held CTF token ID |
conditionId | Bytes32 condition ID used by redeem calldata |
redeemable | Redemption readiness signal |
mergeable | Future merge-readiness signal |
negativeRisk | Chooses the negative-risk collateral adapter |
outcome, outcomeIndex | Held outcome metadata |
oppositeOutcome, oppositeAsset | Operator context and idempotence checks |
endDate | Resolution window context |
There is no separate resolved boolean in the current position schema.
Adapter Selection
| Position | Target |
|---|---|
negativeRisk=false | CtfCollateralAdapter |
negativeRisk=true | NegRiskCtfCollateralAdapter |
Addresses:
CtfCollateralAdapter 0xAdA100Db00Ca00073811820692005400218FcE1fNegRiskCtfCollateralAdapter 0xadA2005600Dec949baf300f4C6120000bDB6eAabThese 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:
- pUSD
approve(CtfCollateralAdapter, MaxUint256) - CTF
setApprovalForAll(CtfCollateralAdapter, true) - pUSD
approve(NegRiskCtfCollateralAdapter, MaxUint256) - 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
# 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 --jsonThe 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:
- Compare local adapter constants to Polymarket’s contracts reference.
- If local constants are stale, update Polygolem and retry only after tests pass.
- 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:
- Dry-run output lists the expected condition IDs.
- Required adapter approvals are present on-chain.
- Batch size is capped; default target is 10 calls.
- The private key and relayer credentials are loaded from ignored env files.
- No market-window mismatch exists for the position lineage.