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

# Curated Packs

> Quote, buy, track, and claim curated Packs with the Python SDK.

Requires `limitless-sdk` **1.3.0 or later**. Read the [Partner Packs workflow](/developers/partner-packs.md) for account ownership, durable retries, amounts, and recovery rules.

## Configure a server-side client

```python theme={null}
import os
import time
from limitless_sdk import (
    Client, HMACCredentials, PackBuyParams, PackQuoteParams,
    pack_min_payout, classify_pack_error,
)

def create_client() -> Client:
    return Client(hmac_credentials=HMACCredentials(
        token_id=os.environ["LMTS_TOKEN_ID"],
        secret=os.environ["LMTS_TOKEN_SECRET"],
    ))
```

Use `async with create_client() as client` to close the client after use. Reuse it across requests. The token needs `trading` and `delegated_signing`, plus `account_creation` for allowances. Do not configure a global `x-on-behalf-of` header.

## Browse and check approval

```python theme={null}
async def browse(client: Client):
    return await client.packs.list()
    # Also: await client.packs.search("football"), await client.packs.get(pack_id)

async def check_vault_approval(client: Client, profile_id: int):
    state = await client.partner_accounts.check_allowances(profile_id)
    vault = next((
        target for target in state.targets
        if target.type == "USDC_ALLOWANCE"
        and target.label == "parlay-treasury-vault"
    ), None)
    if vault is None or not vault.confirmed or vault.status != "confirmed":
        raise RuntimeError("Confirm the Pack vault allowance before preparing a buy")
    return state.wallet_address
```

If approval is missing, call `await client.partner_accounts.retry_allowances(profile_id)` and poll `check_allowances` with a bounded wait until the vault is confirmed. Repair uses the [unlimited-approval policy](/developers/partner-packs.md#2-confirm-the-pack-vault-allowance). Complete this before creating the short-lived intent.

## Prepare once, then submit the saved request

```python theme={null}
async def prepare_purchase(
    client: Client, profile_id: int, pack_id: str, purchase_key: str,
) -> PackBuyParams:
    await check_vault_approval(client, profile_id)
    quote = await client.packs.quote(PackQuoteParams(
        pack_id=pack_id, stake_amount="10000000",
    ))
    if not quote.eligible:
        raise ValueError(f"Ineligible: {quote.ineligible_reasons}")
    if quote.potential_payout is None:
        raise ValueError("Missing quoted payout")
    return PackBuyParams(
        on_behalf_of=profile_id,
        pack_id=pack_id,
        stake_amount="10000000",  # 10 USDC
        min_payout_amount=pack_min_payout(quote.potential_payout, 100),
        deadline=str(int(time.time()) + 120),
        idempotency_key=purchase_key,
    )

async def submit_saved_purchase(client: Client, saved: PackBuyParams):
    buy = await client.packs.buy(saved)
    return await client.packs.wait_for_position(
        saved.on_behalf_of, buy.position.id, max_attempts=60, interval=5,
    )
```

Durably save the prepared request before submission, for example by persisting `request.model_dump_json(by_alias=True)` in your application store. Reload it with `PackBuyParams.model_validate_json(saved_json)`. Keep the environment, partner identity, and managed account with the record.

On restart, submit the saved request; do not prepare a new one for the same purchase. Polling continues through position `FAILED`, stopping on `VAULT_REVERTED`. On an ambiguous result or timeout, retain the request and replay unchanged, including an expired deadline. See [explicit renewal signals](/developers/partner-packs.md#recover-buy-errors).

## Read positions and claim

```python theme={null}
async def read_positions(client: Client, profile_id: int):
    return await client.packs.list_positions(profile_id, limit=20, offset=0)

async def claim_winning_position(client: Client, profile_id: int, position_id: str):
    position = await client.packs.get_position(profile_id, position_id)
    if position.status != "WON":
        raise ValueError("Position is not WON")
    claim = await client.packs.claim(
        profile_id, position_id, max_attempts=60, interval=5,
    )
    return {"wallet": position.wallet, "tx_hash": claim.tx_hash}

async def recover_claim(
    client: Client, profile_id: int, position_id: str, redemption_tx_hash: str,
):
    return await client.packs.confirm_claim(
        profile_id, position_id, redemption_tx_hash,
    )
```

Use `claim_once` for one workflow step. `interval` is in seconds; cancel the async task to cancel a wait. After claiming, the position remains `WON`; verify `claim_tx_hash` or the claim response's `CLAIMED` status.

## Classify errors

`classify_pack_error(error)` returns a typed result with `kind`: `ALLOWANCE_REQUIRED`, `NOT_SUBMITTED`, `PRICE_DRIFTED`, `INTENT_EXPIRED`, `WALLET_BUSY`, `RATE_LIMITED`, `SUBMISSION_UNKNOWN`, `TRANSACTION_REVERTED`, or `OTHER`.

Allowance details include `token_address`, `spender`, and `required_allowance`; pre-broadcast failure details include `submission_state`, `parlay_id`, and `recovery`. The classifier does not retry.

The claim helper surfaces API errors. Back off on rate limits and wallet-busy conflicts, then resume the same position. For `SUBMISSION_UNKNOWN`, reconcile the mined redemption transaction and use `confirm_claim`; do not blindly resubmit.


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