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

# Combos & Parlays

> Quote, execute, cash out, and redeem multi-leg Polymarket parlay positions

Combine two to ten Polymarket outcomes into a single **combo** (also called a *parlay*) — one position that pays out only if every leg wins. Combos are quoted and filled by makers over an **RFQ** (request-for-quote) gateway rather than resting on an order book, so a combo has no limit price and no partial resting state: you take a quote or you don't. Use this page to price a parlay, fill it, list what you hold, sell it back before resolution, and collect a win.

Full parameter reference and a live tester: [API Reference](/api-reference).

## Base URL

```
https://execution.kairos.trade
```

## Provider support

**Combos are Polymarket-only.** There is no `exchange_id` parameter on any endpoint on this page. Connect a supported Polymarket wallet to use combos.

## Authentication

Same API-key headers as [Orders](/rest/orders):

```
X-Client-Id: kairos_ck_...
X-Api-Key: <64 hex chars>
X-Api-Secret: <64 hex chars>
```

Examples below read credentials from `KAIROS_CLIENT_ID`, `KAIROS_API_KEY`, and `KAIROS_API_SECRET` in your shell.

Scopes differ per endpoint, and so does the status code for a bad credential triple:

| Endpoint | Scope | Bad API-key triple |
| - | - | - |
| `POST /combo/quote` | `trade:read` | `401` |
| `GET /combo/positions` | `position:read` | `401` |
| `POST /combo/execute` | `trade:execute` | **`403`** |
| `POST /combo/cash-out-quote` | `position:read` | **`403`** |
| `POST /combo/cash-out` | `trade:execute` | **`403`** |
| `POST /combo/redeem` | `trade:execute` | **`403`** |

If redemption returns `AUTH_POLICY_OUTDATED`, approve the pending trading-permissions update indicated by `update_policies`, then retry.

## Field naming

<Note>
  **Gotcha:** every request and response field on this page is `camelCase` — `legPositionIds`, `notionalUsd`, `blendedPriceE6`, `sharesBalance`. [Orders](/rest/orders) uses `snake_case` (`time_in_force`, `client_order_id`, `filled_quantity`) in the same service. A client that shares one serializer across both will silently send fields the combo endpoints ignore.
</Note>

## Number formats

