# Derive Scoped Token
Source: https://docs.limitless.exchange/api-reference/api-tokens/derive-token
POST /auth/api-tokens/derive
Creates a new scoped API token for the authenticated partner. Requires Privy authentication (Bearer token). The token secret is returned once at creation — store it securely. Requested scopes must be a subset of the partner's allowed scopes.
Requires **Privy authentication**. Pass the `token` field from the Privy authenticate response (the identity token) in the `identity` header as `Bearer `. Do **not** use `privy_access_token`. HMAC and API key auth are not accepted for this endpoint.
The `secret` field is returned **once** at creation time. Store it securely — it cannot be retrieved again.
### Scopes
| Scope | Description |
| ------------------- | ------------------------------------------------------------------------------------------ |
| `trading` | Place and cancel orders. Default scope. Required for `delegated_signing`. |
| `account_creation` | Create sub-account profiles under your partner account. |
| `delegated_signing` | Server signs orders on behalf of sub-accounts via Privy server wallet. Requires `trading`. |
If `scopes` is omitted from the request body, the token is created with `["trading"]` only.
Requested scopes must be a subset of your partner's `allowedScopes` (see [Get Partner Capabilities](/api-reference/api-tokens/get-capabilities)).
### Using the token
After deriving a token, authenticate subsequent requests using HMAC signing with the returned `apiKey` (token ID) and `secret`. See [HMAC Request Signing](/developers/authentication#hmac-request-signing) for the signing protocol.
# Get Partner Capabilities
Source: https://docs.limitless.exchange/api-reference/api-tokens/get-capabilities
GET /auth/api-tokens/capabilities
Returns the partner capability configuration for the authenticated user, including whether token management is enabled and which scopes are allowed for self-service token derivation. Requires Privy authentication (Bearer token).
Requires **Privy authentication** (Bearer token). HMAC and API key auth are not accepted for this endpoint.
This endpoint returns whether token management is enabled for your partner account and which scopes you can request when deriving tokens.
If no capability row exists for your profile, `tokenManagementEnabled` defaults to `false` and `allowedScopes` defaults to an empty array. Contact [help@limitless.network](mailto:help@limitless.network) to get partner capabilities enabled.
# List Active Tokens
Source: https://docs.limitless.exchange/api-reference/api-tokens/list-tokens
GET /auth/api-tokens
Lists all active (non-revoked) API tokens for the authenticated partner. Requires token management to be enabled for the partner.
Returns all active (non-revoked) API tokens for the authenticated partner. Token secrets are never included in the response.
Requires token management to be enabled for the partner (see [Get Partner Capabilities](/api-reference/api-tokens/get-capabilities)).
# Revoke Token
Source: https://docs.limitless.exchange/api-reference/api-tokens/revoke-token
DELETE /auth/api-tokens/{tokenId}
Revokes an active API token. The token becomes immediately unusable. Requires token management to be enabled for the partner.
Revokes an active API token immediately. Once revoked, the token cannot be used for authentication and the action cannot be undone.
Requires token management to be enabled for the partner (see [Get Partner Capabilities](/api-reference/api-tokens/get-capabilities)).
# Create API Key
Source: https://docs.limitless.exchange/api-reference/auth/create-api-key
POST /auth/api-keys
Creates a new API key for programmatic access. Only available for users authenticated via the UI (Privy). Previous active keys are automatically revoked.
API key creation is only available for users authenticated via the Limitless UI (Privy). The key value is only shown once at creation time — store it securely.
# Get Active API Key
Source: https://docs.limitless.exchange/api-reference/auth/get-api-key
GET /auth/api-keys
Returns the currently active API key metadata for the authenticated user. Does not return the key value itself.
# User Logout
Source: https://docs.limitless.exchange/api-reference/auth/logout
POST /auth/logout
Logs out the user
# Revoke API Key
Source: https://docs.limitless.exchange/api-reference/auth/revoke-api-key
DELETE /auth/api-keys
Revokes the currently active API key. The key will immediately stop working for authentication.
Revoking an API key is immediate and irreversible. Any integrations using this key will stop working. Create a new key before revoking the old one if you need uninterrupted access.
# API Reference
Source: https://docs.limitless.exchange/api-reference/introduction
Complete REST API documentation for Limitless Exchange
## Base URL
```
https://api.limitless.exchange
```
Interactive API explorer (Scalar): [https://api.limitless.exchange/api-v1](https://api.limitless.exchange/api-v1)
## Authentication
Most endpoints require authentication via **scoped API tokens** with HMAC-SHA256 request signing:
| Method | Headers | Use case |
| --------------------------- | -------------------------------------------------- | ------------------------------------------------------------- |
| **Scoped API Token (HMAC)** | `lmts-api-key`, `lmts-timestamp`, `lmts-signature` | All integrations — traders, bots, partners, delegated signing |
Public endpoints like market browsing and orderbook data do **not** require authentication. Legacy API keys (`X-API-Key`) are still supported for existing users but are no longer issued.
See the [Authentication guide](/developers/authentication) for HMAC signing details, or the [Programmatic API guide](/developers/programmatic-api) for the full partner integration workflow.
## Endpoint Groups
Derive and manage scoped API tokens for authentication.
Scoped API token management for partner integrations — derive, list, and revoke HMAC-authenticated tokens.
Browse active markets, search, get details and feed events.
Create and cancel orders, batch order status, orderbook, historical prices, and user orders.
Create and manage sub-accounts for partner integrations.
Positions, trades, PnL chart, history, points, and allowance.
Public user positions, traded volume, and PnL data.
## Key Concepts
| Concept | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Venue system** | Each CLOB market has a venue with `exchange` and `adapter` addresses. Fetch via `GET /markets/:slug`. See [Venue System](/developers/venue-system). |
| **EIP-712 signing** | Orders are signed using the venue's `exchange` address as `verifyingContract`. See [EIP-712 Signing](/developers/eip712-signing). |
| **Order types** | **GTC** (Good Till Cancelled) stays on the orderbook. **FOK** (Fill or Kill) executes immediately or cancels. |
| **Token IDs** | Each market has YES and NO position IDs returned in `positionIds`. Use these as `tokenId` in orders. |
| **USDC decimals** | USDC on Base has 6 decimals. Amounts are scaled by 1e6. |
## WebSocket API
For real-time orderbook and position updates, see the [WebSocket Events](/developers/websocket-events) reference.
**URL:** `wss://ws.limitless.exchange` | **Namespace:** `/markets`
## Rate Limits
Rate limits are applied **per API token**, not per domain or IP address.
| Limit | Value |
| --------------------------- | ----- |
| **Max concurrent requests** | 2 |
| **Min delay between calls** | 300ms |
Exceeding rate limits will return `429 Too Many Requests`. Use exponential backoff or the SDK's built-in retry mechanisms. Contact [help@limitless.network](mailto:help@limitless.network) for higher limits.
# Get Active Market Slugs
Source: https://docs.limitless.exchange/api-reference/markets/active-slugs
GET /markets/active/slugs
Retrieves slugs, strike prices, tickers, and deadlines for all active markets and groups. Group markets are nested under their parent group.
# Browse Active Markets
Source: https://docs.limitless.exchange/api-reference/markets/browse-active
GET /markets/active
Retrieves markets and groups that are active and not yet resolved, with optional category filtering
# Browse Active Markets by Category
Source: https://docs.limitless.exchange/api-reference/markets/browse-active-category
GET /markets/active/{categoryId}
Retrieves markets and groups that are active and not yet resolved, with optional category filtering
This is the same as [Browse Active Markets](/api-reference/markets/browse-active) but with the category ID as a path parameter instead of a query parameter. Use [Get Category Counts](/api-reference/markets/category-count) to discover available category IDs.
# Get Category Counts
Source: https://docs.limitless.exchange/api-reference/markets/category-count
GET /markets/categories/count
Returns the number of active markets for each category and the total market count
# Get Feed Events
Source: https://docs.limitless.exchange/api-reference/markets/feed-events
GET /markets/{slug}/get-feed-events
Retrieves the latest feed events related to a specific market with pagination support
# Get Market Details
Source: https://docs.limitless.exchange/api-reference/markets/get-market
GET /markets/{addressOrSlug}
Retrieves market or group data using either an Ethereum address or a slug identifier
This endpoint returns **venue data** (`venue.exchange` and `venue.adapter`) needed for [EIP-712 order signing](/developers/eip712-signing). Fetch once per market and cache — venue data is static.
# Get Oracle Candlesticks
Source: https://docs.limitless.exchange/api-reference/markets/oracle-candles
GET /markets/{addressOrSlug}/oracle-candles
Returns Chainlink candlestick data for markets configured with Chainlink Data Streams chart metadata. Useful for charting the underlying oracle price alongside prediction market prices.
Only markets with a Chainlink Data Streams oracle return data from this endpoint. Use the candlestick data alongside prediction market prices to chart the underlying asset.
# Search Markets
Source: https://docs.limitless.exchange/api-reference/markets/search
GET /markets/search
# Get Navigation Tree
Source: https://docs.limitless.exchange/api-reference/navigation/get-navigation
GET /navigation
Returns the hierarchical navigation structure for market pages
This endpoint returns the hierarchical navigation structure for market pages. Use it to build sidebar navigation or category menus in your application.
# Get Market Page by Path
Source: https://docs.limitless.exchange/api-reference/navigation/get-page-by-path
GET /market-pages/by-path
Resolves a URL path to a market page with its configuration, filters, and breadcrumb
Supports home page resolution via `path=/` or empty `path=`. If no Home page is configured, returns a synthetic page (id `__home__`) with an empty base filter.
May return a `301` redirect if the path has been moved.
# Get Property Key
Source: https://docs.limitless.exchange/api-reference/navigation/get-property-key
GET /property-keys/{id}
Returns a specific property key with its options
# List Markets for a Page
Source: https://docs.limitless.exchange/api-reference/navigation/list-page-markets
GET /market-pages/{id}/markets
Returns paginated list of markets for a specific market page with filtering and sorting
The `id` parameter can be `__home__` to list all markets when Home is not explicitly configured.
Supports both **offset pagination** (`page` + `limit`) and **cursor pagination** (`cursor`). When `cursor` is present, offset pagination is ignored.
Sorting: prefix field with `-` for descending (e.g., `-updatedAt`). Allowed fields: `createdAt`, `updatedAt`, `deadline`, `id`.
# List Property Keys
Source: https://docs.limitless.exchange/api-reference/navigation/list-property-keys
GET /property-keys
Returns all property keys with their options, sorted by slug
# List Property Options
Source: https://docs.limitless.exchange/api-reference/navigation/list-property-options
GET /property-keys/{id}/options
Returns options for a specific property key, optionally filtered by parent option
Omit `parentId` to fetch root-level options. Pass a `parentId` to fetch child options for hierarchical property structures.
# Check Partner Account Allowances
Source: https://docs.limitless.exchange/api-reference/partner-accounts/check-allowances
GET /profiles/partner-accounts/{profileId}/allowances
Checks delegated-trading allowance readiness for a partner-created server-wallet sub-account. Requires HMAC authentication with `account_creation` and `delegated_signing` scopes. Status is based on live chain reads.
Requires **HMAC authentication** with the `account_creation` and `delegated_signing` scopes. API key auth and Privy auth are not accepted.
Checks delegated-trading allowance readiness for a partner-created server-wallet sub-account. The response is based on live chain reads.
* Route: `GET /profiles/partner-accounts/:profileId/allowances`
* Auth: HMAC api-token auth
* Scopes: `account_creation` + `delegated_signing`
* `profileId`: child/server-wallet profile id that belongs to the authenticated partner
## Path parameters
| Parameter | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------------------------------------------------- |
| `profileId` | `number` | Yes | Partner sub-account profile id for the server-wallet child account. |
## Response
```json theme={null}
{
"profileId": 4543,
"partnerProfileId": 4430,
"chainId": 84532,
"walletAddress": "0x1a665817f063Ee15C6C2c05D4315982145825C3D",
"ready": false,
"summary": {
"total": 10,
"confirmed": 0,
"missing": 2,
"submitted": 0,
"failed": 8
},
"targets": [
{
"type": "USDC_ALLOWANCE",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"spenderOrOperator": "0x54d696A602343063000B25a51734E3BbE0Ec80a2",
"label": "ctf-exchange",
"requiredFor": "BUY",
"confirmed": false,
"status": "missing",
"retryable": true
}
]
}
```
## Response fields
| Field | Type | Description |
| ------------------- | --------- | --------------------------------------------------------------------------------------- |
| `profileId` | `number` | Child/server-wallet profile id being checked. |
| `partnerProfileId` | `number` | Authenticated parent partner profile id. |
| `chainId` | `number` | Chain where allowance targets were checked. |
| `walletAddress` | `string` | Managed server-wallet address for the child profile. |
| `ready` | `boolean` | `true` when every target is confirmed on chain. |
| `summary.total` | `number` | Total target count. |
| `summary.confirmed` | `number` | Targets confirmed on chain. |
| `summary.missing` | `number` | Targets still missing. |
| `summary.submitted` | `number` | Targets submitted by the current retry response. For GET responses this is usually `0`. |
| `summary.failed` | `number` | Targets whose latest live read or retry state failed. |
| `targets[]` | `array` | Per-target allowance or approval state. |
## Target fields
| Field | Type | Description |
| ------------------- | --------------------------------------------------- | ------------------------------------------------------------------ |
| `type` | `USDC_ALLOWANCE` \| `CTF_APPROVAL` | Target category. |
| `tokenAddress` | `string` | ERC20 or conditional-token contract address. |
| `spenderOrOperator` | `string` | Allowance spender or operator address. |
| `label` | `string` | Human-readable target label. |
| `requiredFor` | `BUY` \| `SELL` | Trading side that requires this approval. |
| `confirmed` | `boolean` | Whether this target is confirmed on chain. |
| `status` | `confirmed` \| `missing` \| `submitted` \| `failed` | Current target status. |
| `retryable` | `boolean` | Whether `POST /retry` may attempt this target. |
| `transactionId` | `string` | Optional sponsored transaction id when submitted. |
| `txHash` | `string` | Optional transaction hash when available. |
| `userOperationHash` | `string` | Optional user operation hash when available. |
| `errorCode` | `string` | Optional machine-readable failure code, such as `RPC_READ_FAILED`. |
| `errorMessage` | `string` | Optional human-readable failure message. |
## Recommended flow
1. Poll this endpoint.
2. If `ready=true`, continue with delegated trading.
3. If targets are `missing` or `failed` and `retryable=true`, call [Retry Partner Account Allowances](/api-reference/partner-accounts/retry-allowances).
4. If retry submits targets, poll this endpoint again after a short delay.
## Example
```bash theme={null}
curl "https://api.limitless.exchange/profiles/partner-accounts/4543/allowances" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: "
```
# Create Partner Sub-Account
Source: https://docs.limitless.exchange/api-reference/partner-accounts/create-partner-account
POST /profiles/partner-accounts
Creates a new sub-account linked to the authenticated partner. Requires HMAC authentication with the `account_creation` scope.
**Server wallet mode** (`createServerWallet: true`): Creates a Privy server wallet and profile. The partner can then submit orders on behalf of this account using delegated signing.
**EOA mode** (default): Requires wallet ownership verification via `x-account`, `x-signing-message`, and `x-signature` headers. The end user signs their own orders.
Requires **HMAC authentication** with the `account_creation` scope. API key auth and Privy auth are not accepted.
Creates a new sub-account profile linked to the authenticated partner.
### Server wallet mode
Set `createServerWallet: true` to create a Privy server wallet for the sub-account. This enables [delegated signing](/developers/authentication#delegated-signing) — the partner can submit unsigned orders and the server signs them using the managed wallet.
Before the first delegated trade, call [Check Partner Account Allowances](/api-reference/partner-accounts/check-allowances). If any target is `missing` or `failed` with `retryable=true`, call [Retry Partner Account Allowances](/api-reference/partner-accounts/retry-allowances), then poll the check endpoint again.
Server wallet creation requires the `delegated_signing` scope on your API token (in addition to `account_creation`). Without it, the request returns `"Server wallet creation requires delegated_signing scope"`.
```json theme={null}
{
"displayName": "user-bob",
"createServerWallet": true
}
```
### EOA mode
Omit `createServerWallet` (or set it to `false`) to create an account for an externally-owned address. The end user manages their own keys and signs their own orders.
EOA mode requires three additional headers for wallet ownership verification:
| Header | Description |
| ------------------- | --------------------------------------------------------------------- |
| `x-account` | Checksummed Ethereum address (EIP-55) |
| `x-signing-message` | Hex-encoded signing message obtained from `GET /auth/signing-message` |
| `x-signature` | Hex-encoded signature produced by signing the message with the wallet |
#### Signing message format
The `x-signing-message` value is **not** the raw text — it is the **hex-encoded** UTF-8 representation of the message returned by `GET /auth/signing-message`. The raw text (which you sign) looks like:
```
Welcome to Limitless Exchange!
This request will not trigger a blockchain transaction or cost any gas fees.
Signature is required to authenticate an upcoming API request.
Nonce: 0x
```
The full flow:
1. **Fetch** the signing message: `GET /auth/signing-message` → returns the plain-text message with a unique nonce.
2. **Sign** the plain-text message with the wallet (e.g. `personal_sign` / `eth_sign`).
3. **Hex-encode** the plain-text message: prepend `0x` to the UTF-8 hex representation.
4. **Send** all three headers on the request.
```python Python theme={null}
import requests
from eth_account import Account
from eth_account.messages import encode_defunct
# 1. Fetch the signing message
signing_message = requests.get(f"{API_BASE_URL}/auth/signing-message").text
# 2. Sign the plain-text message
message = encode_defunct(text=signing_message)
signed = account.sign_message(message)
# 3. Hex-encode the message for the header
hex_message = "0x" + signing_message.encode("utf-8").hex()
# 4. Use in headers
headers = {
"x-account": account.address, # checksummed address
"x-signing-message": hex_message, # hex-encoded message
"x-signature": "0x" + signed.signature.hex(), # hex-encoded signature
}
```
```typescript TypeScript theme={null}
import { createWalletClient, http, toHex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
// 1. Fetch the signing message
const signingMessage = await fetch(`${API_BASE_URL}/auth/signing-message`).then(r => r.text());
// 2. Sign the plain-text message
const account = privateKeyToAccount(PRIVATE_KEY);
const signature = await account.signMessage({ message: signingMessage });
// 3. Hex-encode the message for the header
const hexMessage = toHex(new TextEncoder().encode(signingMessage));
// 4. Use in headers
const headers = {
'x-account': account.address, // checksummed address
'x-signing-message': hexMessage, // hex-encoded message
'x-signature': signature, // hex-encoded signature
};
```
```json theme={null}
{
"displayName": "user-alice"
}
```
### Constraints
* `displayName` is optional (max 44 characters). Defaults to the wallet address if omitted.
* Returns `409 Conflict` if a profile already exists for the target address.
* Cannot create a sub-account for the partner's own address.
# Retry Partner Account Allowances
Source: https://docs.limitless.exchange/api-reference/partner-accounts/retry-allowances
POST /profiles/partner-accounts/{profileId}/allowances/retry
Retries delegated-trading allowance recovery for a partner-created server-wallet sub-account. The retry takes a short wallet lock, re-checks live chain state, and submits only targets still missing. A `submitted` target means this retry request submitted a sponsored transaction or user operation.
Requires **HMAC authentication** with the `account_creation` and `delegated_signing` scopes. API key auth and Privy auth are not accepted.
Retries delegated-trading allowance recovery for a partner-created server-wallet sub-account.
The retry operation:
* takes a short lock for the wallet
* re-checks live chain state before submitting
* submits only targets that are still missing
* returns `submitted` only for targets this request submitted as a sponsored transaction or user operation
* Route: `POST /profiles/partner-accounts/:profileId/allowances/retry`
* Auth: HMAC api-token auth
* Scopes: `account_creation` + `delegated_signing`
* `profileId`: child/server-wallet profile id that belongs to the authenticated partner
## Path parameters
| Parameter | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------------------------------------------------- |
| `profileId` | `number` | Yes | Partner sub-account profile id for the server-wallet child account. |
## Request body
No request body is required.
```json theme={null}
{}
```
## Response
Returns the same shape as [Check Partner Account Allowances](/api-reference/partner-accounts/check-allowances).
```json theme={null}
{
"profileId": 4543,
"partnerProfileId": 4430,
"chainId": 84532,
"walletAddress": "0x1a665817f063Ee15C6C2c05D4315982145825C3D",
"ready": false,
"summary": {
"total": 10,
"confirmed": 2,
"missing": 0,
"submitted": 8,
"failed": 0
},
"targets": [
{
"type": "USDC_ALLOWANCE",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"spenderOrOperator": "0x54d696A602343063000B25a51734E3BbE0Ec80a2",
"label": "ctf-exchange",
"requiredFor": "BUY",
"confirmed": false,
"status": "submitted",
"transactionId": "tx_...",
"userOperationHash": "0x...",
"retryable": false
}
]
}
```
`submitted` does not mean the target is already confirmed on chain. It means this retry request submitted a sponsored transaction or user operation. Poll the GET endpoint again after a short delay.
## Error responses
| Status | Meaning | Partner action |
| ------ | ------------------------------------------------------------- | ---------------------------------------------- |
| `429` | Retry is rate limited. Response includes `retryAfterSeconds`. | Wait `retryAfterSeconds`, then call GET again. |
| `409` | Another retry is already running for this wallet. | Wait briefly, then call GET again. |
### 429 example
```json theme={null}
{
"message": "Allowance retry rate limited",
"retryAfterSeconds": 30
}
```
### 409 example
```json theme={null}
{
"message": "Allowance retry already running"
}
```
## Recommended flow
1. Poll [Check Partner Account Allowances](/api-reference/partner-accounts/check-allowances).
2. If `ready=true`, continue.
3. If targets are `missing` or `failed` and `retryable=true`, call this endpoint.
4. If this endpoint returns submitted targets, poll GET again after a short delay.
5. If `429`, wait `retryAfterSeconds`.
6. If `409`, wait briefly and call GET again.
## Example
```bash theme={null}
curl -X POST "https://api.limitless.exchange/profiles/partner-accounts/4543/allowances/retry" \
-H "content-type: application/json" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: " \
-d '{}'
```
# Add Withdrawal Address
Source: https://docs.limitless.exchange/api-reference/portfolio/add-withdrawal-address
Requires **Privy authentication**. Pass the Privy identity token in the `identity` header as `Bearer `. HMAC and API-token auth are not accepted for withdrawal-address allowlist management.
Adds a withdrawal destination allowlist entry on the authenticated profile.
* Route: `POST /portfolio/withdrawal-addresses`
* Auth: Privy identity token only
### Request body
| Field | Type | Required | Description |
| --------- | -------- | -------- | ----------------------------------------- |
| `address` | `string` | Yes | Destination wallet address to allowlist. |
| `label` | `string` | No | Human-readable label for the destination. |
### Example
```bash theme={null}
curl -X POST "https://api.limitless.exchange/portfolio/withdrawal-addresses" \
-H "content-type: application/json" \
-H "identity: Bearer " \
-d '{
"address": "0x0F3262730c909408042F9Da345a916dc0e1F9787",
"label": "treasury"
}'
```
### Response
```json theme={null}
{
"id": "34a98cd5-e924-427d-be55-50d96dd44f3c",
"profileId": 294,
"destinationAddress": "0x0F3262730c909408042F9Da345a916dc0e1F9787",
"label": "treasury",
"createdAt": "2026-05-04T12:43:51.721Z",
"deletedAt": null
}
```
### Notes
* The allowlist entry belongs to the authenticated profile.
* For partner `onBehalfOf` withdrawals, allowlist the destination on the authenticated partner profile, not on the child profile.
* This endpoint does not accept HMAC/scoped API-token auth.
# Get Trading Allowance
Source: https://docs.limitless.exchange/api-reference/portfolio/allowance
GET /portfolio/trading/allowance
Check USDC allowance for CLOB or NegRisk trading contracts
# Delete Withdrawal Address
Source: https://docs.limitless.exchange/api-reference/portfolio/delete-withdrawal-address
Requires **Privy authentication**. Pass the Privy identity token in the `identity` header as `Bearer `. HMAC and API-token auth are not accepted for withdrawal-address allowlist management.
Deletes a withdrawal destination allowlist entry from the authenticated profile.
* Route: `DELETE /portfolio/withdrawal-addresses/:address`
* Auth: Privy identity token only
### Path parameters
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | -------------------------------------------------------- |
| `address` | `string` | Yes | Destination wallet address to remove from the allowlist. |
### Example
```bash theme={null}
curl -X DELETE "https://api.limitless.exchange/portfolio/withdrawal-addresses/0x0F3262730c909408042F9Da345a916dc0e1F9787" \
-H "identity: Bearer "
```
### Notes
* The allowlist entry is removed from the authenticated profile.
* This endpoint does not accept HMAC/scoped API-token auth.
# Get Your Profile
Source: https://docs.limitless.exchange/api-reference/portfolio/get-profile
GET /profiles/{account}
Retrieve the authenticated user's profile, including their internal user ID and fee rate. This is useful for API users who need their `feeRateBps` (for order signing) or numeric `id` (used as `ownerId` in other flows). You can only access your own profile — requesting another user's address returns 403.
This endpoint returns your **internal user ID** (`id`) and **fee rate** (`rank.feeRateBps`), which are required when constructing signed orders via the API.
# Get History
Source: https://docs.limitless.exchange/api-reference/portfolio/history
GET /portfolio/history
Paginated history including AMM, CLOB trades, splits/merges, NegRisk conversions. Partner API tokens with `delegated_signing` scope may read a sub-account by sending the `x-on-behalf-of: ` header.
# Get PnL Chart
Source: https://docs.limitless.exchange/api-reference/portfolio/pnl-chart
GET /portfolio/pnl-chart
Hybrid PnL: realised series + current total snapshot
# Get Points Breakdown
Source: https://docs.limitless.exchange/api-reference/portfolio/points
GET /portfolio/points
# Get Positions
Source: https://docs.limitless.exchange/api-reference/portfolio/positions
GET /portfolio/positions
Retrieve all active positions with P&L calculations and market values
For real-time position updates, use the [WebSocket API](/developers/websocket-events) — subscribe to `subscribe_positions`.
`status: RESOLVED` and `winningOutcomeIndex` indicate that the winning outcome is known in the API response. They do **not** guarantee that the underlying conditional token payout has already been settled on-chain or that the position is immediately redeemable. See [Programmatic API - Lifecycle after a trade](/developers/programmatic-api#lifecycle-after-a-trade).
# Redeem Resolved Positions
Source: https://docs.limitless.exchange/api-reference/portfolio/redeem
As of SDK v1.0.6, all three SDKs (TypeScript, Python, Go) include helper methods for this endpoint.
Redeems winning conditional-token positions for a resolved market from a server-wallet sub-account.
* Route: `POST /portfolio/redeem`
* Auth: `apiToken`, Privy, or session auth
* Scope: `trading` when using `apiToken`
### Request body
| Field | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `conditionId` | `string` | Yes | CTF condition id (`bytes32` hex string). |
| `onBehalfOf` | `number` | No | Managed sub-account profile id (partner flow). Must be a child of the authenticated partner profile. |
### Example (HMAC)
```bash theme={null}
curl -X POST "https://api.limitless.exchange/portfolio/redeem" \
-H "content-type: application/json" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: " \
-d '{
"conditionId": "0xa0ba8fd0acc2b86585734c07eb3bef4133f584d5abde78a558a6b8bfc3bbdec0",
"onBehalfOf": 12345
}'
```
### Notes
* Market must be resolved and the position must have redeemable balance.
* API-level resolved status can appear before CTF settlement; on-chain payout must be posted before redemption succeeds.
* Legacy API keys are not supported on server-wallet operations.
# Get Trades
Source: https://docs.limitless.exchange/api-reference/portfolio/trades
GET /portfolio/trades
Retrieve all AMM trades executed by the authenticated user
# Withdraw From Server Wallet
Source: https://docs.limitless.exchange/api-reference/portfolio/withdraw
Official SDKs include helper methods for this endpoint.
Transfers ERC20 funds from a managed server wallet.
* Route: `POST /portfolio/withdraw`
* Auth: `apiToken`, Privy, or session auth
* Scope: `withdrawal` when using `apiToken`
### Request body
| Field | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount` | `string` | Yes | Token amount in smallest unit (for USDC: `1000000` = `1` USDC). |
| `token` | `string` | No | ERC20 token address. Defaults to USDC when omitted. |
| `onBehalfOf` | `number` | No | Managed sub-account profile id. Must be a child of the authenticated partner profile when present. |
| `destination` | `string` | No | Explicit destination address. Must be the authenticated account, authenticated smart wallet, or an active withdrawal address allowlisted on the authenticated profile. |
Use one of these payload shapes:
| Flow | Body | Result |
| ------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ |
| Partner sub-account default destination | `amount` + `onBehalfOf` | Withdraws from the child server wallet to the authenticated partner's default destination. |
| Own server wallet to explicit destination | `amount` + `destination` | Withdraws from the authenticated caller's own server wallet to an allowed destination. |
| Partner sub-account to explicit destination | `amount` + `onBehalfOf` + `destination` | Withdraws from the child server wallet to an allowed destination. |
### Example: sub-account default destination
```bash theme={null}
curl -X POST "https://api.limitless.exchange/portfolio/withdraw" \
-H "content-type: application/json" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: " \
-d '{
"amount": "1000000",
"onBehalfOf": 12345
}'
```
### Example: sub-account to allowlisted destination
```bash theme={null}
curl -X POST "https://api.limitless.exchange/portfolio/withdraw" \
-H "content-type: application/json" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: " \
-d '{
"amount": "1000000",
"onBehalfOf": 12345,
"destination": "0x0F3262730c909408042F9Da345a916dc0e1F9787"
}'
```
### Example: own server wallet to allowlisted destination
```bash theme={null}
curl -X POST "https://api.limitless.exchange/portfolio/withdraw" \
-H "content-type: application/json" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: " \
-d '{
"amount": "1000000",
"destination": "0x0F3262730c909408042F9Da345a916dc0e1F9787"
}'
```
### Destination allowlist
Explicit `destination` values are accepted only when the address is one of:
* the authenticated caller's account address
* the authenticated caller's smart wallet address
* an active withdrawal address allowlisted on the authenticated caller profile
For `onBehalfOf` withdrawals, the allowlist belongs to the authenticated partner profile, not the child `onBehalfOf` profile.
Use [`POST /portfolio/withdrawal-addresses`](/api-reference/portfolio/add-withdrawal-address) and [`DELETE /portfolio/withdrawal-addresses/:address`](/api-reference/portfolio/delete-withdrawal-address) to manage the allowlist.
Withdrawal-address allowlist management requires a Privy identity token. HMAC/scoped API tokens cannot add or delete withdrawal addresses.
### Notes
* When `destination` is omitted, the API uses the authenticated caller's default destination: smart wallet when present, otherwise account address.
* Destination validation blocks withdrawals to third-party addresses that are not allowlisted.
* Legacy API keys are not supported on server-wallet operations.
# Get PnL Chart (Public)
Source: https://docs.limitless.exchange/api-reference/public-portfolio/pnl-chart
GET /portfolio/{account}/pnl-chart
Hybrid PnL: realised series + current total snapshot
# Get User Positions (Public)
Source: https://docs.limitless.exchange/api-reference/public-portfolio/positions
GET /portfolio/{account}/positions
Retrieve all positions for a specific user address
`status: RESOLVED` and `winningOutcomeIndex` indicate that the winning outcome is known in the API response. They do **not** guarantee that the underlying conditional token payout has already been settled on-chain or that the position is immediately redeemable. See [Programmatic API - Lifecycle after a trade](/developers/programmatic-api#lifecycle-after-a-trade).
# Get User Traded Volume
Source: https://docs.limitless.exchange/api-reference/public-portfolio/traded-volume
GET /portfolio/{account}/traded-volume
Get total traded volume for a specific user. Returns combined CLOB and AMM volume (both taker and maker sides) in whole USDC. This is the all-time cumulative trading volume, not the current portfolio value.
# Batch Cancel Orders (Combined)
Source: https://docs.limitless.exchange/api-reference/trading/batch-cancel
POST /orders/batch-cancel
Cancel open orders by either internal orderIds or client-provided clientOrderIds.
Cancel multiple orders by either internal `orderIds` or `clientOrderIds` supplied when creating the orders.
Provide exactly one identifier array. Requests with both `orderIds` and `clientOrderIds`, or with neither, return `400 Bad Request`.
`POST /orders/cancel-batch` remains supported for existing integrations and accepts `orderIds` only.
# Cancel Order (Combined)
Source: https://docs.limitless.exchange/api-reference/trading/cancel
POST /orders/cancel
Cancel one open order by either internal orderId or client-provided clientOrderId.
Cancel one order by either the internal `orderId` or the `clientOrderId` supplied when creating the order.
Provide exactly one identifier. Requests with both identifiers or neither identifier return `400 Bad Request`.
`DELETE /orders/{orderId}` remains supported for existing integrations that cancel only by internal order ID.
# Cancel All Orders
Source: https://docs.limitless.exchange/api-reference/trading/cancel-all
DELETE /orders/all/{slug}
# Cancel Orders (Batch)
Source: https://docs.limitless.exchange/api-reference/trading/cancel-batch
POST /orders/cancel-batch
This endpoint cancels multiple orders by internal `orderIds` only.
To cancel by either `orderIds` or `clientOrderIds`, use [Batch Cancel Orders (Combined)](/api-reference/trading/batch-cancel).
# Cancel Order
Source: https://docs.limitless.exchange/api-reference/trading/cancel-order
DELETE /orders/{orderId}
Cancel an open order and return locked funds
This endpoint cancels by internal `orderId`. To cancel by either `orderId` or `clientOrderId`, use [Cancel Order (Combined)](/api-reference/trading/cancel).
To cancel multiple orders at once, use [Batch Cancel Orders (Combined)](/api-reference/trading/batch-cancel), [Cancel Orders (Batch)](/api-reference/trading/cancel-batch), or [Cancel All](/api-reference/trading/cancel-all).
# Create Order
Source: https://docs.limitless.exchange/api-reference/trading/create-order
POST /orders
Creates a buy/sell order for prediction market positions. Requires signed order data.
Before creating orders:
1. Fetch market data via [Get Market Details](/api-reference/markets/get-market) to get venue and token IDs
2. Sign the order using [EIP-712](/developers/eip712-signing) with `venue.exchange` as `verifyingContract`
3. Ensure you have [token approvals](/developers/venue-system#required-token-approvals) set up
### Optional Fields
| Field | Type | Description |
| --------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientOrderId` | `string` | Idempotency key (max 128 chars). If a duplicate is submitted, the server returns `409 Conflict`. |
| `onBehalfOf` | `number` | Profile ID of the sub-account to place the order for. Requires a [scoped API token](/developers/authentication#scoped-api-tokens-hmac) with `trading` scope and a partner relationship with the target profile. The `maker` and `signer` in the order must match the sub-account's wallet address. |
| `postOnly` | `boolean` | GTC orders only. When `true`, the order is rejected if it would immediately match. Guarantees the order rests on the book as a maker order. Default `false`. |
**Where does `ownerId` come from?** It is the `id` field returned by [`GET /profiles/{address}`](/api-reference/portfolio/get-profile). For partner sub-accounts, it is the `profileId` returned by [`POST /profiles/partner-accounts`](/api-reference/partner-accounts/create-partner-account).
**`ownerId` must match the profile that owns the order** (see [Programmatic API — EOA flow](/developers/programmatic-api#eoa-web3-partners)):
* **Partner EOA + signed order + `onBehalfOf`:** set both `onBehalfOf` and `ownerId` to the **sub-account’s** `profileId`. `order.maker` and `order.signer` must match that sub-account’s wallet. A mismatched `ownerId` returns `400` — `"Profile ID does not match the order owner"`.
* **Delegated signing** (unsigned `order`, `delegated_signing` scope, server wallet sub-account): the server fills in `ownerId` when it signs — omit the client signature per [Delegated Signing](#delegated-signing) below.
* **Trading as yourself** (no `onBehalfOf`): `ownerId` is your own profile id (the SDK often fills this after fetching your profile).
### Delegated Signing
Partners with the `delegated_signing` scope can omit `signature` and `signatureType` from the order object. The server signs the order using a Privy server wallet linked to the target sub-account and sets `maker`/`signer` to the server wallet address. See [Authentication](/developers/authentication#delegated-signing) for details.
### Execution Response
The response includes an `execution` object with settlement details:
| Field | Type | Description |
| ------------------ | --------- | ------------------------------------------------------------------------------------------------------- |
| `matched` | `boolean` | Whether the order was matched immediately |
| `settlementStatus` | `string` | `UNMATCHED`, `MATCHED`, `MINED`, `CONFIRMED`, `RETRYING`, or `FAILED` |
| `tradeEventId` | `string` | Trade event ID (present when matched) |
| `txHash` | `string` | On-chain transaction hash (present when mined) |
| `feeRateBps` | `number` | Fee rate in basis points |
| `effectiveFeeBps` | `number` | Effective fee rate after rebates |
| `totalsRaw` | `object` | Raw execution totals (`contractsGross`, `contractsFee`, `contractsNet`, `usdGross`, `usdFee`, `usdNet`) |
# Get Historical Prices
Source: https://docs.limitless.exchange/api-reference/trading/historical-price
GET /markets/{slug}/historical-price
Retrieve historical price data for a specific market with configurable time intervals
# Get Locked Balance
Source: https://docs.limitless.exchange/api-reference/trading/locked-balance
GET /markets/{slug}/locked-balance
Get the amount of funds locked in open orders for the authenticated user
# Get Market Events
Source: https://docs.limitless.exchange/api-reference/trading/market-events
GET /markets/{slug}/events
Get recent events for a specific market including trades, orders, and liquidity changes
# Get Order Status (Batch)
Source: https://docs.limitless.exchange/api-reference/trading/order-status-batch
POST /orders/status/batch
Fetches historical order statuses for multiple orders by internal order IDs and/or client-provided order IDs. Returns execution details, settlement status, and maker match data for each order.
Look up orders by either `orderId` (internal) or `clientOrderId` (your identifier). Provide exactly one per item — not both.
# Get Orderbook
Source: https://docs.limitless.exchange/api-reference/trading/orderbook
GET /markets/{slug}/orderbook
Retrieve the current orderbook for a market showing all open buy and sell orders
For real-time orderbook updates, use the [WebSocket API](/developers/websocket-events) instead of polling this endpoint. Subscribe to `subscribe_market_prices` with `marketSlugs`.
# Get User Orders
Source: https://docs.limitless.exchange/api-reference/trading/user-orders
GET /markets/{slug}/user-orders
Get all orders placed by the authenticated user for a specific market
# Authentication
Source: https://docs.limitless.exchange/developers/authentication
Scoped token authentication for the Limitless Exchange API
## Overview
All programmatic access to the Limitless Exchange API requires authentication via **scoped API tokens** with HMAC-SHA256 request signing.
API keys are **deprecated** and no longer available for new users. Existing API key users should migrate to scoped API tokens. See [Legacy API Keys](#legacy-api-keys) below.
## Scoped API Tokens (HMAC)
Scoped API tokens are the authentication method for the Limitless Exchange API. Every request is authenticated by signing it with HMAC-SHA256 using your token's secret. Tokens are scoped to control what actions they can perform.
### Scopes
| Scope | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trading` | Place and cancel orders. Required base scope for `delegated_signing`. |
| `account_creation` | Create sub-account profiles under your partner account. |
| `delegated_signing` | Server signs orders on behalf of sub-accounts (requires `trading` scope). Enables Web2 partners to submit orders without end users managing private keys. |
| `withdrawal` | Withdraw ERC20 balances from managed server-wallet sub-accounts to partner-owned addresses. |
### HMAC Request Signing
**Required headers:**
| Header | Description |
| ---------------- | ------------------------------------------------------------- |
| `lmts-api-key` | Your token ID (received at token creation) |
| `lmts-timestamp` | ISO-8601 timestamp (must be within 30 seconds of server time) |
| `lmts-signature` | Base64-encoded HMAC-SHA256 signature |
SDK users should pass HMAC credentials to the SDK client (`hmacCredentials` / `hmac_credentials` / `WithHMACCredentials`) and let the SDK build these headers automatically. Manual header signing is only needed when you call the REST API directly.
**Canonical message format:**
```
{ISO-8601 timestamp}\n{HTTP METHOD}\n{request path with query string}\n{request body}
```
The path component must include the **full request URL path and query string** (e.g., `/orders/all/btc-100k?onBehalfOf=42`), not just the pathname. For requests without a query string, use the plain path (e.g., `/orders`). For GET requests, the body component is an empty string.
**Example (TypeScript):**
```typescript theme={null}
import { createHmac } from 'crypto';
function signRequest(
tokenId: string,
secret: string, // base64-encoded, received at token creation
method: string,
path: string,
body: string = '',
): Record {
const timestamp = new Date().toISOString();
const message = `${timestamp}\n${method}\n${path}\n${body}`;
const signature = createHmac('sha256', Buffer.from(secret, 'base64'))
.update(message)
.digest('base64');
return {
'lmts-api-key': tokenId,
'lmts-timestamp': timestamp,
'lmts-signature': signature,
};
}
// Usage
const headers = signRequest(tokenId, secret, 'POST', '/orders', JSON.stringify(orderPayload));
const res = await fetch('https://api.limitless.exchange/orders', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(orderPayload),
});
```
**Example (Python):**
```python theme={null}
import hmac, hashlib, base64
from datetime import datetime, timezone
def sign_request(token_id: str, secret: str, method: str, path: str, body: str = "") -> dict:
timestamp = datetime.now(timezone.utc).isoformat()
message = f"{timestamp}\n{method}\n{path}\n{body}"
signature = base64.b64encode(
hmac.new(
base64.b64decode(secret),
message.encode("utf-8"),
hashlib.sha256,
).digest()
).decode("utf-8")
return {
"lmts-api-key": token_id,
"lmts-timestamp": timestamp,
"lmts-signature": signature,
}
```
### Getting a Scoped API Token
Go to [limitless.exchange](https://limitless.exchange) and connect your wallet.
Derive a scoped token via `POST /auth/api-tokens/derive`. Pass the `token` field from the Privy authenticate response (not `privy_access_token`) in the `identity` header as `Bearer `. The response includes a `tokenId` and `secret`. This is a one-time setup — use the HMAC credentials for all subsequent requests.
The `trading` scope is available to all users — no application required. For partner-level scopes (`account_creation`, `delegated_signing`), [apply for programmatic API access](/developers/programmatic-api) to get your account enabled.
The token secret is returned **once** at creation time. Store it securely — it cannot be retrieved again.
### Delegated Signing
With the `delegated_signing` scope, partners can submit orders **without** providing `signature` and `signatureType` in the order payload. The server signs the order using a Privy server wallet linked to the target sub-account.
Use the `onBehalfOf` field in the order payload to specify which sub-account the order is for. The target profile must be linked to your partner account.
## Checksummed Addresses
All Ethereum addresses in API requests must use **checksummed format** (EIP-55 mixed-case):
* `x-account` header
* `maker` and `signer` fields in orders
**Example:** `0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed`
Your private key is still required for **EIP-712 order signing** (unless using delegated signing), but the scoped token handles request authentication.
## Legacy API Keys
API keys are **deprecated** and are no longer issued to new users. If you have an existing API key, it will continue to work, but we recommend migrating to scoped API tokens for improved security and access control.
Existing API key users authenticate via the `X-API-Key` header:
```bash theme={null}
curl -H "X-API-Key: lmts_your_key_here" https://api.limitless.exchange/markets
```
| Method | Headers | Use Case |
| ----------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
| Scoped API Token (HMAC) | `lmts-api-key`, `lmts-timestamp`, `lmts-signature` | All new integrations — traders, bots, partners, delegated signing |
| API Key (deprecated) | `X-API-Key: lmts_...` | Legacy users only — no longer issued |
# EIP-712 Order Signing
Source: https://docs.limitless.exchange/developers/eip712-signing
How to sign orders using EIP-712 structured data
## Overview
All orders on Limitless are signed using **EIP-712 structured data**. The venue's exchange address is used as the `verifyingContract` in the signing domain.
If you're using the [Programmatic API](/developers/programmatic-api) with delegated signing, the server handles EIP-712 signing for you via managed wallets. This page is for partners and traders who sign orders with their own private keys.
## EIP-712 Domain
```json theme={null}
{
"name": "Limitless CTF Exchange",
"version": "1",
"chainId": 8453,
"verifyingContract": ""
}
```
The `verifyingContract` must be fetched from the market's venue data via `GET /markets/:slug`. See [Venue System](/developers/venue-system).
## Order Type Definition
```json theme={null}
{
"Order": [
{ "name": "salt", "type": "uint256" },
{ "name": "maker", "type": "address" },
{ "name": "signer", "type": "address" },
{ "name": "taker", "type": "address" },
{ "name": "tokenId", "type": "uint256" },
{ "name": "makerAmount", "type": "uint256" },
{ "name": "takerAmount", "type": "uint256" },
{ "name": "expiration", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "feeRateBps", "type": "uint256" },
{ "name": "side", "type": "uint8" },
{ "name": "signatureType", "type": "uint8" }
]
}
```
## Field Reference
| Field | Type | Description |
| --------------- | ------- | -------------------------------------------------------------------------------------- |
| `salt` | uint256 | Unique order identifier (typically timestamp-based) |
| `maker` | address | Checksummed address of the order creator |
| `signer` | address | Same as maker for EOA wallets |
| `taker` | address | `0x000...000` for open orders (any taker) |
| `tokenId` | uint256 | Position ID — YES or NO token from market data |
| `makerAmount` | uint256 | Amount the maker offers, scaled by 1e6 (see [Amount Calculation](#amount-calculation)) |
| `takerAmount` | uint256 | Amount the maker wants in return, scaled by 1e6 (always `1` for FOK orders) |
| `expiration` | uint256 | `0` for no expiration |
| `nonce` | uint256 | Order nonce |
| `feeRateBps` | uint256 | Fee rate in basis points |
| `side` | uint8 | `0` = BUY, `1` = SELL |
| `signatureType` | uint8 | `0` = EOA signature |
## Order Types
| Type | Description |
| ----------------------------- | ----------------------------------------------------------------------------- |
| **GTC** (Good Till Cancelled) | Remains active until filled or cancelled |
| **FAK** (Fill And Kill) | Matches immediately available liquidity; any unmatched remainder is cancelled |
| **FOK** (Fill Or Kill) | Must fill completely or be cancelled |
## Amount Calculation
USDC has **6 decimals** (1 USDC = 1,000,000 units). Shares are also scaled by **1e6**.
### GTC (limit orders)
| Side | `makerAmount` | `takerAmount` |
| -------- | ------------------------------------ | -------------------------------------- |
| **BUY** | `price * size * 1e6` (USDC to spend) | `size * 1e6` (shares to receive) |
| **SELL** | `size * 1e6` (shares to sell) | `price * size * 1e6` (USDC to receive) |
```
// BUY 10 shares at $0.50
makerAmount = 0.50 * 10 * 1e6 = 5,000,000
takerAmount = 10 * 1e6 = 10,000,000
// SELL 10 shares at $0.50
makerAmount = 10 * 1e6 = 10,000,000
takerAmount = 0.50 * 10 * 1e6 = 5,000,000
```
### FAK (fill-and-kill limit orders)
FAK orders build `makerAmount` and `takerAmount` with the same price/size formulas as GTC, but the matching engine treats the fields differently:
* `makerAmount` is the **hard spend cap** — the order tries to use the full amount (USDC to spend for BUY, shares to sell for SELL).
* `takerAmount` is a **protected minimum** — the minimum output implied by the signed price (shares to receive for BUY, USDC to receive for SELL), acting as slippage protection rather than a hard cap on matched size.
* Any portion not matched at or better than the signed price is cancelled; FAK never rests on the book.
`postOnly` is not supported for FAK.
### FOK (market orders)
| Side | `makerAmount` | `takerAmount` |
| -------- | -------------------- | -------------- |
| **BUY** | `usdcToSpend * 1e6` | `1` (constant) |
| **SELL** | `sharesToSell * 1e6` | `1` (constant) |
FOK orders always set `takerAmount = 1` and omit `price`. The `makerAmount` represents the raw amount being offered.
The SDKs handle amount scaling automatically — you pass human-readable values (`price`, `size`, or `makerAmount`) and the builder calculates the scaled on-chain amounts.
# For Developers
Source: https://docs.limitless.exchange/developers/introduction
REST and WebSocket APIs for programmatic trading on Limitless
Build on Limitless with **REST** and **WebSocket** APIs for programmatic trading, market data, and portfolio management.
**Base URL:** `https://api.limitless.exchange`
**Interactive API Docs (Scalar):** `https://api.limitless.exchange/api-v1`
**WebSocket URL:** `wss://ws.limitless.exchange`
The `/api-v1` path serves the interactive Scalar API documentation. For actual API calls, use the base URL directly (e.g., `GET https://api.limitless.exchange/markets/active`).
## Choose your path
You want to trade on Limitless programmatically — placing orders with your own wallet via a bot, script, or agent. **You do not need to apply for anything.** Get an API key from the UI and start trading.
You are building a product where **other users** trade through your platform. You need sub-accounts, delegated signing, or managed wallets. **Requires a partner application.**
***
## Trading for yourself
If you are an individual trader, bot operator, or agent builder, this is your path. You trade with your own wallet and sign your own orders.
### How it works
Generate an API key from the [Limitless Exchange UI](https://limitless.exchange) → profile menu → Api keys. Use it as the `X-API-Key` header on all requests.
`GET /markets/:slug` returns market info including **venue contract addresses** (fetch once per market and cache).
Construct order payloads and sign with **EIP-712** using the venue's exchange address as `verifyingContract`.
`POST /orders` with your signed order, `ownerId` (your profile ID from `GET /profiles/{address}`), `orderType`, and `marketSlug`.
### Get started
API keys, scoped tokens, and how auth works.
End-to-end trading in Python.
TypeScript/Node.js integration with viem.
How to sign orders for the CLOB.
***
## Building a platform
If you are a partner building a product where other users trade through your platform, this is your path. You manage sub-accounts on behalf of your users and optionally use delegated signing so your users never need to manage private keys.
**You must apply for Programmatic API access** before you can create sub-accounts or use delegated signing. Individual trading with your own wallet does not require an application.
### What you get
| Capability | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| **Scoped API tokens** | HMAC-SHA256 authenticated tokens with granular scopes (`trading`, `account_creation`, `delegated_signing`) |
| **Partner sub-accounts** | Create and manage user profiles linked to your partner account |
| **Delegated signing** | Submit orders without end users managing private keys — the server signs via a managed wallet |
### Get started
Full guide: partner lifecycle, sub-accounts, delegated signing, and order types.
HMAC signing protocol for partner API tokens.
Map Polymarket concepts to Limitless equivalents.
Full endpoint documentation.
***
## Official SDKs
All SDKs support both individual trading and partner integrations.
Type-safe client with signing and real-time streaming.
Async client for markets, orders, and partner features.
Typed Go client for CLOB and NegRisk markets.
Async Rust client for trading, partner flows, and WebSockets.
## Additional resources
Contract addresses and token approvals.
Real-time market and orderbook streaming.
End-to-end trading in Java.
# Migrate from Polymarket
Source: https://docs.limitless.exchange/developers/migrate-from-polymarket
Map Polymarket concepts, endpoints, and agent code to the Limitless API
If you've built bots or agents on Polymarket, this guide maps every concept, endpoint, and code pattern to the Limitless equivalents so you can port your code in minutes.
## What changed
| | Polymarket | Limitless |
| ----------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Chain** | Polygon (137) | Base (8453) |
| **Collateral** | USDC.e | USDC (6 decimals) |
| **REST hosts** | `gamma-api.polymarket.com` (data) + `clob.polymarket.com` (trading) | `api.limitless.exchange` (unified) |
| **WebSocket** | `wss://ws-subscriptions-clob.polymarket.com` | `wss://ws.limitless.exchange` |
| **Auth model** | L1/L2 — derive API creds with EIP-712, sign every request with HMAC | API key (`X-API-Key`) for individual use; HMAC scoped tokens for [partner integrations](/developers/programmatic-api) |
| **SDKs** | `py-clob-client`, `@polymarket/clob-client`, `polymarket-client-sdk` (Rust) | [Python SDK](/developers/sdk/python/getting-started), [TypeScript SDK](/developers/sdk/typescript/getting-started), [Go SDK](/developers/sdk/go/getting-started), [Rust SDK](/developers/sdk/rust/getting-started), or direct REST |
| **Wallet types** | EOA, Proxy, Gnosis Safe | EOA, or server-managed wallets via [delegated signing](/developers/programmatic-api#delegated-order-types) |
| **Market lookup** | `condition_id` or `clobTokenIds` | `slug` or address |
| **Token IDs** | `clobTokenIds[0]` = Yes, `[1]` = No | `positionIds[0]` = Yes, `[1]` = No |
| **Order signing** | EIP-712 `verifyingContract` = CTF Exchange | EIP-712 `verifyingContract` = `venue.exchange` |
| **Neg-risk** | Pass `negRisk` flag when creating orders | Handled via [venue system](/developers/venue-system) — extra approval to `venue.adapter` for SELL |
## Authentication
Polymarket requires deriving API credentials (apiKey, secret, passphrase) through an L1 EIP-712 handshake, then signing every request with HMAC-SHA256 across 5 `POLY_*` headers.
Limitless uses a single API key generated in the UI.
```python theme={null}
from py_clob_client.client import ClobClient
client = ClobClient(
"https://clob.polymarket.com",
key=private_key,
chain_id=137,
)
creds = client.create_or_derive_api_creds()
client = ClobClient(
"https://clob.polymarket.com",
key=private_key,
chain_id=137,
creds=creds,
signature_type=0,
funder=wallet_address,
)
```
```python theme={null}
import requests
API_BASE = "https://api.limitless.exchange"
headers = {
"X-API-Key": "lmts_your_key_here",
"Content-Type": "application/json",
}
```
Generate your key at [limitless.exchange](https://limitless.exchange) → profile → **Api keys**.
No `POLY_*` headers or credential derivation for basic API access. For partner integrations that need sub-account management and delegated order signing, Limitless offers [HMAC-signed scoped tokens](/developers/programmatic-api) — but this is opt-in for partners, not required for individual trading.
## Endpoint mapping
### Market data
| Action | Polymarket | Limitless |
| ------------------- | --------------------------------------------------------- | ------------------------------------- |
| List active markets | `GET gamma-api.polymarket.com/markets?active=true` | `GET /markets/active` |
| Get market by slug | `GET gamma-api.polymarket.com/markets?slug=...` | `GET /markets/:slug` |
| Search markets | `GET gamma-api.polymarket.com/markets?tag=...` | `GET /markets/search?query=...` |
| Orderbook | `GET clob.polymarket.com/book?token_id=...` | `GET /markets/:slug/orderbook` |
| Price history | `GET gamma-api.polymarket.com/markets/:id/prices-history` | `GET /markets/:slug/historical-price` |
| Midpoint price | `GET clob.polymarket.com/midpoint?token_id=...` | Compute from orderbook response |
### Trading
| Action | Polymarket | Limitless |
| --------------- | ------------------------------------------------------- | -------------------------------- |
| Place order | `POST clob.polymarket.com/order` | `POST /orders` |
| Cancel order | `DELETE clob.polymarket.com/order/:id` | `DELETE /orders/:orderId` |
| Cancel multiple | `DELETE clob.polymarket.com/cancel-orders` (up to 3000) | `POST /orders/cancel-batch` |
| Cancel all | `DELETE clob.polymarket.com/cancel-all` | `DELETE /orders/all/:slug` |
| Get open orders | `GET clob.polymarket.com/orders` | `GET /markets/:slug/user-orders` |
| Get order by ID | `GET clob.polymarket.com/order/:id` | `POST /orders/status/batch` |
### Portfolio
| Action | Polymarket | Limitless |
| ------------- | --------------------------------------- | -------------------------- |
| Get positions | `GET clob.polymarket.com/positions` | `GET /portfolio/positions` |
| Get trades | `GET clob.polymarket.com/trades` | `GET /portfolio/trades` |
| PnL chart | N/A | `GET /portfolio/pnl-chart` |
| Trade history | `GET gamma-api.polymarket.com/activity` | `GET /portfolio/history` |
## Fetch a market
```python theme={null}
# Two separate APIs: Gamma for metadata, CLOB for trading
market = requests.get(
"https://gamma-api.polymarket.com/markets",
params={"slug": "will-trump-win"}
).json()[0]
token_id = market["clobTokenIds"][0] # Yes token
condition_id = market["conditionId"]
neg_risk = market["negRisk"]
tick_size = market["minimum_tick_size"]
```
```python theme={null}
market = requests.get(
f"{API_BASE}/markets/will-trump-win",
headers=headers,
).json()
token_id = market["positionIds"][0] # Yes token
venue = market["venue"] # exchange + adapter addresses
```
No `conditionId`, `negRisk`, or `tickSize` needed in the order payload. The [venue system](/developers/venue-system) handles contract routing.
## Build and sign an order
The order structure is almost identical — both use EIP-712 typed data with the same field names. Key differences:
| Field | Polymarket | Limitless |
| ----------------------- | --------------------------- | --------------------------------- |
| `chainId` | `137` (Polygon) | `8453` (Base) |
| `verifyingContract` | CTF Exchange address | `venue.exchange` from market data |
| `name` (EIP-712 domain) | `"Polymarket CTF Exchange"` | `"Limitless CTF Exchange"` |
| `tokenId` source | `clobTokenIds[0\|1]` | `positionIds[0\|1]` |
```python theme={null}
from py_clob_client.order_builder.constants import BUY
order = client.create_and_post_order(
OrderArgs(
token_id=token_id,
price=0.50,
size=10,
side=BUY,
order_type=OrderType.GTC,
),
options={"tick_size": "0.01", "neg_risk": False},
)
```
```python theme={null}
import time
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import Web3
account = Account.from_key(private_key)
price, shares = 0.50, 10
maker_amount = int(price * shares * 1e6)
taker_amount = int(shares * 1e6)
order_data = {
"salt": int(time.time() * 1000),
"maker": Web3.to_checksum_address(account.address),
"signer": Web3.to_checksum_address(account.address),
"taker": "0x0000000000000000000000000000000000000000",
"tokenId": token_id,
"makerAmount": maker_amount,
"takerAmount": taker_amount,
"expiration": 0,
"nonce": 0,
"feeRateBps": 0,
"side": 0, # 0=BUY, 1=SELL
"signatureType": 0, # EOA
}
domain = {
"name": "Limitless CTF Exchange",
"version": "1",
"chainId": 8453,
"verifyingContract": Web3.to_checksum_address(venue["exchange"]),
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"Order": [
{"name": "salt", "type": "uint256"},
{"name": "maker", "type": "address"},
{"name": "signer", "type": "address"},
{"name": "taker", "type": "address"},
{"name": "tokenId", "type": "uint256"},
{"name": "makerAmount", "type": "uint256"},
{"name": "takerAmount", "type": "uint256"},
{"name": "expiration", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "feeRateBps", "type": "uint256"},
{"name": "side", "type": "uint8"},
{"name": "signatureType", "type": "uint8"},
],
}
encoded = encode_typed_data({
"types": types,
"primaryType": "Order",
"domain": domain,
"message": order_data,
})
signed = Account.sign_message(encoded, private_key=private_key)
signature = signed.signature.hex()
resp = requests.post(f"{API_BASE}/orders", headers=headers, json={
"order": {**order_data, "signature": signature},
"ownerId": owner_id,
"orderType": "GTC",
"marketSlug": "will-trump-win",
})
```
`ownerId` is your Limitless profile ID — a required field on every `POST /orders` request. You can find it in the Limitless UI or from your first authenticated API response. Polymarket's SDK handles this internally; on Limitless you pass it explicitly.
If you use the [Python SDK](/developers/sdk/python/getting-started), [TypeScript SDK](/developers/sdk/typescript/getting-started), [Go SDK](/developers/sdk/go/getting-started), or [Rust SDK](/developers/sdk/rust/getting-started), order building and signing are handled for you — similar to how `py-clob-client` works on Polymarket.
## Porting a Polymarket agent
If you're adapting an agent built with the [Polymarket/agents](https://github.com/Polymarket/agents) framework, here's a checklist:
Replace `POLYGON_WALLET_PRIVATE_KEY` with your existing private key on Base. Add `API_KEY` with your `lmts_` key. Remove any HMAC credential derivation code.
Replace both `gamma-api.polymarket.com` and `clob.polymarket.com` with `api.limitless.exchange`.
Update `chainId` from `137` to `8453` in all EIP-712 domain definitions.
Change `"Polymarket CTF Exchange"` to `"Limitless CTF Exchange"`.
Replace `clobTokenIds` lookups with `positionIds`. The index convention is the same: `[0]` = Yes, `[1]` = No.
Fetch `venue.exchange` from `GET /markets/:slug` and use it as `verifyingContract`. On Polymarket you may have hardcoded the CTF Exchange address — on Limitless each market can have its own venue.
Drop `tick_size`, `neg_risk`, `funder`, and `signature_type` from order construction. These are either not needed or handled by the venue system.
`POST /orders` requires an `ownerId` field (your Limitless profile ID). Polymarket's SDK handles this internally; on Limitless you pass it explicitly in every order payload alongside `order`, `orderType`, and `marketSlug`.
Approve USDC on Base (not Polygon) to `venue.exchange`. For NegRisk SELL orders, also approve Conditional Tokens to `venue.adapter`. See [venue system](/developers/venue-system).
## WebSocket
| | Polymarket | Limitless |
| ------------- | --------------------------------------------------- | ----------------------------------- |
| **URL** | `wss://ws-subscriptions-clob.polymarket.com/ws/...` | `wss://ws.limitless.exchange` |
| **Namespace** | Channel-based (`market`, `user`) | Socket.IO namespace `/markets` |
| **Auth** | API key in connection params | `X-API-Key` header during handshake |
See [WebSocket events](/developers/websocket-events) for the full event reference.
## Rate limits
| | Polymarket | Limitless |
| ------------------- | ---------------------- | ----------------------- |
| Concurrent requests | Varies by builder tier | 2 |
| Minimum delay | Varies | 300 ms between requests |
## Quick reference for agents
If you're building an LLM-powered agent or bot, here's the minimal flow:
```python theme={null}
import os, time, requests
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import Web3
API = "https://api.limitless.exchange"
KEY = os.environ["API_KEY"] # lmts_...
PK = os.environ["PRIVATE_KEY"] # 0x...
OWNER_ID = int(os.environ["OWNER_ID"]) # Profile ID from the Limitless UI
H = {"X-API-Key": KEY, "Content-Type": "application/json"}
CHAIN_ID = 8453
ZERO_ADDR = "0x0000000000000000000000000000000000000000"
ORDER_TYPES = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"Order": [
{"name": "salt", "type": "uint256"},
{"name": "maker", "type": "address"},
{"name": "signer", "type": "address"},
{"name": "taker", "type": "address"},
{"name": "tokenId", "type": "uint256"},
{"name": "makerAmount", "type": "uint256"},
{"name": "takerAmount", "type": "uint256"},
{"name": "expiration", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "feeRateBps", "type": "uint256"},
{"name": "side", "type": "uint8"},
{"name": "signatureType", "type": "uint8"},
],
}
# 1. Find a market
markets = requests.get(f"{API}/markets/active", headers=H).json()
slug = markets[0]["slug"]
# 2. Get venue + token IDs (cache per market — these are static)
market = requests.get(f"{API}/markets/{slug}", headers=H).json()
venue_exchange = market["venue"]["exchange"]
yes_token = market["positionIds"][0]
# 3. Build order (BUY 10 YES shares at $0.50)
acct = Account.from_key(PK)
order = {
"salt": int(time.time() * 1000),
"maker": Web3.to_checksum_address(acct.address),
"signer": Web3.to_checksum_address(acct.address),
"taker": ZERO_ADDR,
"tokenId": yes_token,
"makerAmount": 5_000_000, # 0.50 * 10 * 1e6
"takerAmount": 10_000_000, # 10 * 1e6
"expiration": 0,
"nonce": 0,
"feeRateBps": 0,
"side": 0,
"signatureType": 0,
}
# 4. Sign (EIP-712) — same encode_typed_data pattern as Polymarket CTF
encoded = encode_typed_data({
"types": ORDER_TYPES,
"primaryType": "Order",
"domain": {
"name": "Limitless CTF Exchange",
"version": "1",
"chainId": CHAIN_ID,
"verifyingContract": Web3.to_checksum_address(venue_exchange),
},
"message": order,
})
sig = Account.sign_message(encoded, private_key=PK).signature.hex()
# 5. Submit
resp = requests.post(f"{API}/orders", headers=H, json={
"order": {**order, "signature": sig},
"ownerId": OWNER_ID,
"orderType": "GTC",
"marketSlug": slug,
})
print(resp.json())
```
## Partner and platform migrations
If you're migrating a platform that manages multiple user accounts on Polymarket (e.g., a trading desk, social trading app, or embedded prediction market), the Limitless [Programmatic API](/developers/programmatic-api) provides a first-class partner flow:
| Polymarket pattern | Limitless equivalent |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| Manage many wallets and private keys | Create sub-accounts with server-managed wallets — no private key management |
| Sign orders per-user with their keys | [Delegated signing](/developers/programmatic-api#delegated-order-types) — server signs via Privy, submit unsigned orders |
| Derive HMAC creds per integration | Derive [scoped API tokens](/developers/authentication#scoped-api-tokens-hmac) with granular permissions |
**Recommended setup for partners:**
* Store HMAC credentials on your backend — never expose them to frontends
* Use the SDK server-side to sign partner-authenticated requests
* Expose only your own app-specific endpoints to the frontend
* Keep public market reads (orderbooks, prices) directly in the browser
Both GTC (limit) and FOK (market) order types are supported for delegated orders. See the [Delegated Orders](/developers/sdk/typescript/delegated-orders) SDK pages for full examples.
## Next steps
Full end-to-end walkthrough with error handling.
How venue contracts and token approvals work.
Partner integrations, sub-accounts, and delegated signing.
Complete endpoint documentation.
Full order type definition and field reference.
# Programmatic API
Source: https://docs.limitless.exchange/developers/programmatic-api
Partner integrations with scoped API tokens, sub-account management, and delegated signing
The Programmatic API enables partners to build integrations that create and manage user accounts, place orders on behalf of users, and operate with fine-grained access control — all through HMAC-authenticated API tokens with scoped permissions.
**Building a bot or trading for yourself?** You do not need to apply for the Programmatic API. Derive a scoped API token with the `trading` scope from the [Authentication](/developers/authentication) page and start trading immediately. The Programmatic API and the application below are for **platforms and partners** that need to create and manage sub-accounts on behalf of their users.
HMAC credentials contain a secret key and must only be used in **backend or BFF** environments. Never expose them in browser-side code.
## Recommended architecture
**Recommended setup for production integrations:**
* **Store the real HMAC credentials on your backend.** The `tokenId` and `secret` should never leave your server infrastructure.
* **Use the SDK server-side to sign partner-authenticated requests.** All trading, account creation, and delegated signing calls should be made from your backend.
* **Expose only your own app-specific endpoints to the frontend.** Your frontend talks to your backend API — your backend talks to the Limitless API.
* **Keep public market reads in the browser.** Unauthenticated endpoints like market data and orderbooks can be called directly from the frontend.
## Overview
The programmatic API introduces three capabilities:
| Capability | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| **Scoped API tokens** | HMAC-SHA256 authenticated tokens with granular scopes (`trading`, `account_creation`, `delegated_signing`) |
| **Partner sub-accounts** | Create and manage user profiles linked to your partner account |
| **Delegated signing** | Submit orders without end users managing private keys — the server signs via a managed wallet |
## Partner lifecycle
Create a standard Limitless account by logging in with your wallet at [limitless.exchange](https://limitless.exchange). This gives you a `profileId` and wallet address.
Apply for programmatic API access. The team will review your application and enable token management and allowed scopes on your account.
Fill out the partner application form to get started. You'll need your Limitless wallet address from Step 1.
Authenticate with your Privy identity token and call `POST /auth/api-tokens/derive` to create a scoped token. The response includes a `tokenId` and `secret` — store the secret securely, it is only returned once.
```typescript TypeScript theme={null}
import { Client, HMACCredentials } from '@limitless-exchange/sdk';
const client = new Client({
baseURL: 'https://api.limitless.exchange',
});
const derived = await client.apiTokens.deriveToken(identityToken, {
label: 'my-trading-bot',
scopes: ['trading', 'account_creation', 'delegated_signing'],
});
const scopedClient = new Client({
baseURL: 'https://api.limitless.exchange',
hmacCredentials: {
tokenId: derived.tokenId,
secret: derived.secret,
},
});
```
```python Python theme={null}
from limitless_sdk import Client, HMACCredentials, DeriveApiTokenInput
client = Client(base_url="https://api.limitless.exchange")
derived = await client.api_tokens.derive_token(
identity_token,
DeriveApiTokenInput(
label="my-trading-bot",
scopes=["trading", "account_creation", "delegated_signing"],
),
)
scoped_client = Client(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(
token_id=derived.token_id,
secret=derived.secret,
),
)
```
```go Go theme={null}
import "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
client := limitless.NewClient()
derived, err := client.ApiTokens.DeriveToken(ctx, identityToken, limitless.DeriveApiTokenInput{
Label: "my-trading-bot",
Scopes: []string{"trading", "account_creation", "delegated_signing"},
})
scopedClient := limitless.NewClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: derived.TokenID,
Secret: derived.Secret,
}),
)
```
Use the scoped token to create sub-accounts for your end users. Choose server wallet mode for Web2 integrations (delegated signing) or EOA mode for Web3 users who manage their own keys.
```typescript theme={null}
const account = await scopedClient.partnerAccounts.createAccount({
displayName: 'user-alice',
createServerWallet: true,
});
// account.profileId, account.account
```
With the `delegated_signing` scope and server wallet accounts, submit unsigned orders — the server signs them automatically. GTC (resting limit), FAK (fill-and-kill limit), and FOK (fill-or-kill market) order types are supported.
**GTC order** — stays on the orderbook until filled or cancelled:
```typescript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
const gtcOrder = await scopedClient.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.GTC,
onBehalfOf: account.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.55,
size: 10,
postOnly: true, // Optional for GTC only
},
});
```
**FAK order** — matches immediately up to available size and cancels any remainder:
```typescript theme={null}
const fakOrder = await scopedClient.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.FAK,
onBehalfOf: account.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.45,
size: 10,
},
});
```
**FOK order** — executes immediately at market price or is cancelled entirely:
```typescript theme={null}
const fokOrder = await scopedClient.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.FOK,
onBehalfOf: account.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
makerAmount: 10, // spend 10 USDC
},
});
```
## Scopes
| Scope | Description | Self-service |
| ------------------- | ------------------------------------------------------------------------------------------------- | --------------------------- |
| `trading` | Place and cancel orders. Default scope. Required base for `delegated_signing`. | Yes |
| `account_creation` | Create sub-account profiles linked to the partner. | Yes |
| `delegated_signing` | Server signs orders on behalf of sub-accounts via managed wallets. Must be paired with `trading`. | Yes |
| `withdrawal` | Transfer ERC20 balances from managed server-wallet sub-accounts to the partner's own addresses. | Yes |
| `admin` | Access admin-protected endpoints. | No (admin-provisioned only) |
## Scope requirements by operation
| Operation | Required scopes |
| ---------------------------------------------------------------------------------------------- | ---------------------------------------- |
| Place or cancel orders (`POST /orders`) | `trading` |
| Create sub-accounts (`POST /profiles/partner-accounts`) | `account_creation` |
| Create sub-accounts with server wallets (`createServerWallet: true`) | `account_creation` + `delegated_signing` |
| Check server-wallet allowances (`GET /profiles/partner-accounts/:profileId/allowances`) | `account_creation` + `delegated_signing` |
| Retry server-wallet allowances (`POST /profiles/partner-accounts/:profileId/allowances/retry`) | `account_creation` + `delegated_signing` |
| Submit unsigned orders (server signs) | `trading` + `delegated_signing` |
| Redeem resolved positions (`POST /portfolio/redeem`) | `trading` |
| Withdraw funds (`POST /portfolio/withdraw`) | `withdrawal` |
## Sub-account modes
### Server wallet (Web2 partners)
Set `createServerWallet: true` when creating a sub-account. The server provisions a managed Privy wallet and links it to the profile. The partner submits unsigned orders and the server signs them via the managed wallet.
Server wallet mode requires both `account_creation` and `delegated_signing` scopes on your API token.
#### Allowance readiness before trading
Before placing the first delegated order for a server-wallet child profile, check live allowance state:
1. Poll [`GET /profiles/partner-accounts/:profileId/allowances`](/api-reference/partner-accounts/check-allowances).
2. If `ready=true`, continue with delegated trading.
3. If targets are `missing` or `failed` with `retryable=true`, call [`POST /profiles/partner-accounts/:profileId/allowances/retry`](/api-reference/partner-accounts/retry-allowances).
4. If retry returns `submitted` targets, poll GET again after a short delay.
5. If retry returns `429`, wait `retryAfterSeconds`.
6. If retry returns `409`, wait briefly and call GET again.
#### Lifecycle after a trade
For server wallet sub-accounts, it helps to treat **trading**, **market resolution**, and **redemption** as separate stages:
1. **Order execution** - Place and cancel orders through [`POST /orders`](/api-reference/trading/create-order) or the SDK `DelegatedOrderService`.
2. **Portfolio resolution state** - Portfolio endpoints such as [`GET /portfolio/positions`](/api-reference/portfolio/positions) and [`GET /portfolio/{account}/positions`](/api-reference/public-portfolio/positions) may later show `status: RESOLVED` and `winningOutcomeIndex` once the winning side is known.
3. **On-chain payout settlement** - Winning positions become redeemable only after the underlying conditional token payout has been reported on-chain.
`status: RESOLVED` and `winningOutcomeIndex` in the portfolio response do **not** by themselves guarantee that the underlying conditional token position is already redeemable on-chain. Resolution in the API can appear before the CTF condition has been settled on-chain.
If your integration checks settlement directly on-chain, wait until the payout vector has been posted before attempting redemption. For CTF-based positions, a condition is not yet settled if `payoutDenominator(conditionId)` is still `0`.
#### Redemption and withdrawal support
Server wallet partner flows now include direct endpoints for payout settlement and treasury movement:
* [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) to submit `redeemPositions` for a resolved condition.
* [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) to transfer ERC20 funds from a managed sub-account server wallet to the partner's own account, partner smart wallet, or an active allowlisted treasury destination.
* [`POST /portfolio/withdrawal-addresses`](/api-reference/portfolio/add-withdrawal-address) and [`DELETE /portfolio/withdrawal-addresses/:address`](/api-reference/portfolio/delete-withdrawal-address) to manage explicit treasury destination allowlists with Privy identity auth.
The SDKs provide helper methods for server wallet claim (redeem), withdrawal, and withdrawal-address allowlist management. See the SDK getting-started pages for details.
### EOA (Web3 partners)
Omit `createServerWallet` or set it to `false`. The partner proves wallet ownership using `x-account`, `x-signing-message`, and `x-signature` on `POST /profiles/partner-accounts`. The end user keeps their private key in their own wallet and signs each order (EIP-712) themselves — Limitless never holds their key.
Scopes: `trading` and `account_creation` are sufficient for EOA partner flows. You do **not** need `delegated_signing` unless you also use [server wallet](#server-wallet-web2-partners) sub-accounts.
#### End-to-end flow
1. **Register the user's wallet (once per address)** — `POST /profiles/partner-accounts` with [EOA headers](/api-reference/partner-accounts/create-partner-account). The response returns `profileId` and the wallet `account`. If a profile already exists for that address, the API returns `409 Conflict` — your app should reuse the stored `profileId` instead of re-registering.
2. **Persist the mapping in your backend** — save at least `walletAddress → profileId` (and your own user id if you have one). You are **not** storing a Limitless “session object”; you are storing your app’s record so you know which `profileId` to pass on orders. See [What to persist](#what-to-persist-for-eoa-partners) below.
3. **User signs each order** — build the order per [EIP-712 signing](/developers/eip712-signing) and have the user sign in the browser (e.g. wagmi/viem/ethers). `maker` and `signer` must be the user’s EOA address.
4. **Submit the order from your backend (HMAC)** — `POST /orders` with HMAC auth, `onBehalfOf` set to the user’s `profileId`, and **`ownerId` set to that same `profileId`** when you send a **signed** order body. The `order`’s `maker` / `signer` must match the sub-account wallet. (If you use **delegated signing** with an unsigned `order`, the server sets `ownerId` for you — that path is for server-wallet sub-accounts, not pure EOA signing.)
#### Common EOA auth error
If `POST /orders` returns `Signer does not match authenticated profile account`, check all three fields point to the same user account:
* `ownerId` = user's `profileId`
* `onBehalfOf` = same user's `profileId`
* `order.maker` and `order.signer` = that user's wallet address
Using the partner profile ID in `ownerId` while signing with a user wallet causes this error.
#### Architecture (browser + backend)
Typical layout for a Next.js (or any SPA) app:
* **Browser:** connect wallet (e.g. wagmi + RainbowKit); sign the ownership proof for step 1; sign EIP-712 orders for step 3. Never put partner HMAC secrets in client bundles.
* **Backend (Next.js Route Handlers, server actions, or a separate API):** holds the scoped token `tokenId` / `secret`; signs every request to `api.limitless.exchange` with HMAC; forwards `POST /orders` after the client submits the signed order payload (or proxies market data as needed).
```mermaid theme={null}
flowchart LR
subgraph browser [Browser]
W[User wallet]
UI[Your UI]
end
subgraph server [Your server]
BFF[BFF / API routes]
HMAC[HMAC partner token]
end
subgraph lx [Limitless API]
PA["POST /profiles/partner-accounts"]
PO["POST /orders"]
end
UI --> W
UI -->|"signed order JSON"| BFF
BFF --> HMAC
HMAC --> PA
HMAC --> PO
```
There is **no official Next.js sample app** in the public GitHub org today; use the [TypeScript SDK](/developers/sdk/typescript/api-tokens) on the server with the pattern above. Generic Node examples in [Quickstart: Node.js](/developers/quickstart/nodejs) illustrate HMAC and orders — swap in partner `hmacCredentials` and `partnerAccounts.createAccount` for EOA registration.
#### What to persist for EOA partners
| Store in your DB | Why |
| --------------------------------------------------- | -------------------------------------------------------------------------- |
| User’s wallet address and Limitless **`profileId`** | Needed for `onBehalfOf` / `ownerId` on every `POST /orders` for that user. |
| Optional: `feeRateBps` / profile metadata | You can refetch from the API if needed. |
| Do **not** rely on storing | Notes |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The user’s **private key** | Not required and not recommended; the wallet stays in the user’s client. |
| A Limitless “login session” to avoid **order** signing | EOA mode requires an **EIP-712 signature per order** (or a new signing policy you implement). You can avoid repeated **registration** by persisting `profileId`. |
| Partner HMAC `secret` in the browser | Forbidden — keep only on the server. |
There is no partner endpoint to list or lookup previously created sub-accounts by wallet address. `GET /profiles/{account}` returns only the authenticated account's own profile. Treat `walletAddress -> profileId` persistence as required integration state.
**Wallet UX:** Standard wallet connectors remember the user’s connection across visits, so users don’t necessarily “reconnect from scratch” every time — but **each order** still needs a signature unless you move to [server wallet + delegated signing](#server-wallet-web2-partners).
## Delegated order types
Delegated orders support three execution strategies:
### GTC (Good-Til-Cancelled)
Limit orders that rest on the orderbook at a specified price until they are filled or explicitly cancelled. Use `price` and `size` to specify the order.
| Parameter | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `price` | Price between 0 and 1 |
| `size` | Number of contracts |
| `postOnly` | Optional. When `true`, the order is rejected if it would immediately match against existing orders. Guarantees maker-only execution. Default `false`. |
### FAK (Fill-And-Kill)
Limit orders that use `price` and `size` like GTC but only match immediately available liquidity. Any unmatched remainder is cancelled.
| Parameter | Description |
| --------- | --------------------- |
| `price` | Price between 0 and 1 |
| `size` | Number of contracts |
### FOK (Fill-Or-Kill)
Market orders that execute immediately at the best available price or are cancelled entirely — no partial fills. Instead of `price` and `size`, FOK orders use `makerAmount`.
| Parameter | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `makerAmount` | **BUY**: USDC amount to spend (e.g., `50` = spend \$50 USDC). **SELL**: number of shares to sell (e.g., `18.64` = sell 18.64 shares). |
```typescript TypeScript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
// FAK BUY — fill immediately up to 10 shares at 0.45, cancel remainder
const response = await scopedClient.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.FAK,
onBehalfOf: account.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.45,
size: 10,
},
});
if (response.makerMatches?.length) {
console.log(`Matched immediately with ${response.makerMatches.length} fill(s)`);
} else {
console.log('No immediate fill. Remainder was cancelled.');
}
```
```python Python theme={null}
response = await scoped_client.delegated_orders.create_order(
token_id=str(market.tokens.yes),
side=Side.BUY,
order_type=OrderType.FAK,
market_slug="btc-100k",
on_behalf_of=account.profile_id,
price=0.45,
size=10.0,
)
if response.maker_matches:
print(f"Matched immediately with {len(response.maker_matches)} fill(s)")
else:
print("No immediate fill. Remainder was cancelled.")
```
```go Go theme={null}
response, err := scopedClient.DelegatedOrders.CreateOrder(ctx, limitless.CreateDelegatedOrderParams{
MarketSlug: "btc-100k",
OrderType: limitless.OrderTypeFAK,
OnBehalfOf: account.ProfileID,
Args: limitless.FAKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.45,
Size: 10.0,
},
})
if len(response.MakerMatches) > 0 {
fmt.Printf("Matched immediately with %d fill(s)\n", len(response.MakerMatches))
} else {
fmt.Println("No immediate fill. Remainder was cancelled.")
}
```
```typescript TypeScript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
// FOK BUY — spend 1 USDC at market price
const response = await scopedClient.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.FOK,
onBehalfOf: account.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
makerAmount: 1,
},
});
if (response.makerMatches?.length) {
console.log(`Matched with ${response.makerMatches.length} fill(s)`);
} else {
console.log('Not matched — cancelled automatically');
}
```
```python Python theme={null}
response = await scoped_client.delegated_orders.create_order(
token_id=str(market.tokens.yes),
side=Side.BUY,
order_type=OrderType.FOK,
market_slug="btc-100k",
on_behalf_of=account.profile_id,
maker_amount=1.0,
)
if response.maker_matches:
print(f"Matched with {len(response.maker_matches)} fill(s)")
else:
print("Not matched — cancelled automatically")
```
```go Go theme={null}
response, err := scopedClient.DelegatedOrders.CreateOrder(ctx, limitless.CreateDelegatedOrderParams{
MarketSlug: "btc-100k",
OrderType: limitless.OrderTypeFOK,
OnBehalfOf: account.ProfileID,
Args: limitless.FOKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
MakerAmount: 1.0,
},
})
if len(response.MakerMatches) > 0 {
fmt.Printf("Matched with %d fill(s)\n", len(response.MakerMatches))
} else {
fmt.Println("Not matched — cancelled automatically")
}
```
## Reading sub-account data
Partners can read portfolio, order, and position data for their sub-accounts using the `x-on-behalf-of` header. This lets you show users their positions, trade history, and order status without requiring the sub-account to authenticate directly.
### How it works
Add the `x-on-behalf-of` header with the sub-account's `profileId` to any supported read endpoint. The response is exactly what the sub-account would see if they called the endpoint themselves.
```http theme={null}
GET /portfolio/positions HTTP/1.1
Host: api.limitless.exchange
lmts-api-key:
lmts-signature:
lmts-timestamp:
x-on-behalf-of: 1292711
```
### Requirements
1. **Scope** — your API token must include the `delegated_signing` scope.
2. **Ownership** — the target profile must be a sub-account linked to your partner profile.
3. **Auth** — use your existing HMAC flow (`lmts-api-key`, `lmts-signature`, `lmts-timestamp`). Nothing new.
### Supported endpoints
| Method | Path | Description |
| ------ | ---------------------------- | ------------------------------------------ |
| GET | `/portfolio/positions` | Sub-account's current positions |
| GET | `/portfolio/history` | Sub-account's trade history |
| GET | `/markets/:slug/user-orders` | Sub-account's orders for a specific market |
| POST | `/orders/status/batch` | Batch order status check |
More read endpoints will be enabled over time.
### Without the header
If you omit `x-on-behalf-of`, these endpoints behave exactly as before — you read your own data. The header is purely additive.
### Error responses
| Status | When |
| ------ | -------------------------------------------------------------------------------- |
| `200` | Success — data is for the target sub-account |
| `400` | Invalid header value or endpoint does not support delegation |
| `403` | Token lacks `delegated_signing` scope, or target profile is not your sub-account |
Error messages on `403` are intentionally generic — the API does not confirm or deny the existence of specific profile IDs.
## HMAC authentication
All day-to-day programmatic API calls use HMAC-SHA256 request signing. See the [Authentication](/developers/authentication#hmac-request-signing) page for the signing protocol, canonical message format, and code examples.
## API endpoints
| Endpoint | Auth | Description |
| ------------------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------- |
| [`GET /auth/api-tokens/capabilities`](/api-reference/api-tokens/get-capabilities) | Privy | Check partner capability configuration |
| [`POST /auth/api-tokens/derive`](/api-reference/api-tokens/derive-token) | Privy | Create a scoped API token |
| [`GET /auth/api-tokens`](/api-reference/api-tokens/list-tokens) | Any | List active tokens |
| [`DELETE /auth/api-tokens/{tokenId}`](/api-reference/api-tokens/revoke-token) | Any | Revoke a token |
| [`POST /profiles/partner-accounts`](/api-reference/partner-accounts/create-partner-account) | HMAC | Create a sub-account |
| [`POST /orders`](/api-reference/trading/create-order) | HMAC | Place orders (with optional delegated signing) |
| [`GET /portfolio/positions`](/api-reference/portfolio/positions) | HMAC | Read positions (supports `x-on-behalf-of` for sub-accounts) |
| [`GET /portfolio/history`](/api-reference/portfolio/history) | HMAC | Read trade history (supports `x-on-behalf-of` for sub-accounts) |
| [`GET /markets/:slug/user-orders`](/api-reference/trading/user-orders) | HMAC | Read user orders for a market (supports `x-on-behalf-of` for sub-accounts) |
| [`POST /orders/status/batch`](/api-reference/trading/order-status-batch) | HMAC | Batch order status (supports `x-on-behalf-of` for sub-accounts) |
| [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) | Any (`apiToken`, Privy, session) | Redeem resolved positions from a server wallet |
| [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) | Any (`apiToken`, Privy, session) | Withdraw ERC20 funds from a server wallet |
| [`POST /portfolio/withdrawal-addresses`](/api-reference/portfolio/add-withdrawal-address) | Privy | Add an explicit withdrawal destination allowlist entry |
| [`DELETE /portfolio/withdrawal-addresses/:address`](/api-reference/portfolio/delete-withdrawal-address) | Privy | Remove a withdrawal destination allowlist entry |
## SDK support
All four official SDKs provide built-in support for the programmatic API:
Server-wallet redeem, withdraw, and withdrawal-address allowlist helpers are available in the TypeScript, Go, Python, and Rust SDKs.
`Client` with `hmacCredentials`, `apiTokens`, `partnerAccounts`, `delegatedOrders`, `serverWallets`
`Client` with `hmac_credentials`, `api_tokens`, `partner_accounts`, `delegated_orders`, `server_wallets`
`Client` with `WithHMACCredentials`, `ApiTokens`, `PartnerAccounts`, `DelegatedOrders`, `ServerWallets`
`Client` with `HmacCredentials`, `api_tokens`, `partner_accounts`, `delegated_orders`, `server_wallets`
# Java Quick Start
Source: https://docs.limitless.exchange/developers/quickstart/java
End-to-end Java implementation with Web3j for trading on Limitless Exchange
## Overview
This guide walks you through a complete Java implementation for trading on Limitless Exchange: authentication, fetching market data, building and signing orders with EIP-712 via Web3j, and submitting them via the REST API.
## Prerequisites
Initialize a Gradle project with the following dependencies in `build.gradle`:
Include web3j, OkHttp, Jackson, and Bouncy Castle.
### build.gradle
```groovy theme={null}
plugins {
id 'java'
id 'application'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.web3j:core:4.9.8'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'org.bouncycastle:bcprov-jdk18on:1.76'
}
application {
mainClass = 'LimitlessQuickStart'
}
```
**Never share your private key.** Store it in environment variables or a secure secrets manager. Do not hardcode it in source code or commit it to version control.
## Environment Variables
Configure the following before running:
| Variable | Description |
| ------------- | ------------------------------------------------------------------------- |
| `PRIVATE_KEY` | Your wallet private key (with or without `0x` prefix) for EIP-712 signing |
| `API_KEY` | API key from Limitless (starts with `lmts_`) |
| `API_URL` | Optional; defaults to `https://api.limitless.exchange` |
| `OWNER_ID` | Your profile ID (obtain from portfolio or auth endpoints) |
## Authentication
All API requests require the `X-API-Key` header. Cookie-based session auth is deprecated.
Your **private key** is used only for **EIP-712 order signing**. The **API key** handles request authentication. Both are required for trading.
```java theme={null}
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
String apiUrl = System.getenv().getOrDefault("API_URL", "https://api.limitless.exchange");
String apiKey = System.getenv("API_KEY");
OkHttpClient client = new OkHttpClient.Builder()
.cookieJar(new okhttp3.JavaNetCookieJar(new java.net.CookieManager()))
.build();
Request.Builder requestBuilder(String path) {
return new Request.Builder()
.url(apiUrl + path)
.addHeader("X-API-Key", apiKey)
.addHeader("Content-Type", "application/json");
}
```
Cookie-based authentication is **deprecated** and will be removed. Use API keys for all programmatic access.
## Fetching Market Data
Use `GET /markets/:slug` to retrieve market details, including venue addresses and position IDs. Cache this data per market; it is static.
```java theme={null}
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
JsonNode fetchMarket(String slug) throws IOException {
Request req = requestBuilder("/markets/" + slug).get().build();
try (Response resp = client.newCall(req).execute()) {
if (!resp.isSuccessful()) throw new IOException("Market fetch failed: " + resp.code());
return mapper.readTree(resp.body().string());
}
}
```
```java theme={null}
// Example response (CLOB market)
// {
// "slug": "btc-above-100k-march-2025",
// "venue": { "exchange": "0xA1b2...", "adapter": "0xD4e5..." },
// "positionIds": ["19633...", "19633..."] // [0]=YES, [1]=NO
// }
```
```java theme={null}
// Handle both positionIds and position_ids (API may return either)
JsonNode positionIds = market.has("positionIds")
? market.get("positionIds")
: market.get("position_ids");
String yesTokenId = positionIds.get(0).asText();
String noTokenId = positionIds.get(1).asText();
```
**positionIds\[0]** is the YES token, **positionIds\[1]** is the NO token. Use the appropriate one based on your order side and outcome.
## Building Order Payloads
Orders require specific fields. Key values:
| Field | Value | Description |
| --------------- | ----- | ------------------- |
| `side` | `0` | BUY |
| `side` | `1` | SELL |
| `signatureType` | `0` | EOA wallet |
| `orderType` | `GTC` | Good Till Cancelled |
| `orderType` | `FOK` | Fill or Kill |
USDC uses **6 decimals** (1 USDC = 1,000,000 units). Shares are scaled by 1e6.
You pay USDC, receive shares. Use `positionIds[0]` for YES, `positionIds[1]` for NO.
```
makerAmount = priceInDollars * numShares * 1e6 // USDC you pay
takerAmount = numShares * 1e6 // Shares you receive
side = 0
```
Example: BUY 10 YES shares at \$0.65 → `makerAmount` = 6,500,000, `takerAmount` = 10,000,000
You pay shares, receive USDC. Use the token ID of the shares you are selling.
```
makerAmount = numShares * 1e6 // Shares you pay
takerAmount = priceInDollars * numShares * 1e6 // USDC you receive
side = 1
```
Example: SELL 10 YES shares at \$0.65 → `makerAmount` = 10,000,000, `takerAmount` = 6,500,000
## EIP-712 Signing
Sign orders using Web3j's `StructuredDataEncoder` and `Sign.signTypedData`. The venue's exchange address is the `verifyingContract`.
See [EIP-712 Order Signing](/developers/eip712-signing) for the full type definition and field reference.
```java theme={null}
import com.fasterxml.jackson.databind.ObjectMapper;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
static final int CHAIN_ID = 8453; // Base
static final String ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
String signOrder(Map orderData, String verifyingContract, Credentials creds)
throws Exception {
String json = buildEip712Json(orderData, Keys.toChecksumAddress(verifyingContract));
Sign.SignatureData sig = Sign.signTypedData(json, creds.getEcKeyPair());
byte[] sigBytes = new byte[65];
System.arraycopy(sig.getR(), 0, sigBytes, 0, 32);
System.arraycopy(sig.getS(), 0, sigBytes, 32, 32);
System.arraycopy(sig.getV(), 0, sigBytes, 64, 1);
return Numeric.toHexString(sigBytes);
}
String buildEip712Json(Map orderData, String verifyingContract) {
Map domain = Map.of(
"name", "Limitless CTF Exchange",
"version", "1",
"chainId", CHAIN_ID,
"verifyingContract", verifyingContract
);
Map message = new HashMap<>();
message.put("salt", orderData.get("salt"));
message.put("maker", Keys.toChecksumAddress((String) orderData.get("maker")));
message.put("signer", Keys.toChecksumAddress((String) orderData.get("signer")));
message.put("taker", Keys.toChecksumAddress((String) orderData.get("taker")));
message.put("tokenId", orderData.get("tokenId"));
message.put("makerAmount", orderData.get("makerAmount"));
message.put("takerAmount", orderData.get("takerAmount"));
message.put("expiration", orderData.get("expiration"));
message.put("nonce", orderData.get("nonce"));
message.put("feeRateBps", orderData.get("feeRateBps"));
message.put("side", orderData.get("side"));
message.put("signatureType", orderData.get("signatureType"));
return """
{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Order": [
{"name": "salt", "type": "uint256"},
{"name": "maker", "type": "address"},
{"name": "signer", "type": "address"},
{"name": "taker", "type": "address"},
{"name": "tokenId", "type": "uint256"},
{"name": "makerAmount", "type": "uint256"},
{"name": "takerAmount", "type": "uint256"},
{"name": "expiration", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "feeRateBps", "type": "uint256"},
{"name": "side", "type": "uint8"},
{"name": "signatureType", "type": "uint8"}
]
},
"primaryType": "Order",
"domain": %s,
"message": %s
}
""".formatted(
new ObjectMapper().writeValueAsString(domain),
new ObjectMapper().writeValueAsString(message)
);
}
```
All addresses must be **checksummed** (EIP-55). Use `Keys.toChecksumAddress()`.
## Submitting Orders
Send the signed order to `POST /orders`:
```java theme={null}
import okhttp3.MediaType;
import okhttp3.RequestBody;
JsonNode submitOrder(Map payload) throws IOException {
String json = mapper.writeValueAsString(payload);
Request req = requestBuilder("/orders")
.post(RequestBody.create(json, MediaType.parse("application/json")))
.build();
try (Response resp = client.newCall(req).execute()) {
if (!resp.isSuccessful()) throw new IOException("Order failed: " + resp.code());
return mapper.readTree(resp.body().string());
}
}
```
## Complete Example
```java theme={null}
package limitless;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import okhttp3.*;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
public class LimitlessQuickStart {
static final String API_URL = System.getenv().getOrDefault("API_URL", "https://api.limitless.exchange");
static final int CHAIN_ID = 8453;
static final String ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
final OkHttpClient client;
final ObjectMapper mapper = new ObjectMapper();
final String apiKey;
final Credentials credentials;
final long ownerId;
public LimitlessQuickStart() {
apiKey = System.getenv("API_KEY");
credentials = Credentials.create(System.getenv("PRIVATE_KEY"));
ownerId = Long.parseLong(System.getenv().getOrDefault("OWNER_ID", "0"));
client = new OkHttpClient.Builder()
.cookieJar(new okhttp3.JavaNetCookieJar(new java.net.CookieManager()))
.build();
}
Request.Builder req(String path) {
return new Request.Builder()
.url(API_URL + path)
.addHeader("X-API-Key", apiKey)
.addHeader("Content-Type", "application/json");
}
JsonNode fetchMarket(String slug) throws Exception {
try (Response r = client.newCall(req("/markets/" + slug).get().build()).execute()) {
if (!r.isSuccessful()) throw new RuntimeException("Market fetch failed: " + r.code());
return mapper.readTree(r.body().string());
}
}
String signOrder(Map orderData, String verifyingContract) throws Exception {
String json = buildEip712Json(orderData, Keys.toChecksumAddress(verifyingContract));
Sign.SignatureData sig = Sign.signTypedData(json, credentials.getEcKeyPair());
byte[] b = new byte[65];
System.arraycopy(sig.getR(), 0, b, 0, 32);
System.arraycopy(sig.getS(), 0, b, 32, 32);
System.arraycopy(sig.getV(), 0, b, 64, 1);
return Numeric.toHexString(b);
}
String buildEip712Json(Map orderData, String verifyingContract) throws Exception {
Map domain = Map.of(
"name", "Limitless CTF Exchange", "version", "1",
"chainId", CHAIN_ID, "verifyingContract", verifyingContract);
Map msg = new HashMap<>(orderData);
msg.put("maker", Keys.toChecksumAddress((String) msg.get("maker")));
msg.put("signer", Keys.toChecksumAddress((String) msg.get("signer")));
msg.put("taker", Keys.toChecksumAddress((String) msg.get("taker")));
return """
{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Order":[{"name":"salt","type":"uint256"},{"name":"maker","type":"address"},{"name":"signer","type":"address"},{"name":"taker","type":"address"},{"name":"tokenId","type":"uint256"},{"name":"makerAmount","type":"uint256"},{"name":"takerAmount","type":"uint256"},{"name":"expiration","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"feeRateBps","type":"uint256"},{"name":"side","type":"uint8"},{"name":"signatureType","type":"uint8"}]},"primaryType":"Order","domain":%s,"message":%s}
""".formatted(mapper.writeValueAsString(domain), mapper.writeValueAsString(msg));
}
JsonNode placeBuyOrder(String marketSlug, double numShares, double pricePerShare, String orderType)
throws Exception {
JsonNode market = fetchMarket(marketSlug);
JsonNode venue = market.get("venue");
JsonNode posIds = market.has("positionIds") ? market.get("positionIds") : market.get("position_ids");
String tokenId = posIds.get(0).asText();
String exchange = venue.get("exchange").asText();
long makerAmount = (long) (pricePerShare * numShares * 1e6);
long takerAmount = (long) (numShares * 1e6);
long salt = System.currentTimeMillis();
Map orderData = new HashMap<>();
orderData.put("salt", salt);
orderData.put("maker", credentials.getAddress());
orderData.put("signer", credentials.getAddress());
orderData.put("taker", ZERO_ADDRESS);
orderData.put("tokenId", new BigInteger(tokenId));
orderData.put("makerAmount", BigInteger.valueOf(makerAmount));
orderData.put("takerAmount", BigInteger.valueOf(takerAmount));
orderData.put("expiration", BigInteger.ZERO);
orderData.put("nonce", BigInteger.ZERO);
orderData.put("feeRateBps", BigInteger.ZERO);
orderData.put("side", 0);
orderData.put("signatureType", 0);
String sig = signOrder(orderData, exchange);
orderData.put("signature", sig);
ObjectNode payload = mapper.createObjectNode();
payload.putPOJO("order", orderData);
payload.put("ownerId", ownerId);
payload.put("orderType", orderType);
payload.put("marketSlug", marketSlug);
try (Response r = client.newCall(req("/orders")
.post(RequestBody.create(payload.toString(), MediaType.parse("application/json")))
.build()).execute()) {
if (!r.isSuccessful()) throw new RuntimeException("Order failed: " + r.code());
return mapper.readTree(r.body().string());
}
}
public static void main(String[] args) throws Exception {
LimitlessQuickStart app = new LimitlessQuickStart();
JsonNode result = app.placeBuyOrder("your-market-slug", 10.0, 0.65, "GTC");
System.out.println(result);
}
}
```
```bash theme={null}
# Build and run
export PRIVATE_KEY="0x..."
export API_KEY="lmts_your_key_here"
export OWNER_ID="12345"
./gradlew run
```
**ownerId** is your profile ID. Obtain it from your first authenticated response (e.g. portfolio or auth endpoints) or from the Limitless UI. Set `OWNER_ID` in your environment.
## Build and Run
```bash theme={null}
export PRIVATE_KEY="0x..."
export API_KEY="lmts_your_key_here"
export OWNER_ID="12345"
export API_URL="https://api.limitless.exchange" # optional
```
```bash theme={null}
./gradlew build
```
```bash theme={null}
./gradlew run
```
## Troubleshooting
Ensure `X-API-Key` is set correctly and the key starts with `lmts_`. Check that the key is active in the Limitless UI.
* Verify `verifyingContract` is the market's `venue.exchange` address.
* Ensure all addresses are checksummed (EIP-55) via `Keys.toChecksumAddress()`.
* Confirm `makerAmount` and `takerAmount` use 1e6 scaling for USDC and shares.
* Ensure you have enough USDC on Base for BUY orders.
* Approve USDC to `venue.exchange` for BUY; Conditional Tokens to `venue.exchange` (and `venue.adapter` for NegRisk SELL) for SELL.
Use `positionIds[0]` for YES and `positionIds[1]` for NO. Double-check you are trading the correct outcome.
## Next Steps
Full order type definition and field reference.
Understanding venue contracts and token approvals.
Complete endpoint documentation.
API key setup and best practices.
# Node.js / TypeScript Quick Start
Source: https://docs.limitless.exchange/developers/quickstart/nodejs
End-to-end TypeScript implementation with viem for trading on Limitless Exchange
## Overview
This guide walks through a complete Node.js/TypeScript integration with the Limitless Exchange API: authentication, market data, EIP-712 order signing with viem, order submission, and WebSocket subscriptions for real-time data.
**Never expose your private key.** Use environment variables and never commit secrets to version control. For production, consider a dedicated key management solution.
## Prerequisites
Install the required dependencies:
```bash theme={null}
pnpm add socket.io-client cross-fetch ethers viem
```
```bash theme={null}
npm install socket.io-client cross-fetch ethers viem
```
```bash theme={null}
yarn add socket.io-client cross-fetch ethers viem
```
| Package | Purpose |
| ------------------ | ------------------------------------------------- |
| `socket.io-client` | WebSocket connection to real-time market data |
| `cross-fetch` | HTTP requests (or use native `fetch` in Node 18+) |
| `ethers` | Optional; viem handles EIP-712 signing |
| `viem` | EIP-712 signing, wallet client, `parseUnits` |
If using Node 18+, you can omit `cross-fetch` and use the built-in `fetch`.
## Environment Variables
Create a `.env` file (and add it to `.gitignore`):
```bash theme={null}
API_KEY=lmts_your_api_key_here
PRIVATE_KEY=0x...
API_URL=https://api.limitless.exchange
OWNER_ID=12345
```
`OWNER_ID` is your Limitless profile ID (numeric). Obtain it from your account settings or from an authenticated API response. It is required for order submission.
## 1. Authentication
All REST requests require the `X-API-Key` header. WebSocket connections pass the same key during the handshake.
```typescript api.ts theme={null}
const API_URL = process.env.API_URL ?? 'https://api.limitless.exchange';
const API_KEY = process.env.API_KEY;
if (!API_KEY?.startsWith('lmts_')) {
throw new Error('Invalid or missing API_KEY. Get one at limitless.exchange → Api keys');
}
export async function apiGet(path: string): Promise {
const res = await fetch(`${API_URL}${path}`, {
headers: { 'X-API-Key': API_KEY },
});
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}
export async function apiPost(path: string, body: unknown): Promise {
const res = await fetch(`${API_URL}${path}`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}
```
```bash curl theme={null}
# GET request
curl -H "X-API-Key: lmts_your_key" https://api.limitless.exchange/markets/btc-100k-weekly
# POST request
curl -X POST -H "X-API-Key: lmts_your_key" \
-H "Content-Type: application/json" \
-d '{"order":{...},"orderType":"GTC","marketSlug":"btc-100k-weekly","ownerId":12345}' \
https://api.limitless.exchange/orders
```
## 2. Fetching Market Data and Caching Venue Info
Fetch market details via `GET /markets/:slug`. The response includes `venue` (exchange, adapter) and `positionIds` for CLOB markets.
Call `GET /markets/{slug}` to retrieve market data including venue addresses.
Use `venue.exchange` as the EIP-712 `verifyingContract`. Use `positionIds[0]` for YES and `positionIds[1]` for NO.
Venue data is static per market. Fetch once and reuse for all orders on that market.
```typescript theme={null}
interface Venue {
exchange: string;
adapter: string;
}
interface ClobMarket {
slug: string;
venue: Venue;
positionIds: [string, string]; // [YES, NO]
}
export async function getMarket(slug: string): Promise {
const data = await apiGet(`/markets/${slug}`);
const positionIds = (data.positionIds ?? data.position_ids ?? []) as [string, string];
if (!data.venue?.exchange || positionIds.length < 2) {
throw new Error(`Market ${slug} is not a CLOB market or missing venue data`);
}
return { ...data, positionIds };
}
```
For markets that use `position_ids` (snake\_case) in the API response, map to `positionIds` in your code. The first element is YES, the second is NO.
## 3. Creating Signed Orders with viem (EIP-712)
Build the order payload and sign it with viem's `signTypedData`. The domain uses `venue.exchange` as `verifyingContract`.
### EIP-712 Domain and Types
```typescript theme={null}
import { createWalletClient, http, privateKeyToAccount } from 'viem';
import { base } from 'viem/chains';
import { parseUnits } from 'viem';
const chainId = 8453; // Base
const orderType = {
Order: [
{ name: 'salt', type: 'uint256' },
{ name: 'maker', type: 'address' },
{ name: 'signer', type: 'address' },
{ name: 'taker', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
{ name: 'makerAmount', type: 'uint256' },
{ name: 'takerAmount', type: 'uint256' },
{ name: 'expiration', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'feeRateBps', type: 'uint256' },
{ name: 'side', type: 'uint8' },
{ name: 'signatureType', type: 'uint8' },
],
} as const;
```
### Signing Logic
```typescript theme={null}
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export async function createSignedOrder(params: {
market: ClobMarket;
side: 0 | 1; // 0 = BUY, 1 = SELL
outcome: 'yes' | 'no'; // positionIds[0]=YES, positionIds[1]=NO
priceCents: number; // e.g. 65 = $0.65
shares: number; // number of shares
orderType: 'GTC' | 'FOK';
nonce: number;
}): Promise<{ order: Record; signature: string }> {
const { market, side, outcome, priceCents, shares, nonce } = params;
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
if (!privateKey) throw new Error('PRIVATE_KEY required');
const account = privateKeyToAccount(privateKey);
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const tokenId = outcome === 'yes' ? market.positionIds[0] : market.positionIds[1];
const price = priceCents / 100;
const sharesScaled = parseUnits(shares.toString(), 6);
const makerAmount = side === 0
? parseUnits((price * shares).toFixed(6), 6) // BUY: offer USDC
: sharesScaled; // SELL: offer shares
const takerAmount = side === 0
? sharesScaled // BUY: want shares
: parseUnits((price * shares).toFixed(6), 6); // SELL: want USDC
const salt = BigInt(Date.now());
const expiration = 0n;
const order = {
salt,
maker: account.address,
signer: account.address,
taker: ZERO_ADDRESS,
tokenId: BigInt(tokenId),
makerAmount,
takerAmount,
expiration,
nonce: BigInt(nonce),
feeRateBps: 0n,
side: params.side as 0 | 1,
signatureType: 0, // EOA
};
const domain = {
name: 'Limitless CTF Exchange',
version: '1',
chainId,
verifyingContract: market.venue.exchange as `0x${string}`,
};
const signature = await walletClient.signTypedData({
domain,
types: orderType,
primaryType: 'Order',
message: order,
});
return {
order: {
salt: Number(order.salt),
maker: account.address,
signer: account.address,
taker: ZERO_ADDRESS,
tokenId: tokenId,
makerAmount: Number(order.makerAmount),
takerAmount: Number(order.takerAmount),
expiration: 0,
nonce: Number(order.nonce),
feeRateBps: 0,
side: params.side,
signatureType: 0,
signature,
},
signature,
};
}
```
USDC uses 6 decimals. Use `parseUnits(value, 6)` for amounts. Prices are in dollars; multiply `price * shares` before scaling.
## 4. Submitting Orders
Submit the signed order to `POST /orders` with `order`, `orderType`, `marketSlug`, and `ownerId`.
```typescript theme={null}
interface CreateOrderPayload {
order: Record;
orderType: 'GTC' | 'FOK';
marketSlug: string;
ownerId: number;
}
export async function submitOrder(payload: CreateOrderPayload) {
return apiPost<{ order: unknown; makerMatches?: unknown[] }>('/orders', payload);
}
// Example usage
const market = await getMarket('btc-100k-weekly');
const { order } = await createSignedOrder({
market,
side: 0, // BUY
outcome: 'yes',
priceCents: 65,
shares: 10,
orderType: 'GTC',
nonce: 0,
});
const result = await submitOrder({
order,
orderType: 'GTC',
marketSlug: market.slug,
ownerId: Number(process.env.OWNER_ID),
});
```
## 5. WebSocket Subscription for Real-Time Data
Connect to `wss://ws.limitless.exchange` with namespace `/markets` using `socket.io-client`. Emit `subscribe_market_prices` with `marketAddresses` and/or `marketSlugs`.
Subscriptions **replace** previous ones. To subscribe to both AMM (by address) and CLOB (by slug), send both `marketAddresses` and `marketSlugs` in a single `subscribe_market_prices` call.
```typescript theme={null}
import { io } from 'socket.io-client';
const WS_URL = 'wss://ws.limitless.exchange';
const NAMESPACE = '/markets';
const socket = io(`${WS_URL}${NAMESPACE}`, {
transports: ['websocket'],
extraHeaders: {
'X-API-Key': process.env.API_KEY ?? '',
},
});
socket.on('connect', () => {
socket.emit('subscribe_market_prices', {
marketAddresses: ['0x1234...'], // AMM markets
marketSlugs: ['btc-100k-weekly'], // CLOB markets
});
});
socket.on('newPriceData', (data: { marketAddress: string; updatedPrices: { yes: string; no: string } }) => {
console.log('AMM price update:', data.marketAddress, data.updatedPrices);
});
socket.on('orderbookUpdate', (data: { marketSlug: string; orderbook: unknown }) => {
console.log('CLOB orderbook update:', data.marketSlug);
});
socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});
```
| Event | Description |
| ----------------- | ----------------------- |
| `newPriceData` | AMM market price update |
| `orderbookUpdate` | CLOB orderbook update |
## Complete End-to-End Example
```typescript index.ts theme={null}
import { getMarket } from './market';
import { createSignedOrder } from './orders';
import { submitOrder } from './submit';
import { io } from 'socket.io-client';
const API_KEY = process.env.API_KEY!;
const WS_URL = 'wss://ws.limitless.exchange';
async function main() {
const marketSlug = process.argv[2] ?? 'btc-100k-weekly';
const market = await getMarket(marketSlug);
console.log('Market:', market.slug, 'Venue:', market.venue.exchange);
const { order } = await createSignedOrder({
market,
side: 0,
outcome: 'yes',
priceCents: 65,
shares: 5,
orderType: 'GTC',
nonce: 0,
});
const result = await submitOrder({
order,
orderType: 'GTC',
marketSlug: market.slug,
ownerId: Number(process.env.OWNER_ID),
});
console.log('Order submitted:', result);
const socket = io(`${WS_URL}/markets`, {
transports: ['websocket'],
extraHeaders: { 'X-API-Key': API_KEY },
});
socket.on('connect', () => {
socket.emit('subscribe_market_prices', { marketSlugs: [marketSlug] });
});
socket.on('orderbookUpdate', (data) => {
console.log('Orderbook:', data);
});
}
main().catch(console.error);
```
```typescript minimal.ts theme={null}
// Minimal: fetch market, sign order, submit
const market = await getMarket('btc-100k-weekly');
const { order } = await createSignedOrder({
market, side: 0, outcome: 'yes', priceCents: 65, shares: 5, orderType: 'GTC', nonce: 0,
});
await submitOrder({ order, orderType: 'GTC', marketSlug: market.slug, ownerId: Number(process.env.OWNER_ID) });
```
## Reference Summary
| Item | Value |
| ------------------- | -------------------------------- |
| API base URL | `https://api.limitless.exchange` |
| WebSocket URL | `wss://ws.limitless.exchange` |
| Namespace | `/markets` |
| Chain | Base (chainId: 8453) |
| EIP-712 domain name | `Limitless CTF Exchange` |
| EIP-712 version | `1` |
| USDC decimals | 6 |
| Side | 0 = BUY, 1 = SELL |
| signatureType | 0 = EOA |
| Order types | GTC, FOK |
## Next Steps
Deep dive into order structure and signing.
Token approvals and venue addresses.
Full event reference for real-time data.
Complete endpoint documentation.
# Python Quick Start
Source: https://docs.limitless.exchange/developers/quickstart/python
End-to-end Python implementation for trading on Limitless Exchange
## Overview
This guide walks you through a complete Python implementation for trading on Limitless Exchange: authentication, fetching market data, building and signing orders with EIP-712, and submitting them via the REST API.
## Prerequisites
Install the required packages:
```bash theme={null}
pip install eth-account requests web3
```
* **eth-account**: EIP-712 signing and key management
* **requests**: HTTP client for the REST API
* **web3**: Address checksumming and utilities
* **API Key**: Generate at [limitless.exchange](https://limitless.exchange) → profile menu → Api keys. Keys start with `lmts_`.
* **Private Key**: Your wallet's private key for EIP-712 order signing. Never share or commit it.
**Never share your private key.** Store it in environment variables or a secure secrets manager. Do not hardcode it in source code or commit it to version control.
## Authentication
All API requests require the `X-API-Key` header. Cookie-based session auth is deprecated.
Your **private key** is used only for **EIP-712 order signing**. The **API key** handles request authentication. Both are required for trading.
```python theme={null}
import os
import requests
API_BASE = "https://api.limitless.exchange"
API_KEY = os.environ["API_KEY"] # lmts_...
headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}
```
Cookie-based authentication is **deprecated** and will be removed. Use API keys for all programmatic access.
## Fetching Market Data
Use `GET /markets/:slug` to retrieve market details, including venue addresses and position IDs. Cache this data per market; it is static.
```python theme={null}
def fetch_market(slug: str) -> dict:
"""Fetch market data including venue and position IDs."""
resp = requests.get(f"{API_BASE}/markets/{slug}", headers=headers)
resp.raise_for_status()
return resp.json()
```
```python theme={null}
# Example response (CLOB market)
{
"slug": "btc-above-100k-march-2025",
"venue": {
"exchange": "0xA1b2C3d4E5f6...",
"adapter": "0xD4e5F6g7H8i9..."
},
"positionIds": ["19633...", "19633..."] # [0]=YES, [1]=NO
}
```
**positionIds\[0]** is the YES token, **positionIds\[1]** is the NO token. Use the appropriate one based on your order side and outcome.
## Building Order Payloads
Orders require specific fields. Key values:
| Field | Value | Description |
| --------------- | ----- | ------------------- |
| `side` | `0` | BUY |
| `side` | `1` | SELL |
| `signatureType` | `0` | EOA wallet |
| `orderType` | `GTC` | Good Till Cancelled |
| `orderType` | `FOK` | Fill or Kill |
USDC uses **6 decimals** (1 USDC = 1,000,000 units). Shares are scaled by 1e6.
You pay USDC, receive shares. Use `positionIds[0]` for YES, `positionIds[1]` for NO.
```
makerAmount = price_in_dollars * num_shares * 1e6 # USDC you pay
takerAmount = num_shares * 1e6 # Shares you receive
side = 0
```
Example: BUY 10 YES shares at \$0.65 → `makerAmount` = 6,500,000, `takerAmount` = 10,000,000
You pay shares, receive USDC. Use the token ID of the shares you are selling.
```
makerAmount = num_shares * 1e6 # Shares you pay
takerAmount = price_in_dollars * num_shares * 1e6 # USDC you receive
side = 1
```
Example: SELL 10 YES shares at \$0.65 → `makerAmount` = 10,000,000, `takerAmount` = 6,500,000
## EIP-712 Signing
Sign orders using EIP-712 with the venue's exchange address as `verifyingContract`.
See [EIP-712 Order Signing](/developers/eip712-signing) for the full type definition and field reference.
```python theme={null}
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import Web3
CHAIN_ID = 8453 # Base
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
def sign_order(order_data: dict, verifying_contract: str, private_key: str) -> str:
"""Sign order with EIP-712 using venue.exchange as verifyingContract."""
domain = {
"name": "Limitless CTF Exchange",
"version": "1",
"chainId": CHAIN_ID,
"verifyingContract": Web3.to_checksum_address(verifying_contract),
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"Order": [
{"name": "salt", "type": "uint256"},
{"name": "maker", "type": "address"},
{"name": "signer", "type": "address"},
{"name": "taker", "type": "address"},
{"name": "tokenId", "type": "uint256"},
{"name": "makerAmount", "type": "uint256"},
{"name": "takerAmount", "type": "uint256"},
{"name": "expiration", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "feeRateBps", "type": "uint256"},
{"name": "side", "type": "uint8"},
{"name": "signatureType", "type": "uint8"},
],
}
message = {
"salt": order_data["salt"],
"maker": Web3.to_checksum_address(order_data["maker"]),
"signer": Web3.to_checksum_address(order_data["signer"]),
"taker": Web3.to_checksum_address(order_data["taker"]),
"tokenId": int(order_data["tokenId"]),
"makerAmount": order_data["makerAmount"],
"takerAmount": order_data["takerAmount"],
"expiration": order_data["expiration"],
"nonce": order_data["nonce"],
"feeRateBps": order_data["feeRateBps"],
"side": order_data["side"],
"signatureType": order_data["signatureType"],
}
typed_data = {
"types": types,
"primaryType": "Order",
"domain": domain,
"message": message,
}
encoded = encode_typed_data(typed_data)
signed = Account.sign_message(encoded, private_key=private_key)
return signed.signature.hex()
```
All addresses must be **checksummed** (EIP-55). Use `Web3.to_checksum_address()`.
## Submitting Orders
Send the signed order to `POST /orders`:
```python theme={null}
def submit_order(order_payload: dict) -> dict:
"""Submit a signed order to the API."""
resp = requests.post(
f"{API_BASE}/orders",
headers=headers,
json=order_payload,
)
resp.raise_for_status()
return resp.json()
```
## Complete Working Example
```python theme={null}
"""
Limitless Exchange - Python Quick Start
Place a BUY order for YES shares on a CLOB market.
"""
import os
import time
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import Web3
API_BASE = "https://api.limitless.exchange"
CHAIN_ID = 8453
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
# Load from environment
API_KEY = os.environ["API_KEY"]
PRIVATE_KEY = os.environ["PRIVATE_KEY"]
OWNER_ID = int(os.environ.get("OWNER_ID", "0")) # Profile ID from API
headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}
def fetch_market(slug: str) -> dict:
resp = requests.get(f"{API_BASE}/markets/{slug}", headers=headers)
resp.raise_for_status()
return resp.json()
def sign_order(order_data: dict, verifying_contract: str, pk: str) -> str:
domain = {
"name": "Limitless CTF Exchange",
"version": "1",
"chainId": CHAIN_ID,
"verifyingContract": Web3.to_checksum_address(verifying_contract),
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"Order": [
{"name": "salt", "type": "uint256"},
{"name": "maker", "type": "address"},
{"name": "signer", "type": "address"},
{"name": "taker", "type": "address"},
{"name": "tokenId", "type": "uint256"},
{"name": "makerAmount", "type": "uint256"},
{"name": "takerAmount", "type": "uint256"},
{"name": "expiration", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "feeRateBps", "type": "uint256"},
{"name": "side", "type": "uint8"},
{"name": "signatureType", "type": "uint8"},
],
}
msg = {
"salt": order_data["salt"],
"maker": Web3.to_checksum_address(order_data["maker"]),
"signer": Web3.to_checksum_address(order_data["signer"]),
"taker": Web3.to_checksum_address(order_data["taker"]),
"tokenId": int(order_data["tokenId"]),
"makerAmount": order_data["makerAmount"],
"takerAmount": order_data["takerAmount"],
"expiration": order_data["expiration"],
"nonce": order_data["nonce"],
"feeRateBps": order_data["feeRateBps"],
"side": order_data["side"],
"signatureType": order_data["signatureType"],
}
encoded = encode_typed_data({
"types": types,
"primaryType": "Order",
"domain": domain,
"message": msg,
})
signed = Account.sign_message(encoded, private_key=pk)
return signed.signature.hex()
def place_buy_order(
market_slug: str,
num_shares: float,
price_per_share: float,
order_type: str = "GTC",
) -> dict:
market = fetch_market(market_slug)
venue = market["venue"]
position_ids = market["positionIds"]
account = Account.from_key(PRIVATE_KEY)
maker = account.address
# YES token is positionIds[0]
token_id = position_ids[0]
maker_amount = int(price_per_share * num_shares * 1e6)
taker_amount = int(num_shares * 1e6)
salt = int(time.time() * 1000)
order_data = {
"salt": salt,
"maker": Web3.to_checksum_address(maker),
"signer": Web3.to_checksum_address(maker),
"taker": ZERO_ADDRESS,
"tokenId": token_id,
"makerAmount": maker_amount,
"takerAmount": taker_amount,
"expiration": 0,
"nonce": 0,
"feeRateBps": 0,
"side": 0, # BUY
"signatureType": 0, # EOA
}
signature = sign_order(order_data, venue["exchange"], PRIVATE_KEY)
order_payload = {
"order": {**order_data, "signature": signature},
"ownerId": OWNER_ID,
"orderType": order_type,
"marketSlug": market_slug,
}
resp = requests.post(f"{API_BASE}/orders", headers=headers, json=order_payload)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
result = place_buy_order(
market_slug="your-market-slug",
num_shares=10.0,
price_per_share=0.65,
order_type="GTC",
)
print(result)
```
```bash theme={null}
# Run with environment variables
export API_KEY="lmts_your_key_here"
export PRIVATE_KEY="0x..."
export OWNER_ID="12345" # Your profile ID
python quickstart.py
```
**ownerId** is your profile ID. Obtain it from your first authenticated response (e.g. portfolio or auth endpoints) or from the Limitless UI. Set `OWNER_ID` in your environment.
## Troubleshooting
Ensure `X-API-Key` is set correctly and the key starts with `lmts_`. Check that the key is active in the Limitless UI.
* Verify `verifyingContract` is the market's `venue.exchange` address.
* Ensure all addresses are checksummed (EIP-55).
* Confirm `makerAmount` and `takerAmount` use 1e6 scaling for USDC and shares.
* Ensure you have enough USDC on Base for BUY orders.
* Approve USDC to `venue.exchange` for BUY; Conditional Tokens to `venue.exchange` (and `venue.adapter` for NegRisk SELL) for SELL.
Use `positionIds[0]` for YES and `positionIds[1]` for NO. Double-check you are trading the correct outcome.
## Next Steps
Full order type definition and field reference.
Understanding venue contracts and token approvals.
Complete endpoint documentation.
API key setup and best practices.
# WebSocket Integration
Source: https://docs.limitless.exchange/developers/quickstart/websocket
Real-time market data and position updates using Python WebSocket client
## Overview
The Limitless WebSocket API provides real-time market prices, orderbook updates, and position notifications. This guide covers connecting with Python using `python-socketio` with asyncio.
## Prerequisites
Install the required packages:
```bash theme={null}
pip install "python-socketio[asyncio]" eth-account==0.10.0 requests
```
Use a virtual environment to isolate dependencies. Run `python -m venv venv` then `source venv/bin/activate` (Unix) or `venv\Scripts\activate` (Windows) before installing.
## Authentication Modes
Connect without authentication to receive **public market data** only:
* `newPriceData` (AMM market prices)
* `orderbookUpdate` (CLOB orderbook)
* `system` (system notifications)
No API key required. Use this for read-only market monitoring.
Pass the `X-API-Key` header during connection for **authenticated features**:
* All public events
* `positions` (position balance updates)
* `authenticated` (auth confirmation)
Required for `subscribe_positions`. Generate keys at [limitless.exchange](https://limitless.exchange) → profile → Api keys.
**Never** commit API keys to version control or expose them in client-side code. Use environment variables (e.g. `API_KEY`) and load them at runtime.
Cookie-based session authentication is **deprecated**. Use API key authentication via the `X-API-Key` header.
## Connection Details
| Setting | Value |
| ------------- | ----------------------------- |
| **URL** | `wss://ws.limitless.exchange` |
| **Namespace** | `/markets` |
| **Transport** | WebSocket only (no polling) |
```python Public (no auth) theme={null}
import asyncio
import os
import socketio
sio = socketio.AsyncClient(logger=os.environ.get("DEBUG") == "1")
@sio.event(namespace="/markets")
async def connect():
print("Connected to /markets")
async def main():
await sio.connect(
"wss://ws.limitless.exchange",
transports=["websocket"],
namespaces=["/markets"],
)
await sio.wait()
asyncio.run(main())
```
```python Authenticated (API key) theme={null}
import asyncio
import os
import socketio
api_key = os.environ.get("API_KEY")
if not api_key:
raise ValueError("API_KEY environment variable required")
sio = socketio.AsyncClient(logger=os.environ.get("DEBUG") == "1")
@sio.event(namespace="/markets")
async def connect():
print("Connected to /markets")
async def main():
await sio.connect(
"wss://ws.limitless.exchange",
transports=["websocket"],
namespaces=["/markets"],
headers={"X-API-Key": api_key},
)
await sio.wait()
asyncio.run(main())
```
## Subscribing to Market Prices
Emit `subscribe_market_prices` with market identifiers. Subscriptions **replace** previous ones, so include all markets you want in a single call.
```python AMM markets (contract addresses) theme={null}
# Market addresses are Ethereum contract addresses in hex format
await sio.emit(
"subscribe_market_prices",
{"marketAddresses": ["0x1234567890abcdef1234567890abcdef12345678"]},
namespace="/markets",
)
```
```python CLOB markets (slugs) theme={null}
# Use human-readable slugs for CLOB orderbook markets
await sio.emit(
"subscribe_market_prices",
{"marketSlugs": ["btc-100k-weekly", "eth-daily"]},
namespace="/markets",
)
```
```python Both AMM and CLOB theme={null}
# Include both to avoid overwriting subscriptions
await sio.emit(
"subscribe_market_prices",
{
"marketAddresses": ["0x1234567890abcdef1234567890abcdef12345678"],
"marketSlugs": ["btc-100k-weekly"],
},
namespace="/markets",
)
```
Subscriptions **replace** previous ones. If you want both AMM prices and CLOB orderbook, send both `marketAddresses` and `marketSlugs` together in a single `subscribe_market_prices` call.
## Subscribing to Positions
Position updates require authentication. Emit `subscribe_positions` after connecting:
```python theme={null}
# Requires X-API-Key header during connect
await sio.emit("subscribe_positions", {}, namespace="/markets")
```
## Handling Events
Register handlers for each event type. All handlers use `namespace="/markets"`.
| Event | Auth Required | Description |
| ----------------- | ------------- | --------------------------- |
| `newPriceData` | No | AMM market price update |
| `orderbookUpdate` | No | CLOB orderbook update |
| `positions` | Yes | Position balance update |
| `system` | No | System notifications |
| `authenticated` | Yes | Authentication confirmation |
| `exception` | No | Error notifications |
### Event Payloads
**`newPriceData`** — AMM market price update:
```json theme={null}
{
"marketAddress": "0x1234...",
"updatedPrices": { "yes": "0.65", "no": "0.35" },
"blockNumber": 12345678,
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
**`positions`** — Position balance update (auth required):
```json theme={null}
{
"account": "0xabcd...",
"marketAddress": "0x1234...",
"positions": [
{ "tokenId": "123456", "balance": "1000000", "outcomeIndex": 0 }
],
"type": "AMM"
}
```
**`system`** — System notification:
```json theme={null}
{
"message": "Notification text",
"markets": ["0x1234..."]
}
```
## Complete Async Python Client
```python limitless_ws_client.py theme={null}
import asyncio
import logging
import os
from typing import Optional
import socketio
logging.basicConfig(
level=logging.DEBUG if os.environ.get("DEBUG") == "1" else logging.INFO
)
logger = logging.getLogger(__name__)
WS_URL = "wss://ws.limitless.exchange"
NAMESPACE = "/markets"
class LimitlessWebSocketClient:
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or os.environ.get("API_KEY")
self.sio = socketio.AsyncClient(
reconnection=True,
reconnection_attempts=0,
logger=os.environ.get("DEBUG") == "1",
)
self._market_addresses: list[str] = []
self._market_slugs: list[str] = []
self._subscribe_positions = False
self._setup_handlers()
def _setup_handlers(self) -> None:
@self.sio.event(namespace=NAMESPACE)
async def connect():
logger.info("Connected to %s", NAMESPACE)
await self._resubscribe()
@self.sio.event(namespace=NAMESPACE)
async def disconnect():
logger.info("Disconnected from %s", NAMESPACE)
@self.sio.event(namespace=NAMESPACE)
async def newPriceData(data: dict):
logger.info("Price update: %s", data)
@self.sio.event(namespace=NAMESPACE)
async def orderbookUpdate(data: dict):
logger.info("Orderbook update: %s", data)
@self.sio.event(namespace=NAMESPACE)
async def positions(data: dict):
logger.info("Positions: %s", data)
@self.sio.event(namespace=NAMESPACE)
async def system(data: dict):
logger.info("System: %s", data)
@self.sio.event(namespace=NAMESPACE)
async def authenticated(data: dict):
logger.info("Authenticated: %s", data)
@self.sio.event(namespace=NAMESPACE)
async def exception(data: dict):
logger.error("Exception: %s", data)
async def _resubscribe(self) -> None:
if self._market_addresses or self._market_slugs:
payload = {}
if self._market_addresses:
payload["marketAddresses"] = self._market_addresses
if self._market_slugs:
payload["marketSlugs"] = self._market_slugs
await self.sio.emit(
"subscribe_market_prices",
payload,
namespace=NAMESPACE,
)
if self._subscribe_positions:
await self.sio.emit("subscribe_positions", {}, namespace=NAMESPACE)
async def subscribe_market_prices(
self,
market_addresses: Optional[list[str]] = None,
market_slugs: Optional[list[str]] = None,
) -> None:
self._market_addresses = market_addresses or []
self._market_slugs = market_slugs or []
await self._resubscribe()
async def subscribe_positions(self) -> None:
if not self.api_key:
raise ValueError("API key required for position subscription")
self._subscribe_positions = True
await self._resubscribe()
async def connect(self) -> None:
kwargs = {
"transports": ["websocket"],
"namespaces": [NAMESPACE],
}
if self.api_key:
kwargs["headers"] = {"X-API-Key": self.api_key}
await self.sio.connect(WS_URL, **kwargs)
async def disconnect(self) -> None:
await self.sio.disconnect()
async def run(self) -> None:
await self.connect()
await self.sio.wait()
```
## Usage Examples
```python Public market data only theme={null}
import asyncio
from limitless_ws_client import LimitlessWebSocketClient
async def main():
client = LimitlessWebSocketClient()
await client.connect()
await client.subscribe_market_prices(
market_addresses=["0x1234567890abcdef1234567890abcdef12345678"],
market_slugs=["btc-100k-weekly"],
)
await client.sio.wait()
asyncio.run(main())
```
```python Authenticated with positions theme={null}
import asyncio
import os
from limitless_ws_client import LimitlessWebSocketClient
async def main():
api_key = os.environ["API_KEY"]
client = LimitlessWebSocketClient(api_key=api_key)
await client.connect()
await client.subscribe_market_prices(
market_slugs=["btc-100k-weekly"],
)
await client.subscribe_positions()
await client.sio.wait()
asyncio.run(main())
```
## Environment Variables
| Variable | Description |
| --------- | --------------------------------------------------------------------- |
| `API_KEY` | API key for authenticated features (positions). Omit for public-only. |
| `DEBUG` | Set to `1` to enable verbose socket.io logging. |
## Auto-Reconnection
The client uses `reconnection=True` by default. On reconnect, call `_resubscribe()` in the `connect` handler to restore market and position subscriptions, since the server does not persist them across disconnects.
Store your API key securely. Rotate keys if exposure is suspected. Do not log or print API keys.
# API Tokens
Source: https://docs.limitless.exchange/developers/sdk/go/api-tokens
Manage scoped API tokens with the Go SDK
The `ApiTokenService` handles the partner self-service token lifecycle: checking capabilities, deriving tokens, listing active tokens, and revoking them.
Token derivation and capability queries require a **Privy identity token**. The SDK does not obtain this token for you — your application must authenticate the partner via Privy and pass the resulting token.
## Access
The service is available on the root `Client`:
```go theme={null}
import "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
client := limitless.NewClient()
// Use client.ApiTokens.*
```
## Get partner capabilities
Check whether token management is enabled and which scopes are allowed.
```go theme={null}
capabilities, err := client.ApiTokens.GetCapabilities(ctx, identityToken)
if err != nil {
log.Fatal(err)
}
fmt.Println(capabilities.TokenManagementEnabled)
fmt.Println(capabilities.AllowedScopes)
```
## Derive a token
Create a new scoped API token. The `Secret` is returned once — store it securely.
```go theme={null}
derived, err := client.ApiTokens.DeriveToken(ctx, identityToken, limitless.DeriveApiTokenInput{
Label: "production-bot",
Scopes: []string{limitless.ScopeTrading, limitless.ScopeAccountCreation, limitless.ScopeDelegatedSigning},
})
if err != nil {
log.Fatal(err)
}
// derived.TokenID — used as lmts-api-key header
// derived.Secret — base64-encoded HMAC secret (one-time)
// derived.Scopes — granted scopes
// derived.Profile — { ID, Account }
```
### Creating an HMAC-authenticated client
After deriving a token, create a new `Client` with the HMAC credentials:
```go theme={null}
scopedClient := limitless.NewClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: derived.TokenID,
Secret: derived.Secret,
}),
)
```
If `Scopes` is omitted, the token defaults to `["trading"]`. Requested scopes must be a subset of the partner's `AllowedScopes`.
## List active tokens
Returns all non-revoked tokens for the authenticated partner.
```go theme={null}
tokens, err := scopedClient.ApiTokens.ListTokens(ctx)
if err != nil {
log.Fatal(err)
}
for _, token := range tokens {
fmt.Println(token.TokenID, token.Label, token.Scopes, token.LastUsedAt)
}
```
## Revoke a token
Immediately invalidates a token. This cannot be undone.
```go theme={null}
message, err := scopedClient.ApiTokens.RevokeToken(ctx, derived.TokenID)
```
## Scope constants
The SDK exports typed scope constants:
| Constant | Value |
| ----------------------- | --------------------- |
| `ScopeTrading` | `"trading"` |
| `ScopeAccountCreation` | `"account_creation"` |
| `ScopeDelegatedSigning` | `"delegated_signing"` |
# Delegated Orders
Source: https://docs.limitless.exchange/developers/sdk/go/delegated-orders
Place orders on behalf of sub-accounts with the Go SDK
The `DelegatedOrderService` enables partners with the `delegated_signing` scope to place and cancel orders on behalf of their sub-accounts. The server signs orders using the sub-account's managed Privy wallet — no private key management is needed on the partner side.
Delegated signing requires a sub-account created with `CreateServerWallet: true`. EOA sub-accounts sign their own orders.
**Recommended setup:** Store your HMAC credentials (`TokenID` / `Secret`) on your backend. Use this SDK server-side to sign partner-authenticated requests. Expose only your own app-specific endpoints to the frontend. Never expose HMAC secrets in browser bundles or client-side storage.
## Access
```go theme={null}
import "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
client := limitless.NewClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: tokenID,
Secret: secret,
}),
)
// Use client.DelegatedOrders.*
```
## Create a GTC delegated order
Builds an unsigned GTC (Good-Til-Cancelled) limit order locally and submits it to `POST /orders` with the `OnBehalfOf` profile ID. The server signs the order via the sub-account's managed wallet. GTC orders remain on the orderbook until filled or explicitly cancelled.
```go theme={null}
response, err := client.DelegatedOrders.CreateOrder(ctx, limitless.CreateDelegatedOrderParams{
MarketSlug: "btc-100k",
OrderType: limitless.OrderTypeGTC,
OnBehalfOf: partnerAccount.ProfileID,
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.55,
Size: 10,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Order.ID)
```
Use `PostOnly: true` to ensure the order only rests on the book and is never filled immediately as a taker:
```go theme={null}
response, err := client.DelegatedOrders.CreateOrder(ctx, limitless.CreateDelegatedOrderParams{
MarketSlug: "btc-100k",
OrderType: limitless.OrderTypeGTC,
OnBehalfOf: partnerAccount.ProfileID,
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.55,
Size: 10,
PostOnly: true,
},
})
```
## Create a FAK delegated order
FAK (Fill-And-Kill) orders use the same `Price` and `Size` inputs as GTC, but they only consume immediately available liquidity and cancel any unmatched remainder.
`PostOnly` is not supported for FAK orders.
```go theme={null}
response, err := client.DelegatedOrders.CreateOrder(ctx, limitless.CreateDelegatedOrderParams{
MarketSlug: "btc-100k",
OrderType: limitless.OrderTypeFAK,
OnBehalfOf: partnerAccount.ProfileID,
Args: limitless.FAKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.45,
Size: 10.0,
},
})
if err != nil {
log.Fatal(err)
}
if len(response.MakerMatches) > 0 {
fmt.Printf("FAK order matched immediately with %d fill(s)\n", len(response.MakerMatches))
} else {
fmt.Println("FAK remainder was cancelled.")
}
```
## Create a FOK delegated order
FOK (Fill-Or-Kill) orders execute immediately at the best available price or are cancelled entirely — there are no partial fills. Instead of `Price` and `Size`, FOK orders use `MakerAmount`:
* **BUY**: `MakerAmount` is the USDC amount to spend (e.g., `50.0` = spend \$50 USDC)
* **SELL**: `MakerAmount` is the number of shares to sell (e.g., `18.64` = sell 18.64 shares)
```go theme={null}
// BUY FOK — spend 1 USDC at market price
response, err := client.DelegatedOrders.CreateOrder(ctx, limitless.CreateDelegatedOrderParams{
MarketSlug: "btc-100k",
OrderType: limitless.OrderTypeFOK,
OnBehalfOf: partnerAccount.ProfileID,
FeeRateBps: 300,
Args: limitless.FOKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
MakerAmount: 1.0,
},
})
if err != nil {
log.Fatal(err)
}
if len(response.MakerMatches) > 0 {
fmt.Printf("FOK order matched with %d fill(s)\n", len(response.MakerMatches))
} else {
fmt.Println("FOK order was not matched — cancelled automatically")
}
// SELL FOK — sell 10 shares at market price
response, err = client.DelegatedOrders.CreateOrder(ctx, limitless.CreateDelegatedOrderParams{
MarketSlug: "btc-100k",
OrderType: limitless.OrderTypeFOK,
OnBehalfOf: partnerAccount.ProfileID,
Args: limitless.FOKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideSell,
MakerAmount: 10.0,
},
})
```
## Parameters
### GTC order args (`GTCOrderArgs`)
| Field | Type | Description |
| --------------- | ----------- | ---------------------------------------------------------------------------------- |
| `MarketSlug` | `string` | Market identifier |
| `OrderType` | `OrderType` | `limitless.OrderTypeGTC` |
| `OnBehalfOf` | `int` | Profile ID of the sub-account |
| `Args.TokenID` | `string` | Position token ID (YES or NO) from market data |
| `Args.Side` | `Side` | `limitless.SideBuy` or `limitless.SideSell` |
| `Args.Price` | `float64` | Price between 0 and 1 |
| `Args.Size` | `float64` | Number of contracts |
| `Args.PostOnly` | `bool` | Optional. `true` rejects the order if it would immediately match. Default `false`. |
| `FeeRateBps` | `int` | Fee rate in basis points (defaults to 300 if omitted) |
### FAK order args (`FAKOrderArgs`)
| Field | Type | Description |
| -------------- | ----------- | ----------------------------------------------------- |
| `MarketSlug` | `string` | Market identifier |
| `OrderType` | `OrderType` | `limitless.OrderTypeFAK` |
| `OnBehalfOf` | `int` | Profile ID of the sub-account |
| `Args.TokenID` | `string` | Position token ID (YES or NO) from market data |
| `Args.Side` | `Side` | `limitless.SideBuy` or `limitless.SideSell` |
| `Args.Price` | `float64` | Price between 0 and 1 |
| `Args.Size` | `float64` | Number of contracts |
| `FeeRateBps` | `int` | Fee rate in basis points (defaults to 300 if omitted) |
### FOK order args (`FOKOrderArgs`)
| Field | Type | Description |
| ------------------ | ----------- | --------------------------------------------------------- |
| `MarketSlug` | `string` | Market identifier |
| `OrderType` | `OrderType` | `limitless.OrderTypeFOK` |
| `OnBehalfOf` | `int` | Profile ID of the sub-account |
| `Args.TokenID` | `string` | Position token ID (YES or NO) from market data |
| `Args.Side` | `Side` | `limitless.SideBuy` or `limitless.SideSell` |
| `Args.MakerAmount` | `float64` | BUY: USDC to spend. SELL: shares to sell. Max 6 decimals. |
| `FeeRateBps` | `int` | Fee rate in basis points (defaults to 300 if omitted) |
## Cancel an order
```go theme={null}
message, err := client.DelegatedOrders.Cancel(ctx, orderID)
```
### Cancel on behalf of a sub-account
```go theme={null}
message, err := client.DelegatedOrders.CancelOnBehalfOf(ctx, orderID, partnerAccount.ProfileID)
```
## Cancel all orders in a market
```go theme={null}
message, err := client.DelegatedOrders.CancelAll(ctx, marketSlug)
```
### Cancel all on behalf of a sub-account
```go theme={null}
message, err := client.DelegatedOrders.CancelAllOnBehalfOf(ctx, marketSlug, partnerAccount.ProfileID)
```
## How it works
1. The SDK builds an unsigned order locally with a zero verifying address and default fee rate (300 bps)
2. For GTC and FAK orders, `Price` and `Size` are used; for FOK orders, `MakerAmount` is used (with `TakerAmount` always set to 1)
3. The order is posted to `POST /orders` with `OnBehalfOf` and `OwnerID` set to the sub-account's profile ID
4. The server detects the `delegated_signing` scope and missing signature
5. The server looks up the sub-account's server wallet, builds EIP-712 typed data, and signs via Privy
6. The signed order is submitted to the CLOB engine — GTC orders can rest on the book, FAK orders cancel unmatched remainder, and FOK orders either fully fill or cancel
# Error Handling & Retry
Source: https://docs.limitless.exchange/developers/sdk/go/error-handling
Retry logic and error handling with the Go SDK
## Overview
The SDK provides a typed error hierarchy, a generic retry function with exponential backoff, a `RetryableClient` wrapper, and pluggable logging — all following idiomatic Go patterns.
## Error Types
All API errors implement the `error` interface and can be inspected using `errors.As()`:
```go theme={null}
import "errors"
_, err := marketFetcher.GetMarket(ctx, "invalid-slug")
if err != nil {
var apiErr *limitless.APIError
if errors.As(err, &apiErr) {
fmt.Printf("Status: %d\n", apiErr.Status)
fmt.Printf("Message: %s\n", apiErr.Message)
fmt.Printf("URL: %s %s\n", apiErr.Method, apiErr.URL)
}
}
```
### APIError
The base error type for all HTTP errors. Its `Error()` method returns a formatted string like `"API error 401 GET /path: message"`.
| Field | Type | Description |
| --------- | ----------------- | -------------------------------------------------- |
| `Status` | `int` | HTTP status code (e.g. `400`, `401`, `429`, `500`) |
| `Message` | `string` | Human-readable error message from the API |
| `Data` | `json.RawMessage` | Raw response body |
| `URL` | `string` | Request URL path |
| `Method` | `string` | HTTP method |
### Specialized Error Types
The SDK provides three specialized error types that embed `APIError`:
| Type | HTTP Status | Description |
| ---------------------- | ------------ | ----------------------------------------------------- |
| `*ValidationError` | `400` | Bad request — invalid parameters or malformed payload |
| `*AuthenticationError` | `401`, `403` | Missing/invalid API key or insufficient permissions |
| `*RateLimitError` | `429` | Rate limit exceeded |
```go theme={null}
var rateLimitErr *limitless.RateLimitError
if errors.As(err, &rateLimitErr) {
fmt.Println("Rate limited! Back off and retry.")
}
var authErr *limitless.AuthenticationError
if errors.As(err, &authErr) {
fmt.Println("Check your API key.")
}
var validErr *limitless.ValidationError
if errors.As(err, &validErr) {
fmt.Println("Invalid request:", validErr.Message)
}
```
### OrderValidationError
Client-side validation errors thrown before a request is made:
```go theme={null}
var orderErr *limitless.OrderValidationError
if errors.As(err, &orderErr) {
fmt.Printf("Field %s: %s\n", orderErr.Field, orderErr.Message)
}
```
| Field | Type | Description |
| --------- | -------- | -------------------------------------------------------------- |
| `Field` | `string` | The field that failed validation (e.g. `"price"`, `"tokenId"`) |
| `Message` | `string` | Description of the validation failure |
### Common Status Codes
| Code | Meaning | Typical Cause |
| ----- | --------------------- | -------------------------------------------------- |
| `400` | Bad Request | Invalid order parameters, malformed payload |
| `401` | Unauthorized | Missing or invalid API key |
| `403` | Forbidden | Insufficient permissions or geographic restriction |
| `404` | Not Found | Invalid market slug or order ID |
| `429` | Too Many Requests | Rate limit exceeded |
| `500` | Internal Server Error | Transient server-side failure |
### Helper Method
`APIError` provides an `IsAuthError()` method to quickly check for authentication issues:
```go theme={null}
if apiErr.IsAuthError() {
fmt.Println("Authentication problem — check your API key")
}
```
## WithRetry (Generic)
The SDK provides a generic `WithRetry` function that wraps any operation with configurable retry logic using Go generics:
```go theme={null}
market, err := limitless.WithRetry(ctx, func() (*limitless.Market, error) {
return marketFetcher.GetMarket(ctx, "btc-above-100k-march-2025")
}, limitless.RetryConfig{
StatusCodes: []int{429, 500, 502, 503, 504},
MaxRetries: 3,
ExponentialBase: 2.0,
MaxDelay: 60 * time.Second,
OnRetry: func(attempt int, err error, delay time.Duration) {
fmt.Printf("Retry %d after %s: %v\n", attempt, delay, err)
},
})
```
### RetryConfig
| Field | Type | Default | Description |
| ----------------- | --------------------------------- | --------------------------- | -------------------------------------------------------- |
| `StatusCodes` | `[]int` | `[429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry |
| `MaxRetries` | `int` | `3` | Maximum number of retry attempts |
| `Delays` | `[]time.Duration` | `nil` | Fixed delays per attempt (overrides exponential backoff) |
| `ExponentialBase` | `float64` | `2.0` | Base for exponential backoff calculation |
| `MaxDelay` | `time.Duration` | `60s` | Maximum delay between retries |
| `OnRetry` | `func(int, error, time.Duration)` | `nil` | Callback invoked on each retry |
`WithRetry` only retries when the error is an `*APIError` whose `Status` is in the `StatusCodes` list. All other errors propagate immediately.
### Fixed Delays
Override exponential backoff with explicit per-attempt delays:
```go theme={null}
config := limitless.RetryConfig{
StatusCodes: []int{429, 500},
MaxRetries: 3,
Delays: []time.Duration{1 * time.Second, 2 * time.Second, 4 * time.Second},
}
```
## RetryableClient
For broader retry coverage, wrap your `HttpClient` with `RetryableClient`. This applies retry logic to **all** API calls made through the client:
```go theme={null}
client := limitless.NewHttpClient()
retryable := limitless.NewRetryableClient(client, limitless.RetryConfig{
StatusCodes: []int{429, 500, 502, 503, 504},
MaxRetries: 3,
ExponentialBase: 2.0,
})
// Use retryable in place of client
marketFetcher := limitless.NewMarketFetcher(retryable.Client)
```
The `RetryableClient` has the same methods as `HttpClient` (`Get`, `GetRaw`, `Post`, `Delete`) and transparently retries on configured status codes.
Use `RetryableClient` for automated trading bots that need resilience against transient failures without wrapping every call in `WithRetry` individually.
## Debugging with ConsoleLogger
Enable verbose logging to trace requests, responses, and internal operations:
```go theme={null}
logger := limitless.NewConsoleLogger(limitless.LogLevelDebug)
client := limitless.NewHttpClient(limitless.WithLogger(logger))
```
At `LogLevelDebug`, the logger outputs:
* Request and response headers
* Venue cache hits and misses
* Full API response bodies
* Retry attempts and delays
* WebSocket connection state changes
```
[DEBUG] GET /markets/btc-above-100k-march-2025
[DEBUG] Headers: {X-API-Key: lmts_***, Content-Type: application/json}
[DEBUG] Response 200 in 142ms
[DEBUG] Venue cached for btc-above-100k-march-2025: exchange=0xA1b2... adapter=0xD4e5...
[DEBUG] POST /orders
[DEBUG] Response 429 in 38ms
[DEBUG] Retry 1/3 after 1s — status 429
[DEBUG] POST /orders
[DEBUG] Response 200 in 156ms
```
### Custom Logger
Implement the `Logger` interface to plug in your own logging backend:
```go theme={null}
type Logger interface {
Debug(msg string, meta ...map[string]any)
Info(msg string, meta ...map[string]any)
Warn(msg string, meta ...map[string]any)
Error(msg string, err error, meta ...map[string]any)
}
```
The `Error` method has an additional `err error` parameter compared to the other methods, allowing structured error logging.
## Best Practices
Every SDK method returns `error` as the last return value. Always check it:
```go theme={null}
result, err := orderClient.CreateOrder(ctx, params)
if err != nil {
var apiErr *limitless.APIError
if errors.As(err, &apiErr) {
log.Printf("API error %d: %s", apiErr.Status, apiErr.Message)
}
return err
}
```
Use `errors.As()` to match specific error types and handle them differently:
```go theme={null}
var rateLimitErr *limitless.RateLimitError
if errors.As(err, &rateLimitErr) {
// Back off and retry
}
var authErr *limitless.AuthenticationError
if errors.As(err, &authErr) {
// Check API key configuration
}
```
Limit retries to status codes like `429` (rate limit) and `5xx` (server errors). Do not retry `400` (bad request) or `401` (auth failure) as these require corrective action.
The default `RetryConfig` uses exponential backoff with a base of 2.0. Avoid fixed short delays that could overwhelm the API during outages.
Always pass a `context.Context` to allow timeouts and cancellation:
```go theme={null}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
market, err := marketFetcher.GetMarket(ctx, slug)
```
Enable `LogLevelDebug` to see the full request/response cycle. Switch to `LogLevelInfo` or `LogLevelWarn` in production to reduce noise.
# Go SDK
Source: https://docs.limitless.exchange/developers/sdk/go/getting-started
Official Go SDK for the Limitless Exchange API
## Overview
The Limitless Exchange Go SDK (`limitless-exchange-go-sdk`) is a fully typed Go client for interacting with both **CLOB** and **NegRisk** prediction markets. It provides:
* Strongly typed structs for all API requests and responses
* Automatic venue caching for EIP-712 order signing
* Built-in retry logic with exponential backoff (using Go generics)
* WebSocket streaming with auto-reconnect
* Functional options pattern for flexible configuration
The SDK requires **Go 1.22+**. All I/O methods accept a `context.Context` as the first parameter and return `error` as the last return value, following standard Go conventions.
## Installation
```bash theme={null}
go get github.com/limitless-labs-group/limitless-exchange-go-sdk@latest
```
## Quick Start
```go theme={null}
package main
import (
"context"
"fmt"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient() // loads LIMITLESS_API_KEY from env
}
```
```go theme={null}
func main() {
client := limitless.NewHttpClient()
marketFetcher := limitless.NewMarketFetcher(client)
ctx := context.Background()
result, err := marketFetcher.GetActiveMarkets(ctx, &limitless.ActiveMarketsParams{
Limit: 10,
Page: 1,
})
if err != nil {
panic(err)
}
for _, m := range result.Data {
fmt.Println(m.Title, m.Slug)
}
}
```
```go theme={null}
func main() {
client := limitless.NewHttpClient()
portfolio := limitless.NewPortfolioFetcher(client)
ctx := context.Background()
positions, err := portfolio.GetPositions(ctx)
if err != nil {
panic(err)
}
for _, pos := range positions.CLOB {
fmt.Println(pos.Market.Title)
}
}
```
## Authentication
The SDK supports both authentication modes:
* Legacy API key (`X-API-Key`)
* Scoped token HMAC (`lmts-api-key`, `lmts-timestamp`, `lmts-signature`) for partner/programmatic integrations
Set the `LIMITLESS_API_KEY` environment variable. The `HttpClient` loads it automatically:
```bash theme={null}
export LIMITLESS_API_KEY="lmts_your_key_here"
```
```go theme={null}
client := limitless.NewHttpClient() // auto-loads LIMITLESS_API_KEY
```
Use the root client with scoped token credentials:
```go theme={null}
client := limitless.NewClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: "your-token-id",
Secret: "your-base64-secret",
}),
)
```
Pass `WithAPIKey` directly to the constructor:
```go theme={null}
client := limitless.NewHttpClient(
limitless.WithAPIKey("lmts_your_key_here"),
)
```
With `WithHMACCredentials(...)` set, the SDK automatically generates and sends `lmts-api-key`, `lmts-timestamp`, and `lmts-signature`. Do not manually build HMAC headers when using the SDK client.
Never hardcode API keys in source code or commit them to version control. Use environment variables or a secrets manager.
## Root Client
The recommended entrypoint is the root `Client`, which composes all domain services (markets, portfolio, orders, API tokens, partner accounts, delegated orders):
```go theme={null}
client := limitless.NewClient(
limitless.WithAPIKey("lmts_your_key_here"),
)
// client.Markets, client.Portfolio, client.Pages,
// client.ApiTokens, client.PartnerAccounts, client.DelegatedOrders
```
For partner integrations using HMAC authentication:
```go theme={null}
client := limitless.NewClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: "your-token-id",
Secret: "your-base64-secret",
}),
)
```
## HttpClient Options
The `HttpClient` uses the functional options pattern. Pass any combination of options to `NewHttpClient()` or `NewClient()`:
| Option | Type | Default | Description |
| ---------------------------- | ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------- |
| `WithBaseURL(url)` | `string` | `https://api.limitless.exchange` | API base URL |
| `WithAPIKey(key)` | `string` | Reads `LIMITLESS_API_KEY` env | Legacy API key authentication |
| `WithHMACCredentials(creds)` | `HMACCredentials` | -- | HMAC credentials for scoped API token auth (see [Programmatic API](/developers/programmatic-api)) |
| `WithTimeout(d)` | `time.Duration` | `30s` | HTTP request timeout |
| `WithMaxIdleConns(n)` | `int` | `50` | Maximum idle connections in the pool |
| `WithIdleConnTimeout(d)` | `time.Duration` | `60s` | How long idle connections remain in the pool |
| `WithTransport(t)` | `http.RoundTripper` | Default transport | Custom HTTP transport |
| `WithAdditionalHeaders(h)` | `map[string]string` | `nil` | Extra headers merged into every request |
| `WithLogger(l)` | `Logger` | `NoOpLogger` | Logger for request/response tracing |
```go theme={null}
client := limitless.NewHttpClient(
limitless.WithAPIKey("lmts_your_key_here"),
limitless.WithTimeout(15 * time.Second),
limitless.WithLogger(limitless.NewConsoleLogger(limitless.LogLevelDebug)),
)
```
## Logging
The SDK provides a pluggable `Logger` interface with a built-in `ConsoleLogger`:
```go theme={null}
logger := limitless.NewConsoleLogger(limitless.LogLevelDebug)
client := limitless.NewHttpClient(limitless.WithLogger(logger))
```
| Level | Description |
| --------------- | -------------------------------------------------------------------------- |
| `LogLevelDebug` | Verbose output including headers, venue cache operations, and raw payloads |
| `LogLevelInfo` | General operational messages |
| `LogLevelWarn` | Warnings about potential issues |
| `LogLevelError` | Errors only |
Set `LogLevelDebug` during development to see request headers, venue cache hits/misses, and full API responses. Switch to `LogLevelInfo` or `LogLevelWarn` in production.
## Source Code
The SDK is open source. Contributions and issue reports are welcome.
Browse the source, report issues, and contribute at github.com/limitless-labs-group/limitless-exchange-go-sdk
## Disclaimer
**USE AT YOUR OWN RISK.** This SDK is provided as-is with no guarantees. You are solely responsible for any trading activity conducted through this software. Limitless Exchange is not available to users in the United States or other restricted jurisdictions. By using this SDK, you confirm compliance with all applicable local laws and regulations.
## Next Steps
Discover markets, fetch orderbooks, and understand venue caching.
Browse markets by category with navigation, filters, and pagination.
Place GTC and FOK orders, cancel orders, and manage token approvals.
Track your open positions and trading history.
Derive, list, and revoke scoped HMAC tokens for partner integrations.
Create sub-accounts with server wallets or EOA verification.
Place orders on behalf of sub-accounts with server-side signing.
Subscribe to real-time orderbook and price updates.
Retry logic, error types, and debugging strategies.
## Server Wallet Redemption and Withdrawal
For partner server-wallet sub-accounts, the SDK provides helper methods for payout settlement and treasury movement:
* [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) — claim resolved positions
* [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) — transfer ERC20 funds from managed sub-accounts
* [`POST /portfolio/withdrawal-addresses`](/api-reference/portfolio/add-withdrawal-address) — allowlist an explicit treasury destination with Privy identity auth
* [`DELETE /portfolio/withdrawal-addresses/:address`](/api-reference/portfolio/delete-withdrawal-address) — remove an allowlisted destination with Privy identity auth
Required scopes for `apiToken` auth: `trading` for redeem and `withdrawal` for withdraw. Allowlist add/delete calls use a Privy identity token instead of API-token/HMAC auth.
```go theme={null}
identityToken := os.Getenv("PRIVY_IDENTITY_TOKEN")
treasuryAddress := "0x0F3262730c909408042F9Da345a916dc0e1F9787"
_, err := client.PartnerAccounts.AddWithdrawalAddress(ctx, identityToken, limitless.PartnerWithdrawalAddressInput{
Address: treasuryAddress,
Label: "treasury",
})
if err != nil {
log.Fatal(err)
}
withdraw, err := client.ServerWallets.Withdraw(ctx, limitless.WithdrawServerWalletParams{
Amount: "1000000",
OnBehalfOf: childProfileID,
Destination: treasuryAddress,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(withdraw.Destination)
```
Set `OnBehalfOf` when withdrawing from a child server-wallet profile. `Destination` is optional for child withdrawals; when omitted, the API defaults to the authenticated partner smart wallet when present, otherwise the authenticated partner account. Leave `OnBehalfOf` as zero only when withdrawing the authenticated caller's own server wallet to an explicit `Destination`.
See the full flow and scope guidance in [Programmatic API](/developers/programmatic-api).
# Market Pages
Source: https://docs.limitless.exchange/developers/sdk/go/market-pages
Navigate and filter markets with the Go SDK
## Overview
The `MarketPageFetcher` provides access to the Market Navigation API — a hierarchical system for browsing markets by category, applying dynamic filters, and paginating results. All endpoints are public and require no authentication.
## Setup
```go theme={null}
import limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
client := limitless.NewHttpClient()
pageFetcher := limitless.NewMarketPageFetcher(client)
```
## Navigation Tree
Fetch the full navigation hierarchy. Each node represents a browseable category with a URL path.
```go theme={null}
ctx := context.Background()
navigation, err := pageFetcher.GetNavigation(ctx)
if err != nil {
log.Fatal(err)
}
for _, node := range navigation {
fmt.Printf("%s → %s\n", node.Name, node.Path)
for _, child := range node.Children {
fmt.Printf(" %s → %s\n", child.Name, child.Path)
}
}
```
### NavigationNode Fields
| Field | Type | Description |
| ---------- | ------------------ | ------------------------------ |
| `ID` | `string` | Unique identifier |
| `Name` | `string` | Display name |
| `Slug` | `string` | URL-friendly identifier |
| `Path` | `string` | Full URL path (e.g. `/crypto`) |
| `Icon` | `string` | Optional icon name |
| `Children` | `[]NavigationNode` | Nested child nodes |
## Resolving a Page by Path
Convert a URL path into a `MarketPage` with its filters, metadata, and breadcrumb. The SDK handles 301 redirects internally (up to 3 levels).
```go theme={null}
page, err := pageFetcher.GetMarketPageByPath(ctx, "/crypto")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Page: %s\n", page.Name)
fmt.Printf("Filters: %d groups\n", len(page.FilterGroups))
```
### MarketPage Fields
| Field | Type | Description |
| -------------- | ------------------ | ----------------------------------------- |
| `ID` | `string` | Page identifier (used for `GetMarkets()`) |
| `Name` | `string` | Display name |
| `Slug` | `string` | URL-friendly identifier |
| `FullPath` | `string` | Full URL path |
| `Description` | `string` | Page description |
| `BaseFilter` | `map[string]any` | Default filter applied to this page |
| `FilterGroups` | `[]FilterGroup` | Available filter groups |
| `Metadata` | `map[string]any` | Page metadata |
| `Breadcrumb` | `[]BreadcrumbItem` | Navigation breadcrumb |
## Fetching Markets for a Page
Use `GetMarkets()` with the page ID to fetch markets. Supports both offset and cursor pagination, sorting, and dynamic filters.
### Offset Pagination
```go theme={null}
result, err := pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Page: intPtr(1),
Limit: intPtr(20),
Sort: limitless.MarketPageSortNewest,
})
if err != nil {
log.Fatal(err)
}
for _, market := range result.Data {
fmt.Printf("%s — %s\n", market.Slug, market.Title)
}
fmt.Printf("Page %d of %d\n", result.Pagination.Page, result.Pagination.TotalPages)
```
### Cursor Pagination
```go theme={null}
firstPage, err := pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Cursor: "",
Limit: intPtr(20),
Sort: limitless.MarketPageSortNewest,
})
if err != nil {
log.Fatal(err)
}
for _, market := range firstPage.Data {
fmt.Println(market.Slug)
}
if firstPage.Cursor != nil && firstPage.Cursor.NextCursor != "" {
nextPage, err := pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Cursor: firstPage.Cursor.NextCursor,
Limit: intPtr(20),
})
// ...
}
```
You cannot use `Cursor` and `Page` in the same request. Choose one pagination strategy.
### Filtering
Pass a `Filters` map. Values can be a single string or a slice for multi-select filters.
```go theme={null}
// Single filter
result, err := pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Filters: map[string]any{"ticker": "btc"},
})
// Multiple values (OR logic)
result, err = pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Filters: map[string]any{"ticker": []string{"btc", "eth"}},
})
// Combined filters
result, err = pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Limit: intPtr(10),
Sort: limitless.MarketPageSortNewest,
Filters: map[string]any{
"ticker": []string{"btc", "eth"},
"duration": "hourly",
},
})
```
### Parameters
| Parameter | Type | Description |
| --------- | ---------------- | ----------------------------------------------------------------- |
| `Page` | `*int` | Page number (offset pagination) |
| `Limit` | `*int` | Results per page |
| `Sort` | `MarketPageSort` | Sort field |
| `Cursor` | `string` | Cursor token (cursor pagination). Use `""` for the first request. |
| `Filters` | `map[string]any` | Filter key-value pairs. Values can be `string` or `[]string`. |
## Property Keys
Property keys define the available filter dimensions (e.g. "ticker", "duration"). Each key has a set of options.
```go theme={null}
// List all property keys
keys, err := pageFetcher.GetPropertyKeys(ctx)
if err != nil {
log.Fatal(err)
}
for _, key := range keys {
fmt.Printf("%s (%s)\n", key.Name, key.Type)
}
```
### Single Key and Options
```go theme={null}
key, err := pageFetcher.GetPropertyKey(ctx, keys[0].ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s — %d options\n", key.Name, len(key.Options))
// Fetch options separately (supports parent filtering for hierarchical keys)
options, err := pageFetcher.GetPropertyOptions(ctx, key.ID, nil)
// Child options filtered by parent
parentID := options[0].ID
childOptions, err := pageFetcher.GetPropertyOptions(ctx, key.ID, &parentID)
```
### PropertyKey Fields
| Field | Type | Description |
| --------- | ------------------ | --------------------------------------- |
| `ID` | `string` | Unique identifier |
| `Name` | `string` | Display name |
| `Slug` | `string` | URL-friendly identifier |
| `Type` | `string` | `"select"` or `"multi-select"` |
| `Options` | `[]PropertyOption` | Available options (when fetched inline) |
## Complete Example
```go theme={null}
package main
import (
"context"
"fmt"
"log"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func intPtr(i int) *int { return &i }
func main() {
client := limitless.NewHttpClient()
pageFetcher := limitless.NewMarketPageFetcher(client)
ctx := context.Background()
// Browse the navigation tree
navigation, err := pageFetcher.GetNavigation(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Top-level categories: %d\n", len(navigation))
// Resolve a page
page, err := pageFetcher.GetMarketPageByPath(ctx, "/crypto")
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nPage: %s\n", page.Name)
// Fetch markets with filters
result, err := pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Limit: intPtr(5),
Sort: limitless.MarketPageSortNewest,
Filters: map[string]any{"ticker": []string{"btc", "eth"}},
})
if err != nil {
log.Fatal(err)
}
for _, market := range result.Data {
fmt.Printf(" %s — %s\n", market.Slug, market.Title)
}
// Explore property keys
keys, err := pageFetcher.GetPropertyKeys(ctx)
if err != nil {
log.Fatal(err)
}
for _, key := range keys {
fmt.Printf("\n%s (%s)\n", key.Name, key.Type)
options, _ := pageFetcher.GetPropertyOptions(ctx, key.ID, nil)
for i, opt := range options {
if i >= 5 {
break
}
fmt.Printf(" %s\n", opt.Label)
}
}
}
```
# Markets
Source: https://docs.limitless.exchange/developers/sdk/go/markets
Market discovery and orderbook data with the Go SDK
## Overview
The `MarketFetcher` handles market discovery, individual market lookups, and orderbook retrieval. It also automatically caches venue contract addresses needed for order signing.
## Setup
```go theme={null}
import limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
client := limitless.NewHttpClient() // loads LIMITLESS_API_KEY from env
marketFetcher := limitless.NewMarketFetcher(client)
```
## Fetching Active Markets
Use `GetActiveMarkets()` to retrieve a paginated list of all active markets:
```go theme={null}
ctx := context.Background()
result, err := marketFetcher.GetActiveMarkets(ctx, &limitless.ActiveMarketsParams{
Page: 1,
Limit: 10,
SortBy: limitless.SortByLiquidity,
})
if err != nil {
log.Fatal(err)
}
for _, m := range result.Data {
fmt.Println(m.Title, m.Slug)
}
```
| Parameter | Type | Default | Description |
| --------- | --------------------- | ------- | -------------------------- |
| `Page` | `int` | `1` | Page number for pagination |
| `Limit` | `int` | `10` | Number of markets per page |
| `SortBy` | `ActiveMarketsSortBy` | `""` | Sort order |
### Sort Options
| Constant | Description |
| ------------------ | ------------------------------ |
| `SortByLPRewards` | Sort by LP rewards |
| `SortByEndingSoon` | Sort by markets ending soonest |
| `SortByNewest` | Sort by newest markets |
| `SortByHighValue` | Sort by highest value |
| `SortByLiquidity` | Sort by liquidity |
Use pagination parameters to avoid large responses. Start with a small `Limit` and increment `Page` as needed.
## Fetching a Single Market
Use `GetMarket()` to retrieve full details for a specific market:
```go theme={null}
market, err := marketFetcher.GetMarket(ctx, "btc-above-100k-march-2025")
if err != nil {
log.Fatal(err)
}
fmt.Println(market.Title)
fmt.Println("YES token:", market.Tokens.Yes)
fmt.Println("NO token:", market.Tokens.No)
fmt.Println("Exchange:", market.Venue.Exchange)
```
The returned `Market` struct has the following key fields:
| Field | Type | Description |
| ---------------- | ---------- | --------------------------------------------------------------- |
| `Title` | `string` | Human-readable market title |
| `Slug` | `string` | URL-friendly market identifier |
| `Tokens.Yes` | `string` | Token ID for the YES outcome |
| `Tokens.No` | `string` | Token ID for the NO outcome |
| `Venue.Exchange` | `string` | Exchange contract address (used as EIP-712 `verifyingContract`) |
| `Venue.Adapter` | `*string` | Adapter contract address (used for NegRisk token approvals) |
| `Markets` | `[]Market` | Sub-markets (for NegRisk group markets) |
Token IDs are returned as **strings**. Pass them directly to `OrderClient.CreateOrder()` via `FOKOrderArgs` or `GTCOrderArgs`.
## Fetching the Orderbook
Use `GetOrderBook()` to retrieve current bids and asks for a market:
```go theme={null}
orderbook, err := marketFetcher.GetOrderBook(ctx, "btc-above-100k-march-2025")
if err != nil {
log.Fatal(err)
}
fmt.Println("Bids:")
for _, bid := range orderbook.Bids {
fmt.Printf(" Price: %.3f Size: %.2f\n", bid.Price, bid.Size)
}
fmt.Println("Asks:")
for _, ask := range orderbook.Asks {
fmt.Printf(" Price: %.3f Size: %.2f\n", ask.Price, ask.Size)
}
fmt.Printf("Midpoint: %.4f\n", orderbook.AdjustedMidpoint)
```
## Fetching User Orders
Use `GetUserOrders()` to retrieve your open orders on a market (requires authentication):
```go theme={null}
orders, err := marketFetcher.GetUserOrders(ctx, "btc-above-100k-march-2025")
if err != nil {
log.Fatal(err)
}
for _, order := range orders {
fmt.Printf("Order %s: %s %.2f @ %.3f\n", order.ID, order.Side, order.Size, order.Price)
}
```
## Venue Caching
Every call to `GetMarket()` automatically caches the venue contract addresses (exchange and adapter) for that market. The `OrderClient` reads from this cache when signing orders, so you do not need to manage venues manually.
Calling `GetMarket()` retrieves and caches the venue:
```go theme={null}
market, err := marketFetcher.GetMarket(ctx, "btc-above-100k-march-2025")
// Venue is now cached for this slug
```
You can also access the venue cache directly:
```go theme={null}
venue, ok := marketFetcher.GetVenue("btc-above-100k-march-2025")
if ok {
fmt.Println("Exchange:", venue.Exchange)
}
```
The `OrderClient` automatically looks up the cached venue when you pass `MarketSlug`:
```go theme={null}
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeGTC,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.65,
Size: 10.0,
},
})
```
You **must** call `GetMarket(slug)` at least once before placing orders on that market. Without a cached venue, the `OrderClient` cannot determine the correct `verifyingContract` for EIP-712 signing.
## Debugging Venue Cache
Enable `LogLevelDebug` logging to see venue cache operations:
```go theme={null}
logger := limitless.NewConsoleLogger(limitless.LogLevelDebug)
marketFetcher := limitless.NewMarketFetcher(client, limitless.WithMarketLogger(logger))
// Output will include lines like:
// [DEBUG] Venue cached for btc-above-100k-march-2025: exchange=0xA1b2... adapter=0xD4e5...
// [DEBUG] Venue cache hit for btc-above-100k-march-2025
```
## Complete Example
```go theme={null}
package main
import (
"context"
"fmt"
"log"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient()
marketFetcher := limitless.NewMarketFetcher(client)
ctx := context.Background()
// List active markets
active, err := marketFetcher.GetActiveMarkets(ctx, &limitless.ActiveMarketsParams{Limit: 5})
if err != nil {
log.Fatal(err)
}
for _, m := range active.Data {
fmt.Printf("%s — %s\n", m.Title, m.Slug)
}
// Get a specific market
slug := active.Data[0].Slug
market, err := marketFetcher.GetMarket(ctx, slug)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nMarket: %s\n", market.Title)
fmt.Printf("YES token: %s\n", market.Tokens.Yes)
fmt.Printf("NO token: %s\n", market.Tokens.No)
// Fetch the orderbook
orderbook, err := marketFetcher.GetOrderBook(ctx, slug)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nOrderbook — %d bids, %d asks\n", len(orderbook.Bids), len(orderbook.Asks))
}
```
# Trading & Orders
Source: https://docs.limitless.exchange/developers/sdk/go/orders
Create and manage orders with the Go SDK
## Overview
The `OrderClient` handles order creation, EIP-712 signing, and order management. It supports Good-Till-Cancelled (GTC), Fill-And-Kill (FAK), and Fill-or-Kill (FOK) orders.
## Prerequisites
Before placing orders, you need three components:
```go theme={null}
import (
"context"
"log"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
client := limitless.NewHttpClient() // loads LIMITLESS_API_KEY from env
marketFetcher := limitless.NewMarketFetcher(client)
orderClient, err := limitless.NewOrderClient(
client,
"0xYOUR_PRIVATE_KEY", // hex-encoded private key (with or without "0x" prefix)
)
if err != nil {
log.Fatal(err)
}
```
| Component | Purpose |
| --------------- | ---------------------------------------------- |
| `HttpClient` | Authenticated HTTP client for API requests |
| `MarketFetcher` | Fetches market data and caches venue addresses |
| `OrderClient` | Creates, signs, and submits orders |
The `OrderClient` lazily fetches your user profile on the first order to determine your fee rate. The `CHAIN_ID` environment variable defaults to `8453` (Base mainnet).
## Token Approvals
Before your first trade on a given venue, you must approve the exchange contracts to spend your tokens. This is a **one-time on-chain setup** per venue.
For standard CLOB markets, approve USDC and Conditional Tokens to the **exchange** contract:
```go theme={null}
market, _ := marketFetcher.GetMarket(ctx, "your-market-slug")
exchange := market.Venue.Exchange
// Use go-ethereum or any Ethereum client to send approval transactions:
// 1. Approve USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) to the exchange for BUY orders
// 2. Approve Conditional Tokens (0xC9c98965297Bc527861c898329Ee280632B76e18) to the exchange for SELL orders
```
For NegRisk markets, you must additionally approve the **adapter** contract:
```go theme={null}
market, _ := marketFetcher.GetMarket(ctx, "your-negrisk-slug")
exchange := market.Venue.Exchange
adapter := *market.Venue.Adapter
// 1. Approve USDC to the exchange (same as CLOB)
// 2. Approve Conditional Tokens to the exchange (same as CLOB)
// 3. Approve Conditional Tokens to the adapter (NegRisk only)
```
Approvals are on-chain transactions that cost gas. You only need to perform them once per venue. Use `Venue.Exchange` for both CLOB and NegRisk, and additionally `Venue.Adapter` for NegRisk markets.
## GTC Orders (Good-Till-Cancelled)
GTC orders remain on the orderbook until filled or explicitly cancelled. Specify `Price` (0.0–1.0, tick-aligned to 0.001) and `Size` (number of shares):
```go theme={null}
ctx := context.Background()
market, err := marketFetcher.GetMarket(ctx, "btc-above-100k-march-2025")
if err != nil {
log.Fatal(err)
}
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeGTC,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.65,
Size: 10.0,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Order placed: %+v\n", result.Order)
```
### Post-only GTC order
Use `PostOnly: true` to ensure your order is never filled immediately as a taker. If the order would cross the spread (i.e., match against existing orders), it is **rejected** instead. This guarantees you always receive maker fees.
```go theme={null}
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeGTC,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.65,
Size: 10.0,
PostOnly: true,
},
})
```
### GTCOrderArgs
| Field | Type | Description |
| ------------ | --------- | ---------------------------------------------------------------------------------------- |
| `TokenID` | `string` | Token ID from `Market.Tokens.Yes` or `Market.Tokens.No` |
| `Side` | `Side` | `SideBuy` (0) or `SideSell` (1) |
| `Price` | `float64` | Price per share (0.0–1.0, must be tick-aligned to 0.001) |
| `Size` | `float64` | Number of shares to buy or sell |
| `PostOnly` | `bool` | Optional. When `true`, rejects the order if it would immediately match. Default `false`. |
| `Expiration` | `string` | Optional expiration timestamp (default `"0"` = no expiration) |
| `Nonce` | `*int` | Optional nonce for replay protection (auto-generated if `nil`) |
| `Taker` | `string` | Optional taker address (defaults to zero address) |
## FAK Orders (Fill-And-Kill)
FAK orders use the same `Price` and `Size` inputs as GTC, but they only consume immediately available liquidity and cancel any unmatched remainder.
`PostOnly` is not supported for FAK orders.
```go theme={null}
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeFAK,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.FAKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.45,
Size: 10.0,
},
})
if err != nil {
log.Fatal(err)
}
if len(result.MakerMatches) > 0 {
fmt.Printf("FAK order matched immediately with %d fill(s)\n", len(result.MakerMatches))
} else {
fmt.Println("FAK remainder was cancelled.")
}
```
### FAKOrderArgs
| Field | Type | Description |
| ------------ | --------- | -------------------------------------------------------------- |
| `TokenID` | `string` | Token ID from `Market.Tokens.Yes` or `Market.Tokens.No` |
| `Side` | `Side` | `SideBuy` (0) or `SideSell` (1) |
| `Price` | `float64` | Price per share (0.0–1.0, must be tick-aligned to 0.001) |
| `Size` | `float64` | Number of shares to buy or sell |
| `Expiration` | `string` | Optional expiration timestamp (default `"0"` = no expiration) |
| `Nonce` | `*int` | Optional nonce for replay protection (auto-generated if `nil`) |
| `Taker` | `string` | Optional taker address (defaults to zero address) |
## FOK Orders (Fill-or-Kill)
FOK orders execute immediately and fully, or are rejected entirely. Instead of `Price` and `Size`, you specify `MakerAmount`:
When buying, `MakerAmount` is the **total USDC you want to spend** (max 6 decimal places). The exchange fills as many shares as possible at the best available price:
```go theme={null}
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeFOK,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.FOKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
MakerAmount: 10.0, // spend 10 USDC
},
})
```
When selling, `MakerAmount` is the **number of shares to sell**. The exchange returns USDC at the best available price:
```go theme={null}
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeFOK,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.FOKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideSell,
MakerAmount: 10.0, // sell 10 shares
},
})
```
### FOKOrderArgs
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------------------------------ |
| `TokenID` | `string` | Token ID from `Market.Tokens.Yes` or `Market.Tokens.No` |
| `Side` | `Side` | `SideBuy` (0) or `SideSell` (1) |
| `MakerAmount` | `float64` | USDC to spend (buy) or shares to sell (sell), max 6 decimal places |
| `Expiration` | `string` | Optional expiration timestamp (default `"0"` = no expiration) |
| `Nonce` | `*int` | Optional nonce for replay protection (auto-generated if `nil`) |
| `Taker` | `string` | Optional taker address (defaults to zero address) |
## Advanced: Build and Sign Separately
For advanced use cases, you can build and sign orders without submitting them:
```go theme={null}
// Build an unsigned order
unsigned, err := orderClient.BuildUnsignedOrder(ctx, limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.65,
Size: 10.0,
})
if err != nil {
log.Fatal(err)
}
// Sign the order
signature, err := orderClient.SignOrder(unsigned)
if err != nil {
log.Fatal(err)
}
fmt.Println("Signature:", signature)
```
## Cancelling Orders
Cancel a specific order by its ID:
```go theme={null}
msg, err := orderClient.Cancel(ctx, "abc123-def456")
if err != nil {
log.Fatal(err)
}
fmt.Println(msg)
```
Cancel every open order you have on a given market:
```go theme={null}
msg, err := orderClient.CancelAll(ctx, "btc-above-100k-march-2025")
if err != nil {
log.Fatal(err)
}
fmt.Println(msg)
```
## Enums Reference
### Side
| Constant | Value | Description |
| ---------- | ----- | -------------------- |
| `SideBuy` | `0` | Buy shares with USDC |
| `SideSell` | `1` | Sell shares for USDC |
### OrderType
| Constant | Value | Description |
| -------------- | ------- | ----------------------------------------------------------- |
| `OrderTypeGTC` | `"GTC"` | Good-Till-Cancelled limit order (rests on the book) |
| `OrderTypeFAK` | `"FAK"` | Fill-And-Kill limit order (cancels any unmatched remainder) |
| `OrderTypeFOK` | `"FOK"` | Fill-or-Kill market order (fills immediately or rejects) |
## Error Handling
The SDK returns typed errors for order failures. Use `errors.As()` to inspect them:
```go theme={null}
import "errors"
result, err := orderClient.CreateOrder(ctx, params)
if err != nil {
var apiErr *limitless.APIError
if errors.As(err, &apiErr) {
fmt.Printf("Order failed — status %d: %s\n", apiErr.Status, apiErr.Message)
}
var validErr *limitless.OrderValidationError
if errors.As(err, &validErr) {
fmt.Printf("Validation error on field %s: %s\n", validErr.Field, validErr.Message)
}
}
```
See [Error Handling & Retry](/developers/sdk/go/error-handling) for details on error types and the `WithRetry` function.
## Complete Example
```go theme={null}
package main
import (
"context"
"errors"
"fmt"
"log"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient()
marketFetcher := limitless.NewMarketFetcher(client)
ctx := context.Background()
orderClient, err := limitless.NewOrderClient(client, "0xYOUR_PRIVATE_KEY")
if err != nil {
log.Fatal(err)
}
fmt.Println("Wallet:", orderClient.WalletAddress())
// Fetch market (caches venue automatically)
market, err := marketFetcher.GetMarket(ctx, "btc-above-100k-march-2025")
if err != nil {
log.Fatal(err)
}
// Place a GTC BUY order for 10 YES shares at $0.65
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeGTC,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.65,
Size: 10.0,
},
})
if err != nil {
var apiErr *limitless.APIError
if errors.As(err, &apiErr) {
log.Fatalf("API error — status %d: %s", apiErr.Status, apiErr.Message)
}
log.Fatal(err)
}
fmt.Printf("GTC order placed: %+v\n", result.Order)
// Place a FAK BUY order for 10 YES shares at $0.45
fakResult, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeFAK,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.FAKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.45,
Size: 10.0,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("FAK order placed: %+v\n", fakResult.Order)
// Place a FOK BUY order spending 5 USDC
fokResult, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeFOK,
MarketSlug: "btc-above-100k-march-2025",
Args: limitless.FOKOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
MakerAmount: 5.0,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("FOK order placed: %+v\n", fokResult.Order)
// Cancel all orders on this market
msg, err := orderClient.CancelAll(ctx, "btc-above-100k-march-2025")
if err != nil {
log.Fatal(err)
}
fmt.Println("Cancelled:", msg)
}
```
# Partner Accounts
Source: https://docs.limitless.exchange/developers/sdk/go/partner-accounts
Create and manage sub-accounts with the Go SDK
The `PartnerAccountService` creates sub-account profiles linked to the authenticated partner. Requires HMAC authentication with the `account_creation` scope.
## Access
```go theme={null}
import "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
client := limitless.NewClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: tokenID,
Secret: secret,
}),
)
// Use client.PartnerAccounts.*
```
## Server wallet mode
Creates a managed Privy wallet for the sub-account. Enables [delegated signing](/developers/programmatic-api#sub-account-modes) — the partner submits unsigned orders and the server signs them.
```go theme={null}
createServerWallet := true
account, err := client.PartnerAccounts.CreateAccount(ctx, limitless.CreatePartnerAccountInput{
DisplayName: "user-alice",
CreateServerWallet: &createServerWallet,
}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(account.ProfileID)
fmt.Println(account.Account)
```
New server wallets should be checked with `CheckAllowances` before the first delegated trade. If retryable targets are missing or failed, call `RetryAllowances` and poll again.
## Allowance recovery
Server-wallet sub-accounts need delegated-trading approvals before they can trade. The partner allowance helpers use the Partner API only:
* `CheckAllowances(ctx, profileID)` calls `GET /profiles/partner-accounts/:profileId/allowances`
* `RetryAllowances(ctx, profileID)` calls `POST /profiles/partner-accounts/:profileId/allowances/retry`
* both methods require HMAC credentials with `account_creation` and `delegated_signing`
* `profileID` is the child/server-wallet profile id
```go theme={null}
allowances, err := client.PartnerAccounts.CheckAllowances(ctx, account.ProfileID)
if err != nil {
log.Fatal(err)
}
if !allowances.Ready {
retryable := false
for _, target := range allowances.Targets {
if target.Retryable &&
(target.Status == limitless.PartnerAccountAllowanceStatusMissing ||
target.Status == limitless.PartnerAccountAllowanceStatusFailed) {
retryable = true
break
}
}
if retryable {
allowances, err = client.PartnerAccounts.RetryAllowances(ctx, account.ProfileID)
if err != nil {
var rateLimitErr *limitless.RateLimitError
var conflictErr *limitless.ConflictError
switch {
case errors.As(err, &rateLimitErr):
fmt.Println("retryAfterSeconds is in rateLimitErr.Data")
case errors.As(err, &conflictErr):
fmt.Println("Retry already running; poll CheckAllowances again shortly.")
default:
log.Fatal(err)
}
}
}
}
for _, target := range allowances.Targets {
if target.Status == limitless.PartnerAccountAllowanceStatusSubmitted {
// A sponsored tx/user operation was submitted by this retry request.
// Poll CheckAllowances again after a short delay to observe confirmed chain state.
}
}
```
Recommended partner flow:
1. Poll `CheckAllowances(ctx, profileID)`.
2. If `Ready == true`, continue.
3. If targets are `missing` or `failed` with `Retryable == true`, call `RetryAllowances(ctx, profileID)`.
4. If retry returns `submitted` targets, poll `CheckAllowances` again after a short delay.
5. If retry returns `429`, wait `retryAfterSeconds`.
6. If retry returns `409`, wait briefly and call `CheckAllowances` again.
## Withdrawal address allowlist
Explicit treasury destinations for server-wallet withdrawals must be allowlisted on the authenticated partner profile unless the destination is already the partner account or partner smart wallet. Allowlist management uses a Privy identity token, not API-token/HMAC auth.
```go theme={null}
identityToken := os.Getenv("PRIVY_IDENTITY_TOKEN")
treasuryAddress := "0x0F3262730c909408042F9Da345a916dc0e1F9787"
entry, err := client.PartnerAccounts.AddWithdrawalAddress(ctx, identityToken, limitless.PartnerWithdrawalAddressInput{
Address: treasuryAddress,
Label: "treasury",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(entry.DestinationAddress)
if err := client.PartnerAccounts.DeleteWithdrawalAddress(ctx, identityToken, treasuryAddress); err != nil {
log.Fatal(err)
}
```
`AddWithdrawalAddress` and `DeleteWithdrawalAddress` call `POST /portfolio/withdrawal-addresses` and `DELETE /portfolio/withdrawal-addresses/:address` with the `identity: Bearer ` header. `POST /portfolio/withdraw` still uses HMAC auth with the `withdrawal` scope.
## EOA mode
Creates a profile for an externally-owned address. The end user manages their own keys and signs orders themselves.
EOA mode requires wallet ownership verification headers:
```go theme={null}
account, err := client.PartnerAccounts.CreateAccount(ctx, limitless.CreatePartnerAccountInput{
DisplayName: "user-bob",
}, &limitless.CreatePartnerAccountEOAHeaders{
Account: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
SigningMessage: "0x...",
Signature: "0x...",
})
```
## Validation
* `DisplayName` is optional, max 44 characters. Defaults to the wallet address if omitted.
* Returns `409 Conflict` if a profile already exists for the target address.
* Cannot create a sub-account for the partner's own address.
* The SDK validates `DisplayName` length locally before sending the request.
# Portfolio & Positions
Source: https://docs.limitless.exchange/developers/sdk/go/portfolio
Track positions and history with the Go SDK
## Overview
The `PortfolioFetcher` retrieves your open positions, user profile, and trading history from the Limitless Exchange API. It requires an authenticated `HttpClient` with a valid API key.
## Setup
```go theme={null}
import limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
client := limitless.NewHttpClient() // loads LIMITLESS_API_KEY from env
portfolio := limitless.NewPortfolioFetcher(client)
```
`PortfolioFetcher` requires an authenticated client. Ensure your API key is set via the `LIMITLESS_API_KEY` environment variable or the `WithAPIKey` option on `HttpClient`.
## Fetching Your Profile
Use `GetProfile()` to retrieve a user profile by wallet address:
```go theme={null}
ctx := context.Background()
profile, err := portfolio.GetProfile(ctx, "0xYOUR_WALLET_ADDRESS")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Username: %s\n", profile.Username)
fmt.Printf("Display name: %s\n", profile.DisplayName)
if profile.Rank != nil {
fmt.Printf("Rank: %s (fee rate: %d bps)\n", profile.Rank.Name, profile.Rank.FeeRateBps)
}
```
### UserProfile Fields
| Field | Type | Description |
| ------------- | ----------- | ----------------------- |
| `ID` | `int` | User ID |
| `Account` | `string` | Wallet address |
| `Username` | `string` | Username |
| `DisplayName` | `string` | Display name |
| `Rank` | `*UserRank` | User rank with fee rate |
| `Points` | `float64` | Accumulated points |
## Fetching All Positions
Use `GetPositions()` to retrieve all your open positions:
```go theme={null}
positions, err := portfolio.GetPositions(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("CLOB positions: %d\n", len(positions.CLOB))
fmt.Printf("AMM positions: %d\n", len(positions.AMM))
```
The response is a `PortfolioPositionsResponse` with the following fields:
| Field | Type | Description |
| --------- | ---------------- | ------------------------------------------------- |
| `CLOB` | `[]CLOBPosition` | Positions on CLOB (order book) markets |
| `AMM` | `[]AMMPosition` | Positions on AMM (automated market maker) markets |
| `Points` | `float64` | Accumulated points |
| `Rewards` | `float64` | Accumulated rewards |
## CLOB Positions
Use `GetCLOBPositions()` for CLOB-only positions, or iterate over `positions.CLOB`:
```go theme={null}
clobPositions, err := portfolio.GetCLOBPositions(ctx)
if err != nil {
log.Fatal(err)
}
for _, pos := range clobPositions {
fmt.Printf("%s — tokens balance: %s\n", pos.Market.Title, pos.TokensBalance)
}
```
### CLOBPosition Fields
| Field | Type | Description |
| --------------- | ------------------- | ---------------------------------- |
| `Market` | `PositionMarket` | Market title, slug, and metadata |
| `MakerAddress` | `string` | Your wallet address |
| `Positions` | `CLOBPositionSides` | Position details per side |
| `TokensBalance` | `string` | Current token balance |
| `LatestTrade` | `any` | Most recent trade on this position |
| `Orders` | `any` | Open orders |
## AMM Positions
Use `GetAMMPositions()` for AMM-only positions:
```go theme={null}
ammPositions, err := portfolio.GetAMMPositions(ctx)
if err != nil {
log.Fatal(err)
}
for _, pos := range ammPositions {
fmt.Printf("%s — outcome: %d, unrealized PnL: %s\n",
pos.Market.Title, pos.OutcomeIndex, pos.UnrealizedPnl)
}
```
### AMMPosition Fields
| Field | Type | Description |
| -------------------- | ---------------- | -------------------------------- |
| `Market` | `PositionMarket` | Market title, slug, and metadata |
| `Account` | `string` | Your wallet address |
| `OutcomeIndex` | `int` | Outcome index |
| `CollateralAmount` | `string` | Collateral deposited |
| `OutcomeTokenAmount` | `string` | Outcome tokens held |
| `AverageFillPrice` | `string` | Average entry price |
| `RealizedPnl` | `string` | Realized profit and loss |
| `UnrealizedPnl` | `string` | Unrealized profit and loss |
## Trading History
Use `GetUserHistory()` to retrieve paginated trading history:
```go theme={null}
history, err := portfolio.GetUserHistory(ctx, 1, 20) // page 1, 20 items per page
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total trades: %d\n", history.TotalCount)
for _, entry := range history.Data {
fmt.Printf("%+v\n", entry)
}
```
## Complete Example
```go theme={null}
package main
import (
"context"
"fmt"
"log"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient()
portfolio := limitless.NewPortfolioFetcher(client)
ctx := context.Background()
// Fetch all positions
positions, err := portfolio.GetPositions(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("CLOB positions: %d\n", len(positions.CLOB))
for _, pos := range positions.CLOB {
fmt.Printf(" %s — tokens: %s\n", pos.Market.Title, pos.TokensBalance)
}
fmt.Printf("\nAMM positions: %d\n", len(positions.AMM))
for _, pos := range positions.AMM {
fmt.Printf(" %s — unrealized PnL: %s\n", pos.Market.Title, pos.UnrealizedPnl)
}
// Fetch trading history
history, err := portfolio.GetUserHistory(ctx, 1, 10)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nRecent trades: %d (total: %d)\n", len(history.Data), history.TotalCount)
}
```
# WebSocket Streaming
Source: https://docs.limitless.exchange/developers/sdk/go/websocket
Real-time market data and position updates with the Go SDK
## Overview
The `WebSocketClient` provides real-time streaming of orderbook updates, trades, prices, and position data over a persistent WebSocket connection. It supports automatic reconnection with exponential backoff and event-driven message handling. The WebSocket client is standalone and does not require an `HttpClient`.
## Setup
```go theme={null}
import limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
ws := limitless.NewWebSocketClient(
limitless.WithWebSocketAPIKey("lmts_your_key_here"), // or reads LIMITLESS_API_KEY from env
)
```
| Option | Type | Default | Description |
| ----------------------------- | --------------- | ----------------------------- | ------------------------------------------- |
| `WithWebSocketURL(url)` | `string` | `wss://ws.limitless.exchange` | WebSocket server URL |
| `WithWebSocketAPIKey(key)` | `string` | Reads `LIMITLESS_API_KEY` env | API key for authenticated channels |
| `WithAutoReconnect(b)` | `bool` | `true` | Automatically reconnect on disconnection |
| `WithReconnectDelay(d)` | `time.Duration` | `1s` | Initial delay between reconnection attempts |
| `WithMaxReconnectAttempts(n)` | `int` | `0` (unlimited) | Maximum reconnection attempts |
| `WithWebSocketTimeout(d)` | `time.Duration` | `10s` | Connection timeout |
| `WithWebSocketLogger(l)` | `Logger` | `NoOpLogger` | Logger for connection events |
## Connecting
```go theme={null}
ctx := context.Background()
err := ws.Connect(ctx)
if err != nil {
log.Fatal(err)
}
defer ws.Disconnect()
```
### Connection States
| Constant | Description |
| ------------------- | ------------------------- |
| `StateDisconnected` | Not connected |
| `StateConnecting` | Connection in progress |
| `StateConnected` | Connected and ready |
| `StateReconnecting` | Reconnecting after a drop |
| `StateError` | Connection error |
Check the current state with `ws.State()` or `ws.IsConnected()`.
## Event Handlers
Register handlers using `On()` for persistent handlers or `Once()` for one-time handlers. Both return a handler ID that can be used with `Off()` to unregister.
```go theme={null}
// Persistent handler
handlerID := ws.On("orderbookUpdate", func(data json.RawMessage) {
fmt.Println("Orderbook update:", string(data))
})
// One-time handler
ws.Once("orderbookUpdate", func(data json.RawMessage) {
fmt.Println("First orderbook update received")
})
// Remove a handler
ws.Off("orderbookUpdate", handlerID)
```
### Typed Event Handlers
The SDK provides convenience methods that automatically deserialize events into typed structs:
```go theme={null}
ws.OnOrderbookUpdate(func(update limitless.OrderbookUpdate) {
fmt.Printf("[%s] %d bids, %d asks\n",
update.MarketSlug, len(update.Orderbook.Bids), len(update.Orderbook.Asks))
})
ws.OnTrade(func(trade limitless.TradeEvent) {
fmt.Printf("[%s] %s %.2f @ %.3f\n",
trade.MarketSlug, trade.Side, trade.Size, trade.Price)
})
ws.OnOrder(func(order limitless.OrderUpdate) {
fmt.Printf("Order %s: %s\n", order.OrderID, order.Status)
})
ws.OnFill(func(fill limitless.FillEvent) {
fmt.Printf("Fill %s: %.2f @ %.3f\n", fill.FillID, fill.Size, fill.Price)
})
ws.OnNewPriceData(func(price limitless.NewPriceData) {
fmt.Printf("Price update for %s\n", price.MarketAddress)
})
ws.OnTransaction(func(tx limitless.TransactionEvent) {
fmt.Printf("Transaction %s: %s\n", tx.TxHash, tx.Status)
})
ws.OnMarket(func(market limitless.MarketUpdateEvent) {
fmt.Printf("[%s] Last price: %.3f\n", market.MarketSlug, market.LastPrice)
})
```
### Available Events
| Event | Typed Handler | Payload | Description |
| ----------------- | ------------------- | ------------------- | ----------------------------- |
| `orderbookUpdate` | `OnOrderbookUpdate` | `OrderbookUpdate` | Orderbook changes (bids/asks) |
| `trade` | `OnTrade` | `TradeEvent` | New trade executed |
| `order` | `OnOrder` | `OrderUpdate` | Order status change |
| `fill` | `OnFill` | `FillEvent` | Order fill event |
| `newPriceData` | `OnNewPriceData` | `NewPriceData` | AMM price update |
| `tx` | `OnTransaction` | `TransactionEvent` | On-chain transaction update |
| `market` | `OnMarket` | `MarketUpdateEvent` | Market data update |
## Subscribing to Channels
After connecting, subscribe to specific channels to receive updates:
```go theme={null}
err := ws.Subscribe(ctx, limitless.ChannelOrderbook, limitless.SubscriptionOptions{
MarketSlugs: []string{"btc-above-100k-march-2025"},
})
if err != nil {
log.Fatal(err)
}
```
### Public Channels (no authentication required)
| Constant | Description |
| ------------------------------ | -------------------------------------- |
| `ChannelOrderbook` | Orderbook updates for specific markets |
| `ChannelTrades` | Trade events for specific markets |
| `ChannelMarkets` | Market-level updates |
| `ChannelPrices` | AMM price data |
| `ChannelSubscribeMarketPrices` | Subscribe to market price updates |
### Authenticated Channels (require API key)
| Constant | Description |
| ------------------------------ | ------------------------- |
| `ChannelOrders` | Your order status updates |
| `ChannelFills` | Your order fill events |
| `ChannelSubscribePositions` | Your position updates |
| `ChannelSubscribeTransactions` | Your transaction updates |
### SubscriptionOptions
| Field | Type | Description |
| ----------------- | ---------------- | ---------------------------- |
| `MarketSlug` | `string` | Single market slug |
| `MarketSlugs` | `[]string` | Multiple market slugs |
| `MarketAddress` | `string` | Single market address |
| `MarketAddresses` | `[]string` | Multiple market addresses |
| `Filters` | `map[string]any` | Additional filter parameters |
## Unsubscribing
```go theme={null}
err := ws.Unsubscribe(ctx, limitless.ChannelOrderbook, limitless.SubscriptionOptions{
MarketSlugs: []string{"btc-above-100k-march-2025"},
})
```
## Auto-Reconnect
When `WithAutoReconnect(true)` is set (the default), the client automatically reconnects after a disconnection using exponential backoff with jitter (capped at 60 seconds):
1. The connection drops (network issue, server restart, etc.)
2. The client waits with exponential backoff
3. A new connection is established
4. Subscriptions must be re-established
Re-subscribe to your channels after reconnection. You can detect reconnection by checking `ws.State()` or using a handler on the connection event.
## Complete Example
```go theme={null}
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
ws := limitless.NewWebSocketClient(
limitless.WithAutoReconnect(true),
limitless.WithWebSocketLogger(limitless.NewConsoleLogger(limitless.LogLevelInfo)),
)
// Register typed event handlers
ws.OnOrderbookUpdate(func(update limitless.OrderbookUpdate) {
fmt.Printf("[%s] Orderbook: %d bids, %d asks\n",
update.MarketSlug, len(update.Orderbook.Bids), len(update.Orderbook.Asks))
})
ws.OnTrade(func(trade limitless.TradeEvent) {
fmt.Printf("[%s] Trade: %s %.2f @ %.3f\n",
trade.MarketSlug, trade.Side, trade.Size, trade.Price)
})
// Connect
ctx := context.Background()
if err := ws.Connect(ctx); err != nil {
log.Fatal(err)
}
defer ws.Disconnect()
// Subscribe to orderbook and trades
if err := ws.Subscribe(ctx, limitless.ChannelOrderbook, limitless.SubscriptionOptions{
MarketSlugs: []string{"btc-above-100k-march-2025"},
}); err != nil {
log.Fatal(err)
}
if err := ws.Subscribe(ctx, limitless.ChannelTrades, limitless.SubscriptionOptions{
MarketSlugs: []string{"btc-above-100k-march-2025"},
}); err != nil {
log.Fatal(err)
}
fmt.Println("Listening for updates... Press Ctrl+C to exit.")
// Block until interrupt
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
}
```
The `ws.Disconnect()` call cleans up the connection and clears all subscriptions. Always defer it after a successful `Connect()`.
# API Tokens
Source: https://docs.limitless.exchange/developers/sdk/python/api-tokens
Manage scoped API tokens with the Python SDK
The `ApiTokenService` handles the partner self-service token lifecycle: checking capabilities, deriving tokens, listing active tokens, and revoking them.
Token derivation and capability queries require a **Privy identity token**. The SDK does not obtain this token for you — your application must authenticate the partner via Privy and pass the resulting token.
## Access
The service is available on the root `Client`:
```python theme={null}
from limitless_sdk import Client
client = Client(base_url="https://api.limitless.exchange")
# Use client.api_tokens.*
```
## Get partner capabilities
Check whether token management is enabled and which scopes are allowed.
```python theme={null}
capabilities = await client.api_tokens.get_capabilities(identity_token)
print(capabilities.token_management_enabled) # bool
print(capabilities.allowed_scopes) # e.g. ['trading', 'account_creation', 'delegated_signing']
```
## Derive a token
Create a new scoped API token. The `secret` is returned once — store it securely.
```python theme={null}
from limitless_sdk import (
DeriveApiTokenInput,
ScopeTrading,
ScopeAccountCreation,
ScopeDelegatedSigning,
)
derived = await client.api_tokens.derive_token(
identity_token,
DeriveApiTokenInput(
label="production-bot",
scopes=[ScopeTrading, ScopeAccountCreation, ScopeDelegatedSigning],
),
)
# derived.token_id — used as lmts-api-key header
# derived.secret — base64-encoded HMAC secret (one-time)
# derived.scopes — granted scopes
# derived.profile — { id, account }
```
### Creating an HMAC-authenticated client
After deriving a token, create a new `Client` with the HMAC credentials:
```python theme={null}
from limitless_sdk import Client, HMACCredentials
scoped_client = Client(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(
token_id=derived.token_id,
secret=derived.secret,
),
)
```
If `scopes` is omitted, the token defaults to `['trading']`. Requested scopes must be a subset of the partner's `allowed_scopes`.
## List active tokens
Returns all non-revoked tokens for the authenticated partner.
```python theme={null}
tokens = await scoped_client.api_tokens.list_tokens()
for token in tokens:
print(token.token_id, token.label, token.scopes, token.last_used_at)
```
## Revoke a token
Immediately invalidates a token. This cannot be undone.
```python theme={null}
message = await scoped_client.api_tokens.revoke_token(derived.token_id)
```
## Scope constants
The SDK exports typed scope constants:
| Constant | Value |
| ----------------------- | --------------------- |
| `ScopeTrading` | `'trading'` |
| `ScopeAccountCreation` | `'account_creation'` |
| `ScopeDelegatedSigning` | `'delegated_signing'` |
# Delegated Orders
Source: https://docs.limitless.exchange/developers/sdk/python/delegated-orders
Place orders on behalf of sub-accounts with the Python SDK
The `DelegatedOrderService` enables partners with the `delegated_signing` scope to place and cancel orders on behalf of their sub-accounts. The server signs orders using the sub-account's managed Privy wallet — no private key management is needed on the partner side.
Delegated signing requires a sub-account created with `create_server_wallet=True`. EOA sub-accounts sign their own orders.
**Recommended setup:** Store your HMAC credentials (`token_id` / `secret`) on your backend. Use this SDK server-side to sign partner-authenticated requests. Expose only your own app-specific endpoints to the frontend. Never expose HMAC secrets in browser bundles or client-side storage.
## Access
```python theme={null}
from limitless_sdk import Client, HMACCredentials
client = Client(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(token_id=token_id, secret=secret),
)
# Use client.delegated_orders.*
```
## Create a GTC delegated order
Builds an unsigned GTC (Good-Til-Cancelled) limit order and submits it to `POST /orders` with the `on_behalf_of` profile ID. The server signs the order via the sub-account's managed wallet. GTC orders remain on the orderbook until filled or explicitly cancelled.
```python theme={null}
from limitless_sdk import OrderType, Side
response = await client.delegated_orders.create_order(
token_id=market.tokens.yes,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-100k",
on_behalf_of=partner_account.profile_id,
price=0.55,
size=10,
)
print(response.order.id)
```
Use `post_only=True` to ensure the order only rests on the book and is never filled immediately as a taker:
```python theme={null}
response = await client.delegated_orders.create_order(
token_id=market.tokens.yes,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-100k",
on_behalf_of=partner_account.profile_id,
price=0.55,
size=10,
post_only=True,
)
```
## Create a FAK delegated order
FAK (Fill-And-Kill) orders use the same `price` and `size` inputs as GTC, but they only consume immediately available liquidity and cancel any unmatched remainder.
`post_only` is not supported for FAK orders.
```python theme={null}
from limitless_sdk import OrderType, Side
response = await client.delegated_orders.create_order(
token_id=market.tokens.yes,
side=Side.BUY,
order_type=OrderType.FAK,
market_slug="btc-100k",
on_behalf_of=partner_account.profile_id,
price=0.45,
size=10,
)
if response.maker_matches:
print(f"FAK order matched immediately with {len(response.maker_matches)} fill(s)")
else:
print("FAK remainder was cancelled.")
```
## Create a FOK delegated order
FOK (Fill-Or-Kill) orders execute immediately at the best available price or are cancelled entirely — there are no partial fills. Instead of `price` and `size`, FOK orders use `maker_amount`:
* **BUY**: `maker_amount` is the USDC amount to spend (e.g., `50.0` = spend \$50 USDC)
* **SELL**: `maker_amount` is the number of shares to sell (e.g., `18.64` = sell 18.64 shares)
```python theme={null}
from limitless_sdk import OrderType, Side
# BUY FOK — spend 1 USDC at market price
response = await client.delegated_orders.create_order(
token_id=market.tokens.yes,
side=Side.BUY,
order_type=OrderType.FOK,
market_slug="btc-100k",
on_behalf_of=partner_account.profile_id,
maker_amount=1.0,
)
if response.maker_matches:
print(f"FOK order matched with {len(response.maker_matches)} fill(s)")
else:
print("FOK order was not matched — cancelled automatically")
# SELL FOK — sell 10 shares at market price
response = await client.delegated_orders.create_order(
token_id=market.tokens.yes,
side=Side.SELL,
order_type=OrderType.FOK,
market_slug="btc-100k",
on_behalf_of=partner_account.profile_id,
maker_amount=10.0,
)
```
## Parameters
### GTC order parameters
| Parameter | Type | Description |
| -------------- | ----------- | ---------------------------------------------------------------------------------- |
| `token_id` | `str` | Position token ID (YES or NO) from market data |
| `side` | `Side` | `Side.BUY` or `Side.SELL` |
| `order_type` | `OrderType` | `OrderType.GTC` |
| `market_slug` | `str` | Market identifier |
| `on_behalf_of` | `int` | Profile ID of the sub-account |
| `price` | `float` | Price between 0 and 1 |
| `size` | `float` | Number of contracts |
| `post_only` | `bool` | Optional. `True` rejects the order if it would immediately match. Default `False`. |
| `fee_rate_bps` | `int` | Fee rate in basis points (defaults to 300 if omitted) |
### FAK order parameters
| Parameter | Type | Description |
| -------------- | ----------- | ----------------------------------------------------- |
| `token_id` | `str` | Position token ID (YES or NO) from market data |
| `side` | `Side` | `Side.BUY` or `Side.SELL` |
| `order_type` | `OrderType` | `OrderType.FAK` |
| `market_slug` | `str` | Market identifier |
| `on_behalf_of` | `int` | Profile ID of the sub-account |
| `price` | `float` | Price between 0 and 1 |
| `size` | `float` | Number of contracts |
| `fee_rate_bps` | `int` | Fee rate in basis points (defaults to 300 if omitted) |
### FOK order parameters
| Parameter | Type | Description |
| -------------- | ----------- | --------------------------------------------------------- |
| `token_id` | `str` | Position token ID (YES or NO) from market data |
| `side` | `Side` | `Side.BUY` or `Side.SELL` |
| `order_type` | `OrderType` | `OrderType.FOK` |
| `market_slug` | `str` | Market identifier |
| `on_behalf_of` | `int` | Profile ID of the sub-account |
| `maker_amount` | `float` | BUY: USDC to spend. SELL: shares to sell. Max 6 decimals. |
| `fee_rate_bps` | `int` | Fee rate in basis points (defaults to 300 if omitted) |
## Cancel an order
```python theme={null}
message = await client.delegated_orders.cancel(order_id)
```
### Cancel on behalf of a sub-account
```python theme={null}
message = await client.delegated_orders.cancel_on_behalf_of(order_id, partner_account.profile_id)
```
## Cancel all orders in a market
```python theme={null}
message = await client.delegated_orders.cancel_all(market_slug)
```
### Cancel all on behalf of a sub-account
```python theme={null}
message = await client.delegated_orders.cancel_all_on_behalf_of(market_slug, partner_account.profile_id)
```
## How it works
1. The SDK builds an unsigned order locally with a zero verifying address
2. For GTC and FAK orders, `price` and `size` are used; for FOK orders, `maker_amount` is used (with `taker_amount` always set to 1)
3. The order is posted to `POST /orders` with `on_behalf_of` and `owner_id` set to the sub-account's profile ID
4. The server detects the `delegated_signing` scope and missing signature
5. The server looks up the sub-account's server wallet, builds EIP-712 typed data, and signs via Privy
6. The signed order is submitted to the CLOB engine — GTC orders can rest on the book, FAK orders cancel unmatched remainder, and FOK orders either fully fill or cancel
# Error Handling & Retry
Source: https://docs.limitless.exchange/developers/sdk/python/error-handling
Retry logic and error handling with the Python SDK
## Overview
The SDK provides structured error types, a retry decorator for transient failures, and configurable logging to help you build resilient trading applications.
## APIError
All non-2xx HTTP responses from the API raise an `APIError` exception:
```python theme={null}
from limitless_sdk.api import APIError
try:
result = await order_client.create_order(
token_id=market.tokens.yes,
price=0.65,
size=10.0,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-above-100k-march-2025",
)
except APIError as e:
print(f"Status: {e.status_code}")
print(f"Message: {e.message}")
```
| Property | Type | Description |
| ------------- | ----- | -------------------------------------------------- |
| `status_code` | `int` | HTTP status code (e.g. `400`, `401`, `429`, `500`) |
| `message` | `str` | Raw JSON response body from the API |
### Common Status Codes
| Code | Meaning | Typical Cause |
| ----- | --------------------- | -------------------------------------------------- |
| `400` | Bad Request | Invalid order parameters, malformed payload |
| `401` | Unauthorized | Missing or invalid API key |
| `403` | Forbidden | Insufficient permissions or geographic restriction |
| `404` | Not Found | Invalid market slug or order ID |
| `429` | Too Many Requests | Rate limit exceeded |
| `500` | Internal Server Error | Transient server-side failure |
## Retry Decorator
Use the `@retry_on_errors` decorator to automatically retry operations that fail with specific status codes:
```python theme={null}
from limitless_sdk.api import retry_on_errors
@retry_on_errors(
status_codes={500, 429},
max_retries=3,
delays=[1, 2, 4], # seconds between retries (exponential backoff)
on_retry=lambda attempt, error: print(f"Retry {attempt}: {error}"),
)
async def place_order_with_retry():
return await order_client.create_order(
token_id=market.tokens.yes,
price=0.65,
size=10.0,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-above-100k-march-2025",
)
result = await place_order_with_retry()
```
| Parameter | Type | Description |
| -------------- | ------------------ | ----------------------------------------------------------------- |
| `status_codes` | `set[int]` | HTTP status codes that trigger a retry |
| `max_retries` | `int` | Maximum number of retry attempts |
| `delays` | `list[float]` | Delay in seconds before each retry (supports exponential backoff) |
| `on_retry` | `callable \| None` | Optional callback invoked on each retry with `(attempt, error)` |
The decorator only retries on `APIError` exceptions whose `status_code` is in the `status_codes` set. All other exceptions propagate immediately.
## RetryableClient
For broader retry coverage, wrap your `HttpClient` with the `RetryableClient`. This applies retry logic to **all** API calls made through the client:
```python theme={null}
from limitless_sdk.api import HttpClient, RetryableClient
http_client = HttpClient()
retryable_client = RetryableClient(
client=http_client,
status_codes={500, 502, 503, 429},
max_retries=3,
delays=[1, 2, 4],
)
# Use retryable_client in place of http_client
market_fetcher = MarketFetcher(retryable_client)
order_client = OrderClient(retryable_client, account)
```
Use `RetryableClient` for automated trading bots that need resilience against transient failures without decorating every function individually.
## Debugging with ConsoleLogger
Enable verbose logging to trace requests, responses, and internal operations:
```python theme={null}
from limitless_sdk.types import ConsoleLogger, LogLevel
logger = ConsoleLogger(level=LogLevel.DEBUG)
```
At `DEBUG` level, the logger outputs:
* Request and response headers
* Venue cache hits and misses
* Full API response bodies
* Retry attempts and delays
* WebSocket connection state changes
```
[DEBUG] GET /markets/btc-above-100k-march-2025
[DEBUG] Headers: {'X-API-Key': 'lmts_***', 'Content-Type': 'application/json'}
[DEBUG] Response 200 in 142ms
[DEBUG] Venue cached for btc-above-100k-march-2025: exchange=0xA1b2... adapter=0xD4e5...
[DEBUG] POST /orders
[DEBUG] Response 429 in 38ms
[DEBUG] Retry 1/3 after 1s — status 429
[DEBUG] POST /orders
[DEBUG] Response 200 in 156ms
```
## Best Practices
Wrap every API call in a try/except block. Log the `status_code` and `message` for diagnostics:
```python theme={null}
try:
result = await order_client.create_order(...)
except APIError as e:
logger.error(f"Order failed: {e.status_code} — {e.message}")
```
Limit retries to status codes like `429` (rate limit) and `5xx` (server errors). Do not retry `400` (bad request) or `401` (auth failure) as these require corrective action:
```python theme={null}
@retry_on_errors(status_codes={429, 500, 502, 503})
async def resilient_call():
...
```
Increase delays between retries to avoid overwhelming the API:
```python theme={null}
@retry_on_errors(
status_codes={429, 500},
max_retries=4,
delays=[1, 2, 4, 8],
)
async def backoff_call():
...
```
Always close the `HttpClient` to release connections, even if an error occurs:
```python theme={null}
try:
# ... your trading logic
finally:
await http_client.close()
```
Enable `LogLevel.DEBUG` to see the full request/response cycle. Switch to `INFO` or `WARN` in production to reduce noise.
## Known Issues and Workarounds
The SDK's `NoOpLogger` calls `.warning()` but the logger interface only defines `.warn()`. Add this patch at the top of your script:
```python theme={null}
from limitless_sdk.types.logger import NoOpLogger
NoOpLogger.warning = lambda self, msg, context=None: None
```
`MarketFetcher.get_orderbook()` may throw a Pydantic `ValidationError` when `lastTradePrice` is `null`. Use `curl` as a workaround:
```python theme={null}
import subprocess, json
def get_orderbook(slug: str):
r = subprocess.run(
['curl', '-s', f'https://api.limitless.exchange/markets/{slug}/orderbook'],
capture_output=True, text=True,
)
return json.loads(r.stdout)
```
Python's built-in `urllib.request` gets blocked by the API (403 Forbidden). Use the SDK's `HttpClient`, the `requests` library, or `curl` via subprocess instead.
# Python SDK
Source: https://docs.limitless.exchange/developers/sdk/python/getting-started
Official async Python SDK for the Limitless Exchange API
## Overview
The Limitless Exchange Python SDK (`limitless-sdk`) is an **async-first** Python client built on top of aiohttp. It provides:
* Fully asynchronous HTTP and WebSocket clients
* Pydantic models for type-safe request and response handling
* Automatic venue caching for EIP-712 order signing
* Built-in retry logic for transient API failures
* WebSocket streaming with auto-reconnect
The SDK requires **Python 3.9+** and uses `async`/`await` throughout. All network calls must be awaited inside an `asyncio` event loop.
## Installation
```bash theme={null}
pip install limitless-sdk
```
## Quick Start
```python theme={null}
import asyncio
from limitless_sdk.api import HttpClient
http_client = HttpClient() # loads LIMITLESS_API_KEY from env
```
```python theme={null}
from limitless_sdk.markets import MarketFetcher
async def main():
market_fetcher = MarketFetcher(http_client)
markets = await market_fetcher.get_active_markets()
for market in markets["data"]:
print(market["title"], market["slug"])
await http_client.close()
asyncio.run(main())
```
```python theme={null}
from limitless_sdk.portfolio import PortfolioFetcher
async def main():
portfolio = PortfolioFetcher(http_client)
positions = await portfolio.get_positions()
for pos in positions.get("clob", []):
print(pos["market"]["title"], pos["size"])
await http_client.close()
asyncio.run(main())
```
## Authentication
The SDK supports both authentication modes:
* Legacy API key (`X-API-Key`)
* Scoped token HMAC (`lmts-api-key`, `lmts-timestamp`, `lmts-signature`) for partner/programmatic integrations
Set the `LIMITLESS_API_KEY` environment variable. The `HttpClient` loads it automatically:
```bash theme={null}
export LIMITLESS_API_KEY="lmts_your_key_here"
```
```python theme={null}
http_client = HttpClient() # auto-loads LIMITLESS_API_KEY
```
Use the root client with scoped token credentials:
```python theme={null}
from limitless_sdk import Client, HMACCredentials
client = Client(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(
token_id="your-token-id",
secret="your-base64-secret",
),
)
```
Pass `api_key` directly to the constructor:
```python theme={null}
http_client = HttpClient(api_key="lmts_your_key_here")
```
With `hmac_credentials` set, the SDK automatically generates and sends `lmts-api-key`, `lmts-timestamp`, and `lmts-signature`. Do not manually build HMAC headers when using the SDK client.
Never hardcode API keys in source code or commit them to version control. Use environment variables or a secrets manager.
## Client constructor
The recommended entrypoint is the root `Client` class, which composes all domain services (markets, portfolio, orders, API tokens, partner accounts, delegated orders):
```python theme={null}
from limitless_sdk import Client
client = Client(base_url="https://api.limitless.exchange")
```
For partner integrations using HMAC authentication:
```python theme={null}
from limitless_sdk import Client, HMACCredentials
client = Client(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(
token_id="your-token-id",
secret="your-base64-secret",
),
)
```
| Parameter | Type | Default | Description |
| -------------------- | ------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `base_url` | `str` | `https://api.limitless.exchange` | API base URL |
| `api_key` | `str \| None` | `None` (reads `LIMITLESS_API_KEY` env) | Legacy API key authentication |
| `hmac_credentials` | `HMACCredentials \| None` | `None` | HMAC credentials for scoped API token auth (see [Programmatic API](/developers/programmatic-api)) |
| `additional_headers` | `dict \| None` | `None` | Extra headers merged into every request |
### HttpClient (lower-level)
You can also use `HttpClient` directly for more control:
```python theme={null}
from limitless_sdk.api import HttpClient
http_client = HttpClient(
base_url="https://api.limitless.exchange",
api_key="lmts_your_key_here",
)
```
Always call `await http_client.close()` when you are finished to cleanly shut down the underlying aiohttp session.
## Logging
The SDK provides a `ConsoleLogger` with configurable log levels for debugging:
```python theme={null}
from limitless_sdk.types import ConsoleLogger, LogLevel
logger = ConsoleLogger(level=LogLevel.DEBUG)
```
| Level | Description |
| ---------------- | -------------------------------------------------------------------------- |
| `LogLevel.DEBUG` | Verbose output including headers, venue cache operations, and raw payloads |
| `LogLevel.INFO` | General operational messages |
| `LogLevel.WARN` | Warnings about potential issues |
| `LogLevel.ERROR` | Errors only |
Set `LogLevel.DEBUG` during development to see request headers, venue cache hits/misses, and full API responses. Switch to `INFO` or `WARN` in production.
## Source Code
The SDK is open source. Contributions and issue reports are welcome.
Browse the source, report issues, and contribute at github.com/limitless-labs-group/limitless-sdk
## Disclaimer
**USE AT YOUR OWN RISK.** This SDK is provided as-is with no guarantees. You are solely responsible for any trading activity conducted through this software. Limitless Exchange is not available to users in the United States or other restricted jurisdictions. By using this SDK, you confirm compliance with all applicable local laws and regulations.
## Next Steps
Discover markets, fetch orderbooks, and understand venue caching.
Browse markets by category with navigation, filters, and pagination.
Place GTC and FOK orders, cancel orders, and manage token approvals.
Track your open positions and trading history.
Derive, list, and revoke scoped HMAC tokens for partner integrations.
Create sub-accounts with server wallets or EOA verification.
Place orders on behalf of sub-accounts with server-side signing.
Subscribe to real-time orderbook and price updates.
Retry logic, error types, and debugging strategies.
## Server Wallet Redemption and Withdrawal
For partner server-wallet sub-accounts, the SDK provides helper methods for payout settlement and treasury movement:
* [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) — claim resolved positions
* [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) — transfer ERC20 funds from managed sub-accounts
* [`POST /portfolio/withdrawal-addresses`](/api-reference/portfolio/add-withdrawal-address) — allowlist an explicit treasury destination with Privy identity auth
* [`DELETE /portfolio/withdrawal-addresses/:address`](/api-reference/portfolio/delete-withdrawal-address) — remove an allowlisted destination with Privy identity auth
Required scopes for `apiToken` auth: `trading` for redeem and `withdrawal` for withdraw. Allowlist add/delete calls use a Privy identity token instead of API-token/HMAC auth.
```python theme={null}
import os
from limitless_sdk import PartnerWithdrawalAddressInput
identity_token = os.environ["PRIVY_IDENTITY_TOKEN"]
treasury_address = "0x0F3262730c909408042F9Da345a916dc0e1F9787"
await client.partner_accounts.add_withdrawal_address(
identity_token,
PartnerWithdrawalAddressInput(address=treasury_address, label="treasury"),
)
withdraw = await client.server_wallets.withdraw(
amount="1000000",
on_behalf_of=child_profile_id,
destination=treasury_address,
)
print(withdraw.destination)
```
Set `on_behalf_of` when withdrawing from a child server-wallet profile. `destination` is optional for child withdrawals; when omitted, the API defaults to the authenticated partner smart wallet when present, otherwise the authenticated partner account. You can omit `on_behalf_of` only when withdrawing the authenticated caller's own server wallet to an explicit `destination`.
See the full flow and scope guidance in [Programmatic API](/developers/programmatic-api).
# Market Pages
Source: https://docs.limitless.exchange/developers/sdk/python/market-pages
Navigate and filter markets with the Python SDK
## Overview
The `MarketPageFetcher` class provides access to the Market Navigation API — a hierarchical system for browsing markets by category, applying dynamic filters, and paginating results. All endpoints are public and require no authentication.
## Setup
```python theme={null}
from limitless_sdk.api import HttpClient
from limitless_sdk.market_pages import MarketPageFetcher
http_client = HttpClient(base_url="https://api.limitless.exchange")
page_fetcher = MarketPageFetcher(http_client)
```
## Navigation Tree
Fetch the full navigation hierarchy. Each node represents a browseable category with a URL path.
```python theme={null}
navigation = await page_fetcher.get_navigation()
for node in navigation:
print(f"{node.name} → {node.path}")
for child in node.children:
print(f" {child.name} → {child.path}")
```
### NavigationNode Fields
| Field | Type | Description |
| ---------- | ---------------------- | ------------------------------ |
| `id` | `str` | Unique identifier |
| `name` | `str` | Display name |
| `slug` | `str` | URL-friendly identifier |
| `path` | `str` | Full URL path (e.g. `/crypto`) |
| `icon` | `str \| None` | Optional icon name |
| `children` | `list[NavigationNode]` | Nested child nodes |
## Resolving a Page by Path
Convert a URL path into a `MarketPage` with its filters, metadata, and breadcrumb. The SDK handles 301 redirects internally.
```python theme={null}
page = await page_fetcher.get_market_page_by_path("/crypto")
print(f"Page: {page.name}")
print(f"Filters: {len(page.filter_groups)} groups")
print(f"Breadcrumb: {' > '.join(b.name for b in page.breadcrumb)}")
```
### MarketPage Fields
| Field | Type | Description |
| --------------- | ---------------------- | ------------------------------------------ |
| `id` | `str` | Page identifier (used for `get_markets()`) |
| `name` | `str` | Display name |
| `slug` | `str` | URL-friendly identifier |
| `full_path` | `str` | Full URL path |
| `description` | `str \| None` | Page description |
| `base_filter` | `dict` | Default filter applied to this page |
| `filter_groups` | `list[FilterGroup]` | Available filter groups |
| `metadata` | `dict` | Page metadata |
| `breadcrumb` | `list[BreadcrumbItem]` | Navigation breadcrumb |
## Fetching Markets for a Page
Use `get_markets()` with the page ID to fetch markets. Supports offset and cursor pagination, sorting, and dynamic filters.
### Offset Pagination
```python theme={null}
result = await page_fetcher.get_markets(page.id, {
"page": 1,
"limit": 20,
"sort": "-updatedAt",
})
for market in result.data:
print(f"{market.slug} — {market.title}")
print(f"Page {result.pagination.page} of {result.pagination.total_pages}")
```
### Cursor Pagination
```python theme={null}
first_page = await page_fetcher.get_markets(page.id, {
"cursor": "",
"limit": 20,
"sort": "-updatedAt",
})
for market in first_page.data:
print(market.slug)
if first_page.cursor.next_cursor:
next_page = await page_fetcher.get_markets(page.id, {
"cursor": first_page.cursor.next_cursor,
"limit": 20,
})
```
You cannot use `cursor` and `page` in the same request. Choose one pagination strategy.
### Filtering
Pass a `filters` dictionary. Values can be a single string or a list for multi-select filters.
```python theme={null}
# Single filter
result = await page_fetcher.get_markets(page.id, {
"filters": {"ticker": "btc"},
})
# Multiple values (OR logic)
result = await page_fetcher.get_markets(page.id, {
"filters": {"ticker": ["btc", "eth"]},
})
# Combined filters
result = await page_fetcher.get_markets(page.id, {
"limit": 10,
"sort": "-updatedAt",
"filters": {
"ticker": ["btc", "eth"],
"duration": "hourly",
},
})
```
### Parameters
| Parameter | Type | Description |
| --------- | ------ | ----------------------------------------------------------------- |
| `page` | `int` | Page number (offset pagination) |
| `limit` | `int` | Results per page |
| `sort` | `str` | Sort field with optional `-` prefix for descending |
| `cursor` | `str` | Cursor token (cursor pagination). Use `""` for the first request. |
| `filters` | `dict` | Filter key-value pairs. Values can be `str` or `list[str]`. |
### Sort Options
| Value | Description |
| -------------------------- | ---------------------------------------------- |
| `createdAt` / `-createdAt` | Sort by creation date (ascending / descending) |
| `updatedAt` / `-updatedAt` | Sort by last update |
| `deadline` / `-deadline` | Sort by expiration deadline |
| `id` / `-id` | Sort by market ID |
## Property Keys
Property keys define the available filter dimensions (e.g. "ticker", "duration"). Each key has a set of options.
```python theme={null}
# List all property keys
keys = await page_fetcher.get_property_keys()
for key in keys:
print(f"{key.name} ({key.type})")
if key.options:
for opt in key.options[:3]:
print(f" {opt.label}: {opt.value}")
```
### Single Key and Options
```python theme={null}
key = await page_fetcher.get_property_key(keys[0].id)
print(f"{key.name} — {len(key.options or [])} options")
# Fetch options separately (supports parent filtering for hierarchical keys)
options = await page_fetcher.get_property_options(key.id)
# Child options filtered by parent
child_options = await page_fetcher.get_property_options(key.id, parent_id=options[0].id)
```
### PropertyKey Fields
| Field | Type | Description |
| ------------ | ------------------------------ | --------------------------------------- |
| `id` | `str` | Unique identifier |
| `name` | `str` | Display name |
| `slug` | `str` | URL-friendly identifier |
| `type` | `str` | `"select"` or `"multi-select"` |
| `is_system` | `bool` | Whether this is a system-defined key |
| `options` | `list[PropertyOption] \| None` | Available options (when fetched inline) |
| `metadata` | `dict` | Key metadata |
| `created_at` | `str` | ISO 8601 creation timestamp |
| `updated_at` | `str` | ISO 8601 last update timestamp |
## Complete Example
```python theme={null}
import asyncio
from limitless_sdk.api import HttpClient
from limitless_sdk.market_pages import MarketPageFetcher
async def main():
http_client = HttpClient(base_url="https://api.limitless.exchange")
page_fetcher = MarketPageFetcher(http_client)
# Browse the navigation tree
navigation = await page_fetcher.get_navigation()
print(f"Top-level categories: {len(navigation)}")
# Resolve a page
page = await page_fetcher.get_market_page_by_path("/crypto")
print(f"\nPage: {page.name}")
print(f"Available filters: {[fg.name for fg in page.filter_groups]}")
# Fetch markets with filters
result = await page_fetcher.get_markets(page.id, {
"limit": 5,
"sort": "-updatedAt",
"filters": {"ticker": ["btc", "eth"]},
})
for market in result.data:
print(f" {market.slug} — {market.title}")
# Explore property keys
keys = await page_fetcher.get_property_keys()
for key in keys:
print(f"\n{key.name} ({key.type}):")
options = await page_fetcher.get_property_options(key.id)
for opt in options[:5]:
print(f" {opt.label}")
await http_client.close()
asyncio.run(main())
```
# Markets
Source: https://docs.limitless.exchange/developers/sdk/python/markets
Market discovery and orderbook data with the Python SDK
## Overview
The `MarketFetcher` class handles market discovery, individual market lookups, and orderbook retrieval. It also automatically caches venue contract addresses needed for order signing.
## Setup
```python theme={null}
from limitless_sdk.api import HttpClient
from limitless_sdk.markets import MarketFetcher
http_client = HttpClient() # loads LIMITLESS_API_KEY from env
market_fetcher = MarketFetcher(http_client)
```
## Fetching Active Markets
Use `get_active_markets()` to retrieve a paginated list of all active markets:
```python theme={null}
params = {
"page": 1,
"limit": 10,
"sortBy": "volume",
}
result = await market_fetcher.get_active_markets(params)
for market in result["data"]:
print(market["title"], market["slug"])
```
| Parameter | Type | Default | Description |
| --------- | ----- | ---------- | ------------------------------------------- |
| `page` | `int` | `1` | Page number for pagination |
| `limit` | `int` | `10` | Number of markets per page |
| `sortBy` | `str` | `"volume"` | Sort order (e.g. `"volume"`, `"createdAt"`) |
Use pagination parameters to avoid large responses. Start with a small `limit` and increment `page` as needed.
## Fetching a Single Market
Use `get_market(slug)` to retrieve full details for a specific market:
```python theme={null}
market = await market_fetcher.get_market("btc-above-100k-march-2025")
print(market.title)
print("YES token:", market.tokens.yes)
print("NO token:", market.tokens.no)
print("Exchange:", market.venue.exchange)
print("Adapter:", market.venue.adapter)
```
The returned `Market` object is a Pydantic model with the following key fields:
| Field | Type | Description |
| ----------------- | ------------------- | --------------------------------------------------------------- |
| `title` | `str` | Human-readable market title |
| `slug` | `str` | URL-friendly market identifier |
| `tokens.yes` | `str` | Token ID for the YES outcome |
| `tokens.no` | `str` | Token ID for the NO outcome |
| `venue.exchange` | `str` | Exchange contract address (used as EIP-712 `verifyingContract`) |
| `venue.adapter` | `str` | Adapter contract address (used for NegRisk token approvals) |
| `open_interest` | `str \| None` | Open interest value |
| `liquidity` | `str \| None` | Liquidity value |
| `image_url` | `str \| None` | Market image URL |
| `automation_type` | `str \| None` | Market automation type (`"manual"`, `"lumy"`, or `"sports"`) |
| `position_ids` | `list[str] \| None` | Position IDs (alternative to `tokens.yes` / `tokens.no`) |
Token IDs are returned as **strings**. Pass them directly to `OrderClient.create_order()` as `token_id`.
## Fetching the Orderbook
Use `get_orderbook(slug)` to retrieve current bids and asks for a market:
```python theme={null}
orderbook = await market_fetcher.get_orderbook("btc-above-100k-march-2025")
print("Bids:")
for bid in orderbook["bids"]:
print(f" Price: {bid['price']} Size: {bid['size']}")
print("Asks:")
for ask in orderbook["asks"]:
print(f" Price: {ask['price']} Size: {ask['size']}")
```
## Venue Caching
Every call to `get_market()` automatically caches the venue contract addresses (exchange and adapter) for that market. The `OrderClient` reads from this cache when signing orders, so you do not need to manage venues manually.
Calling `get_market(slug)` retrieves and caches the venue:
```python theme={null}
market = await market_fetcher.get_market("btc-above-100k-march-2025")
# Venue is now cached for this slug
```
The `OrderClient` automatically looks up the cached venue when you pass `market_slug`:
```python theme={null}
await order_client.create_order(
token_id=market.tokens.yes,
price=0.65,
size=10.0,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-above-100k-march-2025",
)
```
You **must** call `get_market(slug)` at least once before placing orders on that market. Without a cached venue, the `OrderClient` cannot determine the correct `verifyingContract` for EIP-712 signing.
## Debugging Venue Cache
Enable `DEBUG` logging to see venue cache operations:
```python theme={null}
from limitless_sdk.types import ConsoleLogger, LogLevel
logger = ConsoleLogger(level=LogLevel.DEBUG)
# Output will include lines like:
# [DEBUG] Venue cached for btc-above-100k-march-2025: exchange=0xA1b2... adapter=0xD4e5...
# [DEBUG] Venue cache hit for btc-above-100k-march-2025
```
## Complete Example
```python theme={null}
import asyncio
from limitless_sdk.api import HttpClient
from limitless_sdk.markets import MarketFetcher
async def main():
http_client = HttpClient()
market_fetcher = MarketFetcher(http_client)
# List active markets
active = await market_fetcher.get_active_markets({"limit": 5})
for m in active["data"]:
print(f"{m['title']} — {m['slug']}")
# Get a specific market
slug = active["data"][0]["slug"]
market = await market_fetcher.get_market(slug)
print(f"\nMarket: {market.title}")
print(f"YES token: {market.tokens.yes}")
print(f"NO token: {market.tokens.no}")
# Fetch the orderbook
orderbook = await market_fetcher.get_orderbook(slug)
print(f"\nOrderbook — {len(orderbook['bids'])} bids, {len(orderbook['asks'])} asks")
await http_client.close()
asyncio.run(main())
```
# Trading & Orders
Source: https://docs.limitless.exchange/developers/sdk/python/orders
Create and manage orders with the Python SDK
## Overview
The `OrderClient` handles order creation, EIP-712 signing, and order management. It supports Good-Till-Cancelled (GTC), Fill-And-Kill (FAK), and Fill-or-Kill (FOK) orders.
## Prerequisites
Before placing orders, you need four components:
```python theme={null}
import asyncio
from eth_account import Account
from limitless_sdk.api import HttpClient
from limitless_sdk.markets import MarketFetcher
from limitless_sdk.orders import OrderClient
from limitless_sdk.types import Side, OrderType
http_client = HttpClient() # loads LIMITLESS_API_KEY from env
account = Account.from_key("0xYOUR_PRIVATE_KEY")
market_fetcher = MarketFetcher(http_client)
order_client = OrderClient(http_client, account)
```
| Component | Purpose |
| --------------- | ---------------------------------------------- |
| `HttpClient` | Authenticated HTTP client for API requests |
| `Account` | eth-account wallet for EIP-712 order signing |
| `MarketFetcher` | Fetches market data and caches venue addresses |
| `OrderClient` | Creates, signs, and submits orders |
The `OrderClient` constructor automatically fetches your user profile data (`userData`) from the API. This is used to populate the `ownerId` field on submitted orders.
## Token Approvals
Before your first trade on a given venue, you must approve the exchange contracts to spend your tokens. This is a **one-time on-chain setup** per venue.
For standard CLOB markets, approve USDC and Conditional Tokens to the **exchange** contract:
```python theme={null}
from web3 import Web3
w3 = Web3(Web3.HTTPProvider("https://mainnet.base.org"))
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
CT_ADDRESS = "0xC9c98965297Bc527861c898329Ee280632B76e18" # Conditional Token framework address
market = await market_fetcher.get_market("your-market-slug")
exchange = market.venue.exchange
# Approve USDC for BUY orders
usdc_contract = w3.eth.contract(
address=Web3.to_checksum_address(USDC_ADDRESS),
abi=[{
"name": "approve",
"type": "function",
"inputs": [
{"name": "spender", "type": "address"},
{"name": "amount", "type": "uint256"},
],
"outputs": [{"name": "", "type": "bool"}],
}],
)
tx = usdc_contract.functions.approve(
Web3.to_checksum_address(exchange),
2**256 - 1, # max approval
).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
})
signed = w3.eth.account.sign_transaction(tx, account.key)
w3.eth.send_raw_transaction(signed.raw_transaction)
# Approve Conditional Tokens for SELL orders
ct_contract = w3.eth.contract(
address=Web3.to_checksum_address(CT_ADDRESS),
abi=[{
"name": "setApprovalForAll",
"type": "function",
"inputs": [
{"name": "operator", "type": "address"},
{"name": "approved", "type": "bool"},
],
"outputs": [],
}],
)
tx = ct_contract.functions.setApprovalForAll(
Web3.to_checksum_address(exchange),
True,
).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
})
signed = w3.eth.account.sign_transaction(tx, account.key)
w3.eth.send_raw_transaction(signed.raw_transaction)
```
For NegRisk markets, you must additionally approve the **adapter** contract:
```python theme={null}
market = await market_fetcher.get_market("your-negrisk-slug")
exchange = market.venue.exchange
adapter = market.venue.adapter
# 1. Approve USDC to the exchange (same as CLOB)
# ... (see Standard CLOB tab)
# 2. Approve Conditional Tokens to the exchange
# ... (see Standard CLOB tab)
# 3. Approve Conditional Tokens to the adapter (NegRisk only)
tx = ct_contract.functions.setApprovalForAll(
Web3.to_checksum_address(adapter),
True,
).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
})
signed = w3.eth.account.sign_transaction(tx, account.key)
w3.eth.send_raw_transaction(signed.raw_transaction)
```
Approvals are on-chain transactions that cost gas. You only need to perform them once per venue. Use `venue.exchange` for both CLOB and NegRisk, and additionally `venue.adapter` for NegRisk markets.
## GTC Orders (Good-Till-Cancelled)
GTC orders remain on the orderbook until filled or explicitly cancelled. Specify `price` (in dollars) and `size` (number of shares):
```python theme={null}
market = await market_fetcher.get_market("btc-above-100k-march-2025")
result = await order_client.create_order(
token_id=market.tokens.yes,
price=0.65,
size=10.0,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-above-100k-march-2025",
)
print("Order placed:", result)
```
### Post-only GTC order
Use `post_only=True` to ensure your order is never filled immediately as a taker. If the order would cross the spread (i.e., match against existing orders), it is **rejected** instead. This guarantees you always receive maker fees.
```python theme={null}
result = await order_client.create_order(
token_id=market.tokens.yes,
price=0.65,
size=10.0,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-above-100k-march-2025",
post_only=True,
)
```
| Parameter | Type | Description |
| ------------- | ----------- | ---------------------------------------------------------------------------------------- |
| `token_id` | `str` | Token ID from `market.tokens.yes` or `market.tokens.no` |
| `price` | `float` | Price per share in dollars (0.01 to 0.99) |
| `size` | `float` | Number of shares to buy or sell |
| `side` | `Side` | `Side.BUY` or `Side.SELL` |
| `order_type` | `OrderType` | `OrderType.GTC` |
| `market_slug` | `str` | Market slug for venue lookup |
| `post_only` | `bool` | Optional. When `True`, rejects the order if it would immediately match. Default `False`. |
## FAK Orders (Fill-And-Kill)
FAK orders use the same `price` and `size` inputs as GTC, but they only consume immediately available liquidity and cancel any unmatched remainder.
`post_only` is not supported for FAK orders.
```python theme={null}
response = await order_client.create_order(
token_id=market.tokens.yes,
price=0.45,
size=10.0,
side=Side.BUY,
order_type=OrderType.FAK,
market_slug="btc-above-100k-march-2025",
)
if response.maker_matches:
print(f"FAK order matched immediately with {len(response.maker_matches)} fill(s)")
else:
print("FAK remainder was cancelled.")
```
| Parameter | Type | Description |
| ------------- | ----------- | ------------------------------------------------------- |
| `token_id` | `str` | Token ID from `market.tokens.yes` or `market.tokens.no` |
| `price` | `float` | Price per share in dollars (0.01 to 0.99) |
| `size` | `float` | Number of shares to buy or sell |
| `side` | `Side` | `Side.BUY` or `Side.SELL` |
| `order_type` | `OrderType` | `OrderType.FAK` |
| `market_slug` | `str` | Market slug for venue lookup |
## FOK Orders (Fill-or-Kill)
FOK orders execute immediately and fully, or are rejected entirely. Instead of `price` and `size`, you specify `maker_amount`:
When buying, `maker_amount` is the **total USDC you want to spend**. The exchange fills as many shares as possible at the best available price:
```python theme={null}
result = await order_client.create_order(
token_id=market.tokens.yes,
maker_amount=10.0, # spend 10 USDC
side=Side.BUY,
order_type=OrderType.FOK,
market_slug="btc-above-100k-march-2025",
)
```
When selling, `maker_amount` is the **number of shares to sell**. The exchange returns USDC at the best available price:
```python theme={null}
result = await order_client.create_order(
token_id=market.tokens.yes,
maker_amount=10.0, # sell 10 shares
side=Side.SELL,
order_type=OrderType.FOK,
market_slug="btc-above-100k-march-2025",
)
```
## Cancelling Orders
Cancel a specific order by its ID:
```python theme={null}
await order_client.cancel(order_id="abc123-def456")
```
Cancel every open order you have on a given market:
```python theme={null}
await order_client.cancel_all(market_slug="btc-above-100k-march-2025")
```
## Enums Reference
### Side
| Value | Description |
| ----------- | -------------------- |
| `Side.BUY` | Buy shares with USDC |
| `Side.SELL` | Sell shares for USDC |
### OrderType
| Value | Description |
| --------------- | ----------------------------------------------------------- |
| `OrderType.GTC` | Good-Till-Cancelled limit order (rests on the book) |
| `OrderType.FAK` | Fill-And-Kill limit order (cancels any unmatched remainder) |
| `OrderType.FOK` | Fill-or-Kill market order (fills immediately or rejects) |
## Error Handling
The SDK raises `APIError` for non-2xx responses. Always wrap order calls in try/except:
```python theme={null}
from limitless_sdk.api import APIError
try:
result = await order_client.create_order(
token_id=market.tokens.yes,
price=0.65,
size=10.0,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-above-100k-march-2025",
)
except APIError as e:
print(f"Order failed — status {e.status_code}: {e.message}")
```
See [Error Handling & Retry](/developers/sdk/python/error-handling) for details on `APIError` fields and the `@retry_on_errors` decorator.
## Complete Example
```python theme={null}
import asyncio
from eth_account import Account
from limitless_sdk.api import HttpClient, APIError
from limitless_sdk.markets import MarketFetcher
from limitless_sdk.orders import OrderClient
from limitless_sdk.types import Side, OrderType
async def main():
http_client = HttpClient()
account = Account.from_key("0xYOUR_PRIVATE_KEY")
market_fetcher = MarketFetcher(http_client)
order_client = OrderClient(http_client, account)
try:
# Fetch market (caches venue automatically)
market = await market_fetcher.get_market("btc-above-100k-march-2025")
# Place a GTC BUY order for 10 YES shares at $0.65
result = await order_client.create_order(
token_id=market.tokens.yes,
price=0.65,
size=10.0,
side=Side.BUY,
order_type=OrderType.GTC,
market_slug="btc-above-100k-march-2025",
)
print("GTC order placed:", result)
# Place a FAK BUY order for 10 YES shares at $0.45
fak_result = await order_client.create_order(
token_id=market.tokens.yes,
price=0.45,
size=10.0,
side=Side.BUY,
order_type=OrderType.FAK,
market_slug="btc-above-100k-march-2025",
)
print("FAK order placed:", fak_result)
# Place a FOK BUY order spending 5 USDC
fok_result = await order_client.create_order(
token_id=market.tokens.yes,
maker_amount=5.0,
side=Side.BUY,
order_type=OrderType.FOK,
market_slug="btc-above-100k-march-2025",
)
print("FOK order placed:", fok_result)
# Cancel all orders on this market
await order_client.cancel_all(market_slug="btc-above-100k-march-2025")
print("All orders cancelled")
except APIError as e:
print(f"API error — status {e.status_code}: {e.message}")
finally:
await http_client.close()
asyncio.run(main())
```
Always call `await http_client.close()` when finished. Failing to close the client can leave open connections and cause resource leaks.
# Partner Accounts
Source: https://docs.limitless.exchange/developers/sdk/python/partner-accounts
Create and manage sub-accounts with the Python SDK
The `PartnerAccountService` creates sub-account profiles linked to the authenticated partner. Requires HMAC authentication with the `account_creation` scope.
## Access
```python theme={null}
from limitless_sdk import Client, HMACCredentials
client = Client(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(token_id=token_id, secret=secret),
)
# Use client.partner_accounts.*
```
## Server wallet mode
Creates a managed Privy wallet for the sub-account. Enables [delegated signing](/developers/programmatic-api#sub-account-modes) — the partner submits unsigned orders and the server signs them.
```python theme={null}
from limitless_sdk import CreatePartnerAccountInput
account = await client.partner_accounts.create_account(
CreatePartnerAccountInput(
display_name="user-alice",
create_server_wallet=True,
)
)
print(account.profile_id) # numeric profile ID
print(account.account) # wallet address
```
New server wallets should be checked with `check_allowances()` before the first delegated trade. If retryable targets are missing or failed, call `retry_allowances()` and poll again.
## Allowance recovery
Server-wallet sub-accounts need delegated-trading approvals before they can trade. The partner allowance helpers use the Partner API only:
* `check_allowances(profile_id)` calls `GET /profiles/partner-accounts/:profileId/allowances`
* `retry_allowances(profile_id)` calls `POST /profiles/partner-accounts/:profileId/allowances/retry`
* both methods require HMAC credentials with `account_creation` and `delegated_signing`
* `profile_id` is the child/server-wallet profile id
```python theme={null}
from limitless_sdk import ConflictError, RateLimitError
allowances = await client.partner_accounts.check_allowances(account.profile_id)
if not allowances.ready:
retryable_targets = [
target
for target in allowances.targets
if target.retryable and target.status in {"missing", "failed"}
]
if retryable_targets:
try:
allowances = await client.partner_accounts.retry_allowances(account.profile_id)
except RateLimitError as error:
print(error.response_data.get("retryAfterSeconds"))
except ConflictError:
print("Retry already running; poll check_allowances again shortly.")
if any(target.status == "submitted" for target in allowances.targets):
# A sponsored tx/user operation was submitted by this retry request.
# Poll check_allowances() again after a short delay to observe confirmed chain state.
pass
```
Recommended partner flow:
1. Poll `check_allowances(profile_id)`.
2. If `ready is True`, continue.
3. If targets are `missing` or `failed` with `retryable=True`, call `retry_allowances(profile_id)`.
4. If retry returns `submitted` targets, poll `check_allowances()` again after a short delay.
5. If retry returns `429`, wait `retryAfterSeconds`.
6. If retry returns `409`, wait briefly and call `check_allowances()` again.
## Withdrawal address allowlist
Explicit treasury destinations for server-wallet withdrawals must be allowlisted on the authenticated partner profile unless the destination is already the partner account or partner smart wallet. Allowlist management uses a Privy identity token, not API-token/HMAC auth.
```python theme={null}
import os
from limitless_sdk import PartnerWithdrawalAddressInput
identity_token = os.environ["PRIVY_IDENTITY_TOKEN"]
treasury_address = "0x0F3262730c909408042F9Da345a916dc0e1F9787"
entry = await client.partner_accounts.add_withdrawal_address(
identity_token,
PartnerWithdrawalAddressInput(address=treasury_address, label="treasury"),
)
print(entry.destination_address)
await client.partner_accounts.delete_withdrawal_address(identity_token, treasury_address)
```
`add_withdrawal_address()` and `delete_withdrawal_address()` call `POST /portfolio/withdrawal-addresses` and `DELETE /portfolio/withdrawal-addresses/:address` with the `identity: Bearer ` header. `POST /portfolio/withdraw` still uses HMAC auth with the `withdrawal` scope.
## EOA mode
Creates a profile for an externally-owned address. The end user manages their own keys and signs orders themselves.
EOA mode requires wallet ownership verification headers:
```python theme={null}
from limitless_sdk import CreatePartnerAccountInput, CreatePartnerAccountEOAHeaders
account = await client.partner_accounts.create_account(
CreatePartnerAccountInput(display_name="user-bob"),
eoa_headers=CreatePartnerAccountEOAHeaders(
account="0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
signing_message="0x...",
signature="0x...",
),
)
```
## Validation
* `display_name` is optional, max 44 characters. Defaults to the wallet address if omitted.
* Returns `409 Conflict` if a profile already exists for the target address.
* Cannot create a sub-account for the partner's own address.
* The SDK validates `display_name` length locally before sending the request.
# Portfolio & Positions
Source: https://docs.limitless.exchange/developers/sdk/python/portfolio
Track positions and history with the Python SDK
## Overview
The `PortfolioFetcher` class retrieves your open positions and portfolio data from the Limitless Exchange API. It requires an authenticated `HttpClient` with a valid API key.
## Setup
```python theme={null}
from limitless_sdk.api import HttpClient
from limitless_sdk.portfolio import PortfolioFetcher
http_client = HttpClient() # loads LIMITLESS_API_KEY from env
portfolio = PortfolioFetcher(http_client)
```
`PortfolioFetcher` requires an authenticated client. Ensure your API key is set via the `LIMITLESS_API_KEY` environment variable or the `api_key` parameter on `HttpClient`.
## Fetching Positions
Use `get_positions()` to retrieve all your open positions:
```python theme={null}
positions = await portfolio.get_positions()
```
The response is a dictionary with the following top-level keys:
| Key | Type | Description |
| -------------------- | ------------ | ------------------------------------------------- |
| `clob` | `list[dict]` | Positions on CLOB (order book) markets |
| `amm` | `list[dict]` | Positions on AMM (automated market maker) markets |
| `accumulativePoints` | `dict` | Accumulated points and rewards data |
## Iterating CLOB Positions
Each entry in the `clob` list contains the market details and your position size:
```python theme={null}
positions = await portfolio.get_positions()
for pos in positions.get("clob", []):
market_title = pos["market"]["title"]
size = pos["size"]
outcome = pos.get("outcomeIndex", "N/A")
print(f"{market_title} — size: {size}, outcome index: {outcome}")
```
```python theme={null}
{
"clob": [
{
"market": {
"title": "BTC above 100k by March 2025?",
"slug": "btc-above-100k-march-2025",
},
"size": "15.5",
"outcomeIndex": 0,
# ... additional fields
},
# ... more positions
],
"amm": [
# ... AMM positions
],
"accumulativePoints": {
# ... points data
},
}
```
The `PortfolioFetcher` returns **raw API responses** without heavy parsing or transformation. Field names and types match the REST API directly. Refer to the [API Reference](/api-reference/portfolio/positions) for the complete response schema.
## Complete Example
```python theme={null}
import asyncio
from limitless_sdk.api import HttpClient
from limitless_sdk.portfolio import PortfolioFetcher
async def main():
http_client = HttpClient()
portfolio = PortfolioFetcher(http_client)
positions = await portfolio.get_positions()
clob_positions = positions.get("clob", [])
amm_positions = positions.get("amm", [])
print(f"CLOB positions: {len(clob_positions)}")
for pos in clob_positions:
print(f" {pos['market']['title']} — size: {pos['size']}")
print(f"\nAMM positions: {len(amm_positions)}")
for pos in amm_positions:
print(f" {pos['market']['title']} — size: {pos['size']}")
points = positions.get("accumulativePoints", {})
print(f"\nAccumulative points: {points}")
await http_client.close()
asyncio.run(main())
```
# WebSocket Streaming
Source: https://docs.limitless.exchange/developers/sdk/python/websocket
Real-time orderbook updates with the Python SDK
## Overview
The `WebSocketClient` provides real-time streaming of orderbook updates and price data over a persistent WebSocket connection. It supports automatic reconnection and event-driven message handling via decorators.
## Setup
```python theme={null}
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
config = WebSocketConfig(
url="wss://ws.limitless.exchange",
auto_reconnect=True,
reconnect_delay=5, # seconds between reconnection attempts
)
ws_client = WebSocketClient(config)
```
| Parameter | Type | Default | Description |
| ----------------- | ------ | -------- | --------------------------------------------- |
| `url` | `str` | Required | WebSocket server URL |
| `auto_reconnect` | `bool` | `True` | Automatically reconnect on disconnection |
| `reconnect_delay` | `int` | `5` | Seconds to wait between reconnection attempts |
## Event Handlers
Register handlers for specific events using the `@ws_client.on()` decorator:
```python theme={null}
@ws_client.on("connect")
async def on_connect():
print("Connected to WebSocket")
@ws_client.on("orderbookUpdate")
async def on_orderbook(data):
print("Orderbook update:", data)
@ws_client.on("newPriceData")
async def on_price(data):
print("Price update:", data)
```
### Available Events
| Event | Payload | Description |
| ----------------- | ------- | -------------------------------------------------- |
| `connect` | None | Fired when the WebSocket connection is established |
| `orderbookUpdate` | `dict` | Orderbook changes (new bids/asks, removals) |
| `newPriceData` | `dict` | Latest price data for subscribed markets |
## Subscribing to Markets
After connecting, subscribe to specific markets to receive updates:
```python theme={null}
@ws_client.on("connect")
async def on_connect():
await ws_client.subscribe(
"subscribe_market_prices",
{"marketSlugs": ["btc-above-100k-march-2025"]},
)
print("Subscribed to market prices")
```
The `subscribe()` method takes two arguments:
| Parameter | Type | Description |
| --------- | ------ | ---------------------------------------------------------- |
| `event` | `str` | Subscription event name (e.g. `"subscribe_market_prices"`) |
| `data` | `dict` | Subscription parameters including `marketSlugs` |
You can subscribe to multiple markets at once by passing a list of slugs in `marketSlugs`.
## Auto-Reconnect
When `auto_reconnect` is enabled (the default), the client automatically reconnects after a disconnection:
1. The connection drops (network issue, server restart, etc.)
2. The client waits `reconnect_delay` seconds
3. A new connection is established
4. The `connect` event fires again, so your subscription logic re-executes
Place your `subscribe()` calls inside the `connect` handler to ensure subscriptions are restored after every reconnection.
## Complete Example
```python theme={null}
import asyncio
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
async def main():
config = WebSocketConfig(
url="wss://ws.limitless.exchange",
auto_reconnect=True,
reconnect_delay=5,
)
ws_client = WebSocketClient(config)
@ws_client.on("connect")
async def on_connect():
print("Connected")
await ws_client.subscribe(
"subscribe_market_prices",
{"marketSlugs": ["btc-above-100k-march-2025"]},
)
@ws_client.on("orderbookUpdate")
async def on_orderbook(data):
slug = data.get("marketSlug", "unknown")
bids = len(data.get("bids", []))
asks = len(data.get("asks", []))
print(f"[{slug}] Orderbook: {bids} bids, {asks} asks")
@ws_client.on("newPriceData")
async def on_price(data):
slug = data.get("marketSlug", "unknown")
price = data.get("price", "N/A")
print(f"[{slug}] Price: {price}")
# Start the WebSocket client (runs until interrupted)
await ws_client.connect()
asyncio.run(main())
```
The `await ws_client.connect()` call blocks the event loop until the connection is closed or the program is interrupted. Run it as the last statement in your async function, or use `asyncio.create_task()` if you need to run other coroutines concurrently.
# API Tokens
Source: https://docs.limitless.exchange/developers/sdk/rust/api-tokens
Manage scoped API tokens with the Rust SDK
The `ApiTokenService` handles the partner self-service token lifecycle: capability checks, token derivation, listing active tokens, and revocation.
Token derivation and capability queries require a **Privy identity token**. The SDK does not obtain this token for you — your application must authenticate the partner via Privy and pass the resulting token.
## Access
The service is available on the root `Client`:
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
// Use client.api_tokens.*
```
## Get partner capabilities
Check whether token management is enabled and which scopes are allowed:
```rust theme={null}
let capabilities = client
.api_tokens
.get_capabilities(identity_token)
.await?;
println!("{}", capabilities.token_management_enabled);
println!("{:?}", capabilities.allowed_scopes);
```
## Derive a token
Create a new scoped API token. The `secret` is returned once — store it securely.
```rust theme={null}
use limitless_exchange_rust_sdk::{
DeriveApiTokenInput, SCOPE_ACCOUNT_CREATION, SCOPE_DELEGATED_SIGNING, SCOPE_TRADING,
};
let derived = client
.api_tokens
.derive_token(
identity_token,
&DeriveApiTokenInput {
label: Some("production-bot".to_string()),
scopes: vec![
SCOPE_TRADING.to_string(),
SCOPE_ACCOUNT_CREATION.to_string(),
SCOPE_DELEGATED_SIGNING.to_string(),
],
},
)
.await?;
println!("{}", derived.token_id);
println!("{:?}", derived.scopes);
```
### Create an HMAC-authenticated client
After deriving a token, create a new client with those HMAC credentials:
```rust theme={null}
use limitless_exchange_rust_sdk::{Client, HmacCredentials};
let http = Client::builder()
.hmac_credentials(HmacCredentials {
token_id: derived.token_id.clone(),
secret: derived.secret.clone(),
})
.build()?;
let scoped_client = Client::from_http_client(http)?;
```
If `scopes` is omitted when deriving the token, the backend defaults it to `["trading"]`.
## List active tokens
Returns all non-revoked tokens for the authenticated partner:
```rust theme={null}
let tokens = scoped_client.api_tokens.list_tokens().await?;
for token in tokens {
println!(
"{} {:?} {:?} {:?}",
token.token_id,
token.label,
token.scopes,
token.last_used_at
);
}
```
## Revoke a token
Immediately invalidate a token:
```rust theme={null}
let message = scoped_client.api_tokens.revoke_token(&derived.token_id).await?;
println!("{}", message);
```
## Scope constants
The Rust SDK exports typed string constants:
| Constant | Value |
| ------------------------- | --------------------- |
| `SCOPE_TRADING` | `"trading"` |
| `SCOPE_ACCOUNT_CREATION` | `"account_creation"` |
| `SCOPE_DELEGATED_SIGNING` | `"delegated_signing"` |
| `SCOPE_WITHDRAWAL` | `"withdrawal"` |
# Delegated Orders
Source: https://docs.limitless.exchange/developers/sdk/rust/delegated-orders
Place orders on behalf of sub-accounts with the Rust SDK
The `DelegatedOrderService` enables partners with the `delegated_signing` scope to place and cancel orders on behalf of sub-accounts. The server signs orders using the sub-account's managed wallet, so the partner does not handle private keys directly.
Delegated signing requires a sub-account created with `create_server_wallet: Some(true)`. EOA sub-accounts sign their own orders.
**Recommended setup:** Store your HMAC credentials (`token_id` / `secret`) on your backend. Use this SDK server-side to sign partner-authenticated requests. Expose only your own app-specific endpoints to the frontend. Never expose HMAC secrets in browser bundles or client-side storage.
## Access
```rust theme={null}
use limitless_exchange_rust_sdk::{Client, HmacCredentials};
let http = Client::builder()
.hmac_credentials(HmacCredentials {
token_id: token_id.to_string(),
secret: secret.to_string(),
})
.build()?;
let client = Client::from_http_client(http)?;
// Use client.delegated_orders.*
```
## Create a GTC delegated order
```rust theme={null}
use limitless_exchange_rust_sdk::{
CreateDelegatedOrderParams, GtcOrderArgs, OrderType, Side,
};
let on_behalf_of = 12345;
let token_id = "123456789".to_string();
let response = client
.delegated_orders
.create_order(CreateDelegatedOrderParams {
market_slug: "btc-100k".to_string(),
order_type: OrderType::Gtc,
on_behalf_of,
fee_rate_bps: 300,
args: GtcOrderArgs {
token_id: token_id.clone(),
side: Side::Buy,
price: 0.55,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
post_only: false,
}
.into(),
})
.await?;
println!("{}", response.order.id);
```
Use `post_only: true` when the order must rest on the book:
```rust theme={null}
let response = client
.delegated_orders
.create_order(CreateDelegatedOrderParams {
market_slug: "btc-100k".to_string(),
order_type: OrderType::Gtc,
on_behalf_of,
fee_rate_bps: 300,
args: GtcOrderArgs {
token_id: token_id.clone(),
side: Side::Buy,
price: 0.55,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
post_only: true,
}
.into(),
})
.await?;
```
## Create a FAK delegated order
FAK orders use `price` and `size`, then cancel any unmatched remainder:
```rust theme={null}
use limitless_exchange_rust_sdk::{CreateDelegatedOrderParams, FakOrderArgs, OrderType, Side};
let response = client
.delegated_orders
.create_order(CreateDelegatedOrderParams {
market_slug: "btc-100k".to_string(),
order_type: OrderType::Fak,
on_behalf_of,
fee_rate_bps: 300,
args: FakOrderArgs {
token_id: token_id.clone(),
side: Side::Buy,
price: 0.45,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
}
.into(),
})
.await?;
println!("maker matches {}", response.maker_matches.len());
```
## Create a FOK delegated order
FOK orders execute immediately or cancel entirely. They use `maker_amount`:
```rust theme={null}
use limitless_exchange_rust_sdk::{CreateDelegatedOrderParams, FokOrderArgs, OrderType, Side};
let response = client
.delegated_orders
.create_order(CreateDelegatedOrderParams {
market_slug: "btc-100k".to_string(),
order_type: OrderType::Fok,
on_behalf_of,
fee_rate_bps: 300,
args: FokOrderArgs {
token_id: token_id.clone(),
side: Side::Buy,
maker_amount: 1.0,
expiration: None,
nonce: None,
taker: None,
}
.into(),
})
.await?;
println!("{}", response.order.id);
```
## Parameters
### `CreateDelegatedOrderParams`
| Field | Type | Description |
| -------------- | ----------- | -------------------------------------------------------------------------- |
| `market_slug` | `String` | Market identifier |
| `order_type` | `OrderType` | `OrderType::Gtc`, `OrderType::Fak`, or `OrderType::Fok` |
| `on_behalf_of` | `i32` | Profile ID of the sub-account |
| `fee_rate_bps` | `i32` | Fee rate in basis points. Use `300` or `0` for the default |
| `args` | `OrderArgs` | `GtcOrderArgs`, `FakOrderArgs`, or `FokOrderArgs` converted with `.into()` |
## Cancel an order
```rust theme={null}
let message = client.delegated_orders.cancel(order_id).await?;
println!("{}", message);
```
Cancel on behalf of a sub-account:
```rust theme={null}
let message = client
.delegated_orders
.cancel_on_behalf_of(order_id, on_behalf_of)
.await?;
```
## Cancel all orders in a market
```rust theme={null}
let message = client.delegated_orders.cancel_all("btc-100k").await?;
println!("{}", message);
```
Cancel all on behalf of a sub-account:
```rust theme={null}
let message = client
.delegated_orders
.cancel_all_on_behalf_of("btc-100k", on_behalf_of)
.await?;
```
## How it works
1. The SDK builds an unsigned order locally with a zero verifying address.
2. It sends `owner_id` and `on_behalf_of` as the sub-account profile ID.
3. The backend detects delegated-signing scope and signs the order with the managed wallet.
4. The signed order is submitted to the exchange engine.
# Error Handling
Source: https://docs.limitless.exchange/developers/sdk/rust/error-handling
Retry logic and error handling with the Rust SDK
## Overview
The Rust SDK provides:
* structured `LimitlessError` variants
* HTTP `ApiError` details with status, method, URL, and response payload
* reusable retry helpers via `with_retry` and `RetryableClient`
* optional logging hooks for retry visibility
## Error Types
All SDK methods return:
```rust theme={null}
type Result = std::result::Result;
```
### `LimitlessError`
| Variant | Description |
| ------------------------------------------------------ | ----------------------------------------------------- |
| `LimitlessError::Api(ApiError)` | Non-2xx HTTP response |
| `LimitlessError::AuthenticationRequired { operation }` | Missing API key or HMAC credentials |
| `LimitlessError::InvalidInput(String)` | Local validation failure before a request is sent |
| `LimitlessError::Request(reqwest::Error)` | Transport error such as timeout or connection failure |
| `LimitlessError::Decode(serde_json::Error)` | Failed to decode response JSON |
| `LimitlessError::Base64(base64::DecodeError)` | Invalid HMAC secret encoding |
| `LimitlessError::Signing(String)` | EIP-712 or HMAC signing issue |
| `LimitlessError::WebSocket(String)` | WebSocket error |
### Inspecting API errors
```rust theme={null}
use limitless_exchange_rust_sdk::{Client, LimitlessError};
match client.portfolio.get_positions().await {
Err(LimitlessError::Api(api)) => {
eprintln!("status {}", api.status);
eprintln!("message {}", api.message);
eprintln!("method {}", api.method);
eprintln!("url {}", api.url);
eprintln!("payload {}", api.data);
if api.is_auth_error() {
eprintln!("credentials are invalid or missing required scope");
}
}
Err(err) => eprintln!("other error: {}", err),
Ok(_) => {}
}
```
### Authentication-required errors
The SDK fails early for authenticated methods when no credentials are configured:
```rust theme={null}
match client.portfolio.get_positions().await {
Err(LimitlessError::AuthenticationRequired { operation }) => {
eprintln!("missing auth for {}", operation);
}
_ => {}
}
```
## `with_retry`
Use `with_retry()` to wrap any async operation:
```rust theme={null}
use std::time::Duration;
use limitless_exchange_rust_sdk::{with_retry, RetryConfig};
let markets = client.markets.clone();
let value = with_retry(
RetryConfig {
max_retries: 5,
exponential_base: 2.0,
max_delay: Duration::from_secs(30),
..Default::default()
},
None,
|| {
let markets = markets.clone();
async move { markets.get_market("btc-above-100k-march-2025").await }
},
)
.await?;
```
By default, retries are enabled for:
* `429`
* `500`
* `502`
* `503`
* `504`
* connection failures
* timeouts
## `RetryableClient`
Use `RetryableClient` when you want a retrying HTTP transport wrapper:
```rust theme={null}
use std::sync::Arc;
use limitless_exchange_rust_sdk::{
Client, ConsoleLogger, LogLevel, RetryConfig, RetryableClient,
};
let http = Client::builder()
.api_key(std::env::var("LIMITLESS_API_KEY")?)
.build()?;
let retryable = RetryableClient::new(
http.clone(),
RetryConfig::default(),
Some(Arc::new(ConsoleLogger::new(LogLevel::Info))),
);
let raw_profile: serde_json::Value = retryable.get("/portfolio/positions").await?;
println!("{}", raw_profile);
```
You can still build the root `Client` from the same `HttpClient`:
```rust theme={null}
let client = Client::from_http_client(http)?;
```
## Retry callbacks
Attach a callback to observe retry attempts:
```rust theme={null}
use std::time::Duration;
use limitless_exchange_rust_sdk::RetryConfig;
let config = RetryConfig::default().with_on_retry(|attempt, err, delay| {
eprintln!(
"retry attempt={} delay_ms={} error={}",
attempt,
delay.as_millis(),
err
);
});
```
## Logging
Retry helpers accept an optional `SharedLogger`. Use `ConsoleLogger` during integration work:
```rust theme={null}
use std::sync::Arc;
use limitless_exchange_rust_sdk::{ConsoleLogger, LogLevel};
let logger = Arc::new(ConsoleLogger::new(LogLevel::Debug));
```
For trading systems, treat `InvalidInput` and `AuthenticationRequired` as non-retryable. Retries are most useful for transient HTTP failures, rate limits, and network timeouts.
# Rust SDK
Source: https://docs.limitless.exchange/developers/sdk/rust/getting-started
Official Rust SDK for the Limitless Exchange API
## Overview
The Limitless Exchange Rust SDK (`limitless-exchange-rust-sdk`) is a typed async client for interacting with both **CLOB** and **NegRisk** prediction markets. It provides:
* Strongly typed request and response models
* Built-in EIP-712 order building and signing
* Root `Client` composition for markets, portfolio, navigation, partner flows, and WebSockets
* Pluggable logging and retry helpers
* Async WebSocket streaming with auto-reconnect
The SDK requires **Rust 1.74+** and uses `async` / `await` throughout. The examples below assume a Tokio runtime.
## Installation
```toml theme={null}
[dependencies]
limitless-exchange-rust-sdk = "1.0.10"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
## Quick Start
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = Client::new()?;
Ok(())
}
```
```rust theme={null}
use limitless_exchange_rust_sdk::{ActiveMarketsParams, ActiveMarketsSortBy, Client};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = Client::new()?;
let result = client
.markets
.get_active_markets(Some(&ActiveMarketsParams {
limit: Some(10),
page: Some(1),
sort_by: Some(ActiveMarketsSortBy::Newest),
}))
.await?;
for market in result.data {
println!("{} {}", market.title, market.slug);
}
Ok(())
}
```
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = Client::new()?; // reads LIMITLESS_API_KEY from the environment if set
let positions = client.portfolio.get_positions().await?;
println!("CLOB positions: {}", positions.clob.len());
Ok(())
}
```
## Authentication
The SDK supports both authentication modes:
* Legacy API key (`X-API-Key`)
* Scoped token HMAC (`lmts-api-key`, `lmts-timestamp`, `lmts-signature`) for partner/programmatic integrations
Set `LIMITLESS_API_KEY` and let `Client::new()` or `HttpClientBuilder` load it automatically:
```bash theme={null}
export LIMITLESS_API_KEY="lmts_your_key_here"
```
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
```
Use `Client::builder()` with scoped API-token credentials:
```rust theme={null}
use limitless_exchange_rust_sdk::{Client, HmacCredentials};
let http = Client::builder()
.hmac_credentials(HmacCredentials {
token_id: "your-token-id".to_string(),
secret: "your-base64-secret".to_string(),
})
.build()?;
let client = Client::from_http_client(http)?;
```
Pass the API key directly:
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let http = Client::builder()
.api_key("lmts_your_key_here")
.build()?;
let client = Client::from_http_client(http)?;
```
With `hmac_credentials(...)` set, the SDK automatically generates and sends `lmts-api-key`, `lmts-timestamp`, and `lmts-signature`. Do not manually build HMAC headers when using the SDK client.
Never hardcode API keys, HMAC secrets, or raw wallet private keys in source code or commit them to version control. Use environment variables or a secrets manager.
## Root Client
The recommended entrypoint is the root `Client`, which composes all domain services:
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
// client.markets
// client.portfolio
// client.pages
// client.api_tokens
// client.partner_accounts
// client.delegated_orders
// client.server_wallets
```
For signing orders, create an `OrderClient` from the root client:
```rust theme={null}
let order_client = client.new_order_client(&std::env::var("PRIVATE_KEY")?, None)?;
```
For streaming, create a `WebSocketClient` from the root client:
```rust theme={null}
let ws = client.new_websocket_client(None);
```
## HttpClient Builder Options
The SDK uses a builder pattern on `Client::builder()`:
| Method | Type | Default | Description |
| ------------------------- | ------------------------- | -------------------------------- | ------------------------------------------ |
| `base_url(url)` | `String` | `https://api.limitless.exchange` | API base URL |
| `timeout(duration)` | `Duration` | `30s` | HTTP request timeout |
| `api_key(key)` | `String` | Reads `LIMITLESS_API_KEY` env | Legacy API key authentication |
| `hmac_credentials(creds)` | `HmacCredentials` | -- | HMAC credentials for scoped API-token auth |
| `additional_headers(map)` | `HashMap` | empty | Extra headers merged into every request |
| `logger(logger)` | `SharedLogger` | `NoopLogger` | Logger for request/response tracing |
```rust theme={null}
use std::{collections::HashMap, sync::Arc, time::Duration};
use limitless_exchange_rust_sdk::{Client, ConsoleLogger, LogLevel};
let mut headers = HashMap::new();
headers.insert("X-Strategy-Name".to_string(), "market-maker-a".to_string());
let http = Client::builder()
.timeout(Duration::from_secs(15))
.additional_headers(headers)
.logger(Arc::new(ConsoleLogger::new(LogLevel::Debug)))
.build()?;
let client = Client::from_http_client(http)?;
```
## Logging
The SDK provides a pluggable `Logger` trait with a built-in `ConsoleLogger`:
```rust theme={null}
use std::sync::Arc;
use limitless_exchange_rust_sdk::{Client, ConsoleLogger, LogLevel};
let http = Client::builder()
.logger(Arc::new(ConsoleLogger::new(LogLevel::Debug)))
.build()?;
let client = Client::from_http_client(http)?;
```
| Level | Description |
| ----------------- | ------------------------------------------------------------------------------ |
| `LogLevel::Debug` | Verbose request/response output, venue cache behavior, and WebSocket lifecycle |
| `LogLevel::Info` | General operational messages |
| `LogLevel::Warn` | Warnings about missing auth, cache misses, or retry behavior |
| `LogLevel::Error` | Errors only |
Use `LogLevel::Debug` during development when integrating signing, partner flows, or WebSockets. Switch to `Info` or `Warn` in production.
## Server Wallet Redemption and Withdrawal
For partner server-wallet sub-accounts, the SDK provides helper methods for payout settlement and treasury movement:
* [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) — claim resolved positions
* [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) — transfer ERC20 funds from managed sub-accounts
* [`POST /portfolio/withdrawal-addresses`](/api-reference/portfolio/add-withdrawal-address) — allowlist an explicit treasury destination with Privy identity auth
* [`DELETE /portfolio/withdrawal-addresses/:address`](/api-reference/portfolio/delete-withdrawal-address) — remove an allowlisted destination with Privy identity auth
Required scopes for `apiToken` auth: `trading` for redeem and `withdrawal` for withdraw. Allowlist add/delete calls use a Privy identity token instead of API-token/HMAC auth.
```rust theme={null}
use limitless_exchange_rust_sdk::{
PartnerWithdrawalAddressInput, WithdrawServerWalletParams,
};
let identity_token = std::env::var("PRIVY_IDENTITY_TOKEN")?;
let treasury_address = "0x0F3262730c909408042F9Da345a916dc0e1F9787";
client
.partner_accounts
.add_withdrawal_address(
&identity_token,
&PartnerWithdrawalAddressInput {
address: treasury_address.to_string(),
label: Some("treasury".to_string()),
},
)
.await?;
let withdraw = client
.server_wallets
.withdraw(&WithdrawServerWalletParams {
amount: "1000000".to_string(),
on_behalf_of: Some(child_profile_id),
token: None,
destination: Some(treasury_address.to_string()),
})
.await?;
println!("{}", withdraw.destination);
```
Set `on_behalf_of` when withdrawing from a child server-wallet profile. `destination` is optional for child withdrawals; when omitted, the API defaults to the authenticated partner smart wallet when present, otherwise the authenticated partner account. You can omit `on_behalf_of` only when withdrawing the authenticated caller's own server wallet to an explicit `destination`.
See the full flow and scope guidance in [Programmatic API](/developers/programmatic-api).
## Source Code
The SDK is open source. Contributions and issue reports are welcome.
Browse the source, report issues, and contribute at github.com/limitless-labs-group/limitless-exchange-rust-sdk
# Market Pages
Source: https://docs.limitless.exchange/developers/sdk/rust/market-pages
Navigate and filter markets with the Rust SDK
## Overview
The `MarketPageFetcher` provides access to the Market Navigation API — a hierarchical system for browsing markets by category, applying dynamic filters, and paginating results. All endpoints are public and require no authentication.
## Setup
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
// Access through the root client
let pages = client.pages.clone();
```
## Navigation Tree
Fetch the full navigation hierarchy:
```rust theme={null}
let navigation = client.pages.get_navigation().await?;
for node in navigation {
println!("{} -> {}", node.name, node.path);
for child in node.children {
println!(" {} -> {}", child.name, child.path);
}
}
```
### `NavigationNode`
| Field | Type | Description |
| ---------- | --------------------- | ------------------------------------- |
| `id` | `String` | Unique identifier |
| `name` | `String` | Display name |
| `slug` | `String` | URL-friendly identifier |
| `path` | `String` | Full URL path (for example `/crypto`) |
| `icon` | `Option` | Optional icon name |
| `children` | `Vec` | Nested child nodes |
## Resolving a Page by Path
Convert a URL path into a `MarketPage`. The SDK follows up to 3 redirects automatically:
```rust theme={null}
let page = client.pages.get_market_page_by_path("/crypto").await?;
println!("page {}", page.name);
println!("filter groups {}", page.filter_groups.len());
println!("full path {}", page.full_path);
```
## Fetching Markets for a Page
Use `get_markets()` with the page ID. The Rust SDK supports both offset and cursor pagination.
### Offset pagination
```rust theme={null}
use limitless_exchange_rust_sdk::{MarketPageMarketsParams, MarketPageSort};
let result = client
.pages
.get_markets(
&page.id,
Some(&MarketPageMarketsParams {
page: Some(1),
limit: Some(20),
sort: Some(MarketPageSort::CreatedAtDesc),
cursor: None,
filters: Default::default(),
}),
)
.await?;
for market in result.data {
println!("{} {}", market.slug, market.title);
}
if let Some(pagination) = result.pagination {
println!("page {} of {}", pagination.page, pagination.total_pages);
}
```
### Cursor pagination
```rust theme={null}
use limitless_exchange_rust_sdk::{MarketPageMarketsParams, MarketPageSort};
let first_page = client
.pages
.get_markets(
&page.id,
Some(&MarketPageMarketsParams {
page: None,
limit: Some(20),
sort: Some(MarketPageSort::UpdatedAtDesc),
cursor: Some(String::new()),
filters: Default::default(),
}),
)
.await?;
if let Some(cursor) = first_page.cursor.as_ref().and_then(|c| c.next_cursor.as_deref()) {
let next_page = client
.pages
.get_markets(
&page.id,
Some(&MarketPageMarketsParams {
page: None,
limit: Some(20),
sort: Some(MarketPageSort::UpdatedAtDesc),
cursor: Some(cursor.to_string()),
filters: Default::default(),
}),
)
.await?;
println!("next page size {}", next_page.data.len());
}
```
`page` and `cursor` are mutually exclusive. The Rust SDK returns a local validation error if you set both in the same request.
### Filtering
Filters are passed as `HashMap`:
```rust theme={null}
use std::collections::HashMap;
use serde_json::json;
let mut filters = HashMap::new();
filters.insert("ticker".to_string(), json!(["btc", "eth"]));
filters.insert("duration".to_string(), json!("hourly"));
let result = client
.pages
.get_markets(
&page.id,
Some(&limitless_exchange_rust_sdk::MarketPageMarketsParams {
page: None,
limit: Some(10),
sort: Some(limitless_exchange_rust_sdk::MarketPageSort::UpdatedAtDesc),
cursor: Some(String::new()),
filters,
}),
)
.await?;
```
### Parameters
| Parameter | Type | Description |
| --------- | ------------------------ | -------------------------------------------------------------------- |
| `page` | `Option` | Page number for offset pagination |
| `limit` | `Option` | Results per page |
| `sort` | `Option` | Sort field |
| `cursor` | `Option` | Cursor token. Use `Some(String::new())` for the first cursor request |
| `filters` | `HashMap` | Dynamic filters. Values can be strings, numbers, booleans, or arrays |
## Property Keys
Property keys define the available filter dimensions such as `ticker` and `duration`.
```rust theme={null}
let keys = client.pages.get_property_keys().await?;
for key in &keys {
println!("{} {}", key.name, key.property_type);
}
```
### Single key and options
```rust theme={null}
let key = client.pages.get_property_key(&keys[0].id).await?;
println!("{} {}", key.name, key.slug);
let options = client.pages.get_property_options(&key.id, None).await?;
for option in options {
println!("{} {}", option.label, option.value);
}
```
You can also filter child options with `parent_id`:
```rust theme={null}
let parent_id = "parent-option-id";
let child_options = client
.pages
.get_property_options(&key.id, Some(parent_id))
.await?;
```
# Markets
Source: https://docs.limitless.exchange/developers/sdk/rust/markets
Market discovery and orderbook data with the Rust SDK
## Overview
The `MarketFetcher` handles market discovery, individual market lookups, user-order lookup, and orderbook retrieval. It also caches venue contract addresses used by the order-signing flow.
## Setup
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
// Access through the root client
let markets = client.markets.clone();
```
## Fetching Active Markets
Use `get_active_markets()` to retrieve a paginated list of active markets:
```rust theme={null}
use limitless_exchange_rust_sdk::{ActiveMarketsParams, ActiveMarketsSortBy, Client};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = Client::new()?;
let result = client
.markets
.get_active_markets(Some(&ActiveMarketsParams {
page: Some(1),
limit: Some(10),
sort_by: Some(ActiveMarketsSortBy::Liquidity),
}))
.await?;
for market in result.data {
println!("{} {}", market.title, market.slug);
}
Ok(())
}
```
| Parameter | Type | Default | Description |
| --------- | ----------------------------- | ------- | -------------------------- |
| `page` | `Option` | `None` | Page number for pagination |
| `limit` | `Option` | `None` | Number of markets per page |
| `sort_by` | `Option` | `None` | Sort order |
### Sort options
| Variant | Description |
| --------------------------------- | ------------------------------ |
| `ActiveMarketsSortBy::LpRewards` | Sort by LP rewards |
| `ActiveMarketsSortBy::EndingSoon` | Sort by markets ending soonest |
| `ActiveMarketsSortBy::Newest` | Sort by newest markets |
| `ActiveMarketsSortBy::HighValue` | Sort by highest value |
| `ActiveMarketsSortBy::Liquidity` | Sort by liquidity |
## Fetching a Single Market
Use `get_market()` to retrieve full details for a specific market:
```rust theme={null}
let market = client
.markets
.get_market("btc-above-100k-march-2025")
.await?;
println!("{}", market.title);
if let Some(tokens) = market.tokens.as_ref() {
println!("YES token: {}", tokens.yes);
println!("NO token: {}", tokens.no);
}
if let Some(venue) = market.venue.as_ref() {
println!("Exchange: {}", venue.exchange);
}
```
`market.tokens` and `market.venue` are optional in the Rust models, so unwrap them only after checking they are present.
## Fetching the Orderbook
Use `get_order_book()` to retrieve bids and asks for a market:
```rust theme={null}
let orderbook = client
.markets
.get_order_book("btc-above-100k-march-2025")
.await?;
for bid in &orderbook.bids {
println!("bid {} {}", bid.price, bid.size);
}
for ask in &orderbook.asks {
println!("ask {} {}", ask.price, ask.size);
}
println!("midpoint {}", orderbook.adjusted_midpoint);
```
## Fetching User Orders
Use `get_user_orders()` to retrieve your orders on a market. This requires authentication:
```rust theme={null}
let orders = client
.markets
.get_user_orders("btc-above-100k-march-2025")
.await?;
for order in orders {
println!("{} {:?} {:?}", order.id, order.price, order.filled_size);
}
```
You can also call it from a fetched `Market` instance:
```rust theme={null}
let market = client.markets.get_market("btc-above-100k-march-2025").await?;
let orders = market.get_user_orders().await?;
```
## Venue Caching
Every call to `get_market()` caches the market venue (exchange and adapter addresses). The `OrderClient` reads from this cache when resolving the correct EIP-712 verifying contract.
Calling `get_market()` retrieves and caches the venue:
```rust theme={null}
let market = client.markets.get_market("btc-above-100k-march-2025").await?;
```
Access the cached venue directly:
```rust theme={null}
if let Some(venue) = client.markets.get_venue("btc-above-100k-march-2025") {
println!("exchange {}", venue.exchange);
}
```
The `OrderClient` will use the cached venue for signing:
```rust theme={null}
let private_key = std::env::var("PRIVATE_KEY")?;
let order_client = client.new_order_client(&private_key, None)?;
let market = client.markets.get_market("btc-above-100k-march-2025").await?;
let tokens = market.tokens.as_ref().expect("market tokens");
let response = order_client
.create_order(limitless_exchange_rust_sdk::CreateOrderParams {
order_type: limitless_exchange_rust_sdk::OrderType::Gtc,
market_slug: market.slug.clone(),
args: limitless_exchange_rust_sdk::GtcOrderArgs {
token_id: tokens.yes.clone(),
side: limitless_exchange_rust_sdk::Side::Buy,
price: 0.65,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
post_only: false,
}
.into(),
})
.await?;
println!("{}", response.order.id);
```
Calling `get_market()` before `create_order()` is still the recommended pattern. The Rust SDK will fetch venue data on demand if the cache is empty, but prefetching avoids an extra API roundtrip.
# Trading & Orders
Source: https://docs.limitless.exchange/developers/sdk/rust/orders
Create and manage orders with the Rust SDK
## Overview
The Rust SDK provides `OrderClient` for order creation, EIP-712 signing, and order management. It supports:
* `OrderType::Gtc` for resting limit orders
* `OrderType::Fak` for fill-and-kill limit orders
* `OrderType::Fok` for fill-or-kill market orders
## Prerequisites
Before placing orders, you need:
* an authenticated `Client`
* a private key for EIP-712 signing
* market venue data (fetched via `get_market()` and cached automatically)
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
let private_key = std::env::var("PRIVATE_KEY")?;
let order_client = client.new_order_client(&private_key, None)?;
```
The `OrderClient` lazily fetches your profile on the first order to determine your `owner_id` and fee rate. `CHAIN_ID` defaults to `8453` (Base mainnet) unless you override it.
## Token Approvals
Before your first trade on a given venue, you must approve the exchange contracts to spend your tokens. This is a **one-time on-chain setup** per venue.
Approve USDC and Conditional Tokens to the **exchange** contract:
```rust theme={null}
let market = client.markets.get_market("your-market-slug").await?;
let exchange = market
.venue
.as_ref()
.expect("venue")
.exchange
.clone();
println!("approve USDC and CTF to {}", exchange);
```
NegRisk markets also require Conditional Token approval to the **adapter** contract:
```rust theme={null}
let market = client.markets.get_market("your-negrisk-slug").await?;
let venue = market.venue.as_ref().expect("venue");
println!("exchange {}", venue.exchange);
println!("adapter {}", venue.adapter.as_ref().expect("adapter"));
```
Approvals are on-chain transactions that cost gas. Use `venue.exchange` for both CLOB and NegRisk, and additionally `venue.adapter` for NegRisk markets.
## GTC Orders
GTC orders remain on the book until filled or explicitly cancelled.
```rust theme={null}
use limitless_exchange_rust_sdk::{CreateOrderParams, GtcOrderArgs, OrderType, Side};
let market = client.markets.get_market("btc-above-100k-march-2025").await?;
let tokens = market.tokens.as_ref().expect("market tokens");
let result = order_client
.create_order(CreateOrderParams {
order_type: OrderType::Gtc,
market_slug: market.slug.clone(),
args: GtcOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Buy,
price: 0.65,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
post_only: false,
}
.into(),
})
.await?;
println!("order id {}", result.order.id);
```
### Post-only GTC order
Use `post_only: true` to ensure the order never crosses the spread as a taker:
```rust theme={null}
let result = order_client
.create_order(CreateOrderParams {
order_type: OrderType::Gtc,
market_slug: market.slug.clone(),
args: GtcOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Buy,
price: 0.65,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
post_only: true,
}
.into(),
})
.await?;
```
### `GtcOrderArgs`
| Field | Type | Description |
| ------------ | ---------------- | --------------------------------------------------------- |
| `token_id` | `String` | Token ID from market data |
| `side` | `Side` | `Side::Buy` or `Side::Sell` |
| `price` | `f64` | Price between 0 and 1, tick-aligned to `0.001` |
| `size` | `f64` | Number of shares |
| `post_only` | `bool` | Optional. Rejects the order if it would immediately match |
| `expiration` | `Option` | Optional expiration timestamp. Defaults to `"0"` |
| `nonce` | `Option` | Optional nonce |
| `taker` | `Option` | Optional taker address |
## FAK Orders
FAK orders use the same `price` + `size` inputs as GTC, but any unmatched remainder is cancelled immediately.
```rust theme={null}
use limitless_exchange_rust_sdk::{CreateOrderParams, FakOrderArgs, OrderType, Side};
let response = order_client
.create_order(CreateOrderParams {
order_type: OrderType::Fak,
market_slug: market.slug.clone(),
args: FakOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Buy,
price: 0.45,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
}
.into(),
})
.await?;
println!("order id {}", response.order.id);
println!("maker matches {}", response.maker_matches.len());
```
`post_only` is not supported for `FAK` orders.
## FOK Orders
FOK orders execute immediately and fully, or are cancelled entirely. Instead of `price` and `size`, you pass `maker_amount`.
For buys, `maker_amount` is the total USDC to spend:
```rust theme={null}
use limitless_exchange_rust_sdk::{CreateOrderParams, FokOrderArgs, OrderType, Side};
let response = order_client
.create_order(CreateOrderParams {
order_type: OrderType::Fok,
market_slug: market.slug.clone(),
args: FokOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Buy,
maker_amount: 10.0,
expiration: None,
nonce: None,
taker: None,
}
.into(),
})
.await?;
println!("order id {}", response.order.id);
```
For sells, `maker_amount` is the number of shares to sell:
```rust theme={null}
let response = order_client
.create_order(CreateOrderParams {
order_type: OrderType::Fok,
market_slug: market.slug.clone(),
args: FokOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Sell,
maker_amount: 10.0,
expiration: None,
nonce: None,
taker: None,
}
.into(),
})
.await?;
```
### `FokOrderArgs`
| Field | Type | Description |
| -------------- | ---------------- | -------------------------------------------------------- |
| `token_id` | `String` | Token ID from market data |
| `side` | `Side` | `Side::Buy` or `Side::Sell` |
| `maker_amount` | `f64` | BUY: USDC to spend. SELL: shares to sell. Max 6 decimals |
| `expiration` | `Option` | Optional expiration timestamp |
| `nonce` | `Option` | Optional nonce |
| `taker` | `Option` | Optional taker address |
## Build and Sign Separately
For advanced flows, build and sign orders without submitting them:
```rust theme={null}
use limitless_exchange_rust_sdk::{GtcOrderArgs, Side};
let unsigned = order_client
.build_unsigned_order(
GtcOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Buy,
price: 0.65,
size: 10.0,
expiration: None,
nonce: None,
taker: None,
post_only: false,
}
.into(),
)
.await?;
let signature = order_client.sign_order_for_market(&market.slug, &unsigned).await?;
println!("{}", signature);
```
You can also override the signing config explicitly:
```rust theme={null}
use limitless_exchange_rust_sdk::OrderSigningConfig;
let signature = order_client.sign_order_with_config(
&unsigned,
OrderSigningConfig {
chain_id: 8453,
contract_address: "0xYourExchangeContract".to_string(),
},
)?;
```
## Cancelling Orders
Cancel a single order:
```rust theme={null}
let message = order_client.cancel("abc123-def456").await?;
println!("{}", message);
```
Cancel all orders in a market:
```rust theme={null}
let message = order_client.cancel_all(&market.slug).await?;
println!("{}", message);
```
# Partner Accounts
Source: https://docs.limitless.exchange/developers/sdk/rust/partner-accounts
Create and manage sub-accounts with the Rust SDK
The `PartnerAccountService` creates sub-account profiles linked to the authenticated partner. It requires HMAC authentication with the `account_creation` scope.
## Access
```rust theme={null}
use limitless_exchange_rust_sdk::{Client, HmacCredentials};
let http = Client::builder()
.hmac_credentials(HmacCredentials {
token_id: token_id.to_string(),
secret: secret.to_string(),
})
.build()?;
let client = Client::from_http_client(http)?;
// Use client.partner_accounts.*
```
## Server wallet mode
Set `create_server_wallet` to create a managed wallet for the sub-account. This enables delegated signing:
```rust theme={null}
use limitless_exchange_rust_sdk::CreatePartnerAccountInput;
let account = client
.partner_accounts
.create_account(
&CreatePartnerAccountInput {
display_name: Some("user-alice".to_string()),
create_server_wallet: Some(true),
},
None,
)
.await?;
println!("{}", account.profile_id);
println!("{}", account.account);
```
New server wallets should be checked with `check_allowances` before the first delegated trade. If retryable targets are missing or failed, call `retry_allowances` and poll again.
## Allowance recovery
Server-wallet sub-accounts need delegated-trading approvals before they can trade. The partner allowance helpers use the Partner API only:
* `check_allowances(profile_id)` calls `GET /profiles/partner-accounts/:profileId/allowances`
* `retry_allowances(profile_id)` calls `POST /profiles/partner-accounts/:profileId/allowances/retry`
* both methods require HMAC credentials with `account_creation` and `delegated_signing`
* `profile_id` is the child/server-wallet profile id
```rust theme={null}
use limitless_exchange_rust_sdk::{
LimitlessError, PARTNER_ACCOUNT_ALLOWANCE_STATUS_FAILED,
PARTNER_ACCOUNT_ALLOWANCE_STATUS_MISSING, PARTNER_ACCOUNT_ALLOWANCE_STATUS_SUBMITTED,
};
let mut allowances = client
.partner_accounts
.check_allowances(account.profile_id)
.await?;
if !allowances.ready {
let retryable = allowances.targets.iter().any(|target| {
target.retryable
&& matches!(
target.status.as_str(),
PARTNER_ACCOUNT_ALLOWANCE_STATUS_MISSING
| PARTNER_ACCOUNT_ALLOWANCE_STATUS_FAILED
)
});
if retryable {
match client
.partner_accounts
.retry_allowances(account.profile_id)
.await
{
Ok(retried) => allowances = retried,
Err(LimitlessError::Api(err)) if err.status == 429 => {
println!("retryAfterSeconds is in err.data");
}
Err(LimitlessError::Api(err)) if err.status == 409 => {
println!("Retry already running; poll check_allowances again shortly.");
}
Err(err) => return Err(err.into()),
}
}
}
if allowances
.targets
.iter()
.any(|target| target.status == PARTNER_ACCOUNT_ALLOWANCE_STATUS_SUBMITTED)
{
// A sponsored tx/user operation was submitted by this retry request.
// Poll check_allowances again after a short delay to observe confirmed chain state.
}
```
Recommended partner flow:
1. Poll `check_allowances(profile_id)`.
2. If `ready == true`, continue.
3. If targets are `missing` or `failed` with `retryable == true`, call `retry_allowances(profile_id)`.
4. If retry returns `submitted` targets, poll `check_allowances` again after a short delay.
5. If retry returns `429`, wait `retryAfterSeconds`.
6. If retry returns `409`, wait briefly and call `check_allowances` again.
## Withdrawal address allowlist
Explicit treasury destinations for server-wallet withdrawals must be allowlisted on the authenticated partner profile unless the destination is already the partner account or partner smart wallet. Allowlist management uses a Privy identity token, not API-token/HMAC auth.
```rust theme={null}
use limitless_exchange_rust_sdk::PartnerWithdrawalAddressInput;
let identity_token = std::env::var("PRIVY_IDENTITY_TOKEN")?;
let treasury_address = "0x0F3262730c909408042F9Da345a916dc0e1F9787";
let entry = client
.partner_accounts
.add_withdrawal_address(
&identity_token,
&PartnerWithdrawalAddressInput {
address: treasury_address.to_string(),
label: Some("treasury".to_string()),
},
)
.await?;
println!("{}", entry.destination_address);
client
.partner_accounts
.delete_withdrawal_address(&identity_token, treasury_address)
.await?;
```
`add_withdrawal_address` and `delete_withdrawal_address` call `POST /portfolio/withdrawal-addresses` and `DELETE /portfolio/withdrawal-addresses/:address` with the `identity: Bearer ` header. `POST /portfolio/withdraw` still uses HMAC auth with the `withdrawal` scope.
## EOA mode
For externally-owned accounts, pass ownership-verification headers:
```rust theme={null}
use limitless_exchange_rust_sdk::{
CreatePartnerAccountEoaHeaders, CreatePartnerAccountInput,
};
let account = client
.partner_accounts
.create_account(
&CreatePartnerAccountInput {
display_name: Some("user-bob".to_string()),
create_server_wallet: Some(false),
},
Some(&CreatePartnerAccountEoaHeaders {
account: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string(),
signing_message: "0x...".to_string(),
signature: "0x...".to_string(),
}),
)
.await?;
```
## Validation
* `display_name` is optional and capped at 44 characters
* when `create_server_wallet` is not `Some(true)`, EOA headers are required
* the SDK validates `display_name` length locally before sending the request
* `409 Conflict` is returned if a profile already exists for the target address
Server-wallet sub-accounts and EOA sub-accounts use different signing models. Use server-wallet mode for delegated signing, and EOA mode when the end user will sign orders themselves.
# Portfolio & Positions
Source: https://docs.limitless.exchange/developers/sdk/rust/portfolio
Track positions and history with the Rust SDK
## Overview
The `PortfolioFetcher` retrieves your profile, open positions, and trading history. It requires an authenticated client.
## Setup
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
// Access through the root client
let portfolio = client.portfolio.clone();
```
`PortfolioFetcher` requires authentication. Configure `LIMITLESS_API_KEY` or HMAC credentials before calling these endpoints.
## Fetching Your Profile
Use `get_profile()` to retrieve a profile by wallet address:
```rust theme={null}
let profile = client
.portfolio
.get_profile("0xYOUR_WALLET_ADDRESS")
.await?;
println!("id {}", profile.id);
println!("account {}", profile.account);
if let Some(display_name) = profile.display_name.as_deref() {
println!("display name {}", display_name);
}
if let Some(rank) = profile.rank.as_ref() {
println!("rank {} fee {}", rank.name, rank.fee_rate_bps);
}
```
## Fetching All Positions
Use `get_positions()` to retrieve your portfolio:
```rust theme={null}
let positions = client.portfolio.get_positions().await?;
println!("CLOB positions: {}", positions.clob.len());
println!("AMM positions: {}", positions.amm.len());
```
## CLOB Positions
Use `get_clob_positions()` for CLOB-only positions:
```rust theme={null}
let positions = client.portfolio.get_clob_positions().await?;
for pos in positions {
println!("{} {}", pos.market.title, pos.tokens_balance.yes);
}
```
## AMM Positions
Use `get_amm_positions()` for AMM-only positions:
```rust theme={null}
let positions = client.portfolio.get_amm_positions().await?;
for pos in positions {
println!(
"{} outcome={} unrealized={}",
pos.market.title,
pos.outcome_index,
pos.unrealized_pnl
);
}
```
## User History
The Rust SDK uses the current cursor-based history API:
* pass `None` for the first request
* the SDK sends `cursor=` with an empty value on that first call
* subsequent requests use the returned `next_cursor`
* `limit` defaults to `20` when omitted
```rust theme={null}
let history = client.portfolio.get_user_history(None, Some(20)).await?;
for entry in &history.data {
println!(
"block={} tx={:?} strategy={:?}",
entry.block_timestamp,
entry.transaction_hash,
entry.strategy
);
}
if let Some(next_cursor) = history.next_cursor.as_deref() {
let next_page = client
.portfolio
.get_user_history(Some(next_cursor), Some(20))
.await?;
println!("next page entries {}", next_page.data.len());
}
```
### `HistoryResponse`
| Field | Type | Description |
| ------------- | ------------------- | ------------------------------ |
| `data` | `Vec` | History records |
| `next_cursor` | `Option` | Cursor token for the next page |
### `HistoryEntry`
| Field | Type | Description |
| ----------------------- | ----------------------- | ------------------------------ |
| `block_timestamp` | `i64` | Block timestamp |
| `collateral_amount` | `Option` | Collateral amount when present |
| `market` | `Option` | Market metadata |
| `outcome_index` | `Option` | Outcome index |
| `outcome_token_amount` | `Option` | Single token amount |
| `outcome_token_amounts` | `Option>` | Multi-outcome token amounts |
| `outcome_token_price` | `Option` | Price payload from the API |
| `strategy` | `Option` | Strategy label |
| `transaction_hash` | `Option` | Related transaction hash |
The first cursor request is not `page=1`. The Rust SDK intentionally opts into the new backend flow by sending `cursor=` empty on the first request.
## Authentication Errors
If the client is missing credentials, portfolio methods return `LimitlessError::AuthenticationRequired` before making the HTTP request:
```rust theme={null}
match client.portfolio.get_positions().await {
Err(limitless_exchange_rust_sdk::LimitlessError::AuthenticationRequired { operation }) => {
eprintln!("auth required for {}", operation);
}
other => println!("{:?}", other),
}
```
# WebSocket
Source: https://docs.limitless.exchange/developers/sdk/rust/websocket
Real-time market data and position updates with the Rust SDK
## Overview
The Rust SDK provides a Socket.IO-based `WebSocketClient` for:
* orderbook and trade streams
* market and price updates
* authenticated order, fill, position, and transaction streams
* automatic reconnect with subscription replay
## Setup
Create a WebSocket client from the root SDK client:
```rust theme={null}
use limitless_exchange_rust_sdk::Client;
let client = Client::new()?;
let ws = client.new_websocket_client(None);
```
Or configure it directly:
```rust theme={null}
use std::sync::Arc;
use limitless_exchange_rust_sdk::{ConsoleLogger, LogLevel, WebSocketClient, WebSocketConfig};
let ws = WebSocketClient::new(Some(WebSocketConfig {
logger: Some(Arc::new(ConsoleLogger::new(LogLevel::Info))),
..Default::default()
}));
```
## Connect and Disconnect
```rust theme={null}
ws.connect().await?;
println!("{:?}", ws.state());
println!("{}", ws.is_connected());
ws.disconnect().await?;
```
### `WebSocketConfig`
| Field | Type | Default | Description |
| ------------------------ | ------------------------- | ----------------------------- | -------------------------------------------- |
| `url` | `String` | `wss://ws.limitless.exchange` | WebSocket URL |
| `api_key` | `Option` | Reads `LIMITLESS_API_KEY` env | API key for authenticated channels |
| `hmac_credentials` | `Option` | `None` | HMAC credentials for authenticated channels |
| `auto_reconnect` | `bool` | `true` | Reconnect automatically after disconnects |
| `reconnect_delay_ms` | `u64` | `1000` | Base reconnect delay |
| `max_reconnect_attempts` | `u32` | `0` | Max reconnect attempts (`0` means unlimited) |
| `timeout_ms` | `u64` | `10000` | Connect timeout |
| `logger` | `Option` | `None` | Optional logger |
## Event Handlers
Use `on()` for raw JSON handlers, `once()` for one-shot handlers, and `off()` to unregister:
```rust theme={null}
let id = ws.on("orderbookUpdate", |data| {
println!("{}", data);
});
let once_id = ws.once("trade", |data| {
println!("first trade {}", data);
});
ws.off("trade", &[once_id]);
ws.off("orderbookUpdate", &[id]);
```
### Typed handlers
The SDK also provides typed event handlers:
```rust theme={null}
ws.on_orderbook_update(|update| {
println!(
"{} bids={} asks={}",
update.market_slug,
update.orderbook.bids.len(),
update.orderbook.asks.len()
);
});
ws.on_trade(|trade| {
println!("{} {} {} @ {}", trade.market_slug, trade.side, trade.size, trade.price);
});
ws.on_order(|order| {
println!("{} {}", order.order_id, order.status);
});
ws.on_fill(|fill| {
println!("{} {}", fill.fill_id, fill.order_id);
});
ws.on_transaction(|tx| {
println!("{:?} {}", tx.tx_hash, tx.status);
});
ws.on_market(|market| {
println!("{} {:?}", market.market_slug, market.last_price);
});
ws.on_market_created(|event| {
println!("created {} {}", event.slug, event.title);
});
ws.on_market_resolved(|event| {
println!("resolved {} {}", event.slug, event.winning_outcome);
});
```
## Subscribing to Channels
After connecting, subscribe to specific channels:
```rust theme={null}
use limitless_exchange_rust_sdk::{SubscriptionChannel, SubscriptionOptions};
ws.connect().await?;
ws.subscribe(
SubscriptionChannel::Orderbook,
SubscriptionOptions {
market_slugs: vec!["btc-above-100k-march-2025".to_string()],
..Default::default()
},
)
.await?;
```
### Public channels
| Variant | Description |
| -------------------------------------------- | -------------------------- |
| `SubscriptionChannel::Orderbook` | Orderbook updates |
| `SubscriptionChannel::Trades` | Trade events |
| `SubscriptionChannel::Markets` | Market-level updates |
| `SubscriptionChannel::Prices` | AMM price data |
| `SubscriptionChannel::SubscribeMarketPrices` | Market price subscriptions |
### Authenticated channels
| Variant | Description |
| -------------------------------------------- | ------------------------ |
| `SubscriptionChannel::Orders` | Your order updates |
| `SubscriptionChannel::Fills` | Your fill updates |
| `SubscriptionChannel::SubscribePositions` | Your position updates |
| `SubscriptionChannel::SubscribeTransactions` | Your transaction updates |
### `SubscriptionOptions`
| Field | Type | Description |
| ------------------ | ------------------------- | ------------------------- |
| `market_slug` | `Option` | Single market slug |
| `market_slugs` | `Vec` | Multiple market slugs |
| `market_address` | `Option` | Single market address |
| `market_addresses` | `Vec` | Multiple market addresses |
| `filters` | `BTreeMap` | Additional filters |
## Unsubscribing
```rust theme={null}
ws.unsubscribe(
SubscriptionChannel::Orderbook,
SubscriptionOptions {
market_slugs: vec!["btc-above-100k-march-2025".to_string()],
..Default::default()
},
)
.await?;
```
## Updating credentials
You can rotate credentials at runtime:
```rust theme={null}
ws.set_api_key("lmts_new_key_here");
```
or:
```rust theme={null}
use limitless_exchange_rust_sdk::HmacCredentials;
ws.set_hmac_credentials(HmacCredentials {
token_id: "new-token-id".to_string(),
secret: "new-base64-secret".to_string(),
});
```
If the socket is already connected, the client reconnects automatically with the new credentials.
## Auto-reconnect
When `auto_reconnect` is enabled, the client reconnects and replays saved subscriptions automatically.
The Rust client preserves normalized subscription options internally, so reconnects restore previous subscriptions without extra application code.
# API Tokens
Source: https://docs.limitless.exchange/developers/sdk/typescript/api-tokens
Manage scoped API tokens with the TypeScript SDK
The `ApiTokenService` handles the partner self-service token lifecycle: checking capabilities, deriving tokens, listing active tokens, and revoking them.
Token derivation and capability queries require a **Privy identity token**. The SDK does not obtain this token for you — your application must authenticate the partner via Privy and pass the resulting token.
## Access
The service is available on the root `Client`:
```typescript theme={null}
import { Client } from '@limitless-exchange/sdk';
const client = new Client({
baseURL: 'https://api.limitless.exchange',
});
// Use client.apiTokens.*
```
## Get partner capabilities
Check whether token management is enabled and which scopes are allowed.
```typescript theme={null}
const capabilities = await client.apiTokens.getCapabilities(identityToken);
console.log(capabilities.tokenManagementEnabled); // boolean
console.log(capabilities.allowedScopes); // e.g. ['trading', 'account_creation', 'delegated_signing']
```
## Derive a token
Create a new scoped API token. The `secret` is returned once — store it securely.
```typescript theme={null}
import { ScopeTrading, ScopeAccountCreation, ScopeDelegatedSigning } from '@limitless-exchange/sdk';
const derived = await client.apiTokens.deriveToken(identityToken, {
label: 'production-bot',
scopes: [ScopeTrading, ScopeAccountCreation, ScopeDelegatedSigning],
});
// derived.tokenId — used as lmts-api-key header
// derived.secret — base64-encoded HMAC secret (one-time)
// derived.scopes — granted scopes
// derived.profile — { id, account }
```
### Creating an HMAC-authenticated client
After deriving a token, create a new `Client` with the HMAC credentials:
```typescript theme={null}
const scopedClient = new Client({
baseURL: 'https://api.limitless.exchange',
hmacCredentials: {
tokenId: derived.tokenId,
secret: derived.secret,
},
});
```
If `scopes` is omitted, the token defaults to `['trading']`. Requested scopes must be a subset of the partner's `allowedScopes`.
## List active tokens
Returns all non-revoked tokens for the authenticated partner.
```typescript theme={null}
const tokens = await scopedClient.apiTokens.listTokens();
for (const token of tokens) {
console.log(token.tokenId, token.label, token.scopes, token.lastUsedAt);
}
```
## Revoke a token
Immediately invalidates a token. This cannot be undone.
```typescript theme={null}
const message = await scopedClient.apiTokens.revokeToken(derived.tokenId);
```
## Scope constants
The SDK exports typed scope constants:
| Constant | Value |
| ----------------------- | --------------------- |
| `ScopeTrading` | `'trading'` |
| `ScopeAccountCreation` | `'account_creation'` |
| `ScopeDelegatedSigning` | `'delegated_signing'` |
# Delegated Orders
Source: https://docs.limitless.exchange/developers/sdk/typescript/delegated-orders
Place orders on behalf of sub-accounts with the TypeScript SDK
The `DelegatedOrderService` enables partners with the `delegated_signing` scope to place and cancel orders on behalf of their sub-accounts. The server signs orders using the sub-account's managed Privy wallet — no private key management is needed on the partner side.
Delegated signing requires a sub-account created with `createServerWallet: true`. EOA sub-accounts sign their own orders.
**Recommended setup:** Store your HMAC credentials (`tokenId` / `secret`) on your backend. Use this SDK server-side to sign partner-authenticated requests. Expose only your own app-specific endpoints to the frontend. Never expose HMAC secrets in browser bundles or client-side storage.
## Access
```typescript theme={null}
import { Client } from '@limitless-exchange/sdk';
const client = new Client({
baseURL: 'https://api.limitless.exchange',
hmacCredentials: { tokenId, secret },
});
// Use client.delegatedOrders.*
```
## Create a GTC delegated order
Builds an unsigned GTC (Good-Til-Cancelled) limit order locally and submits it to `POST /orders` with the `onBehalfOf` profile ID. The server signs the order via the sub-account's managed wallet. GTC orders remain on the orderbook until filled or explicitly cancelled.
```typescript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
const response = await client.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.GTC,
onBehalfOf: partnerAccount.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.55,
size: 10,
},
});
console.log(response.order.id);
```
Use `postOnly: true` in the args to ensure the order only rests on the book and is never filled immediately as a taker:
```typescript theme={null}
const response = await client.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.GTC,
onBehalfOf: partnerAccount.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.55,
size: 10,
postOnly: true,
},
});
```
## Create a FAK delegated order
FAK (Fill-And-Kill) orders use the same `price` and `size` inputs as GTC, but they only consume immediately available liquidity and cancel any unmatched remainder.
`postOnly` is not supported for FAK orders.
```typescript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
const response = await client.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.FAK,
onBehalfOf: partnerAccount.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.45,
size: 10,
},
});
if (response.makerMatches?.length) {
console.log(`FAK order matched immediately with ${response.makerMatches.length} fill(s)`);
} else {
console.log('FAK remainder was cancelled.');
}
```
## Create a FOK delegated order
FOK (Fill-Or-Kill) orders execute immediately at the best available price or are cancelled entirely — there are no partial fills. Instead of `price` and `size`, FOK orders use `makerAmount`:
* **BUY**: `makerAmount` is the USDC amount to spend (e.g., `50` = spend \$50 USDC)
* **SELL**: `makerAmount` is the number of shares to sell (e.g., `18.64` = sell 18.64 shares)
```typescript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
// BUY FOK — spend 1 USDC at market price
const buyResponse = await client.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.FOK,
onBehalfOf: partnerAccount.profileId,
feeRateBps: 300,
args: {
tokenId: market.tokens.yes,
side: Side.BUY,
makerAmount: 1,
},
});
if (buyResponse.makerMatches?.length) {
console.log(`FOK order matched with ${buyResponse.makerMatches.length} fill(s)`);
} else {
console.log('FOK order was not matched — cancelled automatically');
}
// SELL FOK — sell 10 shares at market price
const sellResponse = await client.delegatedOrders.createOrder({
marketSlug: 'btc-100k',
orderType: OrderType.FOK,
onBehalfOf: partnerAccount.profileId,
args: {
tokenId: market.tokens.yes,
side: Side.SELL,
makerAmount: 10,
},
});
```
## Parameters
### GTC order args
| Field | Type | Description |
| --------------- | ----------- | ---------------------------------------------------------------------------------- |
| `marketSlug` | `string` | Market identifier |
| `orderType` | `OrderType` | `OrderType.GTC` |
| `onBehalfOf` | `number` | Profile ID of the sub-account |
| `args.tokenId` | `string` | Position token ID (YES or NO) from market data |
| `args.side` | `Side` | `BUY` (0) or `SELL` (1) |
| `args.price` | `number` | Price between 0 and 1 |
| `args.size` | `number` | Number of contracts |
| `args.postOnly` | `boolean` | Optional. `true` rejects the order if it would immediately match. Default `false`. |
| `feeRateBps` | `number` | Fee rate in basis points (defaults to 300 if omitted) |
### FAK order args
| Field | Type | Description |
| -------------- | ----------- | ----------------------------------------------------- |
| `marketSlug` | `string` | Market identifier |
| `orderType` | `OrderType` | `OrderType.FAK` |
| `onBehalfOf` | `number` | Profile ID of the sub-account |
| `args.tokenId` | `string` | Position token ID (YES or NO) from market data |
| `args.side` | `Side` | `BUY` (0) or `SELL` (1) |
| `args.price` | `number` | Price between 0 and 1 |
| `args.size` | `number` | Number of contracts |
| `feeRateBps` | `number` | Fee rate in basis points (defaults to 300 if omitted) |
### FOK order args
| Field | Type | Description |
| ------------------ | ----------- | --------------------------------------------------------- |
| `marketSlug` | `string` | Market identifier |
| `orderType` | `OrderType` | `OrderType.FOK` |
| `onBehalfOf` | `number` | Profile ID of the sub-account |
| `args.tokenId` | `string` | Position token ID (YES or NO) from market data |
| `args.side` | `Side` | `BUY` (0) or `SELL` (1) |
| `args.makerAmount` | `number` | BUY: USDC to spend. SELL: shares to sell. Max 6 decimals. |
| `feeRateBps` | `number` | Fee rate in basis points (defaults to 300 if omitted) |
## Cancel an order
Cancel an order placed by the partner or on behalf of a sub-account.
```typescript theme={null}
await client.delegatedOrders.cancel(orderId);
```
### Cancel on behalf of a sub-account
```typescript theme={null}
await client.delegatedOrders.cancelOnBehalfOf(orderId, partnerAccount.profileId);
```
## Cancel all orders in a market
```typescript theme={null}
await client.delegatedOrders.cancelAll(marketSlug);
```
### Cancel all on behalf of a sub-account
```typescript theme={null}
await client.delegatedOrders.cancelAllOnBehalfOf(marketSlug, partnerAccount.profileId);
```
## How it works
1. The SDK builds an unsigned order locally using `OrderBuilder` with a zero verifying address
2. For GTC and FAK orders, `price` and `size` are used; for FOK orders, `makerAmount` is used (with `takerAmount` always set to 1)
3. The order is posted to `POST /orders` with `onBehalfOf` and `ownerId` set to the sub-account's profile ID
4. The server detects the `delegated_signing` scope and missing signature
5. The server looks up the sub-account's server wallet, builds EIP-712 typed data, and signs via Privy
6. The signed order is submitted to the CLOB engine — GTC orders can rest on the book, FAK orders cancel unmatched remainder, and FOK orders either fully fill or cancel
# Error Handling & Retry
Source: https://docs.limitless.exchange/developers/sdk/typescript/error-handling
Retry logic and error handling with the TypeScript SDK
## ApiError Class
All HTTP errors from the SDK are thrown as `ApiError` instances. Inspect the `status`, `message`, and `data` properties to determine what went wrong.
```typescript theme={null}
import { ApiError } from '@limitless-exchange/sdk';
try {
await orderClient.createOrder({ /* ... */ });
} catch (error) {
if (error instanceof ApiError) {
console.error('Status:', error.status); // HTTP status code
console.error('Message:', error.message); // Human-readable error
console.error('Data:', error.data); // Raw response body (if any)
}
}
```
### ApiError Properties
| Property | Type | Description |
| --------- | --------- | ------------------------------------------- |
| `status` | `number` | HTTP status code (400, 401, 429, 500, etc.) |
| `message` | `string` | Error description from the server |
| `data` | `unknown` | Raw response body, useful for debugging |
## Common Status Codes
| Code | Meaning | Action |
| ----- | ----------------------------------------- | -------------------------------- |
| `400` | Bad request (invalid parameters) | Fix request parameters |
| `401` | Unauthorized (invalid or missing API key) | Check your `LIMITLESS_API_KEY` |
| `403` | Forbidden (insufficient permissions) | Verify account permissions |
| `404` | Not found (invalid slug or resource) | Check market slug or resource ID |
| `429` | Rate limited | Back off and retry with delay |
| `500` | Internal server error | Retry with exponential backoff |
| `502` | Bad gateway | Retry with exponential backoff |
| `503` | Service unavailable | Retry with exponential backoff |
| `504` | Gateway timeout | Retry with exponential backoff |
## withRetry Wrapper
The SDK provides a `withRetry` utility function that wraps any async operation with configurable retry logic.
```typescript theme={null}
import { withRetry, ApiError } from '@limitless-exchange/sdk';
const result = await withRetry(
() => orderClient.createOrder({
marketSlug: 'btc-100k-weekly',
tokenId: market.positionIds[0],
side: 'BUY',
price: 0.65,
size: 100,
orderType: 'GTC',
}),
{
statusCodes: [429, 500, 502, 503, 504],
maxRetries: 3,
delays: [1000, 2000, 4000], // Milliseconds between retries
onRetry: (error, attempt) => {
console.warn(`Retry ${attempt}: ${error.message}`);
},
}
);
```
### withRetry Options
| Option | Type | Default | Description |
| ------------- | -------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------- |
| `statusCodes` | `number[]` | `[429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry |
| `maxRetries` | `number` | `3` | Maximum number of retry attempts |
| `delays` | `number[]` | `[1000, 2000, 4000]` | Delay in ms before each retry. If fewer delays than retries, the last delay is reused. |
| `onRetry` | `(error: ApiError, attempt: number) => void` | -- | Callback invoked before each retry |
Only `ApiError` instances with a matching `status` code trigger retries. Other errors (network failures, timeouts) are thrown immediately.
## @retryOnErrors Decorator
For class-based architectures, use the `@retryOnErrors` decorator to add retry logic to individual methods.
```typescript theme={null}
import { retryOnErrors } from '@limitless-exchange/sdk';
class TradingBot {
private orderClient: OrderClient;
constructor(orderClient: OrderClient) {
this.orderClient = orderClient;
}
@retryOnErrors({
statusCodes: [429, 500, 502, 503, 504],
maxRetries: 5,
exponentialBase: 2,
maxDelay: 30_000,
})
async placeOrder(params: CreateOrderParams) {
return this.orderClient.createOrder(params);
}
}
```
### Decorator Options
| Option | Type | Default | Description |
| ----------------- | ---------- | --------------------------- | ------------------------------------------------------------------- |
| `statusCodes` | `number[]` | `[429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry |
| `maxRetries` | `number` | `3` | Maximum number of retry attempts |
| `exponentialBase` | `number` | `2` | Base for exponential backoff calculation (`base^attempt * 1000` ms) |
| `maxDelay` | `number` | `30000` | Maximum delay in milliseconds between retries |
The delay for each retry is calculated as:
```
delay = min(exponentialBase ^ attempt * 1000, maxDelay)
```
For example, with `exponentialBase: 2` and `maxDelay: 30000`:
| Attempt | Delay |
| ------- | ------------------ |
| 1 | 2,000 ms |
| 2 | 4,000 ms |
| 3 | 8,000 ms |
| 4 | 16,000 ms |
| 5 | 30,000 ms (capped) |
## Rate Limiting with OrderQueue
For high-frequency trading scenarios, implement an `OrderQueue` to throttle outgoing requests and avoid 429 errors:
```typescript theme={null}
import { ApiError } from '@limitless-exchange/sdk';
class OrderQueue {
private queue: Array<() => Promise> = [];
private processing = false;
private minDelayMs: number;
constructor(minDelayMs = 200) {
this.minDelayMs = minDelayMs;
}
async enqueue(fn: () => Promise): Promise {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
resolve(await fn());
} catch (error) {
reject(error);
}
});
this.processQueue();
});
}
private async processQueue() {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
const task = this.queue.shift()!;
await task();
await new Promise((r) => setTimeout(r, this.minDelayMs));
}
this.processing = false;
}
}
// Usage
const orderQueue = new OrderQueue(250); // 250ms between requests
const result = await orderQueue.enqueue(() =>
orderClient.createOrder({
marketSlug: 'btc-100k-weekly',
tokenId: market.positionIds[0],
side: 'BUY',
price: 0.65,
size: 100,
orderType: 'GTC',
})
);
```
## Best Practices
Catch `ApiError` separately from other errors. Network failures, JSON parse errors, and timeouts are not `ApiError` instances and should be handled differently.
```typescript theme={null}
try {
await orderClient.createOrder({ /* ... */ });
} catch (error) {
if (error instanceof ApiError) {
// Server responded with an error status
handleApiError(error);
} else if (error instanceof TypeError) {
// Network error (DNS, connection refused, etc.)
console.error('Network error:', error.message);
} else {
// Unexpected error
throw error;
}
}
```
Client errors (400, 401, 403) indicate a problem with your request or credentials. Retrying them wastes time and API quota. Only retry transient server errors (429, 5xx).
When rate limited, the server may include a `Retry-After` header. If available, respect it. Otherwise, use exponential backoff starting at 1 second.
For the most robust setup, use `OrderQueue` to throttle request rate *and* `withRetry` or `@retryOnErrors` to handle transient failures:
```typescript theme={null}
const result = await orderQueue.enqueue(() =>
withRetry(
() => orderClient.createOrder({ /* ... */ }),
{ statusCodes: [429, 500, 502, 503, 504], maxRetries: 3 }
)
);
```
## Logging
The SDK provides optional logging through a simple `ILogger` interface. Logging is completely opt-in with zero overhead by default.
### Quick Start
```typescript theme={null}
import { HttpClient, ConsoleLogger } from '@limitless-exchange/sdk';
const logger = new ConsoleLogger('info');
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
logger,
});
```
### Log Levels
| Level | What's Logged |
| ------- | ------------------------------------------------------------------------------ |
| `debug` | Request headers (API key redacted), request/response bodies, WebSocket events |
| `info` | API requests (method + URL), successful responses, order creation/cancellation |
| `warn` | Warnings only |
| `error` | API errors (with status code), network errors, WebSocket connection errors |
### Custom Logger for Production
Implement the `ILogger` interface to integrate with your own logging infrastructure:
```typescript theme={null}
import { ILogger } from '@limitless-exchange/sdk';
class MyLogger implements ILogger {
debug(message: string, meta?: Record): void { /* ... */ }
info(message: string, meta?: Record): void { /* ... */ }
warn(message: string, meta?: Record): void { /* ... */ }
error(message: string, error?: Error, meta?: Record): void { /* ... */ }
}
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
logger: new MyLogger(),
});
```
### Passing Logger to SDK Components
All SDK components accept an optional logger:
```typescript theme={null}
const logger = new ConsoleLogger('info');
const httpClient = new HttpClient({ baseURL: '...', logger });
const marketFetcher = new MarketFetcher(httpClient, logger);
const orderClient = new OrderClient({ httpClient, wallet, logger });
```
The SDK automatically redacts API keys in logs (shown as `***`). Private keys are never logged.
# TypeScript SDK
Source: https://docs.limitless.exchange/developers/sdk/typescript/getting-started
Official TypeScript SDK for the Limitless Exchange API
## Overview
The Limitless Exchange TypeScript SDK provides a type-safe client for interacting with both **CLOB** and **NegRisk** prediction markets. It handles authentication, EIP-712 order signing, market data fetching, portfolio tracking, and real-time WebSocket streaming out of the box.
Source code and issue tracker are available on [GitHub](https://github.com/limitless-labs-group/limitless-exchange-ts-sdk).
## Installation
```bash theme={null}
npm install @limitless-exchange/sdk
```
```bash theme={null}
yarn add @limitless-exchange/sdk
```
```bash theme={null}
pnpm add @limitless-exchange/sdk
```
## Quick Start
Create a `.env` file in your project root (and add it to `.gitignore`):
```bash theme={null}
LIMITLESS_API_KEY=lmts_your_api_key_here
PRIVATE_KEY=0xYourPrivateKeyHere
# Partner/programmatic mode (HMAC)
LMTS_TOKEN_ID=your_token_id
LMTS_TOKEN_SECRET=your_base64_secret
```
The SDK automatically reads `LIMITLESS_API_KEY` from the environment if no `apiKey` is passed to the constructor.
```typescript theme={null}
import { HttpClient } from '@limitless-exchange/sdk';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
});
```
```typescript theme={null}
import { MarketFetcher } from '@limitless-exchange/sdk';
const marketFetcher = new MarketFetcher(httpClient);
const markets = await marketFetcher.getActiveMarkets({
limit: 10,
sortBy: 'newest',
});
for (const market of markets) {
console.log(market.slug, market.title);
}
```
For operations that require authentication (placing orders, fetching portfolio), supply your API key and wallet:
```typescript theme={null}
import { ethers } from 'ethers';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
});
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!);
```
## Environment Variables
| Variable | Required | Description |
| ------------------- | ------------------------- | ---------------------------------------------------------------------------------------- |
| `LIMITLESS_API_KEY` | Legacy auth only | Deprecated API key value, auto-loaded by the SDK. |
| `LMTS_TOKEN_ID` | Partner/programmatic mode | Scoped token ID used for HMAC request signing. |
| `LMTS_TOKEN_SECRET` | Partner/programmatic mode | Base64 token secret used for HMAC request signing. |
| `PRIVATE_KEY` | For order signing | Ethereum private key used for EIP-712 order signatures. Never commit to version control. |
## Client constructor
The recommended entrypoint is the root `Client` class, which composes all domain services (markets, portfolio, orders, API tokens, partner accounts, delegated orders):
```typescript theme={null}
import { Client } from '@limitless-exchange/sdk';
const client = new Client({
baseURL: 'https://api.limitless.exchange',
apiKey: 'lmts_...',
});
```
For partner integrations using HMAC authentication:
```typescript theme={null}
const client = new Client({
baseURL: 'https://api.limitless.exchange',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID!,
secret: process.env.LMTS_TOKEN_SECRET!,
},
});
```
With `hmacCredentials` set, the SDK automatically generates and sends `lmts-api-key`, `lmts-timestamp`, and `lmts-signature` on each request. Do not manually build HMAC headers when using the SDK client.
| Option | Type | Default | Description |
| ------------------- | ------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------- |
| `baseURL` | `string` | -- | API base URL |
| `apiKey` | `string` | `process.env.LIMITLESS_API_KEY` | Legacy API key authentication |
| `hmacCredentials` | `{ tokenId, secret }` | -- | HMAC credentials for scoped API token auth (see [Programmatic API](/developers/programmatic-api)) |
| `timeout` | `number` | `30000` | Request timeout in milliseconds |
| `additionalHeaders` | `Record` | `{}` | Additional headers sent with every request |
### HttpClient (lower-level)
You can also use `HttpClient` directly for more control:
```typescript theme={null}
import { HttpClient } from '@limitless-exchange/sdk';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: 'lmts_...',
});
```
## What You Can Build
Discover markets, fetch orderbooks, and query NegRisk groups.
Browse markets by category with navigation, filters, and pagination.
Create GTC and FOK orders, manage approvals, and cancel orders.
Track CLOB and AMM positions, PnL, and trade history.
Derive, list, and revoke scoped HMAC tokens for partner integrations.
Create sub-accounts with server wallets or EOA verification.
Place orders on behalf of sub-accounts with server-side signing.
Real-time orderbook, price, position, and transaction events.
## Server Wallet Redemption and Withdrawal
For partner server-wallet sub-accounts, the SDK provides helper methods for payout settlement and treasury movement:
* [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) — claim resolved positions
* [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) — transfer ERC20 funds from managed sub-accounts
* [`POST /portfolio/withdrawal-addresses`](/api-reference/portfolio/add-withdrawal-address) — allowlist an explicit treasury destination with Privy identity auth
* [`DELETE /portfolio/withdrawal-addresses/:address`](/api-reference/portfolio/delete-withdrawal-address) — remove an allowlisted destination with Privy identity auth
Required scopes for `apiToken` auth: `trading` for redeem and `withdrawal` for withdraw. Allowlist add/delete calls use a Privy identity token instead of API-token/HMAC auth.
```typescript theme={null}
const identityToken = process.env.PRIVY_IDENTITY_TOKEN!;
const treasuryAddress = '0x0F3262730c909408042F9Da345a916dc0e1F9787';
await client.partnerAccounts.addWithdrawalAddress(identityToken, {
address: treasuryAddress,
label: 'treasury',
});
await client.serverWallets.withdraw({
amount: '1000000',
onBehalfOf: childProfileId,
destination: treasuryAddress,
});
```
Set `onBehalfOf` when withdrawing from a child server-wallet profile. `destination` is optional for child withdrawals; when omitted, the API defaults to the authenticated partner smart wallet when present, otherwise the authenticated partner account. You can omit `onBehalfOf` only when withdrawing the authenticated caller's own server wallet to an explicit `destination`.
See the full flow and scope guidance in [Programmatic API](/developers/programmatic-api).
## Disclaimer
**USE AT YOUR OWN RISK.** This SDK and the Limitless Exchange protocol are provided as-is with no warranties. You are solely responsible for any funds used or lost. By using this SDK, you acknowledge that prediction markets involve financial risk. The Limitless Exchange is not available to users in the United States or other restricted jurisdictions. Ensure compliance with your local laws before trading.
# Market Pages
Source: https://docs.limitless.exchange/developers/sdk/typescript/market-pages
Navigate and filter markets with the TypeScript SDK
## Overview
The `MarketPageFetcher` class provides access to the Market Navigation API — a hierarchical system for browsing markets by category, applying dynamic filters, and paginating results. All endpoints are public and require no authentication.
## Setup
```typescript theme={null}
import { HttpClient, MarketPageFetcher } from '@limitless-exchange/sdk';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
});
const pageFetcher = new MarketPageFetcher(httpClient);
```
## Navigation Tree
Fetch the full navigation hierarchy. Each node represents a browseable category with a URL path.
```typescript theme={null}
const navigation = await pageFetcher.getNavigation();
for (const node of navigation) {
console.log(`${node.name} → ${node.path}`);
for (const child of node.children) {
console.log(` ${child.name} → ${child.path}`);
}
}
```
### NavigationNode Fields
| Field | Type | Description |
| ---------- | --------------------- | ------------------------------ |
| `id` | `string` | Unique identifier |
| `name` | `string` | Display name |
| `slug` | `string` | URL-friendly identifier |
| `path` | `string` | Full URL path (e.g. `/crypto`) |
| `icon` | `string \| undefined` | Optional icon name |
| `children` | `NavigationNode[]` | Nested child nodes |
## Resolving a Page by Path
Convert a URL path into a `MarketPage` with its filters, metadata, and breadcrumb. The SDK handles 301 redirects internally.
```typescript theme={null}
const page = await pageFetcher.getMarketPageByPath('/crypto');
console.log(`Page: ${page.name}`);
console.log(`Filters: ${page.filterGroups.length} groups`);
console.log(`Breadcrumb: ${page.breadcrumb.map(b => b.name).join(' > ')}`);
```
### MarketPage Fields
| Field | Type | Description |
| -------------- | ------------------------- | ----------------------------------------- |
| `id` | `string` | Page identifier (used for `getMarkets()`) |
| `name` | `string` | Display name |
| `slug` | `string` | URL-friendly identifier |
| `fullPath` | `string` | Full URL path |
| `description` | `string \| undefined` | Page description |
| `baseFilter` | `Record` | Default filter applied to this page |
| `filterGroups` | `FilterGroup[]` | Available filter groups |
| `metadata` | `Record` | Page metadata |
| `breadcrumb` | `BreadcrumbItem[]` | Navigation breadcrumb |
## Fetching Markets for a Page
Use `getMarkets()` with the page ID to fetch markets. Supports offset and cursor pagination, sorting, and dynamic filters.
### Offset Pagination
```typescript theme={null}
const result = await pageFetcher.getMarkets(page.id, {
page: 1,
limit: 20,
sort: '-updatedAt',
});
for (const market of result.data) {
console.log(`${market.slug} — ${market.title}`);
}
if ('pagination' in result) {
console.log(`Page ${result.pagination.page} of ${result.pagination.totalPages}`);
}
```
### Cursor Pagination
```typescript theme={null}
const firstPage = await pageFetcher.getMarkets(page.id, {
cursor: '',
limit: 20,
sort: '-updatedAt',
});
for (const market of firstPage.data) {
console.log(market.slug);
}
if ('cursor' in firstPage && firstPage.cursor.nextCursor) {
const nextPage = await pageFetcher.getMarkets(page.id, {
cursor: firstPage.cursor.nextCursor,
limit: 20,
});
}
```
You cannot use `cursor` and `page` in the same request. Choose one pagination strategy.
### Filtering
Pass a `filters` object. Values can be a single primitive or an array for multi-select filters.
```typescript theme={null}
// Single filter
const result = await pageFetcher.getMarkets(page.id, {
filters: { ticker: 'btc' },
});
// Multiple values (OR logic)
const result = await pageFetcher.getMarkets(page.id, {
filters: { ticker: ['btc', 'eth'] },
});
// Combined filters
const result = await pageFetcher.getMarkets(page.id, {
limit: 10,
sort: '-updatedAt',
filters: {
ticker: ['btc', 'eth'],
duration: 'hourly',
},
});
```
### Parameters
| Parameter | Type | Description |
| --------- | --------------------------------------- | ----------------------------------------------------------------- |
| `page` | `number` | Page number (offset pagination) |
| `limit` | `number` | Results per page |
| `sort` | `string` | Sort field with optional `-` prefix for descending |
| `cursor` | `string` | Cursor token (cursor pagination). Use `""` for the first request. |
| `filters` | `Record` | Filter key-value pairs. Values can be primitives or arrays. |
### Sort Options
| Value | Description |
| -------------------------- | ---------------------------------------------- |
| `createdAt` / `-createdAt` | Sort by creation date (ascending / descending) |
| `updatedAt` / `-updatedAt` | Sort by last update |
| `deadline` / `-deadline` | Sort by expiration deadline |
| `id` / `-id` | Sort by market ID |
## Property Keys
Property keys define the available filter dimensions (e.g. "ticker", "duration"). Each key has a set of options.
```typescript theme={null}
const keys = await pageFetcher.getPropertyKeys();
for (const key of keys) {
console.log(`${key.name} (${key.type})`);
if (key.options) {
for (const opt of key.options.slice(0, 3)) {
console.log(` ${opt.label}: ${opt.value}`);
}
}
}
```
### Single Key and Options
```typescript theme={null}
const key = await pageFetcher.getPropertyKey(keys[0].id);
console.log(`${key.name} — ${key.options?.length ?? 0} options`);
// Fetch options separately (supports parent filtering for hierarchical keys)
const options = await pageFetcher.getPropertyOptions(key.id);
// Child options filtered by parent
const childOptions = await pageFetcher.getPropertyOptions(key.id, options[0]?.id);
```
### PropertyKey Fields
| Field | Type | Description |
| ----------- | ------------------------------- | --------------------------------------- |
| `id` | `string` | Unique identifier |
| `name` | `string` | Display name |
| `slug` | `string` | URL-friendly identifier |
| `type` | `string` | `"select"` or `"multi-select"` |
| `isSystem` | `boolean` | Whether this is a system-defined key |
| `options` | `PropertyOption[] \| undefined` | Available options (when fetched inline) |
| `metadata` | `Record` | Key metadata |
| `createdAt` | `string` | ISO 8601 creation timestamp |
| `updatedAt` | `string` | ISO 8601 last update timestamp |
## Complete Example
```typescript theme={null}
import { HttpClient, MarketPageFetcher } from '@limitless-exchange/sdk';
async function main() {
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
});
const pageFetcher = new MarketPageFetcher(httpClient);
// Browse the navigation tree
const navigation = await pageFetcher.getNavigation();
console.log(`Top-level categories: ${navigation.length}`);
// Resolve a page
const page = await pageFetcher.getMarketPageByPath('/crypto');
console.log(`\nPage: ${page.name}`);
console.log(`Available filters: ${page.filterGroups.map(fg => fg.name)}`);
// Fetch markets with filters
const result = await pageFetcher.getMarkets(page.id, {
limit: 5,
sort: '-updatedAt',
filters: { ticker: ['btc', 'eth'] },
});
for (const market of result.data) {
console.log(` ${market.slug} — ${market.title}`);
}
// Explore property keys
const keys = await pageFetcher.getPropertyKeys();
for (const key of keys) {
console.log(`\n${key.name} (${key.type}):`);
const options = await pageFetcher.getPropertyOptions(key.id);
for (const opt of options.slice(0, 5)) {
console.log(` ${opt.label}`);
}
}
}
main();
```
# Markets
Source: https://docs.limitless.exchange/developers/sdk/typescript/markets
Market discovery and orderbook data with the TypeScript SDK
## MarketFetcher Setup
`MarketFetcher` provides read-only access to market data. No API key is required for public endpoints.
```typescript theme={null}
import { HttpClient, MarketFetcher } from '@limitless-exchange/sdk';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
});
const marketFetcher = new MarketFetcher(httpClient);
```
## Active Markets
Retrieve a paginated list of currently active markets with optional sorting.
```typescript theme={null}
const markets = await marketFetcher.getActiveMarkets({
limit: 20,
page: 1,
sortBy: 'newest',
});
for (const market of markets) {
console.log(`${market.slug} — ${market.title}`);
}
```
### Parameters
| Parameter | Type | Default | Description |
| --------- | -------- | ---------- | -------------------------- |
| `limit` | `number` | `10` | Number of markets per page |
| `page` | `number` | `1` | Page number (1-indexed) |
| `sortBy` | `string` | `'newest'` | Sort order for results |
### Sort Options
| Value | Description |
| --------------- | -------------------------------------------- |
| `'lp_rewards'` | Markets with the highest LP rewards |
| `'ending_soon'` | Markets closest to resolution |
| `'newest'` | Most recently created markets |
| `'high_value'` | Markets with the highest volume or liquidity |
## Single Market
Fetch a single market by slug. The response includes venue contract addresses and token IDs needed for trading.
```typescript theme={null}
const market = await marketFetcher.getMarket('btc-100k-weekly');
console.log('Slug:', market.slug);
console.log('Venue exchange:', market.venue.exchange);
console.log('YES token ID:', market.positionIds[0]);
console.log('NO token ID:', market.positionIds[1]);
```
`getMarket()` automatically caches the venue data for the returned market. Subsequent calls for the same slug return the cached result. Always fetch the market before placing orders to ensure the venue is cached.
### Market Response Structure
```typescript theme={null}
interface Market {
slug: string;
title: string;
venue: {
exchange: string; // EIP-712 verifyingContract address
adapter: string | null; // Adapter contract address (null for some CLOB markets)
};
positionIds: [string, string]; // [YES token ID, NO token ID]
openInterest?: string;
liquidity?: string;
imageUrl?: string | null;
automationType?: 'manual' | 'lumy' | 'sports';
// ... additional fields
}
```
## Orderbook
Fetch the current orderbook for a CLOB market. Returns bids, asks, and the current spread.
```typescript theme={null}
const orderbook = await marketFetcher.getOrderBook('btc-100k-weekly');
console.log('Best bid:', orderbook.bids[0]?.price);
console.log('Best ask:', orderbook.asks[0]?.price);
// Calculate spread
if (orderbook.bids.length > 0 && orderbook.asks.length > 0) {
const spread = orderbook.asks[0].price - orderbook.bids[0].price;
console.log('Spread:', spread.toFixed(4));
}
```
### Orderbook Response Structure
```typescript theme={null}
interface Orderbook {
bids: OrderbookLevel[];
asks: OrderbookLevel[];
}
interface OrderbookLevel {
price: number; // 0 to 1
size: number; // Number of shares
}
```
### Handling illiquid markets
Markets with empty or one-sided orderbooks can produce misleading midpoint prices. When there are no bids, the midpoint defaults to `(0 + bestAsk) / 2`, which may show 50% even though no one is actively trading. To detect these markets:
```typescript theme={null}
const orderbook = await marketFetcher.getOrderBook('some-market');
const hasBids = orderbook.bids.length > 0;
const hasAsks = orderbook.asks.length > 0;
if (!hasBids || !hasAsks) {
console.log('No two-sided market — skip or flag as illiquid');
} else {
const spread = orderbook.asks[0].price - orderbook.bids[0].price;
if (spread > 0.20) {
console.log('Wide spread — market may have low liquidity');
}
}
```
## User Orders
Retrieve your open orders for a specific market using the fluent API. Requires an API key.
```typescript theme={null}
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
});
const marketFetcher = new MarketFetcher(httpClient);
const market = await marketFetcher.getMarket('btc-100k-weekly');
const orders = await market.getUserOrders();
for (const order of orders) {
console.log(order.id, order.side, order.price, order.status);
}
```
The `getUserOrders()` method is available on the market object returned by `getMarket()`. It requires an authenticated `HttpClient` (with an API key).
## NegRisk Group Markets
NegRisk markets are organized into groups. Each group contains multiple submarkets (outcomes). To trade a specific outcome, fetch the group first, then access the submarket you need.
```typescript theme={null}
const group = await marketFetcher.getMarket('us-election-2024');
console.log('Group:', group.slug);
console.log('Submarkets:', group.submarkets.length);
```
```typescript theme={null}
const submarket = group.submarkets[0];
console.log('Submarket:', submarket.slug, submarket.title);
```
You must call `getMarket()` on the submarket slug to get the venue and token IDs required for placing orders.
```typescript theme={null}
const submarketDetail = await marketFetcher.getMarket(submarket.slug);
console.log('YES token:', submarketDetail.positionIds[0]);
console.log('NO token:', submarketDetail.positionIds[1]);
```
Always use the **submarket slug** (not the group slug) when placing orders on NegRisk markets. The group slug does not have token IDs associated with it.
## Best Practices
Always call `getMarket()` before placing orders. The SDK caches venue data (exchange and adapter addresses) internally. This avoids redundant API calls and ensures the `OrderClient` has the information it needs to sign orders.
For real-time orderbook data, subscribe to WebSocket events rather than polling `getOrderBook()`. The WebSocket pushes `orderbookUpdate` events whenever the book changes, reducing latency and API usage. See the [WebSocket Streaming](/developers/sdk/typescript/websocket) guide.
When fetching active markets, always use `limit` and `page` parameters. Requesting unbounded result sets can cause timeouts and high memory usage.
# Trading & Orders
Source: https://docs.limitless.exchange/developers/sdk/typescript/orders
Create and manage orders with the TypeScript SDK
## Prerequisites
Before placing orders, initialize the required clients:
```typescript theme={null}
import { ethers } from 'ethers';
import {
HttpClient,
MarketFetcher,
OrderClient,
} from '@limitless-exchange/sdk';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
});
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!);
const marketFetcher = new MarketFetcher(httpClient);
const orderClient = new OrderClient({
httpClient,
wallet,
marketFetcher, // Optional: enables venue caching for faster order creation
});
```
## OrderClient Constructor
| Option | Type | Required | Description |
| --------------- | --------------- | -------- | --------------------------------------------------------------------------------- |
| `httpClient` | `HttpClient` | Yes | Authenticated HTTP client |
| `wallet` | `ethers.Wallet` | Yes | Wallet for EIP-712 signing |
| `marketFetcher` | `MarketFetcher` | No | Enables automatic venue caching. If omitted, venue data is fetched on each order. |
The `OrderClient` automatically fetches your user profile data (including `ownerId`) from the API on the first order. You do not need to supply it manually.
## Token Approvals
Before trading, your wallet must approve the relevant venue contracts to spend USDC and conditional tokens. This is a one-time setup per venue.
### Approval Requirements
| Market Type | USDC Approval | Conditional Token Approval |
| ----------- | --------------------------- | --------------------------- |
| CLOB | Venue `exchange` contract | Venue `exchange` contract |
| NegRisk | NegRisk `exchange` contract | NegRisk `exchange` contract |
### Manual Approval with ethers
```typescript theme={null}
import { ethers } from 'ethers';
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const ERC20_ABI = [
'function approve(address spender, uint256 amount) returns (bool)',
];
const ERC1155_ABI = [
'function setApprovalForAll(address operator, bool approved)',
];
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
async function approveVenue(venueExchange: string) {
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, signer);
const tx1 = await usdc.approve(venueExchange, ethers.MaxUint256);
await tx1.wait();
console.log('USDC approved for', venueExchange);
}
```
Run approvals once per venue. After approval, all subsequent orders on markets using that venue work without additional on-chain transactions.
## GTC Orders (Good-Til-Cancelled)
GTC orders remain on the orderbook until filled, cancelled, or the market resolves. Specify `price` (probability between 0 and 1) and `size` (number of shares).
### Buy YES shares
```typescript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
const market = await marketFetcher.getMarket('btc-100k-weekly');
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.positionIds[0], // YES token
side: Side.BUY,
price: 0.65,
size: 100,
orderType: OrderType.GTC,
});
console.log('Order created:', result.order.id);
```
### Post-only GTC order
Use `postOnly: true` to ensure your order is never filled immediately as a taker. If the order would cross the spread (i.e., match against existing orders), it is **rejected** instead. This guarantees you always receive maker fees.
```typescript theme={null}
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.positionIds[0], // YES token
side: Side.BUY,
price: 0.65,
size: 100,
orderType: OrderType.GTC,
postOnly: true,
});
```
### Sell NO shares
```typescript theme={null}
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.positionIds[1], // NO token
side: Side.SELL,
price: 0.40,
size: 50,
orderType: OrderType.GTC,
});
```
### GTC Parameter Reference
| Parameter | Type | Description |
| ------------ | ----------- | ---------------------------------------------------------------------------------------- |
| `marketSlug` | `string` | Market slug identifier |
| `tokenId` | `string` | YES or NO token ID from `market.positionIds` |
| `side` | `Side` | `Side.BUY` (0) or `Side.SELL` (1) |
| `price` | `number` | Price between 0 and 1 (exclusive), tick-aligned |
| `size` | `number` | Number of shares (positive, step-aligned) |
| `orderType` | `OrderType` | `OrderType.GTC` |
| `postOnly` | `boolean` | Optional. When `true`, rejects the order if it would immediately match. Default `false`. |
## FAK Orders (Fill-And-Kill)
FAK orders use the same `price` and `size` inputs as GTC, but they only consume immediately available liquidity and cancel any unmatched remainder.
`postOnly` is not supported for FAK orders.
### Buy with FAK
```typescript theme={null}
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.positionIds[0], // YES token
side: Side.BUY,
price: 0.45,
size: 100,
orderType: OrderType.FAK,
});
if (result.makerMatches.length > 0) {
console.log(`FAK order matched immediately with ${result.makerMatches.length} fill(s)`);
} else {
console.log('FAK remainder was cancelled.');
}
```
### Sell with FAK
```typescript theme={null}
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.positionIds[1], // NO token
side: Side.SELL,
price: 0.40,
size: 50,
orderType: OrderType.FAK,
});
```
### FAK Parameter Reference
| Parameter | Type | Description |
| ------------ | ----------- | ----------------------------------------------- |
| `marketSlug` | `string` | Market slug identifier |
| `tokenId` | `string` | YES or NO token ID from `market.positionIds` |
| `side` | `Side` | `Side.BUY` (0) or `Side.SELL` (1) |
| `price` | `number` | Price between 0 and 1 (exclusive), tick-aligned |
| `size` | `number` | Number of shares (positive, step-aligned) |
| `orderType` | `OrderType` | `OrderType.FAK` |
## FOK Orders (Fill-Or-Kill)
FOK orders execute immediately against the existing orderbook or are rejected entirely. Instead of `price` and `size`, specify `makerAmount` which represents the total value to trade.
### Buy with FOK
For a BUY FOK order, `makerAmount` is the amount of USDC you are willing to spend:
```typescript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.positionIds[0], // YES token
side: Side.BUY,
makerAmount: 50, // Spend up to 50 USDC
orderType: OrderType.FOK,
});
```
### Sell with FOK
For a SELL FOK order, `makerAmount` is the number of shares you want to sell:
```typescript theme={null}
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.positionIds[0], // YES token
side: Side.SELL,
makerAmount: 100, // Sell 100 shares
orderType: OrderType.FOK,
});
```
### FOK Parameter Reference
| Parameter | Type | Description |
| ------------- | ----------- | --------------------------------------------------------- |
| `marketSlug` | `string` | Market slug identifier |
| `tokenId` | `string` | YES or NO token ID from `market.positionIds` |
| `side` | `Side` | `Side.BUY` (0) or `Side.SELL` (1) |
| `makerAmount` | `number` | BUY: USDC to spend. SELL: shares to sell. Max 6 decimals. |
| `orderType` | `OrderType` | `OrderType.FOK` |
## NegRisk Orders
When trading NegRisk markets, always use the **submarket slug** -- not the group slug. Fetch the group to discover submarkets, then fetch the specific submarket to get its token IDs.
```typescript theme={null}
import { OrderType, Side } from '@limitless-exchange/sdk';
// 1. Fetch the NegRisk group
const group = await marketFetcher.getMarket('us-election-2024');
// 2. Pick a submarket from the group's markets array
const submarket = group.markets[0];
// 3. Fetch submarket details to get token IDs
const submarketDetail = await marketFetcher.getMarket(submarket.slug);
// 4. Place an order using the submarket slug and token IDs
const result = await orderClient.createOrder({
marketSlug: submarketDetail.slug,
tokenId: submarketDetail.positionIds[0], // YES token
side: Side.BUY,
price: 0.55,
size: 25,
orderType: OrderType.GTC,
});
```
Passing the group slug to `createOrder()` will fail. The group slug does not resolve to a tradeable market. Always use the submarket slug.
## Cancel Orders
Cancel an open order by its ID:
```typescript theme={null}
const result = await orderClient.cancel('order_abc123');
console.log(result.message);
```
Cancel all orders in a market:
```typescript theme={null}
const result = await orderClient.cancelAll('btc-100k-weekly');
console.log(result.message);
```
## Order States
| State | Description |
| ------------------ | --------------------------------------------------------- |
| `OPEN` | Order is live on the orderbook |
| `PARTIALLY_FILLED` | Some shares have been matched |
| `FILLED` | Order fully matched |
| `CANCELLED` | Order cancelled by user or system |
| `REJECTED` | Order rejected (insufficient funds, invalid params, etc.) |
## Error Handling
The SDK throws `ApiError` for HTTP-level failures. Inspect `status` and `message` for details.
```typescript theme={null}
import { ApiError, OrderType, Side } from '@limitless-exchange/sdk';
try {
await orderClient.createOrder({
marketSlug: 'btc-100k-weekly',
tokenId: market.positionIds[0],
side: Side.BUY,
price: 0.65,
size: 100,
orderType: OrderType.GTC,
});
} catch (error) {
if (error instanceof ApiError) {
switch (error.status) {
case 400:
console.error('Bad request:', error.message);
break;
case 401:
console.error('Unauthorized: check your API key');
break;
case 429:
console.error('Rate limited: slow down requests');
break;
default:
console.error(`API error ${error.status}:`, error.message);
}
} else {
throw error;
}
}
```
See the [Error Handling and Retry](/developers/sdk/typescript/error-handling) guide for retry strategies.
## Validation Rules
Prices must be between 0 and 1 (exclusive). A price of `0.65` means you value the outcome at 65%. Prices outside this range will be rejected by the API.
```typescript theme={null}
// Valid
price: 0.01
price: 0.99
// Invalid (will throw)
price: 0
price: 1
price: 1.5
price: -0.1
```
Size must be a positive number. Fractional sizes are allowed if they align to the shares step (0.001). Max 3 decimal places.
```typescript theme={null}
// Valid
size: 1
size: 100
size: 1.5
size: 0.001
// Invalid (will throw)
size: 0
size: -1
size: 0.0001 // too many decimals
```
# Partner Accounts
Source: https://docs.limitless.exchange/developers/sdk/typescript/partner-accounts
Create and manage sub-accounts with the TypeScript SDK
The `PartnerAccountService` creates sub-account profiles linked to the authenticated partner. Requires HMAC authentication with the `account_creation` scope.
## Access
```typescript theme={null}
import { Client } from '@limitless-exchange/sdk';
const client = new Client({
baseURL: 'https://api.limitless.exchange',
hmacCredentials: { tokenId, secret },
});
// Use client.partnerAccounts.*
```
## Server wallet mode
Creates a managed Privy wallet for the sub-account. Enables [delegated signing](/developers/programmatic-api#sub-account-modes) — the partner submits unsigned orders and the server signs them.
```typescript theme={null}
const account = await client.partnerAccounts.createAccount({
displayName: 'user-alice',
createServerWallet: true,
});
console.log(account.profileId); // numeric profile ID
console.log(account.account); // wallet address
```
New server wallets should be checked with `checkAllowances()` before the first delegated trade. If retryable targets are missing or failed, call `retryAllowances()` and poll again.
## Allowance recovery
Server-wallet sub-accounts need delegated-trading approvals before they can trade. The partner allowance helpers use the Partner API only:
* `checkAllowances(profileId)` calls `GET /profiles/partner-accounts/:profileId/allowances`
* `retryAllowances(profileId)` calls `POST /profiles/partner-accounts/:profileId/allowances/retry`
* both methods require HMAC credentials with `account_creation` and `delegated_signing`
* `profileId` is the child/server-wallet profile id
```typescript theme={null}
import { APIError, RateLimitError } from '@limitless-exchange/sdk';
let allowances = await client.partnerAccounts.checkAllowances(account.profileId);
if (!allowances.ready) {
const retryableTargets = allowances.targets.filter(
(target) =>
target.retryable &&
(target.status === 'missing' || target.status === 'failed'),
);
if (retryableTargets.length > 0) {
try {
allowances = await client.partnerAccounts.retryAllowances(account.profileId);
} catch (error) {
if (error instanceof RateLimitError) {
console.log(error.data?.retryAfterSeconds);
} else if (error instanceof APIError && error.status === 409) {
console.log('Retry already running; poll checkAllowances again shortly.');
} else {
throw error;
}
}
}
}
if (allowances.targets.some((target) => target.status === 'submitted')) {
// A sponsored tx/user operation was submitted by this retry request.
// Poll checkAllowances() again after a short delay to observe confirmed chain state.
}
```
Recommended partner flow:
1. Poll `checkAllowances(profileId)`.
2. If `ready === true`, continue.
3. If targets are `missing` or `failed` with `retryable === true`, call `retryAllowances(profileId)`.
4. If retry returns `submitted` targets, poll `checkAllowances()` again after a short delay.
5. If retry returns `429`, wait `retryAfterSeconds`.
6. If retry returns `409`, wait briefly and call `checkAllowances()` again.
## Withdrawal address allowlist
Explicit treasury destinations for server-wallet withdrawals must be allowlisted on the authenticated partner profile unless the destination is already the partner account or partner smart wallet. Allowlist management uses a Privy identity token, not API-token/HMAC auth.
```typescript theme={null}
const identityToken = process.env.PRIVY_IDENTITY_TOKEN!;
const treasuryAddress = '0x0F3262730c909408042F9Da345a916dc0e1F9787';
const entry = await client.partnerAccounts.addWithdrawalAddress(identityToken, {
address: treasuryAddress,
label: 'treasury',
});
console.log(entry.destinationAddress);
await client.partnerAccounts.deleteWithdrawalAddress(identityToken, treasuryAddress);
```
`addWithdrawalAddress()` and `deleteWithdrawalAddress()` call `POST /portfolio/withdrawal-addresses` and `DELETE /portfolio/withdrawal-addresses/:address` with the `identity: Bearer ` header. `POST /portfolio/withdraw` still uses HMAC auth with the `withdrawal` scope.
## EOA mode
Creates a profile for an externally-owned address. The end user manages their own keys and signs orders themselves.
EOA mode requires wallet ownership verification headers:
```typescript theme={null}
const account = await client.partnerAccounts.createAccount(
{
displayName: 'user-bob',
},
{
account: '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed',
signingMessage: '0x...',
signature: '0x...',
},
);
```
| Header | Description |
| ---------------- | ------------------------------------- |
| `account` | Checksummed Ethereum address (EIP-55) |
| `signingMessage` | Hex-encoded signing message |
| `signature` | Hex-encoded signature from the wallet |
## Validation
* `displayName` is optional, max 44 characters. Defaults to the wallet address if omitted.
* Returns `409 Conflict` if a profile already exists for the target address.
* Cannot create a sub-account for the partner's own address.
* The SDK validates `displayName` length locally before sending the request.
# Portfolio & Positions
Source: https://docs.limitless.exchange/developers/sdk/typescript/portfolio
Track positions and history with the TypeScript SDK
## PortfolioFetcher Setup
`PortfolioFetcher` provides access to your positions, trade history, and accumulated points. An API key is required.
```typescript theme={null}
import { HttpClient, PortfolioFetcher } from '@limitless-exchange/sdk';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
});
const portfolio = new PortfolioFetcher(httpClient);
```
## All Positions
Fetch all positions across CLOB and AMM markets in a single call:
```typescript theme={null}
const positions = await portfolio.getPositions();
console.log('CLOB positions:', positions.clob.length);
console.log('AMM positions:', positions.amm.length);
console.log('Accumulative points:', positions.accumulativePoints);
```
### Response Structure
```typescript theme={null}
interface PositionsResponse {
clob: ClobPosition[];
amm: AmmPosition[];
accumulativePoints: number;
}
```
## CLOB Positions
Retrieve only your CLOB market positions:
```typescript theme={null}
const clobPositions = await portfolio.getCLOBPositions();
for (const position of clobPositions) {
console.log(`Market: ${position.market.slug}`);
console.log(` YES — size: ${position.positions.yes.size}, PnL: ${position.positions.yes.unrealizedPnl}`);
console.log(` NO — size: ${position.positions.no.size}, PnL: ${position.positions.no.unrealizedPnl}`);
}
```
### CLOB Position Structure
```typescript theme={null}
interface ClobPosition {
market: {
slug: string;
title: string;
};
positions: {
yes: {
size: number;
collateral: number;
unrealizedPnl: number;
};
no: {
size: number;
collateral: number;
unrealizedPnl: number;
};
};
}
```
| Field | Description |
| --------------- | ---------------------------------------------------------------- |
| `size` | Number of shares held |
| `collateral` | USDC value of collateral locked |
| `unrealizedPnl` | Unrealized profit/loss in USDC based on current orderbook prices |
## AMM Positions
Retrieve only your AMM market positions:
```typescript theme={null}
const ammPositions = await portfolio.getAMMPositions();
for (const position of ammPositions) {
console.log(`Market: ${position.market.slug}`);
console.log(` YES — size: ${position.positions.yes.size}`);
console.log(` NO — size: ${position.positions.no.size}`);
}
```
## Trade History
Fetch paginated trade history:
```typescript theme={null}
const history = await portfolio.getUserHistory(1, 25);
for (const entry of history) {
console.log(entry.type, entry.market.slug, entry.amount, entry.timestamp);
}
```
### Parameters
| Parameter | Type | Default | Description |
| --------- | -------- | ------- | -------------------------- |
| `page` | `number` | `1` | Page number (1-indexed) |
| `limit` | `number` | `10` | Number of entries per page |
## Combining Positions with Market Data
For a richer view, combine position data with live market information:
```typescript theme={null}
import { MarketFetcher } from '@limitless-exchange/sdk';
const marketFetcher = new MarketFetcher(httpClient);
const positions = await portfolio.getCLOBPositions();
for (const position of positions) {
const market = await marketFetcher.getMarket(position.market.slug);
const orderbook = await marketFetcher.getOrderBook(position.market.slug);
const bestBid = orderbook.bids[0]?.price ?? 0;
const bestAsk = orderbook.asks[0]?.price ?? 1;
const midPrice = (bestBid + bestAsk) / 2;
console.log(`${market.title}`);
console.log(` Mid price: ${midPrice.toFixed(4)}`);
console.log(` YES: ${position.positions.yes.size} shares, PnL: ${position.positions.yes.unrealizedPnl}`);
console.log(` NO: ${position.positions.no.size} shares, PnL: ${position.positions.no.unrealizedPnl}`);
}
```
For real-time PnL tracking, combine `PortfolioFetcher` with WebSocket orderbook updates rather than repeatedly polling `getOrderBook()`. See the [WebSocket Streaming](/developers/sdk/typescript/websocket) guide.
## Error Handling
Portfolio endpoints require a valid API key. If the key is missing or invalid, the SDK throws an `ApiError` with status `401`.
```typescript theme={null}
import { ApiError } from '@limitless-exchange/sdk';
try {
const positions = await portfolio.getPositions();
} catch (error) {
if (error instanceof ApiError) {
if (error.status === 401) {
console.error('Authentication failed. Check your LIMITLESS_API_KEY.');
} else {
console.error(`API error ${error.status}: ${error.message}`);
}
} else {
throw error;
}
}
```
See the [Error Handling and Retry](/developers/sdk/typescript/error-handling) guide for retry strategies and common error codes.
# WebSocket Streaming
Source: https://docs.limitless.exchange/developers/sdk/typescript/websocket
Real-time market data and position updates with the TypeScript SDK
## WebSocketClient Setup
The `WebSocketClient` connects to the Limitless Exchange WebSocket server for real-time market data, position updates, and transaction notifications.
```typescript theme={null}
import { WebSocketClient } from '@limitless-exchange/sdk';
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
autoReconnect: true,
});
ws.connect();
```
### Constructor Options
| Option | Type | Default | Description |
| --------------- | --------- | ------- | ------------------------------------------------------------------ |
| `url` | `string` | -- | WebSocket server URL |
| `autoReconnect` | `boolean` | `true` | Automatically reconnect on disconnection |
| `apiKey` | `string` | -- | Required for authenticated subscriptions (positions, transactions) |
Public subscriptions (market prices) do not require an API key. Authenticated subscriptions (positions, transactions) require the `apiKey` option to be set.
## Public Subscriptions
### Market Prices
Subscribe to real-time price updates for one or more markets. No authentication required.
```typescript theme={null}
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
});
ws.connect();
ws.subscribe('subscribe_market_prices', {
marketSlugs: ['btc-100k-weekly', 'eth-5k-daily'],
marketAddresses: ['0x1234...'], // For AMM markets
});
```
Subscriptions **replace** previous ones. To listen to both CLOB (by slug) and AMM (by address) markets, include both `marketSlugs` and `marketAddresses` in a single `subscribe_market_prices` call.
## Authenticated Subscriptions
These subscriptions require an API key passed to the constructor.
```typescript theme={null}
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
});
ws.connect();
```
### Positions
Subscribe to real-time updates when your positions change:
```typescript theme={null}
ws.subscribe('subscribe_positions');
ws.on('positions', (data) => {
console.log('Position update:', data);
});
```
### Transactions
Subscribe to transaction confirmations:
```typescript theme={null}
ws.subscribe('subscribe_transactions');
ws.on('tx', (data) => {
console.log('Transaction:', data.hash, data.status);
});
```
## Events
### orderbookUpdate (CLOB markets)
Fired when a CLOB market orderbook changes. Contains the full updated orderbook.
```typescript theme={null}
ws.on('orderbookUpdate', (data: OrderbookUpdate) => {
console.log('Market:', data.marketSlug);
console.log('Bids:', data.orderbook.bids.length);
console.log('Asks:', data.orderbook.asks.length);
if (data.orderbook.bids.length > 0 && data.orderbook.asks.length > 0) {
const spread = data.orderbook.asks[0].price - data.orderbook.bids[0].price;
console.log('Spread:', spread.toFixed(4));
}
});
```
### newPriceData (AMM markets)
Fired when AMM market prices update.
```typescript theme={null}
ws.on('newPriceData', (data: NewPriceData) => {
console.log('Market:', data.marketAddress);
console.log('YES:', data.updatedPrices.yes);
console.log('NO:', data.updatedPrices.no);
});
```
### positions
Fired when any of your positions change (fill, cancel, resolution).
```typescript theme={null}
ws.on('positions', (data: PositionEvent) => {
console.log('Updated positions:', data);
});
```
### tx
Fired on transaction events related to your account.
```typescript theme={null}
ws.on('tx', (data: TransactionEvent) => {
console.log('Tx hash:', data.hash);
console.log('Status:', data.status);
});
```
## Type Definitions
```typescript theme={null}
interface OrderbookUpdate {
marketSlug: string;
orderbook: {
bids: { price: number; size: number }[];
asks: { price: number; size: number }[];
};
}
interface NewPriceData {
marketAddress: string;
updatedPrices: {
yes: string;
no: string;
};
}
interface PositionEvent {
clob: Array<{
market: { slug: string };
positions: {
yes: { size: number; collateral: number };
no: { size: number; collateral: number };
};
}>;
}
interface TransactionEvent {
hash: string;
status: 'pending' | 'confirmed' | 'failed';
type: string;
}
```
## Connection Management
### Disconnect handling
Listen for disconnections and clean up resources:
```typescript theme={null}
ws.on('disconnect', (reason: string) => {
console.log('Disconnected:', reason);
});
ws.on('reconnect', () => {
console.log('Reconnected — resubscribing...');
ws.subscribe('subscribe_market_prices', {
marketSlugs: ['btc-100k-weekly'],
});
});
```
When `autoReconnect` is enabled, the client automatically attempts to reconnect. However, you must re-send your subscriptions after reconnection. Listen for the `reconnect` event to resubscribe.
### Graceful shutdown
Clean up the WebSocket connection on process exit:
```typescript theme={null}
process.on('SIGINT', () => {
console.log('Shutting down...');
ws.disconnect();
process.exit(0);
});
```
## Debugging
Log all raw events to inspect the data the server sends:
```typescript theme={null}
ws.onAny((eventName: string, ...args: unknown[]) => {
console.log(`[WS] ${eventName}:`, JSON.stringify(args, null, 2));
});
```
Use raw event logging during development to discover event shapes and debug subscription issues. Remove it before deploying to production.
## Full Example
A complete script that subscribes to market prices and positions:
```typescript theme={null}
import { WebSocketClient } from '@limitless-exchange/sdk';
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
autoReconnect: true,
});
ws.connect();
ws.subscribe('subscribe_market_prices', {
marketSlugs: ['btc-100k-weekly'],
});
ws.subscribe('subscribe_positions');
ws.subscribe('subscribe_transactions');
ws.on('orderbookUpdate', (data) => {
const best_bid = data.orderbook.bids[0];
const best_ask = data.orderbook.asks[0];
console.log(`[${data.marketSlug}] Bid: ${best_bid?.price ?? '-'} | Ask: ${best_ask?.price ?? '-'}`);
});
ws.on('positions', (data) => {
console.log('[Positions]', JSON.stringify(data));
});
ws.on('tx', (data) => {
console.log(`[Tx] ${data.hash} — ${data.status}`);
});
ws.on('disconnect', (reason) => {
console.log('[Disconnected]', reason);
});
ws.on('reconnect', () => {
ws.subscribe('subscribe_market_prices', {
marketSlugs: ['btc-100k-weekly'],
});
ws.subscribe('subscribe_positions');
ws.subscribe('subscribe_transactions');
});
process.on('SIGINT', () => {
ws.disconnect();
process.exit(0);
});
console.log('Listening for events... (Ctrl+C to stop)');
```
# Venue System
Source: https://docs.limitless.exchange/developers/venue-system
Understanding the venue system for CLOB market trading
## Overview
CLOB markets use a **venue system** where each market is associated with specific contract addresses. Before placing orders, you must fetch venue data for the market.
## How It Works
`GET /markets/:slug` returns venue information for the market.
This address is the `verifyingContract` for EIP-712 order signing.
Venue data is **static** per market — fetch once and reuse.
## Venue Response
```json theme={null}
{
"venue": {
"exchange": "0xA1b2C3...",
"adapter": "0xD4e5F6..."
}
}
```
## Required Token Approvals
Before trading, set up token approvals based on your order type:
| Order Type | Market Type | Approve To |
| ---------- | ----------------- | ------------------------------------------------------------- |
| **BUY** | All CLOB | USDC → `venue.exchange` |
| **SELL** | Simple CLOB | Conditional Tokens → `venue.exchange` |
| **SELL** | NegRisk / Grouped | Conditional Tokens → `venue.exchange` **AND** `venue.adapter` |
For NegRisk SELL orders, you must approve to **both** the exchange and the adapter addresses.
# WebSocket Events
Source: https://docs.limitless.exchange/developers/websocket-events
Real-time event reference for the Limitless WebSocket API
## Connection
**URL:** `wss://ws.limitless.exchange`
**Namespace:** `/markets`
**Transport:** WebSocket only (no polling fallback)
```typescript theme={null}
import { io } from 'socket.io-client';
const socket = io('wss://ws.limitless.exchange/markets', {
transports: ['websocket'],
extraHeaders: { 'X-API-Key': 'lmts_your_key_here' }
});
```
## Subscribing to Market Data
Subscribe to price and orderbook updates by emitting `subscribe_market_prices`:
```typescript theme={null}
// AMM price updates
socket.emit('subscribe_market_prices', {
marketAddresses: ['0x1234...']
});
// CLOB orderbook updates
socket.emit('subscribe_market_prices', {
marketSlugs: ['btc-100k-weekly']
});
// Both at once (recommended to avoid overwriting subscriptions)
socket.emit('subscribe_market_prices', {
marketAddresses: ['0x1234...'],
marketSlugs: ['btc-100k-weekly']
});
```
Subscriptions **replace** previous ones. If you want both AMM prices and CLOB orderbook, send both `marketAddresses` and `marketSlugs` together in a single call.
## Subscribing to Position Updates
Subscribe to real-time position changes by emitting `subscribe_positions`. This requires authentication via the `X-API-Key` header.
`subscribe_positions` accepts the same payload as `subscribe_market_prices`:
| Field | Type | Description |
| ----------------- | ---------- | ---------------------------------- |
| `marketAddresses` | `string[]` | Contract addresses for AMM markets |
| `marketSlugs` | `string[]` | Slugs for CLOB markets |
```typescript theme={null}
import { io } from 'socket.io-client';
const socket = io('wss://ws.limitless.exchange/markets', {
transports: ['websocket'],
extraHeaders: { 'X-API-Key': 'lmts_your_key_here' },
});
socket.on('connect', () => {
// Subscribe to position updates for specific markets
socket.emit('subscribe_positions', {
marketAddresses: ['0x1234...'], // AMM markets
marketSlugs: ['btc-100k-weekly'], // CLOB markets
});
});
// Confirmation
socket.on('system', (data) => {
console.log(data);
// { message: 'Successfully subscribed to position updates', markets: { addresses: [...], slugs: [...] } }
});
// Position updates
socket.on('positions', (data) => {
console.log('Position update:', data);
});
```
Like `subscribe_market_prices`, calling `subscribe_positions` again **replaces** the previous subscription. Include all markets in a single call.
Position updates are pushed automatically when your balances change (e.g. after a trade is mined). You do not need to poll.
## Subscribing to Order Events
Subscribe to the full lifecycle of your CLOB orders by emitting `subscribe_order_events`. Requires authentication via the `X-API-Key` header. The subscription takes no payload — it is a per-user channel that delivers events for every order you are party to.
Two distinct event shapes arrive on the same Socket.IO event name `orderEvent`. Distinguish by the `source` field:
* **`OME`** — off-chain matching engine state changes: `PLACEMENT`, `UPDATE`, `CANCELLATION`.
* **`SETTLEMENT`** — on-chain settlement results for CLOB trades: `MINED`, `FAILED`.
```typescript theme={null}
import { io } from 'socket.io-client';
const socket = io('wss://ws.limitless.exchange/markets', {
transports: ['websocket'],
extraHeaders: { 'X-API-Key': 'lmts_your_key_here' },
});
socket.on('connect', () => {
socket.emit('subscribe_order_events');
});
socket.on('system', (msg) => {
// { message: 'Successfully subscribed to order event updates' }
console.log(msg);
});
socket.on('orderEvent', (data) => {
if (data.source === 'OME') {
console.log(`[OME ${data.type}] order=${data.orderId} ${data.side} ${data.remainingSize} @ ${data.price}`);
} else {
console.log(`[SETTLEMENT ${data.type}] tx=${data.txHash ?? '(none)'} takerOrder=${data.takerOrderId ?? '(none)'}`);
}
});
```
**One subscription per connection.** Sending `subscribe_order_events` a second time re-binds; the previous subscription is cancelled. There is no payload.
**Channel is per-user, not per-market.** You receive events for every order you are party to (as taker or maker) — the subscription cannot be narrowed by market. Filter client-side on `marketId` / `marketSlug` if needed.
**Ordering is not guaranteed across sources.** OME and SETTLEMENT events for the same order can arrive in either order within a few seconds — settlement can land before the corresponding `UPDATE`, or vice versa. Treat them as independent streams that both reference `orderId` / `takerOrderId`.
**Resubscribe on reconnect.** Subscriptions are not persisted server-side across disconnects — your `connect` handler must re-emit `subscribe_order_events`.
**Server-side deduplication.** Repeated emissions within a 60-second sliding window are dropped, so retries and replays will not double-deliver. Client-side dedup is only required if you persist events across reconnects yourself.
**Auth failures surface on the `exception` channel.** If the `X-API-Key` header is missing, invalid, or revoked, the guard emits a `WsException` to the client's `exception` event and no `orderEvent` frames arrive. Subscribe to `socket.on('exception', ...)` if you want to detect auth failures rather than silently miss the stream.
## Subscribing to Market Lifecycle Events
Subscribe to market creation and resolution events by emitting `subscribe_market_lifecycle`. No authentication required.
```typescript theme={null}
socket.on('connect', () => {
socket.emit('subscribe_market_lifecycle');
});
socket.on('marketCreated', (data) => {
console.log('New market:', data.slug, data.title);
});
socket.on('marketResolved', (data) => {
console.log('Market resolved:', data.slug, data.winningOutcome);
});
```
`marketResolved` is also emitted to existing per-market room subscribers automatically (CLOB: `market:{slug}`, AMM: `market:{address}`) — you don't need a separate lifecycle subscription to receive resolution events for markets you're already watching.
To unsubscribe:
```typescript theme={null}
socket.emit('unsubscribe_market_lifecycle');
```
## Event Reference
| Event | Direction | Auth Required | Description |
| ------------------------------ | --------------- | ------------- | ----------------------------------------------------------- |
| `connect` | Server → Client | No | Connection established |
| `disconnect` | Server → Client | No | Connection lost |
| `subscribe_market_prices` | Client → Server | No | Subscribe to price/orderbook updates |
| `subscribe_positions` | Client → Server | Yes | Subscribe to position updates |
| `subscribe_order_events` | Client → Server | Yes | Subscribe to OME and settlement events for your CLOB orders |
| `subscribe_market_lifecycle` | Client → Server | No | Subscribe to market creation/resolution events |
| `unsubscribe_market_lifecycle` | Client → Server | No | Unsubscribe from market lifecycle events |
| `newPriceData` | Server → Client | No | AMM market price update |
| `orderbookUpdate` | Server → Client | No | CLOB orderbook update |
| `marketCreated` | Server → Client | No | New market created and visible |
| `marketResolved` | Server → Client | No | Market resolved with winning outcome |
| `positions` | Server → Client | Yes | Position balance update |
| `orderEvent` | Server → Client | Yes | OME or settlement update for one of your CLOB orders |
| `system` | Server → Client | No | System notifications |
| `authenticated` | Server → Client | Yes | Authentication confirmation |
| `exception` | Server → Client | No | Error notifications |
## Event Payloads
### `newPriceData`
```json theme={null}
{
"marketAddress": "0x1234...",
"updatedPrices": {
"yes": "0.65",
"no": "0.35"
},
"blockNumber": 12345678,
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
### `orderbookUpdate`
```json theme={null}
{
"marketSlug": "btc-100k-weekly",
"orderbook": { ... },
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
### `positions`
Position updates have different shapes depending on market type.
**AMM markets:**
```json theme={null}
{
"account": "0xabcd...",
"marketAddress": "0x1234...",
"positions": [
{
"tokenId": "123456",
"balance": "1000000",
"outcomeIndex": 0,
"collateralOutOnSell": "950000"
}
],
"type": "AMM"
}
```
**CLOB markets:**
```json theme={null}
{
"account": "0xabcd...",
"marketSlug": "btc-100k-weekly",
"positions": [
{
"tokenId": "19633204485790...",
"ctfBalance": "10000000",
"averageFillPrice": "0.65",
"costBasis": "6500000",
"marketValue": "7000000",
"marketId": 7348
}
],
"tokenIds": ["19633204485790..."],
"type": "CLOB"
}
```
### `marketCreated`
Emitted when a new market is funded and visible. Hidden markets are excluded.
```json theme={null}
{
"slug": "btc-above-110k-apr-2026",
"title": "$BTC above $110,000 on Apr 5, 2026?",
"type": "CLOB",
"groupSlug": "btc-price-markets",
"categoryIds": [1, 5],
"createdAt": "2026-04-02T12:00:00.000Z"
}
```
| Field | Type | Description |
| ------------- | ----------------- | ------------------------------------------ |
| `slug` | `string` | Market slug identifier |
| `title` | `string` | Market title |
| `type` | `'AMM' \| 'CLOB'` | Market type |
| `groupSlug` | `string?` | Parent group slug (for NegRisk submarkets) |
| `categoryIds` | `number[]?` | Category IDs the market belongs to |
| `createdAt` | `string` | ISO-8601 creation timestamp |
### `marketResolved`
Emitted when a market resolves. Sent to both `market_lifecycle` subscribers and existing `market:{slug}` room subscribers.
```json theme={null}
{
"slug": "btc-above-110k-apr-2026",
"type": "CLOB",
"winningOutcome": "YES",
"winningIndex": 0,
"resolutionDate": "2026-04-05T14:00:00.000Z"
}
```
| Field | Type | Description |
| ---------------- | ----------------- | ---------------------------------------------- |
| `slug` | `string` | Market slug identifier |
| `type` | `'AMM' \| 'CLOB'` | Market type |
| `winningOutcome` | `'YES' \| 'NO'` | The winning outcome |
| `winningIndex` | `0 \| 1` | Index of the winning outcome (0 = YES, 1 = NO) |
| `resolutionDate` | `string` | ISO-8601 resolution timestamp |
### `orderEvent`
Emitted for both OME state changes and on-chain settlement results. Distinguish the two shapes by the `source` field.
#### OME event (`source: "OME"`)
Emitted for every order state change recorded by the matching engine. `price` and `remainingSize` are decimal strings — do not parse them as `number` for comparisons.
```json theme={null}
{
"source": "OME",
"type": "PLACEMENT",
"eventId": 1234567,
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"clientOrderId": "client-order-001",
"userId": 42,
"marketId": "17",
"token": "87893014956437093847...",
"side": "BUY",
"price": "0.53",
"remainingSize": "100",
"timestamp": "2026-04-20T10:15:30.000Z"
}
```
| Field | Type | Description |
| --------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `source` | `'OME'` | Discriminator for the OME shape |
| `type` | `'PLACEMENT' \| 'UPDATE' \| 'CANCELLATION'` | OME state transition |
| `eventId` | `number` | Monotonic OME event id |
| `orderId` | `string` | UUID of the order |
| `clientOrderId` | `string?` | Client-supplied id from `POST /orders`, when the order was placed with one. Field is omitted (not `null`) when the originating order had no client id. |
| `userId` | `number` | Internal user id of the order owner |
| `marketId` | `string` | Numeric market id (CLOB) |
| `token` | `string` | CTF token id (decimal string) |
| `side` | `'BUY' \| 'SELL'` | Order side |
| `price` | `string` | Limit price (decimal string) |
| `remainingSize` | `string` | Size remaining on the book (decimal string) |
| `timestamp` | `string` | ISO-8601 event timestamp |
`type` transitions:
* `PLACEMENT` — order accepted by the OME.
* `UPDATE` — remaining size changed (partial fill or amend).
* `CANCELLATION` — removed from the book.
#### Settlement event (`source: "SETTLEMENT"`)
Emitted when the taker side of a CLOB trade settles on-chain. One event per user involved in the trade (taker plus each maker), keyed by `userId`. `takerAccount` and `makerMatches[].account` are on-chain addresses — for smart-wallet users this is the smart-wallet proxy, for EOA users it is the EOA.
```json theme={null}
{
"source": "SETTLEMENT",
"type": "MINED",
"eventId": "settlement:1b3a…",
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"clientOrderId": "client-order-001",
"userId": 42,
"takerOrderId": "e4c3…",
"takerAccount": "0xAbC…123",
"makerMatches": [
{
"account": "0xDeF…456",
"orderId": "cb12…",
"matchedSize": "25",
"price": "0.53"
}
],
"marketSlug": "will-abc-happen-by-2026",
"txHash": "0xabc…",
"timestamp": "2026-04-20T10:15:40.000Z"
}
```
| Field | Type | Description |
| --------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source` | `'SETTLEMENT'` | Discriminator for the settlement shape |
| `type` | `'MINED' \| 'FAILED'` | Settlement outcome |
| `eventId` | `string` | `settlement:` |
| `orderId` | `string?` | UUID of the order this event is keyed to |
| `clientOrderId` | `string?` | Client-supplied id of the recipient's own order (`orderId`), when the order was placed with one. Field is omitted (not `null`) when the originating order had no client id. Counterparty (`takerOrderId`, `makerMatches[].orderId`) ids are never resolved to client ids. |
| `userId` | `number` | Internal user id of the recipient |
| `takerOrderId` | `string?` | UUID of the taker order in the trade |
| `takerAccount` | `string?` | On-chain address of the taker (smart-wallet proxy or EOA) |
| `makerMatches` | `array?` | One entry per matched maker (account, orderId, matchedSize, price) |
| `marketSlug` | `string?` | CLOB market slug |
| `txHash` | `string?` | Settlement transaction hash, when known |
| `timestamp` | `string` | ISO-8601 event timestamp |
**Reconciling WS events with `POST /orders`.** Submit an order with a `clientOrderId` and every `orderEvent` for that order — `PLACEMENT`, `UPDATE`, `CANCELLATION`, and the `MINED` / `FAILED` settlement frame — echoes the same `clientOrderId`. Match WS events to the originating request by `clientOrderId` rather than waiting for the `POST /orders` HTTP response, which only returns once settlement is `MINED`.
* `type: "MINED"` — on-chain settlement confirmed.
* `type: "FAILED"` — settlement failed on-chain; the taker order did not execute and funds were not moved.
# Introduction
Source: https://docs.limitless.exchange/index
Limitless Exchange — the leading prediction market on Base
The leading prediction market on Base. Predict crypto & stock prices with nonstop hourly and daily markets. Trade anytime, anywhere. **\$1B+ traded.**
Learn how prediction markets work and why Limitless exists.
Get started trading in minutes.
Build on the Limitless API — REST + WebSocket.
Full endpoint documentation with examples.
## Quick Overview
On Limitless, you can **buy and sell shares** representing future event outcomes (e.g. "Will BTC reach \$100k this week?").
* Shares are **always priced between \$0.01 and \$0.99** USDC
* Every pair of "YES" + "NO" shares is fully collateralized by \$1.00 USDC
* Shares are created when **opposing sides come to an agreement on odds** — the sum of what each side is willing to pay equals \$1.00
* Winning shares pay out **\$1.00**. Losing shares are worthless after market resolution.
Go to limitless.exchange to start trading now.
Chat with traders and the Limitless team.
Need help? Use the **support channel** in the footer at [limitless.exchange](https://limitless.exchange), or reach out at [help@limitless.network](mailto:help@limitless.network).
# Analytics
Source: https://docs.limitless.exchange/user-guide/analytics
Explore Limitless Exchange metrics on Dune
## Dune Dashboard
Explore the metrics on the Dune dashboard to understand the dynamics of our prediction markets and the growth of the ecosystem.
View live analytics: volume, users, markets, and more.
# CLOB Overview
Source: https://docs.limitless.exchange/user-guide/clob-overview
How the Limitless Central Limit Order Book works
## Overview
Interacting with Limitless's order book is simple and familiar for both traders and market makers. Market orders provide instant execution at the best available price, while limit orders allow you to set your price, participate in price discovery, and earn rewards.
## The Orderbook
Each market has a **Yes** and **No** order book, with separate **Bids** (buy orders) and **Asks** (sell orders).
For example, in a market about the BTC price:
* Traders are offering to **sell** "Yes" shares at **51 cents** — this is the price you'd pay if you placed a Market Order to buy.
* Traders are **bidding** 49 cents to **buy** "Yes" shares — the amount you'd receive if you placed a Market Order to sell.
The difference between them is the **Spread** — the lower the better.
## Order Types
| Type | Description | Best For |
| ----------------------------------------- | ----------------------------------------- | --------------------------------- |
| [Market Order](/user-guide/market-orders) | Instant execution at best available price | Quick trades |
| [Limit Order](/user-guide/limit-orders) | Set your own price | Price control, earning LP rewards |
## Advanced Features
* **[Merge / Split shares](/user-guide/merge-split)** — Convert between collateral and share pairs
* **[Negrisk markets](/user-guide/negrisk-overview)** — Category markets where only one outcome wins
# Converting Shares
Source: https://docs.limitless.exchange/user-guide/converting-shares
Swap between outcomes in Negrisk markets
## Converting 1 "No" Share
If you hold a **No share** in a Negrisk market (like "ETH is NOT \$2,000 on Friday"), you can **convert it into 1 Yes share** of *all* the other outcomes.
Why? Because only one outcome can be true, so a No on one is logically equivalent to a bet *for* the others.
This lets you switch positions quickly and cheaply — without selling one share and buying another.
Access the **Convert** button from the portfolio tab in each market.
## Converting Multiple "No" Shares
If you hold **multiple No shares** and there aren't enough remaining Yes shares to convert into, you can convert the extras into **USDC**.
The combined value of "Event" and "Not Event" in a Negrisk market always adds up to \$1.
**Example:**
If you hold 5 No shares in a 3-outcome market:
* Convert 1 into a Yes of the remaining outcome
* **Receive USDC for the second**
It's built-in arbitrage that helps keep prices aligned — and it's gas-efficient.
# Fees
Source: https://docs.limitless.exchange/user-guide/fees
How trading fees work on Limitless Exchange
## Overview
Fees on Limitless adapt to both the **market price** and your **trading experience**. The system is designed to reward early conviction, discourage manipulation, and give loyal traders significant discounts.
## Fee Structure by Market Type
### AMM Markets
A flat **0.40%** fee applies to all trades. No curves, no complexity. Certain promotional or specific markets may have a reduced or zero fee.
### Order Book (CLOB) Markets
Fees are dynamic and adjust based on the current market price:
| Side | Fee Range | Paid In |
| -------- | ------------- | -------------------------- |
| **Buy** | 0.40% – 3.00% | Outcome tokens (contracts) |
| **Sell** | 0.42% – 1.50% | Collateral (USDC) |
## Makers vs Takers
**Fees only apply to takers** — orders that instantly settle against the orderbook.
| Order Type | Fees? | Details |
| -------------------------- | ---------- | ------------------------------------------------------------------------------------------------------ |
| **Limit orders (makers)** | No fees | Even though you sign a transaction with fee terms, you pay nothing if your order is placed in the book |
| **Market orders (takers)** | Fees apply | Only orders that match immediately are charged |
If you provide liquidity to the book, you can trade **completely fee-free**.
## The Fee Curve
The fee curve adjusts dynamically based on where the market price is when you trade.
### Buy Fee Curve
Buy fees start at 3.00% for low-probability markets and decrease as probability increases:
| Price | Buy Fee |
| --------------- | ------- |
| \$0.01 – \$0.50 | 3.00% |
| \$0.55 | 2.52% |
| \$0.60 | 2.13% |
| \$0.65 | 1.80% |
| \$0.70 | 1.51% |
| \$0.75 | 1.26% |
| \$0.80 | 1.05% |
| \$0.85 | 0.85% |
| \$0.90 | 0.68% |
| \$0.95 | 0.53% |
| \$0.99 | 0.42% |
| \$0.999 | 0.40% |
### Sell Fee Curve
Sell fees peak at the midpoint (\$0.50) and decrease toward the extremes:
| Price | Sell Fee |
| ------- | ---------------- |
| \$0.01 | 0.42% |
| \$0.05 | 0.60% |
| \$0.10 | 0.78% |
| \$0.20 | 1.11% |
| \$0.30 | 1.32% |
| \$0.40 | 1.44% |
| \$0.50 | **1.50%** (peak) |
| \$0.60 | 1.44% |
| \$0.70 | 1.32% |
| \$0.80 | 1.11% |
| \$0.90 | 0.78% |
| \$0.95 | 0.60% |
| \$0.99 | 0.45% |
| \$0.999 | 0.42% |
By discouraging exits at the midpoint, the fee curve protects market integrity when uncertainty is highest.
## Example: How Fees Play Out
Imagine you're buying **YES** shares in a market where the current probability is **40%** (\$0.40):
Your buy fee = **3.00%**. You pay the full fee, but you're getting in early with better payout odds.
If you sell when the market hits 70%, you pay a **1.32% sell fee** because you're away from the 50/50 uncertainty zone.
They pay a **1.51% buy fee** — lower than yours, but they get worse odds since the price is already high.
It's all about conviction. Act early and stay smart — the system gives you the edge.
## Why This Fee Model Works
The fee structure is inspired by proven models (including Polymarket and Kalshi) and is designed to:
* **Reward early traders** for taking risk
* **Discourage flip-flopping** and short-term manipulation
* **Incentivize long-term** loyal participation
* **Keep markets fair**, liquid, and meaningful
# Limit Orders
Source: https://docs.limitless.exchange/user-guide/limit-orders
Set your own price and earn LP rewards
## Overview
Limit orders let you set a specific price at which you're willing to trade. This gives you more flexibility and control over your orders.
## How to Place a Limit Order
1. Decide whether you're **buying** (bid) or **selling** (ask)
2. Specify your desired **price**
3. Enter the **number of shares**
4. Submit — the system adds your order to the order book, visible to other traders
Orders closer to the current market price have a **higher chance of being filled**.
## Benefits
* **Price control** — Trade at the exact price you want
* **Spread management** — Help narrow the bid-ask spread
* **LP rewards** — Earn rewards through the [LP Rewards program](/user-guide/lp-rewards) for providing liquidity
Engaging with limit orders strategically can earn you rewards as part of the LP Rewards program, incentivizing tighter spreads and greater market depth.
# LP Rewards
Source: https://docs.limitless.exchange/user-guide/lp-rewards
Earn rewards for providing liquidity with limit orders
## Overview
The LP Rewards program incentivizes providing liquidity by rewarding well-placed limit orders. These rewards promote tighter spreads, thicker order books, and a healthy marketplace.
The design iterates on an industry-standard approach used by dYdX and Polymarket, with specifically tuned parameters to ensure competitiveness and fairness.
## Methodology
* Rewards are earned by placing limit orders **within a specific spread from the market midpoint**\*
* The **closer** an order is to the midpoint and the **more competitive** its size, the higher the reward
* To qualify, orders must exceed the **minimum size threshold** set on a per-market basis
* Rewards are calculated **every minute** across all markets and paid out daily at **12:00 UTC**
\*To maintain a fair market-making experience, only limit orders that exceed the minimum shares threshold count toward the midpoint calculation.
## Rewards Parameters
Each market has 3 parameters that define how rewards are paid out:
| Parameter | Example | Description |
| -------------- | ------- | ---------------------------------------------- |
| **Rewards** | \$200 | Daily USDC rewards available for the market |
| **Spread** | 3c | Maximum distance from midpoint to earn rewards |
| **Min Shares** | 100 | Minimum order size to qualify |
These parameters are set to support positive market activity while accounting for each market's uniqueness.
## Bonus Multiplier
Limit orders placed **closer to the midpoint** earn additional rewards through a bonus multiplier. The tighter your spread, the greater your share of rewards.
## One-Sided Liquidity
To promote tail-end speculation, earning from one-sided liquidity is only allowed while the odds are between **5% - 95%**. This provides LPs with greater flexibility in managing their positions while still fostering a robust and efficient market structure.
## Note on Adjustments
These parameters are dynamic and may be adjusted over time to meet the exchange's needs and maintain a thriving marketplace.
# Maker rebates
Source: https://docs.limitless.exchange/user-guide/maker-rebates
Earn daily USDC rewards for providing maker liquidity
## Overview
Maker Rebates is a daily USDC rewards program for market makers. When a taker removes liquidity from the order book and pays a taker fee, a portion of that fee is credited back to the maker whose resting order was filled. Rebates are based on real executed trading activity and paid in USDC.
The program works in two stages:
1. **Rebate credit accrual (continuous):** as trades execute, the system calculates rebate credit for eligible maker fills.
2. **USDC payout (daily):** once per day (UTC), the system distributes USDC to eligible makers in proportion to the rebate credit they generated that day.
A simple mental model:
* Your fills generate **rebate credit** during the day.
* All eligible makers generate a **daily total** rebate credit.
* You receive a **pro-rata share** of the daily USDC payout based on your share of that total.
## How rebate credit is generated
Rebate credit is generated only by **executed fills** where your order acted as maker liquidity. Orders that remain on the book but are never filled do not generate rebate credit.
For each eligible fill, rebate credit is determined using the fill's execution details (including size and price), the applicable taker fee for that trade, and the program's current eligibility settings (such as market and category eligibility).
### Conceptual formula
For a single eligible maker fill:
```
Executed Value (USD) = Executed Size × Fee-Applicable Price
Rebate Credit = Executed Value × Taker Fee Rate × Program Eligibility Factor
```
Where:
* **Executed Size** is the size of the maker fill.
* **Fee-Applicable Price** is the price basis used for fee calculation on that execution.
* **Taker Fee Rate** is the taker fee applied to the trade.
* **Program Eligibility Factor** is the program's active market/category eligibility configuration for that fill.
Your rebate credit grows when your resting liquidity gets filled and generates taker fees.
## Daily accrual window
Rebate credit accrues continuously throughout the day and is grouped into **UTC-day windows**.
* A trading fill contributes to the day in which it was finalized.
* At the end of the UTC day, all eligible rebate credit is totaled for payout.
Your payout for a given day depends on all eligible fills finalized during that UTC day.
## Daily payout mechanics
Maker Rebates are calculated daily and paid in **USDC**. For each UTC day, the system aggregates all eligible rebate credit generated by filled maker orders and determines each maker's share of that day's total.
Your payout is proportional to your contribution. If your fills generated 8% of the day's eligible rebate credit, you receive approximately 8% of that day's rebate payout.
The daily rebate payout is funded from the rebate wallet and tied to eligible trading activity under the current program configuration. In normal conditions, the system distributes the full scheduled amount for that day. If the rebate wallet balance is lower than the scheduled payout amount, the available USDC is distributed pro-rata and the shortfall is recorded.
### Payout formula
For a given UTC day:
* **Your share** = Your eligible rebate credit / Total eligible rebate credit
* **Your payout (USDC)** = Your share × Daily rebate payout amount
Rebates are performance-based: the more eligible fee-generating maker fills you provide (relative to other makers), the larger your share of the daily payout.
## Example
Assume **Hourly Crypto** and **15-minute Crypto** markets are configured with a **100% rebate rate** (i.e., 100% of eligible taker fees funds the rebate pool for those markets).
During one UTC day, the following trades execute in an eligible hourly or 15-minute market:
| **Trade** | **Taker action** | **Price** | **Size** | **Executed value** | **Taker fee rate** | **Taker fee paid** | **Maker** |
| --------- | ---------------- | --------- | -------- | ------------------ | ------------------ | ------------------ | --------- |
| 1 | Buy | \$0.60 | 1,000 | \$600 | 2.0% | \$12.00 | Maker A |
| 2 | Sell | \$0.55 | 500 | \$275 | 2.0% | \$5.50 | Maker B |
| 3 | Buy | \$0.65 | 800 | \$520 | 2.0% | \$10.40 | Maker A |
Only executed maker fills generate rebate credit. Each trade contributes rebate credit based on the executed trade and the taker fee paid.
* **Maker A rebate credit:** \$12.00 + \$10.40 = **\$22.40**
* **Maker B rebate credit:** **\$5.50**
* **Total rebate credit:** **\$27.90**
Total eligible taker fees collected in this market for the day:
\$12.00 + \$5.50 + \$10.40 = **\$27.90**
Hourly / 15-minute Crypto rebate rate = **100%**
Daily rebate pool (this market) = 100% × \$27.90 = **\$27.90 USDC**
Each maker receives a share of the rebate pool proportional to their share of the day's rebate credit:
* **Maker A share:** 22.40 / 27.90 = **80.29%** → **\$22.40 USDC**
* **Maker B share:** 5.50 / 27.90 = **19.71%** → **\$5.50 USDC**
The market generated **\$27.90** in eligible taker fees, **\$27.90 USDC** was allocated to the rebate pool (100%), and that amount was split between makers based on their contribution to the day's eligible fee-generating fills.
## Eligibility
Maker Rebates only applies to **eligible trading activity**.
If a fill occurs in a market that is not included in the current program configuration, that fill does not generate rebate credit and does not contribute to payouts.
Eligibility may be configured by:
* **Specific markets**
* **Market categories**
* A combination of both
### What counts toward maker rebates
* Executed fills where your order provided maker liquidity
* Fills in currently eligible markets/categories
* Fills that generate taker fees under the active fee schedule
### What does not count
* Orders that were posted but never filled
* Fills in non-eligible markets/categories
* Trading activity outside the program's active configuration
### Current program parameters
| Market | Rebate rate |
| ---------------------------- | --------------------------- |
| **Daily markets** | 100% of eligible taker fees |
| **Hourly Crypto** | 100% of eligible taker fees |
| **15-minute Crypto markets** | 100% of eligible taker fees |
## Funding and program economics
Maker Rebates is funded by taker fees and paid from a dedicated rebate wallet in USDC.
The program is defined by three parameters:
| Parameter | Description |
| ----------------------- | --------------------------------------- |
| **Rebate percentage** | What portion of taker fees is rebated |
| **Eligible markets** | Which markets generate rebate credit |
| **Eligible categories** | Which categories generate rebate credit |
These parameters determine where rebate credit is generated and how much is available for daily payouts.
## Payment timing and settlement
Rebates are calculated on a **UTC-day basis** and paid **daily** in USDC.
* Rebate credit accrues continuously as trades are finalized.
* The system computes the daily payout after the UTC day closes.
* USDC is distributed to eligible maker wallets.
Exact payout timing may depend on the daily payout run schedule, but payouts are always tied to the corresponding UTC-day accrual window.
## Tips for market makers
Maker Rebates rewards **executed maker liquidity**, not just quote presence. Makers tend to generate more rebate credit when they:
* Maintain competitive quotes
* Provide size where demand exists
* Achieve higher fill rates on eligible markets
# Making Your First Trade
Source: https://docs.limitless.exchange/user-guide/making-your-first-trade
A step-by-step guide to placing your first trade on Limitless Exchange
## Getting Started
Visit [limitless.exchange](https://limitless.exchange) and connect your wallet. Limitless supports wallets on the **Base** network.
You'll need **USDC on Base** to trade. Bridge or transfer USDC to your Base wallet address.
Browse active markets — crypto prices, stocks, macro events, and more. Each market has a clear resolution condition and deadline.
Choose "Yes" or "No" and enter the number of shares you want to buy. A **market order** fills instantly at the best available price.
Track your positions in your portfolio. You can sell shares anytime before the market resolves, or hold until resolution.
## What Happens at Resolution?
* **Winning shares** → pay out **\$1.00** each
* **Losing shares** → worthless (\$0.00)
Your profit is `$1.00 - purchase price` per winning share.
## Next Steps
Learn about instant-execution market orders.
Set your own price with limit orders.
# Managing Orders
Source: https://docs.limitless.exchange/user-guide/managing-orders
View and cancel your open orders
## Viewing Open Orders
Navigate to the market's order book page to view your active orders. Each order shows its side (buy/sell), price, size, and status.
## Cancelling Orders
To cancel an order, simply click the **cancel button** next to it. Your locked funds will be returned immediately.
You can also cancel orders programmatically via the API:
* [Cancel a single order](/api-reference/trading/cancel-order)
* [Cancel orders in batch](/api-reference/trading/cancel-batch)
* [Cancel all orders in a market](/api-reference/trading/cancel-all)
# Market Orders
Source: https://docs.limitless.exchange/user-guide/market-orders
Instant execution at the best available price
## Overview
Placing a market order is straightforward and ideal for quick trades. Simply choose the number of shares you wish to buy or sell, and the system matches your order to the best available price.
## How It Works
1. Select **Buy** or **Sell**
2. Choose the outcome — **Yes** or **No**
3. Enter the number of shares
4. Click **Trade** — your order is matched instantly
Market orders are simple and efficient, especially for those seeking immediate execution.
Market orders execute at the **best available price**, which may differ from the displayed price if the orderbook moves between when you view it and when your order is matched.
## When to Use Market Orders
* You want **instant execution**
* The market has tight spreads and good liquidity
* You're not sensitive to small price differences
# Market Resolution
Source: https://docs.limitless.exchange/user-guide/market-resolution
How markets are resolved on Limitless Exchange
## Overview
Every market on Limitless has a defined resolution condition and deadline. When the deadline passes, the market is resolved based on objective data from the designated oracle source.
## Resolution Process
Each market has a specific deadline (e.g., "Feb 16, 17:00 UTC"). No new trades are accepted after this time.
The designated oracle source provides the verified result. The majority of markets are now resolved automatically by **Pyth Network**.
For manually resolved markets, the Limitless team reviews the outcome against the resolution criteria defined on the market page. Resolution typically occurs within **24–72 hours** of the market deadline, depending on event complexity and data availability.
Winning shares ("Yes" or "No" depending on outcome) are redeemable for \$1.00 each after the payout has been settled on-chain. Losing shares become worthless.
## Oracle Sources
Market resolution and on-chain redemption readiness are related but not identical. A market can be shown as resolved in the API before the underlying payout is fully settled on-chain, so direct redemption attempts may need to wait until the payout data has been posted.
The exact method of market resolution is normally determined on the market page. Always check the resolution source and criteria on the specific market before trading.
The majority of markets are now resolved by **Pyth Network**. Other markets may use manual resolution by the Limitless team.
| Oracle | Used For | Resolution Time |
| ---------------- | ----------------------------------------------------------------------- | ------------------------------------ |
| **Pyth Network** | Majority of markets — crypto prices, stock prices, and other data feeds | Automatic at deadline |
| **Manual** | Custom event markets (sports, politics, etc.) | Typically 24–72 hours after deadline |
## Resolution for Different Market Types
### Standard (CLOB) Markets
Binary Yes/No outcome. One side wins, the other loses.
### Negrisk (Category) Markets
Only one outcome in the group can win. All other outcomes resolve to "No". See [Negrisk Markets](/user-guide/negrisk-overview) for details.
## Disputes and Misresolutions
In cases of misresolved markets, the bet amount will be refunded. See the [Refund Policy](/user-guide/refund-policy) for details.
If you believe a market was resolved incorrectly, contact the team through the **support channel** on [limitless.exchange](https://limitless.exchange) (footer), via email at [help@limitless.network](mailto:help@limitless.network), or on [Discord](https://discord.com/invite/yb4SscD8RZ).
# Merging & Splitting Shares
Source: https://docs.limitless.exchange/user-guide/merge-split
Convert between collateral and outcome share pairs
## Overview
Prediction markets have two native mechanisms that don't exist in traditional instruments: **Merging** and **Splitting** shares.
## Splitting
**Split \$1** into **1 Yes share + 1 No share**.
This is useful when you want to take a position on one side without buying from the orderbook — split collateral and sell the side you don't want.
## Merging
**Merge 1 Yes share + 1 No share** back into **\$1** collateral.
This lets you exit both sides of a position and recover your collateral without selling on the orderbook.
## Why Use Merge / Split?
* **Capital efficiency** — Open and close positions without relying on orderbook liquidity
* **Better execution** — Avoid paying the spread
* **Liquidity defragmentation** — Under the hood, this enables better orderbook matching across Yes/No assets
Merge and split operations are available directly in the market UI. Look for the **Merge/Split** option on the market page.
# Negrisk / Category Markets
Source: https://docs.limitless.exchange/user-guide/negrisk-overview
Multi-outcome markets where only one result can win
## What Are Negrisk Markets?
Negrisk markets are a special kind of prediction market bundle where **only one outcome can win**. Think of them like a multiple-choice question — only one answer is correct.
Each outcome has a Yes and No orderbook, but here's the twist: **all the "No" contracts across the different outcomes are linked**.
This setup lets traders interact with bundles of outcomes more efficiently and powers a unique feature: **[share conversion](/user-guide/converting-shares)**.
## Capital Efficiency
Traditional markets require you to open separate positions for each outcome. With Negrisk, you can:
* **Switch outcomes on the fly** by converting No → Yes
* **Take broad positions** (e.g., "I think it's *not* A or B") and refine later
* **Unlock USDC** by converting extra No shares back into cash
This boosts **capital efficiency** — you do more with fewer trades and less capital tied up.
## Powering Financial Dashboards
Negrisk markets unlock forward-looking data streams for real-world decisions:
| Dashboard | Example |
| --------------------------- | -------------------------------------------------------------------- |
| **Price Feeds** | "According to Limitless, ETH will close the week at \$2,150" |
| **Interest Rate Forecasts** | "Markets expect the Fed to cut rates in July by 0.25bps" |
| **Funding Round Forecasts** | "Anthropic's post-money valuation will be \$6.4B" |
| **Portfolio Advice** | "Your portfolio's risk-adjusted future is RED. Reduce BTC exposure." |
These dashboards aren't just informational — they're **tradeable signals**.
# Privacy Policy
Source: https://docs.limitless.exchange/user-guide/privacy-policy
Limitless Exchange Privacy Policy
*Effective: March 19, 2025*
This Privacy Policy ("**Policy**") outlines how Street Chow Inc., a company incorporated under the laws of Panama ("**we**", "**us**", or "**our**"), collects, uses, shares, and protects personal information of users ("**you**" or "**users**") of our online predictions market platform (the "**Platform**"). By accessing or using our Platform, you agree to the terms of this Privacy Policy.
## 1. Introduction
This Privacy Policy governs all aspects of information collection, processing, and protection across our Platform's services, including but not limited to account creation, market participation, transaction processing, and user interactions. It applies to all users accessing our Platform, whether as registered members or visitors.
By using our Platform, you acknowledge that you have read and understood this Privacy Policy and agree to be bound by its terms. If you do not agree with any aspect of this Policy, you should immediately discontinue use of our Platform.
## 2. Information We Collect
We collect and process two main categories of information:
### Personal Information
Information that can directly or indirectly identify you as an individual, including:
* Full name, email address, postal address, phone number
* Date of birth, government-issued identification documents
* Banking and financial information
* Account login credentials
We collect this information when you create an account, complete your profile, make transactions, or communicate with us.
### Usage Data
Information about how you interact with our Platform, including:
* IP address, browser type and version, device information
* Operating system, time zone setting
* Access times and duration, pages viewed, features used
* Transaction history, betting patterns, and other diagnostic data
We collect this information automatically through cookies, web beacons, and similar tracking technologies.
We maintain detailed records of all predictions market transactions, including stakes placed, outcomes, and associated timestamps.
## 3. How We Use Your Information
We process your personal information for the following purposes:
* **Account Management and Platform Operations**: Creating and maintaining your account, processing transactions, providing customer support, verifying identity, and processing payments.
* **Platform Improvement and Analytics**: Analyzing user behavior and platform performance to enhance services, develop new features, and optimize user experience using industry-standard analytics tools.
* **Legal Compliance and Risk Management**: Complying with applicable laws and regulations, including anti-money laundering requirements, tax reporting, fraud detection, and responding to legal requests from authorities.
We retain your information only for as long as necessary to fulfill these purposes or as required by applicable law.
## 4. Information Sharing and Disclosure
We may share your personal information with third parties in the following circumstances:
* With **affiliated companies and subsidiaries** who assist in operating our Platform, subject to confidentiality obligations
* With **third-party service providers** (payment processors, cloud storage, analytics services, customer support) contractually bound to protect your information
* When **required by law**, regulation, or legal process, including court orders, subpoenas, or government requests
* In connection with a **merger, acquisition, bankruptcy, dissolution**, or similar corporate event
We do not sell, rent, or lease your personal information to third parties for their marketing purposes without your explicit consent.
## 5. Data Security
We implement and maintain appropriate technical and organizational security measures to protect your Personal Information, including:
* Industry-standard encryption protocols for data in transit and at rest
* Secure socket layer (SSL) technology
* Firewalls and restricted access controls
* Regular security assessments and audits
Access to Personal Information is strictly limited to authorized employees, agents, and service providers bound by confidentiality obligations.
In the event of a security breach that affects your Personal Information, we will notify you and the relevant authorities as required by applicable law.
## 6. Your Rights and Choices
You have certain rights regarding your personal information:
* **Right to access** your personal information and request details about how we process it
* **Right to correction** of inaccurate personal information
* **Right to deletion** of your personal information, subject to legal obligations
* **Right to object** to or restrict certain processing activities
* **Right to opt-out** of marketing communications at any time
To exercise these rights, contact our Data Protection Officer at [help@limitless.network](mailto:help@limitless.network). We will respond to all legitimate requests within thirty (30) days.
## 7. International Data Transfers
We may transfer, store, and process your personal information in countries other than Panama. By using the Platform, you acknowledge and consent to such transfers. For transfers outside the European Economic Area (EEA), we implement appropriate safeguards including Standard Contractual Clauses approved by the European Commission.
## 8. Children's Privacy
The Platform is not directed to children under the age of 18. We do not knowingly collect personal information from children under 18. If we become aware that we have inadvertently collected such information, we will take steps to delete it as soon as possible.
If you believe a child under 18 has submitted personal information to our Platform, please contact us immediately at [help@limitless.network](mailto:help@limitless.network).
## 9. Changes to This Privacy Policy
We reserve the right to update or modify this Privacy Policy at any time without prior notice. Material changes will be communicated through a prominent notice on our Platform or by email. Your continued use of the Platform following any changes constitutes your acceptance of the revised Policy.
## 10. Contact Us
For any questions, concerns, or requests related to your personal information or this Privacy Policy:
**Email**: [help@limitless.network](mailto:help@limitless.network)
We will endeavor to respond to your inquiries within 30 days of receipt.
## 11. Governing Law and Dispute Resolution
This Privacy Policy shall be governed by and construed in accordance with the laws of Panama. Any dispute arising out of or relating to this Privacy Policy shall be settled by binding arbitration in Panama City, Panama, in accordance with the Rules of Arbitration of the International Chamber of Commerce.
YOU HEREBY WAIVE ANY RIGHT TO TRIAL BY JURY IN ANY ACTION ARISING OUT OF OR RELATING TO THIS PRIVACY POLICY.
# Refund Policy
Source: https://docs.limitless.exchange/user-guide/refund-policy
Limitless Exchange refund policy for misresolved markets
## Refund Policy
In cases of misresolved markets, **only the bet amount will be refunded**.
### How Misresolution Refunds Work
If a market is resolved to the wrong outcome:
* Users who held shares on the **incorrectly winning side** will have those shares voided
* Users who held shares on the **actual winning side** (the side that should have won) will receive a refund equal to the amount they originally paid for those shares
* The refund covers the **cost basis** of the shares, not the potential payout
In other words, if the market was resolved to side A but should have been resolved to side B, holders of side B shares receive a refund of the amount they paid for those shares. This ensures no user is financially harmed by the misresolution beyond their original position.
## How to Report a Misresolution
If you believe a market was resolved incorrectly, contact the team through any of these channels:
* **Support**: Use the support channel on [limitless.exchange](https://limitless.exchange) (in the footer)
* **Email**: [help@limitless.network](mailto:help@limitless.network)
* **Discord**: [Join our Discord](https://discord.com/invite/yb4SscD8RZ)
# Security
Source: https://docs.limitless.exchange/user-guide/security
Smart contract architecture and security audits for Limitless Core
## Overview
Limitless Core is the onchain primitive layer that powers the exchange. It runs on Base and is built on three contract suites, with no protocol-level custody of user funds.
## Limitless Core contracts
Three contract suites compose the core protocol. Full addresses for every deployed version are listed on the [Smart Contracts](/user-guide/smart-contracts) page.
### Gnosis Conditional Tokens Framework (CTF)
The ERC-1155 token primitive that represents Yes and No outcome shares for every market. Limitless uses the canonical Gnosis Conditional Tokens contract deployed on Base. It is the underlying settlement layer for share minting, merging, splitting, and redemption.
Reference: [Gnosis CTF documentation](https://docs.gnosis.io/conditionaltokens/)
### CTF Exchange (CLOB)
The central limit order book for trading Yes and No shares. Orders are signed off-chain with EIP-712 and matched on-chain against the CTF, with fees routed through a separate fee module. The CLOB is deployed in three concurrent versions (v1, v2, v3) to support active trading and gradual migration.
### NegRisk
The contract suite that powers mutually exclusive multi-outcome markets, for example "who wins the election". It includes an adapter, exchange, fee module, vault, and operator. Traders can convert a complete set of No shares across outcomes back into collateral. NegRisk is also deployed in three concurrent versions.
## Security audits
The smart contracts that power Limitless Core are forked from independently audited Polymarket contracts.
ChainSecurity audit of the upstream Polymarket CTF Exchange.
Audit of the upstream Polymarket multi-outcome markets adapter.
### Limitless modifications
Limitless maintains a public fork of the CTF Exchange at [limitless-labs-group/limitless-ctf-exchange](https://github.com/limitless-labs-group/limitless-ctf-exchange). Changes against the upstream audited codebase include:
* Added `LIMITLESS_SAFE` signature type for Limitless Safe smart-wallet orders (EIP-1271 compatible), replacing the upstream `POLY_GNOSIS_SAFE` type.
* Base-specific deployment configuration and domain separator.
These changes are not covered by the upstream ChainSecurity audit.
The Gnosis CTF is the canonical Conditional Tokens contract; refer to the upstream Gnosis audits for that codebase.
# Smart Contracts
Source: https://docs.limitless.exchange/user-guide/smart-contracts
Onchain contract addresses for Limitless Exchange on Base
## Overview
Limitless Exchange runs on **Base** (Ethereum L2). All trading is settled onchain via smart contracts.
To pull Limitless trading metrics, query **all three venue versions** of smart contracts. Each processes active trades — partial queries give you incomplete data.
## Key Token Addresses
| Token | Address | Standard |
| ---------------------------- | -------------------------------------------- | -------- |
| **USDC** (Native on Base) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | ERC-20 |
| **Conditional Tokens (CTF)** | `0xC9c98965297Bc527861c898329Ee280632B76e18` | ERC-1155 |
## Conditional Tokens
Limitless uses the [Gnosis Conditional Tokens Framework (CTF)](https://docs.gnosis.io/conditionaltokens/) — an ERC-1155 token standard that enables the creation of conditional outcome tokens for any future event. When a market is created, the CTF contract mints Yes and No tokens representing each outcome. These tokens are freely tradeable and composable with other DeFi protocols.
The CTF is the underlying primitive that powers all market mechanics on Limitless, including share merging/splitting, Negrisk conversions, and market resolution payouts.
## Simple Markets
### v1
| Contract | Address |
| ------------ | -------------------------------------------- |
| CTF Exchange | `0xa4409D988CA2218d956BeEFD3874100F444f0DC3` |
| Fee Module | `0x6d8A7D1898306CA129a74c296d14e55e20aaE87D` |
### v2
| Contract | Address |
| ------------ | -------------------------------------------- |
| CTF Exchange | `0xF1De958F8641448A5ba78c01f434085385Af096D` |
| Fee Module | `0xEECD2Cf0FF29D712648fC328be4EE02FC7931c7A` |
### v3
| Contract | Address |
| ------------ | -------------------------------------------- |
| CTF Exchange | `0x05c748E2f4DcDe0ec9Fa8DDc40DE6b867f923fa5` |
| Fee Module | `0x5130c2c398F930c4f43B15635410047cBEa9D6EB` |
## Negrisk Markets
### v1
| Contract | Address |
| -------------------- | -------------------------------------------- |
| NegRisk Adapter | `0xb8DAA4C8C9f690396f671BB601727A4c3741340C` |
| NegRisk CTF Exchange | `0x5a38afc17F7E97ad8d6C547ddb837E40B4aEDfC6` |
| NegRisk Fee Module | `0x73FC1B1395bA964FEa8705BfF7eF8EA5C23CC661` |
| NegRisk Operator | `0xAE363abc7B264755e8706D81475C3586D4543992` |
| NegRisk Vault | `0x80307da4d8EA92Cd7a13bBf6B3309431Ca7a1c0E` |
| Wrapped Collateral | `0x5d6c6a4fea600e0b1a3ab3ef711060310e27886a` |
### v2
| Contract | Address |
| -------------------- | -------------------------------------------- |
| NegRisk Adapter | `0x7afeB946986211950d17f24176039F12c2aB2436` |
| NegRisk CTF Exchange | `0x46e607D3f4a8494B0aB9b304d1463e2F4848891d` |
| NegRisk Fee Module | `0x18B3E1192c01286050A0994Bc26f7226Ae4A483d` |
| NegRisk Operator | `0xd2e4a23E57F67a90Bfc999d420fDA16De0Fb2751` |
| NegRisk Vault | `0xd7d245CB2cbE55633e270aF8379E5D4AbA87BD58` |
| Wrapped Collateral | `0x428F0FC93221A9957dC667baa07E62d50c6b8c03` |
### v3
| Contract | Address |
| -------------------- | -------------------------------------------- |
| NegRisk Adapter | `0x6151EF8368b6316c1aa3C68453EF083ad31E712D` |
| NegRisk CTF Exchange | `0xe3E00BA3a9888d1DE4834269f62ac008b4BB5C47` |
| NegRisk Fee Module | `0xfeb646D32a2A558359419a1C9c5dfb47fD92dADb` |
| NegRisk Operator | `0xB7D463037836cFf84FA9ddC25c1136756B4b5F61` |
| NegRisk Vault | `0x2eC22ee9381D0b3570cCb5887960DDFd05d210b3` |
| Wrapped Collateral | `0xBd8Ff5Ac78A3739037FEaA18278cC157C4798B01` |
# Terms of Service
Source: https://docs.limitless.exchange/user-guide/terms-of-service
Limitless Exchange Terms of Service
*Last Updated: March 26, 2025*
These Terms of Service ("**Terms**") govern Your use of the online cryptocurrency-powered predictions market (the "**Platform**") operated by Street Chow Inc., a Panamanian company ("**Company**," "**we**," "**us**," or "**our**"). By accessing or using the Platform, You agree to be bound by these Terms. If You do not agree, You must not use the Platform.
## 1. Eligibility
To access and use the Platform, You must: (a) be at least eighteen (18) years of age or the age of legal majority in Your jurisdiction, whichever is higher; (b) have the legal capacity to enter into these Terms and form a binding contract; (c) not be a citizen or resident of, or located in, any country or jurisdiction where use of the Platform would be prohibited by applicable law or regulation; and (d) not be included on the List of Specially Designated Nationals and Blocked Persons maintained by the US Treasury Department's Office of Foreign Assets Control (OFAC) or on any list pursuant to European Union (EU) and/or United Kingdom (UK) regulations or placed on any other sanctions lists maintained by applicable governmental authorities.
The Company reserves the right to require You to provide proof of age, identity, and/or eligibility at any time. Use of the Platform is void where prohibited by applicable law. You are responsible for determining whether Your use of the Platform complies with applicable laws in Your jurisdiction.
The Platform is not available to users accessing it from the following jurisdictions: **United States of America**, People's Republic of China, Russia, Belarus, Cuba, Iran, North Korea, Syria, and the Crimea, Donetsk and Luhansk regions.
Any access or use of the Platform in violation of these eligibility requirements is unauthorized and strictly prohibited. The Company reserves the right to terminate Your access immediately and without notice if it believes You do not meet the eligibility requirements.
## 2. Account Creation and Wallet Connection
To create an account on the Platform (an "**Account**"), You must connect a compatible cryptographic wallet. By connecting Your wallet, You acknowledge and agree that this connection is necessary for the proper functioning of our services, including but not limited to identity verification, transaction processing, and security protocols. The Platform currently supports those wallets identified on the Platform. We reserve the right to modify the list of supported wallets at any time.
You are solely responsible for maintaining the security of Your cryptographic wallet, including but not limited to safeguarding private keys, seed phrases, and other access credentials. We shall not be liable for any loss or damage resulting from Your failure to maintain adequate security measures for Your wallet. You may disconnect Your wallet from the Platform at any time; however, doing so may limit or prevent access to certain features or services until a compatible wallet is reconnected.
We may require verification of wallet ownership through cryptographic signing of messages or other means to authenticate Your identity and prevent fraud. You are responsible for ensuring that Your device and software environment meet the technical requirements necessary for proper wallet connection and functionality.
The Company reserves the right, in its sole discretion, to suspend, disable, or terminate Your Account if we believe that You have violated these Terms, engaged in fraudulent activity, or if Your Account shows suspicious behavior. We may also suspend Accounts to comply with applicable laws, court orders, or regulatory requirements. During any suspension period, You may be prevented from accessing Your Account.
You may not sell, transfer, or assign Your Account to another person. You may not create multiple Accounts or create an Account on behalf of another person. The Company may require additional verification of Your identity at any time and may suspend Your Account until such verification is satisfactorily completed.
## 3. Use of the Platform
The Platform enables Users to participate in prediction markets by placing wagers on the outcomes of future events ("**Prediction Markets**"). Users may create or participate in Prediction Markets by staking cryptocurrency on potential outcomes. All Prediction Markets must be based on objectively verifiable events and outcomes that can be determined through reliable third-party data sources ("**Oracle Sources**").
The Company reserves the right to determine which Oracle Sources will be used to verify outcomes. The Company will clearly indicate the designated Oracle Source(s) for each Prediction Market at the time of market creation. Once an outcome has been determined based on data from the designated Oracle Source(s), it shall be considered final and binding.
Users acknowledge that:
* They are wagering actual cryptocurrency with real value
* All wagers are final once confirmed on the blockchain
* The Company does not guarantee the accuracy of Oracle Sources or prediction outcomes
* Technical delays or blockchain network issues may affect the timing of market resolution
* The Company may suspend or cancel any Prediction Market that becomes impossible to resolve definitively using the designated Oracle Sources
The Company maintains sole discretion over which types of events may be the subject of Prediction Markets and may remove or suspend any markets that violate these Terms or applicable laws. Users may not create or participate in markets related to illegal activities, personal violence, terrorism, or other prohibited subjects as determined by the Company.
## 4. Cryptocurrency Transactions
Users acknowledge that cryptocurrency transactions are subject to network congestion, blockchain processing times, and variable network fees. The Company does not guarantee specific transaction processing times and is not responsible for delays caused by blockchain network conditions.
## 5. Fees and Commissions
The Company charges transaction fees for the use of the Platform. Transaction fees may include, but are not limited to, market creation fees, trading fees, settlement fees, and withdrawal fees.
The Company reserves the right to modify the transaction fees at any time in its sole discretion. Any changes to the fee structure will be communicated to Users through the Platform and will take effect immediately upon posting. Users' continued use of the Platform following the posting of modified transaction fees constitutes acceptance of such modifications.
Transaction fees will be automatically deducted from Your Account balances at the time of the relevant transaction. Users acknowledge and agree that all transaction fees are non-refundable unless otherwise required by applicable law. The Company may offer promotional fee discounts or waivers at its discretion, which may be subject to additional terms and conditions.
## 6. Prohibited Activities
Users are strictly prohibited from engaging in any of the following activities on the Platform:
* **Market Manipulation**: Users shall not engage in any activities intended to artificially influence market prices or outcomes, including but not limited to wash trading, spoofing, layering, or coordinated trading with other users. Users shall not create multiple accounts to circumvent trading restrictions or manipulate market outcomes.
* **Fraud and Deceptive Practices**: Users shall not engage in any fraudulent or deceptive activities, including but not limited to: providing false or misleading information, attempting to hack or compromise the Platform's security, exploiting technical vulnerabilities, or using the Platform for money laundering or other illegal purposes.
* **Criminal Activity**: Users shall not engage in any criminal activity.
The Company reserves the right to monitor all Platform activity for signs of prohibited activities. Upon reasonable suspicion of engagement in prohibited activities, the Company may, in its sole discretion and without prior notice: (i) immediately suspend or terminate Your Account; (ii) to the extent possible, forfeit and seize any funds in Your Account; (iii) share information with relevant legal and regulatory authorities; and (iv) take any other actions deemed necessary to protect the integrity of the Platform.
Users acknowledge that engagement in prohibited activities may result in civil and criminal penalties under applicable laws, in addition to the remedies available to the Company under these Terms.
## 7. Risk Disclosure
THE USER EXPRESSLY ACKNOWLEDGES AND AGREES THAT USING THE PLATFORM AND PARTICIPATING IN PREDICTION MARKETS INVOLVES SIGNIFICANT FINANCIAL RISK. THE USER MAY LOSE PART OR ALL OF THE FUNDS DEPOSITED OR WAGERED ON THE PLATFORM.
Cryptocurrency markets are highly volatile and prediction market outcomes are inherently uncertain. Past performance does not guarantee future results.
The User understands and accepts that smart contract code execution, blockchain network delays, market volatility, oracle failures, or other technical issues may impact transaction execution and settlement. The Company makes no guarantees regarding the profitability of any prediction market or wager. Users are strongly advised to only risk funds they can afford to lose.
THE USER ACKNOWLEDGES THAT THEY ARE SOLELY RESPONSIBLE FOR CONDUCTING THEIR OWN DUE DILIGENCE AND ASSESSMENT OF RISKS BEFORE PARTICIPATING IN ANY PREDICTION MARKET OR PLACING ANY WAGER. The Company does not provide investment advice or recommendations.
## 8. Intellectual Property
All content on the Platform, including but not limited to text, graphics, logos, button icons, images, audio clips, digital downloads, data compilations, and software ("**Platform Content**"), is the property of the Company or its content suppliers and is protected by international copyright laws.
Users are granted a limited, non-exclusive, non-transferable, and revocable license to access and use the Platform Content solely for personal, non-commercial purposes related to using the Platform's prediction markets. Users may not copy, modify, distribute, transmit, display, perform, reproduce, publish, license, create derivative works from, transfer, or sell any Platform Content without the Company's prior written permission.
## 9. Privacy Policy
Our collection, use, disclosure, and protection of Your personal information is governed by our [Privacy Policy](/user-guide/privacy-policy), which is hereby incorporated by reference into these Terms.
## 10. Representations and Warranties
By accessing or using the Platform, You represent and warrant that:
* You meet all eligibility requirements outlined in Section 1 of these Terms, including being at least 18 years of age and not accessing the Platform from a prohibited jurisdiction.
* You have full power and authority to enter into these Terms and perform Your obligations hereunder.
* Your use of the Platform will not violate any applicable law, regulation, or obligation You may have to a third party.
* You are not: (i) subject to economic or trade sanctions administered or enforced by any governmental authority; (ii) located, organized, or resident in a country or territory that is subject to comprehensive sanctions; or (iii) named on any applicable sanctions lists.
* All information You provide to the Company is true, accurate, and complete.
ANY BREACH OF THE FOREGOING REPRESENTATIONS AND WARRANTIES SHALL CONSTITUTE A MATERIAL BREACH OF THESE TERMS AND MAY RESULT IN THE IMMEDIATE TERMINATION OF YOUR ACCESS TO THE PLATFORM AND (TO THE EXTENT POSSIBLE) FORFEITURE OF ANY FUNDS ASSOCIATED WITH YOUR ACCOUNT.
## 11. Limitation of Liability
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE COMPANY AND ITS OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, AFFILIATES, AND REPRESENTATIVES SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO:
* Loss of profits, revenue, data, use, goodwill, or other intangible losses
* Damages or losses arising from any errors, mistakes, or inaccuracies in the Platform
* Losses resulting from smart contract vulnerabilities, bugs, malfunctions, or other technical issues
* Damages resulting from regulatory changes or enforcement actions affecting cryptocurrency or prediction markets
* Any losses or damages arising from unauthorized access to Your Account or the Platform
THE PLATFORM IS PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS. THE COMPANY MAKES NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE PLATFORM'S CONTENT.
## 12. Dispute Resolution
Any dispute, controversy, or claim arising out of or relating to these Terms shall be finally settled by binding arbitration administered by the International Chamber of Commerce under its Rules of Arbitration. The arbitration shall be conducted in English by a single arbitrator in Panama City, Panama. The arbitrator shall apply the laws of Panama without regard to principles of conflicts of law.
TO THE FULLEST EXTENT PERMITTED BY LAW, YOU AGREE TO WAIVE ANY RIGHT TO PARTICIPATE IN A CLASS ACTION LAWSUIT OR CLASS-WIDE ARBITRATION.
## 13. Termination
The Company reserves the right, in its sole discretion, to suspend, restrict, or permanently terminate Your access to the Platform and Your Account at any time, with or without prior notice, for any reason, including but not limited to: (i) violations of these Terms, (ii) suspected fraudulent, manipulative, or illegal activity, (iii) regulatory requirements or changes, or (iv) technical or security concerns.
## 14. Changes to Terms
The Company reserves the right to modify, amend, or update these Terms at any time in its sole discretion. Your continued use of the Platform following the posting of any changes constitutes Your acceptance of such changes.
## 15. Contact Information
For any questions, concerns, or support requests regarding the Platform or these Terms, You may contact us at [help@limitless.network](mailto:help@limitless.network).
# Wallet Types
Source: https://docs.limitless.exchange/user-guide/wallet-types
Overview of the three wallet types supported on Limitless Exchange
Limitless supports three wallet types: **Externally Owned Accounts (EOA)**, **Embedded Wallets**, and **Smart Wallets**. Each type has different characteristics and tradeoffs for users.
## Externally Owned Account (EOA)
An **Externally Owned Account (EOA)** is a blockchain account controlled by a user via a private key. The private key is a secret value used to sign transactions. It must be kept secure and never shared.
**Common EOA wallet interfaces:** [MetaMask](https://metamask.io/), [Rainbow](https://rainbow.me/en/), [Coinbase Wallet](https://www.coinbase.com/en-pt/wallet), [Rabby](https://rabby.io/), and similar browser extensions or mobile apps.
**On Limitless:** Users connect an EOA from the main login menu. For every on-chain transaction, the user must approve it in their wallet interface (e.g., MetaMask).
## Embedded Wallet
An **Embedded Wallet** is a non-custodial wallet created and operated within an application by a third-party provider, without a separate wallet app.
**On Limitless:** When users sign in via social login (e.g., Google, Twitter), an embedded wallet is created automatically after authentication. Transactions can be executed directly from the app without an extra approval step in an external wallet.
## Smart Wallet
A **Smart Wallet** is an account implemented as a smart contract on-chain. It supports features such as:
* Account recovery
* Programmable logic for custom behavior
* Gas fee abstraction and batched transactions
**On Limitless:** Smart wallets are used primarily to remove the need for users to hold native tokens (e.g., ETH) to pay transaction fees. Users sign in via social login, and a smart wallet instance is created based on their embedded wallet. Transaction fees are sponsored by Limitless, while users only need to hold **USDC** for trading activity.
## Summary
| Wallet Type | Created When | Fee Responsibility | User Experience |
| ------------ | ----------------------------------- | --------------------- | ----------------------------------------------------- |
| **EOA** | External wallet connection | User pays gas in ETH | Requires approval per transaction in external wallet |
| **Embedded** | Social login | User pays gas in ETH | In-app transactions; no external approval step |
| **Smart** | Social login (from embedded wallet) | Platform sponsors gas | Users hold only USDC; no native token needed for fees |
# What is Limitless?
Source: https://docs.limitless.exchange/user-guide/what-is-limitless
An overview of Limitless Exchange and how prediction markets work
## Overview
Limitless Exchange allows natural language expressions of arbitrary market conditions to be traded freely, creating a forecasting and risk engine for the global economy.
Users buy shares in the outcomes — usually "Yes" or "No" — of particular conditions. For example:
> Buy "Yes" shares that BTC will reach \$65k at **\$0.15**, implying a **15% chance** the outcome will be realized, with a potential ROI of **\~560%**.
That's because **winning shares expire at \$1**, but losing shares are worthless after market resolution.
## How It Works
1. **Pick a market** — Browse live markets on crypto prices, stocks, macro events, and more.
2. **Buy shares** — Purchase "Yes" or "No" shares at the current market price.
3. **Hold or trade** — Trade your position anytime before the market resolves.
4. **Collect winnings** — Winning shares pay out \$1.00 each.
## Collateral
Prior to market resolution, **1 "Yes" + 1 "No" share** in a particular market can always be redeemed for **1 collateral token**.
The collateral token is the cryptocurrency locked in an onchain smart contract to issue outcome shares — currently **USDC**.
## Key Facts
| Stat | Detail |
| ---------------- | --------------------------------- |
| **Chain** | Base (Ethereum L2) |
| **Volume** | \$1B+ traded |
| **Collateral** | USDC |
| **Market types** | Hourly, daily, weekly, and custom |
Chat with traders and the Limitless team on Discord.