> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xo.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Redeem positions

> How to redeem settled Conditional Tokens through the XO bundler.

The orderbook API does not move collateral. After a market settles, your XO smart account still holds ERC-1155 outcome tokens on [`ConditionalTokens`](https://github.com/gnosis/conditional-tokens-contracts/blob/master/contracts/ConditionalTokens.sol). To convert them into XO USDC, the smart account must call `redeemPositions` as an ERC-4337 UserOperation through the XO bundler.

Mainnet bundler: [`https://mainnet-bundler.xo.market/`](https://mainnet-bundler.xo.market/)

## When a position is redeemable

`GET /claimable?address=<smart-account>` lists positions the orderbook considers settled. Treat that list as a discovery hint, not the on-chain gate.

`ConditionalTokens.redeemPositions` reverts with `result for condition not received yet` until the oracle has reported payouts. That happens when the market is **closed** on-chain (after the resolution period), not merely when the winner is recorded.

Confirm on-chain before sending a UserOperation:

```text theme={null}
payoutDenominator(conditionId) > 0
```

If the denominator is still `0`, wait and poll. A non-zero denominator means both resolved winners and voided refunds can be redeemed with the same call.

<Note>
  XO CLOB markets are binary. The manager reports `[1, 0]` or `[0, 1]` when a winner is recorded, and `[1, 1]` when the market is voided. You do not choose an amount — `redeemPositions` burns the smart account's full balance in each requested index set and pays `stake × numerator / denominator` in XO USDC.
</Note>

## Call shape

```solidity theme={null}
function redeemPositions(
    IERC20 collateralToken,
    bytes32 parentCollectionId,
    bytes32 conditionId,
    uint[] calldata indexSets
) external;
```

| Argument             | Mainnet value                                | Why                                                  |
| -------------------- | -------------------------------------------- | ---------------------------------------------------- |
| `collateralToken`    | `0x80c12230ce677e6f304027a14780Edd2A829ab0c` | XO USDC. Same collateral used to mint the positions. |
| `parentCollectionId` | `0x0000…0000` (32 zero bytes)                | CLOB markets are not nested.                         |
| `conditionId`        | `market_id` from `/claimable` or `/markets`  | `0x`-prefixed 32-byte hex.                           |
| `indexSets`          | `[1, 2]`                                     | YES is slot 0 (`0b01`), NO is slot 1 (`0b10`).       |

Always pass both index sets. The losing side burns with a zero payout (a no-op on the transfer). Passing both also covers voided markets, where each side pays out at 50%.

`msg.sender` must be the smart account that holds the tokens. That is why the call is wrapped in a UserOperation executed by the smart account — signing `redeemPositions` from the owner EOA as a normal transaction does not redeem the smart account's positions.

## Submit through the bundler

1. Encode `redeemPositions` calldata against Conditional Tokens (`0xCcf1b2C676E2f9A13e5674be0e0DdFF6d0B13909`).
2. Build an ERC-4337 v0.7 UserOperation whose inner call is that calldata (`to` = Conditional Tokens, `value` = `0`).
3. Attach the XO paymaster (address from onboarding) with empty `paymasterData`. The bundler estimates paymaster gas limits.
4. Sign the bundler-computed `userOpHash` with the **owner EOA** of the smart account (raw ECDSA, same key that signs orders).
5. `eth_sendUserOperation` to `https://mainnet-bundler.xo.market/`.
6. Wait for the UserOperation receipt and check `success`. A reverted UserOperation is still included in a bundle transaction that succeeds — do not treat the outer transaction hash as proof that the redeem ran.

The XO bundler accepts zero EIP-1559 fees:

```text theme={null}
maxFeePerGas         = 0
maxPriorityFeePerGas = 0
```

EntryPoint is ERC-4337 v0.7: `0x0000000071727De22E5E9d8BAf0edAc6f37da032`.

<Note>
  The paymaster allowlists `ConditionalTokens.redeemPositions`, so redemption is gas-sponsored. The smart account does not need a native-token balance. Ask your XO contact for the mainnet paymaster address during onboarding.
</Note>

## TypeScript (viem)

```ts theme={null}
import {
  createPublicClient,
  encodeFunctionData,
  http,
  type Address,
  type Hex,
} from "viem";
import { createBundlerClient } from "viem/account-abstraction";

const BUNDLER_URL = "https://mainnet-bundler.xo.market/";
const RPC_URL = "https://rpc-mainnet-2.xo.market?api_key=<API_KEY>";

const CONDITIONAL_TOKENS = "0xCcf1b2C676E2f9A13e5674be0e0DdFF6d0B13909" as Address;
const XO_USDC = "0x80c12230ce677e6f304027a14780Edd2A829ab0c" as Address;
const PARENT_COLLECTION_ID =
  "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex;
const BINARY_INDEX_SETS = [1n, 2n];

const redeemAbi = [
  {
    type: "function",
    name: "redeemPositions",
    stateMutability: "nonpayable",
    inputs: [
      { name: "collateralToken", type: "address" },
      { name: "parentCollectionId", type: "bytes32" },
      { name: "conditionId", type: "bytes32" },
      { name: "indexSets", type: "uint256[]" },
    ],
    outputs: [],
  },
  {
    type: "function",
    name: "payoutDenominator",
    stateMutability: "view",
    inputs: [{ name: "conditionId", type: "bytes32" }],
    outputs: [{ type: "uint256" }],
  },
] as const;

async function redeemCondition(
  account: Parameters<typeof createBundlerClient>[0]["account"],
  conditionId: Hex,
  paymaster: Address,
) {
  const publicClient = createPublicClient({
    transport: http(RPC_URL),
  });

  const denominator = await publicClient.readContract({
    address: CONDITIONAL_TOKENS,
    abi: redeemAbi,
    functionName: "payoutDenominator",
    args: [conditionId],
  });
  if (denominator === 0n) {
    throw new Error("payouts not reported yet — market is not closed on-chain");
  }

  const bundler = createBundlerClient({
    account,
    transport: http(BUNDLER_URL),
    paymaster: {
      getPaymasterData: async () => ({
        paymaster,
        paymasterData: "0x" as Hex,
      }),
      getPaymasterStubData: async () => ({
        paymaster,
        paymasterData: "0x" as Hex,
      }),
    },
    userOperation: {
      estimateFeesPerGas: async () => ({
        maxFeePerGas: 0n,
        maxPriorityFeePerGas: 0n,
      }),
    },
  });

  const userOpHash = await bundler.sendUserOperation({
    calls: [
      {
        to: CONDITIONAL_TOKENS,
        data: encodeFunctionData({
          abi: redeemAbi,
          functionName: "redeemPositions",
          args: [XO_USDC, PARENT_COLLECTION_ID, conditionId, BINARY_INDEX_SETS],
        }),
      },
    ],
  });

  const receipt = await bundler.waitForUserOperationReceipt({ hash: userOpHash });
  if (!receipt.success) {
    throw new Error(receipt.reason ?? "UserOperation reverted");
  }
  return receipt.receipt.transactionHash;
}
```

`account` is the same XO smart account you use as `order.maker`. How that object encodes `execute` / `executeBatch` is wallet-specific (Kernel ERC-7579 `execute(bytes32,bytes)` on accounts created at [`beta.xo.market`](https://beta.xo.market)). The inner call above is what must reach Conditional Tokens.

## Several markets in one UserOperation

De-dupe by `(collateralToken, conditionId)` and batch one `redeemPositions` per condition. Passing `[1, 2]` redeems every outcome the smart account holds on that condition, so two `/claimable` rows for the same voided market become a single call.

```ts theme={null}
const unique = new Map<string, Hex>();
for (const row of claimable) {
  unique.set(row.market_id.toLowerCase(), row.market_id as Hex);
}

const calls = [...unique.values()].map((conditionId) => ({
  to: CONDITIONAL_TOKENS,
  data: encodeFunctionData({
    abi: redeemAbi,
    functionName: "redeemPositions",
    args: [XO_USDC, PARENT_COLLECTION_ID, conditionId, BINARY_INDEX_SETS],
  }),
}));
```

## Payout math

| Settlement   | Reported payouts | Denominator | 1 share of YES | 1 share of NO |
| ------------ | ---------------- | ----------- | -------------- | ------------- |
| Resolved YES | `[1, 0]`         | `1`         | 1 XO USDC      | 0             |
| Resolved NO  | `[0, 1]`         | `1`         | 0              | 1 XO USDC     |
| Voided       | `[1, 1]`         | `2`         | 0.5 XO USDC    | 0.5 XO USDC   |

Shares and collateral are 6-decimal. A `/claimable` `balance` of `"50000000"` is 50 shares.

On success, Conditional Tokens emits `PayoutRedemption(redeemer, collateralToken, parentCollectionId, conditionId, indexSets, payout)` and transfers `payout` XO USDC to the smart account.

## Troubleshooting

| Symptom                                 | Check                                                                                                                                                                                |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `result for condition not received yet` | `payoutDenominator(conditionId)` is still `0`. Winner recorded is not enough — wait until the market is closed on-chain.                                                             |
| UserOperation receipt `success: false`  | The inner `redeemPositions` reverted. Inspect `reason`. A successful bundle transaction is not a successful redeem.                                                                  |
| Paymaster rejects the op                | Confirm the paymaster address from onboarding, empty `paymasterData`, and that the inner target/selector is `ConditionalTokens.redeemPositions`.                                     |
| Signature / `AA24`                      | The UserOperation must be signed by an owner EOA registered on the smart account. `maker` on orders is the smart account; the signer of the UserOperation is the owner key.          |
| Zero collateral received                | The smart account held only the losing side, or the balance was already redeemed. `redeemPositions` is idempotent on a zero balance — it emits `PayoutRedemption` with `payout = 0`. |
| `got invalid index set`                 | Use `[1, 2]` for binary CLOB markets. `0` and the full set (`3`) are rejected.                                                                                                       |
| Tokens still in the owner EOA           | Positions live on the smart account. Redeem from that address, not the owner wallet.                                                                                                 |

## Related

* [Market lifecycle](/guides/market-lifecycle) — when `/claimable` starts listing a position.
* [Smart accounts](/guides/smart-accounts) — owner vs smart account, and why on-chain CT calls go through the bundler.
* `GET /claimable` — settled positions the orderbook knows about.