> **Gotcha:** every field suffixed `E6` is **six-decimal fixed point, serialized as a decimal string** — `"16393"` means `0.016393`. This applies to prices, proceeds, fees, and share counts alike. Divide by 1,000,000 to get a human value.
>
> Two fields break that pattern:
>
> * `legs[].currentPrice` on [combo positions](#list-combo-positions) is a plain **0–1 decimal**, not e6 and not cents.
> * `maxPriceCents` on [execute](#execute-a-combo) is in **cents per share** (`60` = \$0.60), the only cents-scaled field on this page.

Combos settle in pUSD. The `*Usdc` field names are retained for compatibility.

BUY execution and cash-out share the per-user order submission limit. Exceeding it returns `429`; wait before submitting another order.

## Wallet-type limits

Combo support depends on the kind of wallet backing your account, and the limits differ per action:

| Wallet type | Quote | Execute | Cash out | Redeem |
| - | - | - | - | - |
| Kairos deposit wallet | yes | yes | yes | yes |
| EOA | **no** | **no** | **no** | **no** |
| Imported Safe / Proxy | **no** | **no** | **no** | **no** |
| Imported Deposit Wallet (ERC-1271) | yes | **no** | conditional | **no** |

Imported Deposit Wallets can request quotes. Buying and redemption are unavailable; cash-out requires an eligible wallet and returns `400` if unsupported.

## Quote a combo

```
POST /combo/quote
```

Prices a prospective combo without placing it. Use this endpoint while building a parlay.

Scope `trade:read`. A bad credential triple is `401`. BUY and cash-out quotes share a limit of 20 requests per minute per user by default; exceeding it returns `429`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `legPositionIds` | string\[] | Yes | — | 2–10 entries. Polymarket position ids, one per leg |
| `notionalUsd` | number | Yes | — | Finite, > 0. Stake in USD |

```bash theme={null}
curl -X POST https://execution.kairos.trade/combo/quote \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "legPositionIds": ["9876543210", "9876543211"],
    "notionalUsd": 1
  }'
```

Fewer than two legs is `400 a combo needs at least 2 legs`; more than ten is `400 a combo supports at most 10 legs`. A `notionalUsd` that rounds to zero at six decimals is `400 notionalUsd too small or invalid`.

### Response

```json theme={null}
{
  "yesPositionId": "1234567890",
  "conditionId": "0x1f2e...",
  "blendedPriceE6": "16393",
  "totalRequiredE6": "1000000"
}
```

| Field | Type | Description |
| - | - | - |
| `yesPositionId` | string | Position id of the combo's YES token |
| `conditionId` | string \| null | Combo condition id; `null` (not omitted) when the combo has not been minted yet |
| `blendedPriceE6` | string | Blended price per combo share, e6 |
| `totalRequiredE6` | string | Total pUSD required, e6 |

A quote is a maker price, not a reservation. If no maker will price the parlay you get `400` with a message explaining why — see [Quote failures](#quote-failures).

## Execute a combo

```
POST /combo/execute
```

Requests a quote and fills it in one call. There is no separate "accept quote" step and no quote id to pass in from `/combo/quote`.

Scope `trade:execute`. A bad credential triple is `403`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `legPositionIds` | string\[] | Yes | — | 2–10 entries. One position id per leg |
| `notionalUsd` | number | Yes | — | Finite, > 0. Stake in USD |
| `maxPriceCents` | integer | No | — | Price ceiling per share, **in cents** (`60` = \$0.60). Omit for no ceiling |

```bash theme={null}
curl -X POST https://execution.kairos.trade/combo/execute \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "legPositionIds": ["9876543210", "9876543211"],
    "notionalUsd": 1,
    "maxPriceCents": 60
  }'
```

### Response

```json theme={null}
{
  "rfqId": "0f8c...",
  "quoteId": "a41b...",
  "conditionId": "0x1f2e...",
  "yesPositionId": "1234567890",
  "blendedPriceE6": "16393",
  "totalRequiredE6": "1000000",
  "status": "filled",
  "txHash": "0xabc..."
}
```

| Field | Type | Description |
| - | - | - |
| `rfqId` | string | RFQ identifier for this execution |
| `quoteId` | string | The maker quote that was filled |
| `conditionId` | string \| null | Combo condition id |
| `yesPositionId` | string | Position id of the combo's YES token |
| `blendedPriceE6` | string | Blended fill price per share, e6 |
| `totalRequiredE6` | string | pUSD actually spent, e6 |
| `status` | string | Execution status; use the HTTP status to determine success |
| `txHash` | string \| null | Settlement transaction hash; nullable, so do not assume it is present |

If the price moved past `maxPriceCents` between quote and fill, nothing is traded and the call is `400 The price moved and the quote came back worse than your limit, so nothing was traded. Please try again.`

## List combo positions

```
GET /combo/positions
```

Lists combo positions held by your wallet.

Scope `position:read`. A bad credential triple is `401`.

### Request

No parameters.

```bash theme={null}
curl https://execution.kairos.trade/combo/positions \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET"
```

<Note>
  **Gotcha:** The response includes up to 50 positions. Pagination is unavailable.
</Note>

### Response

```json theme={null}
{
  "combos": [
    {
      "comboConditionId": "0x1f2e...",
      "comboPositionId": "1234567890",
      "sharesBalance": "25",
      "entryCostUsdc": "4.10",
      "totalCostUsdc": "4.10",
      "realizedPayoutUsdc": "0",
      "status": "OPEN",
      "redeemable": false,
      "firstEntryAt": "2026-08-01T12:00:00Z",
      "legsTotal": 3,
      "legsResolved": 1,
      "legsPending": 2,
      "legs": [
        {
          "legPositionId": "9876543210",
          "outcomeLabel": "Yes",
          "currentPrice": "0.62",
          "legStatus": "OPEN",
          "title": "Will BTC hit $100K?",
          "slug": "will-btc-hit-100k",
          "outcome": "Yes",
          "imageUrl": "https://..."
        }
      ]
    }
  ]
}
```

| Field | Type | Description |
| - | - | - |
| `comboConditionId` | string | 31-byte (`bytes31`) combo condition id — this is what [redeem](#redeem-a-combo) takes |
| `comboPositionId` | string | Position id of the combo token |
| `sharesBalance` | string | Share count as a plain decimal, **not e6**. A winning combo redeems 1:1, so this is also the maximum payout in pUSD |
| `entryCostUsdc` / `totalCostUsdc` / `realizedPayoutUsdc` | string | Cost basis and realized payout |
| `status` | string | Position status. `OPEN` persists on a won-but-unredeemed combo, so it is **not** a redemption signal |
| `redeemable` | boolean | The authoritative "can redeem now" flag |
| `firstEntryAt` | string | Timestamp of first entry |
| `legsTotal` / `legsResolved` / `legsPending` | integer | Leg counts |
| `legs[].legPositionId` | string | Position id of that leg |
| `legs[].outcomeLabel` / `outcome` | string | Outcome label for the leg |
| `legs[].currentPrice` | string | **0–1 decimal**, not e6 |
| `legs[].legStatus` | string | `OPEN`, `RESOLVED_WIN`, or `RESOLVED_LOSS`. Authoritative for that leg — use it rather than inferring a win from `currentPrice` |
| `legs[].title` / `slug` / `imageUrl` | string | Display fields; **empty strings** (not null) when market details are unavailable |

<Warning>
  **Gotcha:** branch on `redeemable`, never on `status`. `status` stays `OPEN` on a combo that has won and not yet been redeemed.
</Warning>

Every field is always present — this response contains no nulls and omits nothing.

If positions cannot be loaded, the endpoint returns `500`. Retry later.

## Quote a cash-out

```
POST /combo/cash-out-quote
```

Prices selling a combo position back to a maker before its legs resolve. Unlike [cash out](#cash-out-a-combo) itself, this accepts any positive share count, so you can price a hypothetical partial exit even though only full exits can be executed.

Scope `position:read`. A bad credential triple is `403`.

**Rate limit: 20 requests/minute per user by default, shared with BUY quotes.** Exceeding it returns `429` with `error_details.code = EXCHANGE_POLYMARKET_RATE_LIMITED`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `legPositionIds` | string\[] | Yes | — | 2–10 entries. The combo's legs |
| `shares` | number | Yes | — | Finite, > 0. Share count to price |

```bash theme={null}
curl -X POST https://execution.kairos.trade/combo/cash-out-quote \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "legPositionIds": ["9876543210", "9876543211"],
    "shares": 25
  }'
```

### Response

```json theme={null}
{
  "rfqId": "0f8c...",
  "proceedsE6": "5000000",
  "feeE6": "50000",
  "netProceedsE6": "4950000",
  "blendedPriceE6": "200000"
}
```

| Field | Type | Description |
| - | - | - |
| `rfqId` | string | RFQ identifier |
| `proceedsE6` | string | Proceeds after venue fees and before the Kairos platform fee, e6 |
| `feeE6` | string | Platform fee, e6 |
| `netProceedsE6` | string | `proceedsE6 − feeE6`, e6 — what actually lands in the wallet |
| `blendedPriceE6` | string | Sell price per share, e6 |

`minProceedsUsd` on [cash out](#cash-out-a-combo) is compared against `proceedsE6`, after venue fees but before the Kairos platform fee — not `netProceedsE6`. Set your floor accordingly, or the fee will eat into the amount you thought you were protecting.

## Cash out a combo

```
POST /combo/cash-out
```

Sells a combo position back to a maker before resolution.

Scope `trade:execute`. A bad credential triple is `403`.

**Partial cash-outs are not supported.** `shares` must equal your entire held balance exactly; anything else is `400 partial parlay cash-outs are unavailable; cash out the full position`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `legPositionIds` | string\[] | Yes | — | 2–10 entries. The combo's legs |
| `shares` | number | Yes | — | Finite, > 0, and **exactly the full held balance**. Shares to sell |
| `minProceedsUsd` | number | No | — | ≥ 0, USD (not cents, not e6). Slippage floor, compared against proceeds **after venue fees and before the Kairos platform fee** — see the callout below. Omit for no floor |

```bash theme={null}
curl -X POST https://execution.kairos.trade/combo/cash-out \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "legPositionIds": ["9876543210", "9876543211"],
    "shares": 25,
    "minProceedsUsd": 5.05
  }'
```

<Note>
  **Gotcha:** `minProceedsUsd` does not protect the amount you receive. The floor is checked after venue fees, and the Kairos platform fee is deducted afterwards, so a cash-out that passes your floor can still land *below* it in the wallet. To protect a true net figure, quote first and set `minProceedsUsd` to your target plus the `feeE6` the quote returned.
</Note>

### Response

```json theme={null}
{
  "rfqId": "0f8c...",
  "sharesSoldE6": "25000000",
  "proceedsE6": "5000000",
  "blendedPriceE6": "200000",
  "status": "filled",
  "txHash": "0xabc..."
}
```

| Field | Type | Description |
| - | - | - |
| `rfqId` | string | RFQ identifier |
| `sharesSoldE6` | string | Shares sold, e6 |
| `proceedsE6` | string | Proceeds after venue fees, e6. The Kairos platform fee is charged separately and is *not* reflected here — this figure will not match the wallet delta |
| `blendedPriceE6` | string | Blended sell price per share, e6 |
| `status` | string | Execution status |
| `txHash` | string \| null | Settlement transaction hash; nullable |

### When cash-out is unavailable

A resolved combo has no maker to sell to, so these are all `400`:

| Condition | Message |
| - | - |
| Every leg won | `this parlay has fully resolved and won — cash-out is unavailable (a settled combo has no maker to sell to). Redeem it to collect your winnings.` |
| Any leg lost | `a leg of this parlay has already lost, so it can no longer pay out — there is nothing to cash out.` |
| Fully resolved, did not win | `this parlay has fully resolved and did not win — there is nothing to cash out.` |
| Combo absent from the positions feed | `couldn't verify the complete parlay balance; cash-out was not submitted` |

## Redeem a combo

```
POST /combo/redeem
```

Collects the payout on a combo whose legs have all resolved in your favour. Redemption always takes the **entire** on-chain balance — there is no amount parameter.

Scope `trade:execute`. A bad credential triple is `403`.

**Deposit wallets only.** EOA and imported wallets are rejected `400 combo redemption is currently supported only for Kairos deposit wallets`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `comboConditionId` | string | Yes | — | 31-byte hex, `0x` prefix optional. From `comboConditionId` on [combo positions](#list-combo-positions). Must belong to the authenticated wallet |
| `outcomeIndex` | integer | No | `0` | `0` means "all legs won", which is the only case a winning combo redeems under |

```bash theme={null}
curl -X POST https://execution.kairos.trade/combo/redeem \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "comboConditionId": "0x1f2e3d4c5b6a79880123456789abcdef0123456789abcdef0123456789abcd",
    "outcomeIndex": 0
  }'
```

A condition id of the wrong length is `400 conditionId must be 31 bytes (a bytes31), got <n> bytes from '<value>'`; one that is not on your wallet is `400 no combo with that conditionId on this wallet`.

### Response

```json theme={null}
{
  "batchTxId": "b7c1...",
  "amountE6": "25000000",
  "status": "redeemed"
}
```

| Field | Type | Description |
| - | - | - |
| `batchTxId` | string | Redemption reference. This response does not contain a transaction hash |
| `amountE6` | string | Amount redeemed, e6. A winning combo pays 1:1, so this is both the share count and the pUSD payout |
| `status` | string | Always the literal `"redeemed"` |

<Warning>
  **Gotcha:** `batchTxId` is a redemption reference, **not** an on-chain transaction hash. Do not feed it to a block explorer.
</Warning>

Redemption is idempotent on-chain, but a second call is reported as a **`400`**, not a success: once the balance is zero you get `400 this parlay isn't redeemable — it hasn't resolved as a win yet, or has already been redeemed` or `400 combo has no redeemable share balance (already redeemed?)`. Check `redeemable` on the position before calling.

## Quote failures

`POST /combo/quote`, `/combo/execute`, `/combo/cash-out-quote`, and `/combo/cash-out` can return these quote-related `400`s. All four are transient and safe to retry:

| Condition | Message |
| - | - |
| No maker pricing the parlay | `No maker is pricing this parlay right now — this can happen while one of its games is in play. You can try again in a moment, or once the match has settled.` |
| Size too large for available makers | `No maker will take a parlay this size right now. This usually clears on its own — try again in a moment.` |
| Price moved past your limit | `The price moved and the quote came back worse than your limit, so nothing was traded. Please try again.` |
| Quote expired before filling | `The quote expired before it could be filled. Please try again.` |

If combo trading is unavailable, the API returns `503` with
`EXCHANGE_POLYMARKET_API_ERROR`. Rate limits return `429`.

An execution whose outcome cannot be confirmed returns `503`
with `TRANSACTION_TIMEOUT` and an RFQ reference in `error_details.details`.
**Do not automatically resubmit:** the trade may still execute. Check your combo
positions and retain the RFQ reference for support.

## Errors

Endpoints on this page return the same structured envelope as order submission:

```json theme={null}
{
  "error": "a combo needs at least 2 legs",
  "code": "ValidationInvalidOrder",
  "error_details": {
    "code": "VALIDATION_INVALID_ORDER",
    "message": "a combo needs at least 2 legs",
    "actions": [{ "action": "review_order", "label": "Review order", "primary": true }]
  }
}
```

As on [Orders](/rest/orders#errors), top-level `code` is a legacy PascalCase string and `error_details.code` is the canonical SCREAMING\_SNAKE\_CASE wire code — match on `error_details.code`. `error_details.details` and `error_details.metadata` are **omitted when absent**, never `null`.

Authentication failures can return a bare `{"error": "<message>"}` with **no** `error_details`.

The Code column below holds `error_details.code` where this page names one; most statuses on this page cover several conditions that do not share a distinct code, and those are `—`.

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `400` | `VALIDATION_INVALID_ORDER` | Leg count outside 2–10; non-positive or unrepresentable `notionalUsd`/`shares`; partial cash-out attempt; unredeemable or already-redeemed combo; malformed `comboConditionId`; combo not on your wallet; missing Polymarket credentials; a wallet type that cannot execute or cash out | Fix the request and resend — deterministic, retrying unchanged returns the same error. For an already-redeemed combo, re-read `redeemable` on the position instead of retrying |
| `400` | — | Any of the four [quote failures](#quote-failures) | Safe to retry — all four are transient; nothing was traded |
| `401` | — | Missing/invalid/expired/revoked credentials on `/combo/quote` or `/combo/positions` | Fix the credential triple and resend |
| `403` | `AUTH_INSUFFICIENT_SCOPE` | Missing scope on the requested endpoint | Issue a key carrying the scope in the table under [Authentication](#authentication) |
| `403` | `AUTH_POLICY_OUTDATED` | Trading permissions need an update | Approve the pending update (action `update_policies`), then retry |
| `403` | — | Bad credential triple on execute, cash-out, cash-out-quote, or redeem; API-key access to Polymarket disabled; IP not allow-listed | Check the credential triple, that Polymarket is enabled for the key, and that your source IP is allow-listed |
| `408` | — | Request exceeded the service request timeout | Retry; on a mutating endpoint check `GET /combo/positions` first — the call may have landed |
| `413` | — | Request body over the service-wide size cap | Shrink the body and resend |
| `422` | — | Malformed or missing JSON body | Send valid JSON with `Content-Type: application/json` |
| `429` | `EXCHANGE_POLYMARKET_RATE_LIMITED` | Quote limit (20/min per user by default), order submission limit shared by execute and cash-out, or provider rate limit | Back off and retry — the limiter is per user and per minute |
| `429` | — | Too many failed auth attempts from one source IP | Stop retrying with the failing credentials; fix them first |
| `500` | — | Unable to prepare the trade, approve tokens, or retrieve positions | Report to support with the response body |
| `503` | `EXCHANGE_POLYMARKET_API_ERROR` | Combo trading is temporarily unavailable | Try later; contact support if persistent |
| `503` | `TRANSACTION_TIMEOUT` | Trade outcome could not be confirmed | Check positions and retain the RFQ reference before submitting another trade |
| `503` | — | Service temporarily unavailable | Retry — the condition is service-side and transient |

The platform fee is returned separately by `cash-out-quote`; the cash-out response does not include a separate fee field.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.