# 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`. |
| `withdrawal` | Withdraw ERC20 funds from a managed server wallet via `POST /portfolio/withdraw`. Partner-only (must be in your `allowedScopes`). |
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 partner capability row exists for your profile, the endpoint returns `tokenManagementEnabled: true` with `allowedScopes: ["trading"]` — the public self-service default, so any signed-in user can derive a `trading`-scoped token. Partner-level scopes (`account_creation`, `delegated_signing`, `withdrawal`) only appear in `allowedScopes` after your account is approved. 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.
# 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.
# 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.
(HMAC) 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.
Requires authentication (session cookie, Privy identity token, or HMAC). Returns metadata only; the key value is never returned after creation.
# User Logout
Source: https://docs.limitless.exchange/api-reference/auth/logout
POST /auth/logout
Logs out the user by clearing the session cookie
# 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.
Only available to users authenticated via the Limitless UI (Privy). HMAC or session callers receive `403 API key management is only available via the UI`.
# Get Trading Activity Feed
Source: https://docs.limitless.exchange/api-reference/feed/trading-feed
GET /feed/trading
Returns trading activity from the last 24 hours for one audience: everyone (all), the profiles the signed-in viewer follows (following), or a curated set of featured traders (featured). Entries are Bought, Sold, Resolved, Parlay placed, and Parlay resolved events, newest first.
Returns a feed of trading activity from the last 24 hours, newest first. Entries cover buys, sells, market resolutions, and pack (parlay) placements and resolutions. Public for the `all` and `featured` audiences; the `following` audience requires authentication.
## When to use
Use this endpoint to render an activity feed of what traders are doing on the platform right now. Choose a global stream, a curated list of featured traders, or a personal stream of the profiles the signed-in viewer follows. The API filters out small activity server-side so the feed stays meaningful.
## Audiences
The `audience` query parameter selects which profiles the feed reads:
* `all` (default): activity from every profile. Public.
* `featured`: activity from a curated set of featured traders. Public.
* `following`: activity from the profiles the viewer follows. Requires authentication; an unauthenticated request returns `401 Unauthorized`.
If the first `featured` page has no entries, the API serves the `all` stream instead and sets `effectiveAudience: "all"` in the response. A new viewer never lands on an empty screen. The returned cursor carries the effective audience, and continuation requests keep paging that same stream.
On the `following` audience, `emptyReason` distinguishes two empty states without a second request: `NO_FOLLOWS` when the viewer follows nobody, `NO_RECENT_ACTIVITY` when the followed profiles have been quiet. It is `null` everywhere else.
## Entry types
Each entry carries an `entryType` and a `facts` object with immutable event-time values:
* `BOUGHT` / `SOLD`: side, outcome, contracts, execution price, notional, and collateral symbol. `SOLD` entries also carry `realizedPnl`, `averageEntryPrice`, and `roi` when close data fully covers the fill; otherwise those fields are `null` and the entry shows the fill only. Close data typically lands a few minutes after the fill. The API backfills the figures onto recent `SOLD` entries within minutes rather than waiting for hourly reconciliation.
* `RESOLVED`: the aggregated result (`WON`, `LOST`, or `BREAK_EVEN`) for a profile in one market or group, with realized PnL, cost basis, payout, and a per-market position breakdown.
* `PARLAY_PLACED` / `PARLAY_RESOLVED`: the pack's legs, stake, multiplier, and potential payout, plus the payout, realized PnL, and result once resolved.
## Display fields
Beyond `facts`, each entry carries display data that the API resolves at read time, when you fetch the page:
* `profile`: the trader's account, display name, username, profile picture, rank name, connected X handle (`xHandle`, `null` when no X account is connected), and `discordConnected` (boolean; no Discord identity is exposed).
* `subject`: the market or group the entry is about. `null` on parlay entries. See [Subject fields](#subject-fields).
* `parlay` (parlay entries only): the source pack's name (`null` for a custom parlay or a deleted pack) and ordered leg details, including crest image, named outcomes, league, teams, live score, and fixture kickoff, plus `sourcePackId` (the pack id the parlay was bought from, kept even if the pack was later deleted; `null` for a custom parlay). Use `parlay.legs[].startsAt` for match times; `facts.legs[].startsAt` is the market deadline, not the kickoff.
There is no per-trader live position block. A card values its own `facts.contracts` at the subject's current `prices`.
## Subject fields
The subject identifies the market (`kind: "MARKET"`) or group (`kind: "GROUP"`) the entry is about, with its `id`, `slug`, `title`, `status`, and `imageUrl` card image.
* `tradable` is `true` only while the subject can actually be traded: the market must be funded, unresolved, and before its `deadline`. A group is tradable only when at least one visible child market meets those conditions.
* A market inside a group carries `groupTitle`, `groupSlug`, and `groupImageUrl`, so a card can lead with the group question and its artwork while the market's own title names the outcome. All three are `null` for a standalone market.
Market subjects also carry lifecycle and price data:
* `createdAt`, `startAt`, and `deadline`: when the market was created, when trading opened, and when trading closes.
* `resolvedAt`: when the market resolved. `null` until the market resolves. This is the resolution time, not the deadline; a market resolved early carries its actual resolution time.
* `winningOutcomeIndex` and `payoutNumerators`: the resolution result. `null` until the market resolves.
* `prices`: current `[yes, no]` prices as fractions between 0 and 1, on the same scale as `facts.price`. A resolved market carries its resolution outcome; an unresolved orderbook market carries its latest stored midpoint. `null` when no honest price exists, such as an AMM market or a market with no price observations. The API never invents a price.
Group subjects return all lifecycle and price keys as `null`. A group settles many markets at once and has no single lifecycle or price; use a child market's own data instead.
## Pagination
* `limit` accepts 1-30 and defaults to 30.
* Pass `nextCursor` from the previous page as `cursor` to fetch the next page. `nextCursor` is `null` on the last page.
* The cursor is opaque and pinned to the audience that issued it. A malformed or tampered cursor returns `400` with code `TRADING_FEED_CURSOR_INVALID`; a cursor sent with a different audience returns `400` with code `TRADING_FEED_CURSOR_AUDIENCE_MISMATCH`.
## Availability
The feed fails closed rather than serving incomplete data:
* `503` with code `TRADING_FEED_INITIALIZING` while the API is still building the first 24-hour window after a deployment.
* `503` with code `TRADING_FEED_STALE` when the feed is temporarily behind.
Both are transient; retry later.
## Caching
Responses for `all` and `featured` carry `Cache-Control: public, s-maxage=60, stale-while-revalidate=30`, so CDN edges may serve a shared cached copy for up to a minute. `following` responses are per-viewer and carry `private, no-store`.
## Example
```bash theme={null}
curl "https://api.limitless.exchange/feed/trading?audience=all&limit=10"
```
```json theme={null}
{
"requestedAudience": "all",
"effectiveAudience": "all",
"events": [
{
"id": "clob:123456:42",
"entryType": "BOUGHT",
"occurredAt": "2026-09-02T14:03:11.482910Z",
"facts": {
"side": "BUY",
"outcome": "YES",
"contracts": "120",
"price": "0.540000",
"notional": "64.800000",
"symbol": "USDC"
},
"profile": {
"id": 42,
"account": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"displayName": "Alice",
"username": "alice",
"pfpUrl": "https://...",
"rankName": "Expert",
"xHandle": "alice_trades"
},
"subject": {
"kind": "MARKET",
"id": 9876,
"slug": "will-x-happen-123",
"title": "Will X happen?",
"status": "FUNDED",
"imageUrl": "https://...",
"groupTitle": null,
"groupSlug": null,
"groupImageUrl": null,
"createdAt": "2026-09-01T09:00:00.000000Z",
"startAt": "2026-09-01T10:00:00.000000Z",
"deadline": "2026-09-05T00:00:00.000000Z",
"resolvedAt": null,
"winningOutcomeIndex": null,
"payoutNumerators": null,
"prices": [0.57, 0.43],
"tradable": true
},
"parlay": null
}
],
"hasMore": true,
"nextCursor": "eyJ2IjoxLCJhIjoi...",
"emptyReason": null
}
```
# API Reference
Source: https://docs.limitless.exchange/api-reference/introduction
Complete REST API documentation for Limitless Exchange
## Base URL
```
https://api.limitless.exchange
```
The raw OpenAPI document is served at [https://api.limitless.exchange/api-json](https://api.limitless.exchange/api-json); this site is the interactive reference.
This is a single global endpoint. There are no regional base URLs or region selection. All clients use the same base URL regardless of location.
Building on the API? Join the [Limitless Builders Chat](https://t.me/LimitlessBuildersChat) on Telegram for API/SDK updates, help, and to connect with other builders.
## 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. All authenticated requests use [scoped API tokens with HMAC signing](/developers/authentication) (legacy static API-key headers are deprecated and 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.
Check public maintenance status and temporary trading restrictions.
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-til-cancelled) rests on the orderbook. **FOK** (Fill-or-kill) fills in full immediately or is killed. **FAK** (Fill-and-kill) fills what it can immediately and kills the remainder. |
| **Token IDs** | Each market returns YES and NO token IDs in `tokens` (`tokens.yes` / `tokens.no`). Use these as `tokenId` in orders. |
| **USDC decimals** | USDC on Base has 6 decimals. Amounts are scaled by 1e6. |
| **Receive window** | Optional `timestamp` + `recvWindow` on `POST /orders` reject stale signed orders with HTTP `425`. Opt-in (default disabled). See [Receive Window](/api-reference/trading/create-order#receive-window). |
| **Maintenance mode** | Trading can be temporarily restricted during maintenance. Check [`GET /maintenance/status`](/api-reference/system/maintenance-status) and handle `425` responses with a trading-mode `code` from trading endpoints. |
## WebSocket API
For real-time orderbook and position updates, see the [WebSocket Events](/developers/websocket/overview) reference.
**URL:** `wss://ws.limitless.exchange` | **Namespace:** `/markets`
## Rate limits
The API enforces rate limits. When you exceed a limit, requests return HTTP `429 Too Many Requests`. Handle `429` responses with:
* **Respect `Retry-After`** — when present on a `429` response, wait for that many seconds before retrying.
* **Exponential backoff** — if no `Retry-After` header is returned, back off starting at 1 second and double on each retry.
* **Throttle at the source** — for high-frequency workloads (e.g. market making), queue outgoing orders instead of firing them concurrently. The TypeScript SDK ships an [`OrderQueue`](/developers/sdk/typescript/error-handling#rate-limiting-with-orderqueue); Python, Go, and Rust SDKs expose configurable retry-on-429 helpers in their error-handling guides.
* **Never retry `400` or `401`** — those indicate a bad request or invalid credentials and will not succeed on retry.
Partner allowance endpoints return a `retryAfterSeconds` field on `429` — see [Retry Partner Account Allowances](/api-reference/partner-accounts/retry-allowances).
For higher published limits or a dedicated quota, contact [help@limitless.network](mailto:help@limitless.network).
# Biggest Open Positions
Source: https://docs.limitless.exchange/api-reference/leaderboard/biggest-positions
GET /leaderboard/pnl/unrealized/biggest-positions
Live leaderboard of the largest currently open position lines across all markets, ranked by position size. Public - no authentication required.
Responses are served from a live Redis projection with `ETag` support and short client cache. When the projection is still being rebuilt, callers may receive a `BUILDING`, `STALE`, or `DEGRADED` snapshot with `Cache-Control: no-store` and (for `BUILDING`) an empty `data` array - retry after a short delay.
Returns the largest currently open position lines across all markets. Each entry pins the market it belongs to. Public — no authentication required.
By default, the list is ranked by position size in collateral. Pass `metric=pnl` to rank by live Unrealized PnL instead.
A dedicated worker computes the rankings from a live projection. Successful responses include a strong `ETag`; pass it back as `If-None-Match` to get `304 Not Modified` while the projection is unchanged. `ETag` values are specific to the requested `metric`, `marketPageId`, and `limit` — validators do not cross variants.
## Ranking metric
The optional `metric` query parameter selects the ranking:
| Metric | Ranking | Notes |
| - | - | - |
| `position_size` (default) | Largest open positions by position size in collateral. | Legacy behavior; omitting `metric` keeps existing clients unchanged. |
| `pnl` | Open positions with the highest live Unrealized PnL. | Supports the optional `marketPageId` filter. |
The response echoes the effective `metric` so you can render the right column headers without tracking request state.
## Filter by market page
With `metric=pnl`, pass `marketPageId` (a UUID) to scope the ranking to one public root navigation page, for example, a top-level category like Crypto or Sports. Get page ids from the [navigation tree](/api-reference/navigation/get-navigation).
* `marketPageId` is accepted only together with `metric=pnl`. Sending it with the default position-size metric returns `400`.
* A `marketPageId` that does not identify an active public root navigation page returns `400`.
* The response echoes `marketPageId` (`null` when unfiltered).
```bash Example request theme={null}
curl "https://api.limitless.exchange/leaderboard/pnl/unrealized/biggest-positions?metric=pnl&marketPageId=1f0f7f3a-2c7e-4b9a-9c1d-3d2b6f4e8a11&limit=20"
```
## Entry fields
Each entry identifies the trader and the market, and includes:
* `heldShares`, `mark`, and `unrealizedPnl` — the open position line and its live valuation.
* `avgEntry` — the position's average entry price per share in collateral.
* `market.routeSlug` — the slug to use when building frontend market links; falls back to `market.slug` when no dedicated route exists.
* `market.imageUrl` — the market's image, or `null` when none is set.
## State field
Every response carries a `state`. Treat it as the readiness signal:
| State | Meaning | How to handle |
| - | - | - |
| `READY` | Complete, current snapshot. | Render `data` and cache per `Cache-Control`. |
| `STALE` | Last complete snapshot is being replaced. | Render `data`; response is `Cache-Control: no-store`, refetch shortly. |
| `DEGRADED` | Snapshot returned with a `staleReason`. | Render `data`; treat as best-effort and refetch shortly. |
| `BUILDING` | No complete snapshot yet. | `data` is empty; retry after a short delay. |
For live refresh, subscribe to the [`unrealizedPnlProjectionChanged`](/developers/websocket/unrealized-pnl#unrealizedpnlprojectionchanged) WebSocket event for the `BIGGEST_POSITIONS` scope and refetch this endpoint when a hint arrives. The same hint covers every `metric` and `marketPageId` variant of this endpoint.
# Unrealized PnL Leaderboard by Market
Source: https://docs.limitless.exchange/api-reference/leaderboard/unrealized-pnl-market
GET /leaderboard/pnl/unrealized/markets/{marketId}
Live leaderboard of open positions for one market, ranked by Unrealized PnL or ROI. Public - no authentication required. Ranks are absolute across pages, so page 2 with `limit=10` returns ranks 11-20.
Results are paginated with `limit` (1-100, default 100) and `page` (1-100, default 1). The response also includes `totalRows` and `totalPages` describing the committed Top-100 generation for the market. `ETag` values are page-specific, so a validator returned for one page will not match another.
Responses are served from a live Redis projection maintained by a dedicated worker. When the projection is warm the response is a `READY` snapshot with a strong `ETag` and short client cache. When the projection is still being rebuilt, callers may receive a `BUILDING`, `STALE`, or `DEGRADED` snapshot with `Cache-Control: no-store` and (for `BUILDING`) an empty `data` array - retry after a short delay. If the market is not open (`RESOLVED`, hidden, or otherwise ineligible), the endpoint returns `404`.
Ranks every currently open position in one market by live Unrealized PnL or ROI, using the same mark price the frontend renders. Public — no authentication required.
A dedicated worker computes the rankings from a live projection. Successful responses include a strong `ETag`; pass it back as `If-None-Match` to get `304 Not Modified` while the projection is unchanged. `ETag` values are page-specific — validators do not cross pages.
## Pagination
Results are paginated with `limit` (1–100, default `100`) and `page` (1–100, default `1`). Ranks stay absolute across pages, so page 2 with `limit=10` returns ranks 11–20. Each response includes:
* `page` — the page returned.
* `totalRows` — number of ranked rows in the committed Top-100 generation for the market.
* `totalPages` — total number of pages given the requested `limit`.
Request the first page of a "Top Holders" view with `limit=10&page=1`, then increment `page` until `page === totalPages`. To fetch the complete Top-100 in one call, use `limit=100&page=1`. Omitting `page` is treated as page 1, so existing clients keep working.
## State field
Every response carries a `state`. Treat it as the readiness signal:
| State | Meaning | How to handle |
| - | - | - |
| `READY` | Complete, current snapshot. | Render `data` and cache per `Cache-Control`. |
| `STALE` | Last complete snapshot is being replaced. | Render `data`; response is `Cache-Control: no-store`, refetch shortly. |
| `DEGRADED` | Snapshot returned with a `staleReason`. | Render `data`; treat as best-effort and refetch shortly. |
| `BUILDING` | No complete snapshot yet. | `data` is empty; retry after a short delay. |
For live refresh, subscribe to the [`unrealizedPnlProjectionChanged`](/developers/websocket/unrealized-pnl#unrealizedpnlprojectionchanged) WebSocket event and refetch this endpoint when a hint arrives for the same `marketId`.
This endpoint returns `404` for markets that are not open — resolved, hidden, or otherwise ineligible for a live leaderboard.
# 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
For category- and property-based filtering (the structured filters Limitless uses to organize markets), use the **Market Pages** endpoints. Resolve a page with [Get Market Page by Path](/api-reference/navigation/get-page-by-path) to read its `baseFilter` and `filterGroups`, then call [List Markets for a Page](/api-reference/navigation/list-page-markets). `GET /markets/active` returns a flat list with basic filtering.
**`limit` is capped at 25.** Values above `25` are rejected with `400` (`limit must not be greater than 25`) — the endpoint never silently truncates. To read more than 25 markets, page with the `page` parameter; the response's `totalMarketsCount` tells you how many are available in total.
**`tradeType` filter values.** Selects the market's trading model:
* `amm` — Automated market maker. Prices are set algorithmically against a liquidity pool, and you trade directly against the curve.
* `clob` — Central limit order book. Independent YES/NO bids and asks match at prices set by makers and takers. Supports market and limit orders. See [CLOB Overview](/user-guide/clob-overview).
* `group` — Multi-outcome group (also called NegRisk / category markets). Multiple outcomes are bundled and only one can win; the NO shares across outcomes are linked, which makes collateral more efficient. See [NegRisk Markets](/user-guide/negrisk-overview).
**`automationType` filter values.** Selects how a market is created and resolved:
* `lumy` — Recurring price markets: rows with a price oracle (`price_oracle_id`) plus the Polymarket up/down replicas keyed by `canonical_key`. Standalone markets only; groups are never returned by this filter (a group's `automationType` is always `sports` or `manual`). These resolve automatically from the oracle. The `Lumy` category tag is a branding label and is not the same signal — always filter by `automationType`, not by tag.
* `manual` — Markets that are not oracle-driven and not sports/esports fixtures. Resolved by the Limitless team against the criteria on the market page, typically within 24–72 hours of the deadline. See [Market Resolution](/user-guide/market-resolution).
* `sports` — Sports fixture markets (identified by a `fixtureId` or the sports event provider). Resolved from the fixture result feed.
**`sortBy` (optional).** `newest` (launch time desc), `ending_soon` (deadline asc), `lp_rewards` (only rewardable markets, priority order). `high_value`, `trending` and the default all use priority order (priority index desc, deadline asc, id asc).
**`includeNextMarket` (optional).** Opt in to receive the next round of upfront-scheduled markets, such as crypto price ladders that run in back-to-back windows. When `true`, each affected market includes a `navigation.nextMarket` object with the successor's `slug`, `startAt`, `deadline`, and `tradable` flag. The current market also stays listed briefly during the activation gap, so the series is always represented by exactly one row.
Use this when you want to prefetch the next round or open a websocket subscription before the current market expires. Leave it unset for the default listing behavior. Cursor tokens are scoped to this option, so do not switch it mid-pagination.
```bash theme={null}
GET /markets/active?includeNextMarket=true
```
The `tradable` flag is conservative in listings — a successor that has not yet activated reports `false`. Fetch the successor directly with [Get Market](/api-reference/markets/get-market) before submitting an order.
# 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.
Supports `includeNextMarket=true` for upfront-scheduled successor navigation. See [Browse Active Markets](/api-reference/markets/browse-active) for the response shape and usage guidance.
# 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
**Category IDs vs category names.** The response's `category` object uses numeric **category IDs** as keys (`{ "1": 10, "2": 5, ... }`). Use these IDs only for filtering — they are not human-readable labels, and their mapping to names is not fixed across environments.
To display or match category names (`"Crypto"`, `"Sports"`, `"Hourly"`, etc.), read the human-readable `categories` array on each market from [Browse Active Markets](/api-reference/markets/browse-active) or [Get Market Details](/api-reference/markets/get-market). Every market returns `categories: string[]`, and those strings are the labels shown in the UI.
To build an ID → name mapping in your client:
1. Call `GET /markets/categories/count` to list active category IDs and counts.
2. For each ID, call [`GET /markets/active/{categoryId}`](/api-reference/markets/browse-active-category) with `limit=1` and read `categories[0]` from the returned market to learn the human-readable name.
3. Cache the mapping — category IDs are stable across a session.
# 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. On [new trading venues](/developers/venue-system#new-trading-venues) the venue also includes `feeModule` and `feeModuleType: "COLLATERAL"`; legacy venues omit both.
**Outcome index convention.** On a resolved market the response carries `winningOutcomeIndex`, an index into the fixed `outcomeTokens: ['Yes', 'No']` array:
* `winningOutcomeIndex: 0` — **YES** resolved
* `winningOutcomeIndex: 1` — **NO** resolved
* `null` — either the market is not yet resolved, or it resolved to a **split** (see below)
The same mapping applies anywhere this field appears (portfolio positions, feed events). For NegRisk groups, the winning index lives on the individual sub-market that resolved YES, not on the group object.
On recurring up-or-down price markets the outcomes are labeled **Up** and **Down** with the same indices: `0` = Up and `1` = Down. Up is the YES side. On Chainlink-resolved markets, the market resolves Up when the closing reference price is greater than or equal to the Price to Beat (a tie resolves Up); see `metadata.chainlinkDataStream` below. Picking Up means buying `tokens.yes`.
**`prices` format.** On CLOB markets, `prices` is `[yesMidpoint, noMidpoint]` as decimal fractions between `0` and `1` (for example `[0.715, 0.285]`; the pair sums to ≈ 1). AMM markets quote percent-style values between `0` and `100` — the schema example showing values like `42.8` reflects that AMM shape. Branch on `tradeType` before parsing rather than assuming one scale.
**Detecting the resolution stream on price markets.** Chainlink-resolved up/down markets expose their stream configuration on `metadata.chainlinkDataStream`. Read `streamType` to tell the two apart: `"spot"` markets resolve on an instantaneous price, while `"twap"` markets resolve on a time-weighted average over `twapWindowSeconds`. Resolution compares the closing TWAP against the Price to Beat captured from the same stream at open; greater than or equal resolves Up. The market's `description` always names the exact feed, window, and tolerance. See [Market Resolution](/user-guide/market-resolution#chainlink-twap-resolution).
**Split resolution.** A binary market can resolve to a payout split (for example 50/50 when a market is unwound at pre-cancellation odds) rather than a single winner. In that case the response has `status: RESOLVED`, `winningOutcomeIndex: null`, and a `payoutNumerators` array (indexed `[YES, NO]`) with **both entries positive** — the ratio defines the payout. `[50, 50]` pays each side equally; `[70, 30]` pays YES holders 70% and NO holders 30%. Each side redeems at `numerator / sum(numerators)` through the same [redeem endpoint](/api-reference/portfolio/redeem).
Winner-take-all resolutions populate `winningOutcomeIndex` (`0` or `1`). Note that `payoutNumerators` **may still be populated** on them (`[1, 0]` / `[0, 1]`) or may be `null`, depending on which resolution path wrote the record. Do not branch on the presence of `payoutNumerators` alone. The reliable split test mirrors the server's own:
```ts theme={null}
const isSplit =
m.status === 'RESOLVED' &&
m.winningOutcomeIndex === null &&
(m.payoutNumerators?.filter(n => n > 0).length ?? 0) >= 2;
```
**Mapping `tokens.yes` / `tokens.no` to human outcome labels.** Every individual market's token pair is always `YES` / `NO` in the API — there is no per-token label field for custom outcomes like "Up" / "Down" or "Norway" / "England". The mapping to a human label depends on the market type:
* **Binary single market** (e.g. "Will ETH close above \$3,000?"): `tokens.yes` = the question resolves true, `tokens.no` = the question resolves false. The market's `title` / `proxyTitle` carries the semantic meaning.
* **Recurring up-or-down price market** (CLOB, e.g. "BTC Up or Down 15 Min"): a single binary market whose outcomes are labeled Up/Down. Up **is** the YES side: on Chainlink-resolved markets, the market resolves Up when the closing reference price is greater than or equal to the open (ties resolve Up). Picking Up = buying `tokens.yes`; picking Down = buying `tokens.no`.
* **Directional AMM market** (e.g. "BTC Up or Down - 5 Min"): each direction is a separate individual market. `tokens.yes` on the "Up" market = betting on Up; `tokens.no` on the same market = the opposite side of that bet. To find the "Down" side, fetch the sibling market from the group.
* **NegRisk group market** (e.g. "Norway vs England: team to advance?"): the group response carries `outcomeTokens: ['Yes', 'No']` (fixed, like every market) and `markets[]`, one submarket per outcome. The outcome's name is the submarket's `title` / `proxyTitle`; sports submarkets may also carry `metadata.homeTeam` / `metadata.awayTeam` or `metadata.outcomeLabels` (`{"0": "...", "1": "..."}`). To trade a specific outcome, call `getMarket()` on that submarket's slug and use its `tokens.yes` — that YES token represents "this outcome resolves true". See the [NegRisk overview](/user-guide/negrisk-overview) and the TypeScript [NegRisk group markets](/developers/sdk/typescript/markets#negrisk-group-markets) walkthrough.
There is no separate `tokens.up` / `tokens.down` field, and `outcomeTokens` is never a list of outcome names. Resolve labels from the submarket `title` / `proxyTitle` (or the sports metadata keys above).
**Market volume.** The response includes lifetime trading volume in the `volume` field (raw, base units) and `volumeFormatted` (human-readable USDC). The API does not expose a rolling 24-hour volume field; for time-windowed activity, derive it client-side from [Get Feed Events](/api-reference/markets/feed-events) (filter trade events by timestamp).
**Market `status` values.** The response's `status` field is one of:
* `CREATED` / `DRAFTED` - the market exists but is not funded yet and is not tradeable.
* `FUNDED` - the market is live and accepting trades. The default for active markets.
* `LOCKED` - legacy value. Nothing in the resolution pipeline sets it; a market only shows `LOCKED` if an operator manually overrides its status. Treat it as not tradeable. A market normally goes straight from `FUNDED` to `RESOLVED`.
* `RESOLVED` - the winning outcome is known. `winningOutcomeIndex` is populated and winners can redeem their CTF positions. See [Lifecycle after a trade](/developers/programmatic-api#lifecycle-after-a-trade).
These are the only values the API emits (`CREATED`, `DRAFTED`, `FUNDED`, `LOCKED`, `RESOLVED`). Treat any unrecognized future value defensively rather than assuming the market is tradeable.
**Taker delay.** A CLOB market's `settings` object carries `takerDelayMs` — the market's taker delay in milliseconds (`0` = none, the default). When it is greater than `0`, the matching engine briefly holds marketable (taker) orders before filling them, and order submission becomes asynchronous. The create-order response returns `settlementStatus: "DELAYED"` with an `eligibleAt`; track the fill over [`subscribe_order_events`](/developers/websocket/order-events#subscribing-to-order-events). Read `settings.takerDelayMs` to detect delay-enabled markets before placing taker orders. `postOnly` (maker) orders are never delayed.
# 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.
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.
# Resolve Stable Slug
Source: https://docs.limitless.exchange/api-reference/markets/resolve-stable-slug
GET /markets/stable/{slug}
Resolve a stable recurring URL slug (e.g. btc-hourly-price) to the currently active market or ladder group
Recurring series (5-minute, hourly, daily crypto, etc.) each expose a **stable slug**. The stable slug always points at the currently active slot instead of a specific dated slug. Use this endpoint to link to or fetch "the current BTC hourly market" without knowing which slot is open.
The path parameter is `slug` (not `stableSlug`). Pass the stable identifier, for example `btc-hourly-price`.
* `200` — resolved market or ladder group data (single market or group).
* `404` — no market or group is currently active for that stable slug.
To pre-fetch upcoming slots for the same series, use [Market Timeline](/api-reference/markets/timeline).
# Search Markets
Source: https://docs.limitless.exchange/api-reference/markets/search
GET /markets/search
Pass `includeNextMarket=true` to attach `navigation.nextMarket` (upfront-scheduled successor) to each affected result. See [Browse Active Markets](/api-reference/markets/browse-active) for the response shape and usage guidance.
# Market Timeline
Source: https://docs.limitless.exchange/api-reference/markets/timeline
Look ahead to upcoming slots of a recurring market (e.g. 5-minute crypto) before they open
Recurring markets (5-minute, hourly, daily crypto, etc.) run as a series of timed slots. `/markets/active` only ever returns the slot that is currently open, so you cannot use it to pre-fetch the next one. The timeline endpoints return the upcoming and past slots for a recurring series, including each slot's `slug` and a `tradable` flag. That lets you resolve the next slug and warm up an orderbook subscription before it opens.
Both endpoints are public and require no authentication.
## By current slug
If you already have the current slug (e.g. from `/markets/active`):
* Route: `GET /markets/{slug}/timeline`
* Query: `before` (default `1`), `after` (default `24`) — number of slots to return on each side of the anchor.
```bash theme={null}
curl "https://api.limitless.exchange/markets/btc-up-or-down-5-min-1771934700/timeline?before=1&after=24"
```
## By symbol and frequency
If you do not have a current slug, anchor on the recurring series directly:
* Route: `GET /markets/timeline`
| Query param | Type | Required | Description |
| - | - | - | - |
| `symbol` | `string` | Yes | Underlying symbol, e.g. `BTC`. |
| `frequency` | `string` | Yes | One of `minutely`, `hourly`, `daily`, `weekly`. |
| `subFrequency` | `string` | Required for `minutely` / `hourly` | Slot size, e.g. `minutes_5`, `minutes_15`, `hours_1`. |
| `before` | `number` | No | Past slots to return (default `10`, max `500`). |
| `after` | `number` | No | Future slots to return (default `10`, max `500`). |
```bash theme={null}
# Upcoming 5-minute BTC slots
curl "https://api.limitless.exchange/markets/timeline?symbol=BTC&frequency=minutely&subFrequency=minutes_5&before=2&after=10"
```
## Response
```json theme={null}
{
"current": {
"slotId": 4821,
"slug": "btc-up-or-down-5-min-1771934700",
"state": "OPEN",
"tradable": true,
"startAt": "2026-05-28T12:05:00.000Z",
"endAt": "2026-05-28T12:10:00.000Z",
"countdownSec": 142
},
"next": {
"slotId": 4822,
"slug": "btc-up-or-down-5-min-1771935000",
"state": "PRE_OPEN",
"tradable": false,
"startAt": "2026-05-28T12:10:00.000Z",
"endAt": "2026-05-28T12:15:00.000Z",
"countdownSec": 442
},
"batch": [
{
"slotId": 4822,
"index": 1,
"slug": "btc-up-or-down-5-min-1771935000",
"state": "PRE_OPEN",
"tradable": false,
"startAt": "2026-05-28T12:10:00.000Z",
"endAt": "2026-05-28T12:15:00.000Z"
}
]
}
```
### Slot fields
| Field | Type | Description |
| - | - | - |
| `slug` | `string` | Market slug for this slot. Use it to fetch the orderbook or place orders once tradable. |
| `state` | `string` | `PRE_OPEN`, `OPEN`, `SETTLING`, `SETTLED`, or `FAILED`. |
| `tradable` | `boolean` | Whether orders are currently accepted for this slot. |
| `startAt` / `endAt` | `string` | ISO 8601 slot window bounds. |
| `countdownSec` | `number` | Seconds to the next boundary (`current` / `next` only). |
| `index` | `number` | Slot offset from the anchor (`batch` only). |
Both variants also return `anchor` (`slug`, `jobId`, `periodId`) and `job` (`id`, `frequency`, `frequencySubType`, `periodDuration`, `deployLeadTime`) describing the recurring series. `next` is `null` when no successor slot is scheduled yet.
A slot can exist in `PRE_OPEN` before orders are accepted. Gate order submission on `tradable: true` / `state: "OPEN"` rather than on `startAt` alone.
## Pre-fetch pattern
1. Poll the timeline a few seconds before the current slot ends.
2. Read `next.slug` and open its orderbook subscription early.
3. Submit the moment `next` flips to `state: "OPEN"` / `tradable: true`.
Responses are cached for a few seconds, so polling once per second per series is safe.
# 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`). Send either `cursor` or `page`. An empty `cursor` starts cursor mode. Sending `cursor` together with `page` greater than 1 returns `400 pagination mode conflict`.
Sorting: prefix field with `-` for descending (e.g., `-updatedAt`). Allowed fields: `createdAt`, `updatedAt`, `deadline`, `id`.
**`includeNextMarket` (optional).** Pass `includeNextMarket=true` to attach the next round of upfront-scheduled markets to each affected result:
```json theme={null}
"navigation": {
"nextMarket": { "slug": "...", "startAt": "...", "deadline": "...", "tradable": false }
}
```
Use this to prefetch or subscribe to the successor before the current round expires. During the short activation gap between rounds, the expired predecessor stays in the list so the series is represented by exactly one row at all times.
Cursor tokens embed this option — do not toggle `includeNextMarket` mid-pagination. The `tradable` flag is conservative in listings; fetch the successor directly with [Get Market](/api-reference/markets/get-market) before trading.
# 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.
# Search Active Packs
Source: https://docs.limitless.exchange/api-reference/packs/search-packs
GET /parlay/packs/search
Free-text search over the packs the active-pack listing returns. The search applies the same ACTIVE, trade-window, and eligibility gating as the listing, so it never surfaces a pack the listing hides. Matches the pack name, each leg's team names, match titles, market titles, and league names. Matching is case- and accent-insensitive and tokenized: every word in the query must appear somewhere in the fields being tested, but words can match in any order and across different fields. Results are ranked by relevance: packs matched on their name first, then packs where a single leg's teams or match title carries the whole query, then matches spread across other fields. When packs are paused platform-wide, the search returns an empty array.
Free-text search over active packs. Public, no authentication required. The search covers exactly the packs the active-pack listing returns, with the same `ACTIVE`, trade-window, and eligibility gating, so it never surfaces a pack the listing hides.
## When to use
Use this endpoint to power a pack search box. Pack names are often editorial and say nothing about their contents, so the search also matches what is inside each pack: leg team names, match titles, market titles, and league names. A user searching for a team or league finds every active pack that carries it, even when the pack name mentions neither.
## Matching
* Matching is case- and accent-insensitive: `koln` matches `1. FC Köln`, `fenerbahce` matches `Fenerbahçe`.
* The search tokenizes the query, so word order does not matter and words can match across different fields: `man city` finds a Manchester City pack, and `vitality 9z` finds a pack holding both teams in different legs.
* Every word in the query must appear somewhere for a pack to match.
* `query` must be 2-100 characters. A query shorter than 2 characters after trimming returns an empty array rather than an error, matching `/markets/search`.
## Ranking
Results come back in three relevance tiers:
1. Packs whose name matches the query.
2. Packs where a single leg's team names or match title carries the whole query. A query spread across different legs does not earn this tier.
3. Packs matched across any other fields, such as market titles or league names.
Within a tier, packs keep the listing's order (earliest deadline first).
## Limits and availability
* `limit` caps the number of results at 1-50. The default is 20.
* When packs are paused platform-wide, the search returns an empty array, matching the listing.
## Caching
Responses carry `Cache-Control: public, max-age=30, stale-while-revalidate=60`, so results can trail the live listing by up to a minute.
# Check Partner Account Allowances
Source: https://docs.limitless.exchange/api-reference/partner-accounts/check-allowances
GET /profiles/partner-accounts/{profileId}/allowances
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` \| `SPLIT` | Operation that requires this approval; `SPLIT` is the USDC allowance for `splitPosition` (ConditionalTokens or NegRiskAdapter). |
| `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`. |
| `error` | `string` | Optional human-readable failure message. |
| `lastFailedAt` | `string` | Optional ISO 8601 timestamp of the last failed submission for this target. |
| `currentAllowance` | `string` | Optional current on-chain allowance in raw token units, set when the target was checked on-chain. |
## 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.
If account creation succeeds but your app fails to persist the returned
`profileId`, use [List Partner Sub-Accounts](/api-reference/partner-accounts/list-partner-accounts)
with the `account` filter to recover it.
### Server wallet mode
Set `createServerWallet: true` to create a Privy server wallet for the sub-account. This enables [delegated signing](/developers/authentication#delegated-signing) — you 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.
# List Partner Sub-Accounts
Source: https://docs.limitless.exchange/api-reference/partner-accounts/list-partner-accounts
GET /profiles/partner-accounts
Lists partner-owned sub-accounts created under the authenticated partner profile, or recovers a specific sub-account by account address. Requires HMAC authentication with the `account_creation` scope. Do not send `x-on-behalf-of`; results are always scoped to the authenticated partner.
Requires **HMAC authentication** with the `account_creation` scope. API key
auth, Privy auth, and session auth are not accepted.
Lists partner-owned sub-accounts created under the authenticated partner profile.
Use the optional `account` query parameter to recover the `profileId` for a
known sub-account address.
Do not send `x-on-behalf-of` to this endpoint. It always scopes results to the
authenticated partner.
## Query parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `account` | `string` | No | Exact EVM address filter. Matching is case-insensitive. |
| `limit` | `number` | No | Positive integer page size. Defaults to `25`; values above `25` are capped to `25`. |
| `page` | `number` | No | Positive integer page number. Defaults to `1`. |
## Response
```json theme={null}
{
"data": [
{
"profileId": 294,
"account": "0x1676716Ef7F19B5C5d690631CB57cf0bFD900A3d",
"displayName": "user-bob"
}
],
"limit": 25,
"page": 1,
"hasMore": false
}
```
## Errors
| Status | Description |
| - | - |
| `400` | Invalid `account`, `limit`, or `page` query parameter. |
| `401` | Missing or invalid authentication. |
| `403` | Requires api-token HMAC auth with `account_creation` scope. |
# Retry Partner Account Allowances
Source: https://docs.limitless.exchange/api-reference/partner-accounts/retry-allowances
POST /profiles/partner-accounts/{profileId}/allowances/retry
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. |
| `503` | Rate limiter backend unavailable (`Allowance retry rate limiter is unavailable`). | Retry after a short delay. |
### 429 example
```json theme={null}
{
"statusCode": 429,
"message": "Allowance retry is rate limited. Please wait before retrying.",
"retryAfterSeconds": 60
}
```
### 409 example
```json theme={null}
{
"statusCode": 409,
"message": "Retry already in progress",
"error": "Conflict"
}
```
## 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 Current Profile
Source: https://docs.limitless.exchange/api-reference/portfolio/get-current-profile
GET /profiles/me
Retrieve the authenticated caller's private profile without passing an address. Returns the same private profile shape as `GET /profiles/{account}`, including internal profile ID and effectiveFeeRateBps for signing orders on fee-enabled markets.
Use this endpoint when your client is already authenticated and you do not want
to pass a wallet address. It returns the same private profile shape as
`GET /profiles/{account}`.
Returns the authenticated caller's private profile, including the internal
profile `id` and `effectiveFeeRateBps` used when constructing signed orders on fee-enabled markets.
## Errors
| Status | Description |
| - | - |
| `401` | Authentication is missing, invalid, or did not resolve to an account. |
| `404` | No profile exists for the authenticated account. |
# Get Your Profile
Source: https://docs.limitless.exchange/api-reference/portfolio/get-profile
GET /profiles/{account}
Retrieve the authenticated user's private profile by address, including `effectiveFeeRateBps` for signing on fee-enabled markets and numeric `id` for `ownerId`. You can only access your own profile; requesting another user's address returns 403. If you do not want to pass an address, use `GET /profiles/me`.
This endpoint returns your **internal user ID** (`id`) and **effective fee rate** (`effectiveFeeRateBps`). For fee-enabled markets, use `effectiveFeeRateBps` in the signed order, not `rank.feeRateBps`: a profile-specific cap can lower the rank rate. For fee-disabled markets, sign with `feeRateBps: 0`. If `effectiveFeeRateBps` is `null`, the profile has no rank and cannot supply a fee rate for a fee-enabled order.
The `account` path parameter must be your wallet address exactly as stored on your profile (case-sensitive; a lowercase or otherwise re-cased address returns `403`). Prefer [`GET /profiles/me`](/api-reference/portfolio/get-current-profile).
If your client is already authenticated and you do not want to pass an address,
use [`GET /profiles/me`](/api-reference/portfolio/get-current-profile).
# 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.
**CLOB fills live here.** Limitless does not expose a dedicated fills endpoint. This endpoint returns every CLOB fill for the authenticated account as a history entry. Use the `strategy` field to distinguish fill types:
* `Limit Buy` / `Limit Sell` — CLOB **maker** fills (a resting limit order of yours was hit).
* `Market Buy` / `Market Sell` — CLOB **taker** fills (your order crossed the book on submission).
* `Buy` / `Sell` — AMM trades (also available via [Get Trades](/api-reference/portfolio/trades)).
* `Split` / `Merge` — position split/merge.
* `Convert` — NegRisk conversion.
* `Claim` — winnings redemption.
Each entry includes `outcomeTokenPrice` (effective fill price), `outcomeTokenAmounts`, `collateralAmount`, `blockTimestamp`, and `transactionHash` (present once settled on-chain). Paginate with `cursor` and `limit`.
**Deposits and withdrawals are not included.** This endpoint covers on-platform activity only (fills, splits, merges, conversions, and claims). Raw USDC deposits and withdrawal transfers are not returned here and the API does not expose a dedicated deposit/withdrawal history endpoint. [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) executes a withdrawal but does not return past withdrawals. To reconstruct transfers, query the on-chain USDC transfers to and from your account on Base.
**No historical orderbook.** The API does not expose historical orderbook snapshots. [Get Orderbook](/api-reference/trading/orderbook) returns only the current live snapshot. To reconstruct past activity for a market, combine your own fill history from this endpoint with [Get Historical Prices](/api-reference/trading/historical-price) and [Get Feed Events](/api-reference/markets/feed-events).
# Merge Outcome Set Into Collateral
Source: https://docs.limitless.exchange/api-reference/portfolio/merge
Submits an on-chain `mergePositions` transaction that converts a full set of outcome tokens (1 Yes share + 1 No share → 1 USDC) back into collateral from a server-wallet sub-account.
* Route: `POST /portfolio/merge`
* Auth: `apiToken`, Privy, or session auth
* Scope: `trading` when using `apiToken`
For what merging is and when to use it, see [Merging & Splitting Shares](/user-guide/merge-split).
### Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `conditionId` | `string` | Yes | CTF condition id (`bytes32` hex string) of the market. |
| `amount` | `string` | Yes | Number of complete sets to convert back, in collateral base units (USDC = 6 decimals, so `1000000` = 1 full set). |
| `venue` | `object` | Yes | Venue object from the [market response](/api-reference/markets/get-market). Pass `venue.exchange` for simple/CLOB markets and `venue.adapter` for NegRisk markets. When `adapter` is present, the operation routes through the NegRiskAdapter; otherwise it routes through ConditionalTokens. |
| `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/merge" \
-H "content-type: application/json" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: " \
-d '{
"conditionId": "0xa0ba8fd0acc2b86585734c07eb3bef4133f584d5abde78a558a6b8bfc3bbdec0",
"amount": "1000000",
"venue": { "exchange": "0xe3E00BA3a9888d1DE4834269f62ac008b4BB5C47" }
}'
```
### Response
Returns the submitted transaction: `conditionId`, `operation` (`MERGE`), `route` (`clob` or `negrisk`), `walletAddress`, and, when available, `transactionId` and `userOperationHash`. Sponsored 4337 user operations return no synchronous transaction hash, so track the operation via `userOperationHash` and `transactionId`.
### Errors
| Status | Meaning |
| - | - |
| `400` | Invalid `amount`, `conditionId`, or adapter address. |
| `401` | Unauthorized. |
| `403` | No server wallet on the target profile, or insufficient token scopes. |
| `409` | A split or merge for this profile is already in progress. |
### Notes
* The execute path is a single on-chain transaction. Limitless provisions the NegRiskAdapter ERC-1155 approval on the server wallet ahead of time, so no approval transaction is submitted here.
* Merging burns the wallet's own outcome tokens, so simple/CLOB merges need no allowance at all.
* Validation is minimal by design: on-chain failures pass through to the caller, who retries if needed.
* Legacy API keys are not supported on server-wallet operations.
**Why this endpoint is server-wallet only.** Merging outcome tokens is an on-chain transaction that must be signed by the wallet holding the shares. This endpoint can only sign for wallets Limitless manages (Privy-backed server-wallet sub-accounts). EOA and user-owned smart-wallet users merge through the **Merge/Split** option on the market page in the Limitless UI, or by calling `mergePositions` on the Conditional Tokens Framework contract directly. See [Smart Contracts](/user-guide/smart-contracts) for the deployed addresses.
# Get PnL Chart
Source: https://docs.limitless.exchange/api-reference/portfolio/pnl-chart
GET /portfolio/pnl-chart
Realised PnL series and current realised snapshot
# Get Points Breakdown
Source: https://docs.limitless.exchange/api-reference/portfolio/points
GET /portfolio/points
**Response fields.** The points breakdown returns:
* `currentSeason` — live running points total for the season in progress. This field updates in real time and reflects every points distribution as soon as it lands.
* `currentMonth` — points earned in the current 4-week month bucket. This value resets to 0 at each month rollover, so use `currentSeason` when you need a season-to-date running total.
* `season1`, `season2`, `season3`, … — archived per-season totals for finished seasons.
Reward amounts are not returned here; see the `rewards` object on [Get Positions](/api-reference/portfolio/positions).
# 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. Partner API tokens with `delegated_signing` scope may read a sub-account by sending the `x-on-behalf-of: ` header.
For real-time position updates, use the [WebSocket API](/developers/websocket/positions) — 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).
**Split-resolved positions.** A resolved position with `winningOutcomeIndex: null` and a non-null `payoutNumerators` array on its market is a payout **split** (for example `[50, 50]` for 50/50). Both YES and NO holdings on that market are redeemable at the ratio through [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) in a single transaction.
**`rewards` object: reward-field semantics (as currently computed).** All reward amounts are 1e6-scaled USDC integers. Divide by `1,000,000` to get dollars (`"11458"` = `$0.011458`).
* `todaysRewards` — despite the schema description, this is **not** a calendar-day total. It reflects the **latest processed reward epoch**, and epochs run once per minute, so read it as "rewards earned in the last completed minute."
* `totalUserRewardsLastEpoch` — the same one-epoch window, so it currently carries the same value as `todaysRewards`.
* `totalUnpaidRewards` — everything accrued since your last payout, across all markets and epochs. Use this field for balances. Expect it to be much larger than the other two.
Payouts run once a day at 22:30 UTC and pay the full unpaid balance. There is no minimum, and sub-cent amounts are included. See [LP Rewards](/user-guide/lp-rewards).
# 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.
* One `conditionId` per request. There is no batch redeem endpoint, so call it once per resolved market. See the rate-limit guidance on the [API Introduction](/api-reference/introduction) when looping over many markets.
* 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.
* For markets that resolved to a payout **split** (`winningOutcomeIndex: null` with a non-null `payoutNumerators`), one call redeems both YES and NO holdings at the ratio defined by `payoutNumerators`. See the split-resolution note on [Get Market Details](/api-reference/markets/get-market).
* NegRisk markets (`negRiskRequestId` set) are redeemed through `NegRiskAdapter.redeemPositions` using the wallet's `yesPositionId` / `noPositionId` balances; standard markets through ConditionalTokens with the full partition.
### Response
```json theme={null}
{
"hash": "0x...",
"userOperationHash": "0x...",
"transactionId": "...",
"conditionId": "0x...",
"walletAddress": "0x...",
"marketId": 123
}
```
**Why this endpoint is server-wallet only.** Redeeming a CTF position is an on-chain transaction that must be signed by the wallet holding the shares. This endpoint can only sign for wallets Limitless manages (Privy-backed server-wallet sub-accounts), so it does not accept EOA-owned or user-managed smart-wallet profiles. There is no "auto redeem" performed by Limitless on behalf of EOA users — self-custodial by design.
**EOA and user-owned smart-wallet users** redeem their own positions on-chain instead:
* Through the Limitless UI **Portfolio → Positions → Redeem**, which prompts the connected wallet to sign the `redeemPositions` call.
* Or directly on-chain by calling `redeemPositions(conditionId, indexSets)` on the Conditional Tokens Framework contract (or `NegRiskAdapter.redeemPositions(conditionId, amounts)` for NegRisk markets) from the wallet holding the shares. See [Smart Contracts](/user-guide/smart-contracts) for the deployed addresses.
# Split Collateral Into Outcome Set
Source: https://docs.limitless.exchange/api-reference/portfolio/split
Submits an on-chain `splitPosition` transaction that converts USDC collateral into a full set of outcome tokens (1 USDC → 1 Yes share + 1 No share) from a server-wallet sub-account.
* Route: `POST /portfolio/split`
* Auth: `apiToken`, Privy, or session auth
* Scope: `trading` when using `apiToken`
For what splitting is and when to use it, see [Merging & Splitting Shares](/user-guide/merge-split).
### Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `conditionId` | `string` | Yes | CTF condition id (`bytes32` hex string) of the market. |
| `amount` | `string` | Yes | Collateral to convert, in base units (USDC = 6 decimals, so `1000000` = 1 USDC). |
| `venue` | `object` | Yes | Venue object from the [market response](/api-reference/markets/get-market). Pass `venue.exchange` for simple/CLOB markets and `venue.adapter` for NegRisk markets. When `adapter` is present, the operation routes through the NegRiskAdapter; otherwise it routes through ConditionalTokens. |
| `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/split" \
-H "content-type: application/json" \
-H "lmts-api-key: " \
-H "lmts-signature: " \
-H "lmts-timestamp: " \
-d '{
"conditionId": "0xa0ba8fd0acc2b86585734c07eb3bef4133f584d5abde78a558a6b8bfc3bbdec0",
"amount": "1000000",
"venue": { "exchange": "0xe3E00BA3a9888d1DE4834269f62ac008b4BB5C47" }
}'
```
### Response
Returns the submitted transaction: `conditionId`, `operation` (`SPLIT`), `route` (`clob` or `negrisk`), `walletAddress`, and, when available, `transactionId` and `userOperationHash`. Sponsored 4337 user operations return no synchronous transaction hash, so track the operation via `userOperationHash` and `transactionId`.
### Errors
| Status | Meaning |
| - | - |
| `400` | Invalid `amount`, `conditionId`, or adapter address. |
| `401` | Unauthorized. |
| `403` | No server wallet on the target profile, or insufficient token scopes. |
| `409` | A split or merge for this profile is already in progress. |
### Notes
* The execute path is a single on-chain transaction. Limitless provisions the required collateral allowances on the server wallet ahead of time, so no approval transaction is submitted here.
* Validation is minimal by design: on-chain failures pass through to the caller, who retries if needed.
* Legacy API keys are not supported on server-wallet operations.
**Why this endpoint is server-wallet only.** Splitting collateral is an on-chain transaction that must be signed by the wallet holding the funds. This endpoint can only sign for wallets Limitless manages (Privy-backed server-wallet sub-accounts). EOA and user-owned smart-wallet users split through the **Merge/Split** option on the market page in the Limitless UI, or by calling `splitPosition` on the Conditional Tokens Framework contract directly. See [Smart Contracts](/user-guide/smart-contracts) for the deployed addresses.
# Get Trades
Source: https://docs.limitless.exchange/api-reference/portfolio/trades
GET /portfolio/trades
Retrieve all trades executed by the authenticated user
**Scope: AMM trades only.** This endpoint returns your AMM (Automated Market Maker) trades. It does not return CLOB (order book) fills, including maker fills (`Limit Buy` / `Limit Sell`) and taker fills (`Market Buy` / `Market Sell`). To read CLOB fills alongside AMM activity, splits, merges, NegRisk conversions, and claims, use [Get History](/api-reference/portfolio/history) and filter on the `strategy` field.
# 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. Must be a collateral token known to the platform; otherwise `400` (`... is not a recognized collateral token`). 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: the smart wallet for Etherspot profiles that have one, otherwise the account address.
* Destination validation blocks withdrawals to third-party addresses that are not allowlisted.
* Legacy API keys are not supported on server-wallet operations.
### Response
```json theme={null}
{
"hash": "0x...",
"userOperationHash": "0x...",
"transactionId": "...",
"walletAddress": "0x...",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"destination": "0x...",
"amount": "1000000"
}
```
# Get User Accuracy (Public)
Source: https://docs.limitless.exchange/api-reference/public-portfolio/accuracy
GET /user-stats/accuracy/{account}
Returns accuracy statistics for a user by Ethereum address
Returns a user's accuracy score by Ethereum address: how many resolved markets they won and lost, the total resolved, and when tracking started. Public, no authentication required.
## When to use
Use this endpoint to show an accuracy stat on a public profile, feed, or leaderboard view. Pass any wallet address; you do not need to be that user.
## Response contract
The `200` body is either a stats object or `null`:
* An object with `won`, `lost`, `total`, and `since` (ISO date string of the earliest resolved market tracked) when accuracy data exists for the address.
* `null` when no accuracy data is available yet. Treat `null` as "no data", not as an error.
Compute the accuracy percentage yourself as `won / total`.
## Address handling
The `account` path parameter accepts a checksummed (EIP-55) or lowercase Ethereum address. The API normalizes it server-side, so both forms resolve to the same user. A malformed address returns `400 Bad Request`.
## Caching
Successful responses (object or `null`) carry `Cache-Control: public, max-age=0, s-maxage=15`, so CDN edges may serve a cached copy for up to 15 seconds. Error responses are `no-store`.
The endpoint is feature-flagged. When accuracy scoring is disabled platform-wide, it returns `404 Not Found`.
# Get User History (Public)
Source: https://docs.limitless.exchange/api-reference/public-portfolio/history
GET /portfolio/{account}/history
Retrieve trading history for a specific user with cursor-based pagination
This is the public counterpart to the authenticated [`/portfolio/history`](/api-reference/portfolio/history) endpoint. It returns the same paginated history shape for any address, without requiring HMAC authentication.
## When to use
Use this endpoint to read another user's trading history. Common cases include rendering a public profile, a feed, or a leaderboard drill-down. If you are reading your own history and want a stable, private view, use the authenticated `/portfolio/history` endpoint instead.
## Pagination
The endpoint uses cursor-based pagination:
* Omit `cursor` on the first request.
* The response includes a `nextCursor` value; pass it as `cursor` on the next request to fetch the following page.
* When `nextCursor` is `null` or absent, you have reached the end of the history.
* `limit` accepts values between `1` and `100`. If omitted, the default is `20`.
## Address handling
The `account` path parameter accepts any valid Ethereum address in either checksummed (EIP-55) or lowercase form. The API normalizes the address server-side, so `0xABC…` and `0xabc…` resolve to the same user.
* A malformed address returns `400 Bad Request`.
* An address that has never interacted with Limitless returns `404 Not Found`.
# Get User Pack Positions (Public)
Source: https://docs.limitless.exchange/api-reference/public-portfolio/pack-positions
GET /parlay/{account}/positions
Returns a profile's public pack (parlay) positions by Ethereum address, newest first. Only ACTIVE, WON, and LOST positions are included.
Returns a user's pack (parlay) positions by Ethereum address, newest first. Public, no authentication required. The response includes only `ACTIVE`, `WON`, and `LOST` positions; internal lifecycle states never appear here.
## When to use
Use this endpoint to render a packs positions list on a public profile view. Pass any wallet address; you do not need to be that user. For your own positions with private fields (wallet, transaction hashes, vig), use the authenticated positions endpoint instead.
## Response contract
The `200` body is a paginated page: `data`, `limit`, `page`, `totalPages`, and `totalRows`.
* Each position carries its status, stake, multiplier, claim state (`isClaimed`), leg details, and the source pack it was bought from (`null` for custom packs).
* The public payload deliberately excludes wallet addresses, token IDs, transaction hashes, and vig fields.
* The API sorts positions by creation time, newest first.
## Filtering and pagination
* `status` narrows the list to a single status: `ACTIVE`, `WON`, or `LOST`. Any other value returns `400 Bad Request`. Omit it to return all three.
* The API clamps `limit` server-side to the range 1-50. The default is 20.
* `offset` defaults to 0. The API caps it at 10,000; a larger offset returns `400 Bad Request`.
* `totalRows` always reflects the full matching count, while `totalPages` stops at the last browsable page under the offset cap.
## Address handling
The `account` path parameter accepts a checksummed (EIP-55) or lowercase Ethereum address. A malformed address returns `400 Bad Request`. An address with no profile returns `404 Not Found`.
## Caching
Responses carry `Cache-Control: public, max-age=5, s-maxage=15`, so CDN edges may serve a cached copy for up to 15 seconds. Status and claim state can trail the live database by roughly 30 seconds in the worst case. The authenticated owner endpoint is uncached.
# Get PnL Chart (Public)
Source: https://docs.limitless.exchange/api-reference/public-portfolio/pnl-chart
GET /portfolio/{account}/pnl-chart
Deprecated legacy aggregate realised-PnL series and current realised snapshot. Use /portfolio/:account/realized-pnl for public profile PnL.
Deprecated. Use [`GET /portfolio/{account}/realized-pnl`](/api-reference/public-portfolio/realized-pnl) for public-profile realized PnL. Limitless keeps this route for existing consumers and will remove it after clients migrate.
# 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 Realized PnL (Public)
Source: https://docs.limitless.exchange/api-reference/public-portfolio/realized-pnl
GET /portfolio/{account}/realized-pnl
Projected realised PnL timeline with backend-owned category grouping and hover breakdown for public profiles.
Returns a projected realized PnL timeline for any public profile, grouped by market category with a per-point hover breakdown. The service computes values best-effort from the canonical five-minute PnL buckets available at request time.
* `timeframe` — echoes the requested window (`1d`, `1w`, `1m`, or `all`). Defaults to `1w`. `all` reads permanent hourly history and returns no category breakdown (`categories` and per-point `breakdown` are empty).
* `windowStart` — start of the effective timeline, anchored on the current closed UTC five-minute boundary.
* `current` — realized PnL over the effective window.
* `categories` — per-category realized PnL sorted descending. Markets attributed through navigation pages fall under their category slug. Anything unattributed collapses into the synthetic `other` bucket, which has no `slug`, `path`, or `icon`.
* `data` — cumulative realized PnL timeline. Each point carries a `breakdown` with the top 3 named categories at that timestamp plus the synthetic `Other` residual.
The legacy [`GET /portfolio/{account}/pnl-chart`](/api-reference/public-portfolio/pnl-chart) endpoint remains as the deprecated contract for existing consumers. Use this endpoint for new integrations.
# Get User Traded Volume
Source: https://docs.limitless.exchange/api-reference/public-portfolio/traded-volume
GET /portfolio/{account}/traded-volume
Get total traded volume and statistics for a specific user
The response body is `{ "data": "" }`, where `data` is the account's total traded volume in whole USDC as a decimal string (raw amount divided by 1e6 with integer division, so fractional USDC is dropped).
# Get Referral Leaderboard
Source: https://docs.limitless.exchange/api-reference/referral/leaderboard
GET /referral/usdc/leaderboard
The global referral leaderboard. Public - no authentication required. When the request is authenticated, the response additionally carries a `me` block pinning your own rank and entry even when outside the returned page.
This endpoint is public — no authentication required. When the request is authenticated, the response additionally carries a `me` block pinning your own rank and entry, even when you are outside the returned page.
Anonymous responses are CDN-cached for up to 5 minutes; authenticated responses are private with a 60-second cache. Ranks are absolute across pages and follow the active `sortBy`.
# Get Friends Leaderboard
Source: https://docs.limitless.exchange/api-reference/referral/leaderboard-friends
GET /referral/usdc/leaderboard/friends
The global leaderboard scoped to your own referred users: each row is that user's own referral standing (their referred count and referral earnings), keeping the global ranking. Only referred users who are themselves earning referrers appear. The response never carries a `me` block.
The global leaderboard scoped to your own referred users. Each row is that user's **own** referral standing (their referred count and their referral earnings — not your cut from them), keeping the global ranking.
Only referred users who are themselves earning referrers appear, so the board can be sparse until your network grows. The response never carries a `me` block.
# Get My Referred Users
Source: https://docs.limitless.exchange/api-reference/referral/my-referrals
GET /referral/usdc/referrals
Your own referred users, paginated, filtered, and sorted server-side. `counts` covers all your referred users regardless of the active filter; `total` counts only rows matching the current `status` filter.
Money and volume fields are raw USDC with 6 decimals, returned as strings — divide by `1e6` for the dollar amount.
The `status` filter distinguishes referred users who have traded (`earning`) from those who have signed up but not traded yet (`awaiting`). `counts` always covers all your referred users regardless of the active filter, so filter chips stay accurate across pages; `total` counts only the rows matching the current `status` filter.
# Get My Referral Stats
Source: https://docs.limitless.exchange/api-reference/referral/my-stats
GET /referral/usdc/me
Your own referral standing: cumulative CLOB trading volume (the tier basis), total USDC earned from the referral program, and the active tier ladder. The current tier is not sent - resolve it as the highest ladder entry whose minBasisRaw your totalBasisRaw clears.
Money and volume fields are raw USDC with 6 decimals, returned as strings — divide by `1e6` for the dollar amount.
Your current tier is not returned directly. Resolve it client-side as the highest `tiers` entry whose `minBasisRaw` your `totalBasisRaw` clears. When a `customTier` pin is present it acts as a floor: the effective tier is the higher of the pin and the volume-derived tier.
`currentRate` (optional) is the live materialized referral rate from your open tier-state row; it already reflects any pin or rate floor and any promotion past it, and is absent until a tier state has been materialized. `customTierRate` (optional, exclusive with `customTier`) is an ops rate floor: when it is higher than your volume tier's ladder rate, your current tier is a custom tier at this rate, and volume can still promote you past it.
`totalEarnedRaw` is your accrued referral earnings. Earnings become payable once the underlying markets resolve and are then included in the daily payout — see the [Referral Program guide](/user-guide/referral-program).
# Maintenance Status
Source: https://docs.limitless.exchange/api-reference/system/maintenance-status
GET /maintenance/status
Returns active and scheduled maintenance information. Use this endpoint to detect temporary trading restrictions and planned maintenance notices.
Use this endpoint to check whether trading is currently restricted or whether a future maintenance notice is scheduled.
This endpoint is public and does not require authentication.
For integration guidance, see [Maintenance Mode](/developers/maintenance-mode).
# 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 non-empty identifier array. Each request accepts 1–50 internal UUIDv4 IDs or 1–50 client order IDs of at most 128 characters. Requests with both arrays or neither array return `400 Bad Request`.
`POST /orders/cancel-batch` remains supported for existing integrations and accepts `orderIds` only.
## Authentication and delegation
Use a scoped API token with HMAC signing and the `trading` scope. To cancel for a partner sub-account, add `?onBehalfOf=`; the target must belong to the authenticated partner and the token must also have `delegated_signing`. Include the query string in the HMAC-signed path.
## Results
* `200 OK` means every resolved order was cancelled.
* `207 Multi-Status` means at least one order was cancelled and at least one failed.
* `400 Bad Request` with a `failed` array means no order could be cancelled.
* Internal-ID results use order-ID strings in `canceled` and `{ orderId, reason, message }` records in `failed`.
* Client-ID results use `{ clientOrderId, orderId }` records in `canceled`. Failures are keyed by `clientOrderId`; `orderId` is included only if the client ID resolved before cancellation failed.
For an internal-ID request, unknown IDs are omitted when at least one supplied ID resolves. If none resolve, the endpoint returns `404 Orders not found`. Client-ID requests report every unresolved client ID as `ORDER_NOT_FOUND`.
Cancellation failures use `ORDER_NOT_FOUND` with `Order not found or already canceled`, `ORDER_RELEASE_IN_PROGRESS` with `Order release is in progress; cancellation is not confirmed` and `retryable: true`, or `UNKNOWN_ERROR` with `Failed to cancel order`.
Cancellation remains available in `normal`, `post_only`, and `cancel_only`. Trading mode `disabled` returns `425 Too Early`.
# Cancel Order (Combined)
Source: https://docs.limitless.exchange/api-reference/trading/cancel
POST /orders/cancel
Cancel an 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.
## Authentication
Cancelling is **not** EIP-712 signed. Unlike placing an order, you do not include a `signature` in the request. Cancellation is an off-chain operation that removes the order from the book.
Use a scoped API token with HMAC signing and the `trading` scope. No additional scope is needed to cancel your own orders.
### Cancelling a sub-account's orders (partner flow)
If you placed an order with `onBehalfOf` (so it is owned by a managed sub-account, not your partner profile), cancel it through your partner token with the `onBehalfOf` query parameter:
```
POST /orders/cancel?onBehalfOf=
```
This on-behalf-of path additionally requires the **`delegated_signing`** scope on your partner token. The scope is reused as the authorization gate for acting on a sub-account. It does **not** mean the order must be server-signed, and it works for EOA sub-accounts that signed their own orders. See [Delegated Signing](/developers/authentication).
The HMAC signature must be computed over the **full path including the query string**, so sign `/orders/cancel?onBehalfOf=42`, not `/orders/cancel`. See [Authentication](/developers/authentication).
## Cancellation behavior
* Cancellation itself is never subject to the taker delay; it goes straight through.
* A cancellation can remove a delayed order before execution. This returns `Delayed order canceled successfully`.
* FAK and FOK orders can only be cancelled while waiting for the taker delay. On markets without a taker delay, they finish immediately and cannot be cancelled.
* An order that can no longer be cancelled returns `400 Bad Request` with `Order not found or already canceled`.
* A delayed order whose release is in progress returns `409 Conflict` with `reason: "ORDER_RELEASE_IN_PROGRESS"` and `retryable: true`.
## Error distinctions
| Status | Meaning |
| - | - |
| `400` | Invalid request, resolved market, or an order that can no longer be cancelled. |
| `401` | Authentication failed, or the resolved order is owned by another profile. |
| `403` | The HMAC token lacks `trading`, or delegated cancellation is not permitted. |
| `404` | No order resolves from the supplied internal or client order ID. |
| `409` | The delayed order's release is in progress. |
| `425` | Trading mode is `disabled`. Cancellation remains available in `normal`, `post_only`, and `cancel_only`. |
| `500` | An unexpected cancellation failure occurred. |
# Cancel All Orders
Source: https://docs.limitless.exchange/api-reference/trading/cancel-all
DELETE /orders/all/{slug}
Cancel every LIVE order owned by the authenticated profile for a market or market group.
The required `slug` path parameter accepts either a market slug or a group slug. For a group, cancellation fans out across all child markets and combines their results.
This is the widest cancel available to an API caller: one market or one group per request. There is no account-wide cancel across all markets. To cancel everything, call this endpoint once per slug you hold orders in, using group slugs where a group covers several of your markets. The call is idempotent, so a slug with nothing LIVE is safe to include.
## Authentication and delegation
Use a scoped API token with HMAC signing and the `trading` scope. To cancel for a partner sub-account, add `?onBehalfOf=`; the target must belong to the authenticated partner and the token must also have `delegated_signing`. Include the query string in the HMAC-signed path.
## Results
The operation is idempotent. If there are no LIVE orders to cancel, it returns `200 OK` with `{ "message": "Orders canceled successfully" }` and omits `canceled`.
* `200 OK` means all matching orders were cancelled, or there were no matching LIVE orders.
* `207 Multi-Status` returns both cancelled IDs and normalized failures across the market or group.
* `400 Bad Request` with `Failed to cancel any orders` means every attempted cancellation failed.
* `404 No entity found by slug` means the slug matches neither a market nor a group.
Cancellation failures use `ORDER_NOT_FOUND` with `Order not found or already canceled`, `ORDER_RELEASE_IN_PROGRESS` with `Order release is in progress; cancellation is not confirmed` and `retryable: true`, or `UNKNOWN_ERROR` with `Failed to cancel order`. Cancellation remains available in `normal`, `post_only`, and `cancel_only`; `disabled` returns `425 Too Early`.
# 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. Send `{ "orderIds": [...] }` with at least one UUIDv4 order ID. This legacy endpoint has no documented maximum batch size, but all orders that resolve must belong to one market.
To cancel by either `orderIds` or `clientOrderIds`, use [Batch Cancel Orders (Combined)](/api-reference/trading/batch-cancel).
## Authentication and delegation
Use a scoped API token with HMAC signing and the `trading` scope. To cancel for a partner sub-account, add `?onBehalfOf=`; the target must belong to the authenticated partner and the token must also have `delegated_signing`. Include the query string in the HMAC-signed path.
## Results
* `200 OK` returns the cancelled order IDs.
* `207 Multi-Status` returns both `canceled` and normalized `failed` records.
* `400 Bad Request` with `Failed to cancel any orders` means every attempted cancellation failed.
* `404 Orders not found` means none of the supplied IDs resolved.
If some supplied IDs do not resolve but at least one does, missing IDs are omitted rather than returned as failures. All resolved orders must belong to the authenticated owner and to the same market.
Cancellation failures use `ORDER_NOT_FOUND` with `Order not found or already canceled`, `ORDER_RELEASE_IN_PROGRESS` with `Order release is in progress; cancellation is not confirmed` and `retryable: true`, or `UNKNOWN_ERROR` with `Failed to cancel order`. Cancellation remains available in `normal`, `post_only`, and `cancel_only`; `disabled` returns `425 Too Early`.
# 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).
Cancellations remain available in `cancel_only` and `post_only`, but return `425 Too Early` with `code: "trading_disabled"` when trading mode is `disabled`.
## Authentication and delegation
Use a scoped API token with HMAC signing and the `trading` scope. To cancel for a partner sub-account, add `?onBehalfOf=`; the target must belong to the authenticated partner and the token must also have `delegated_signing`. Include the query string in the HMAC-signed path.
## Cancellation behavior
A delayed order can be cancelled before execution, including a delayed FAK or FOK order. That path returns `Delayed order canceled successfully`. FAK and FOK orders can only be cancelled while waiting for the taker delay. On markets without a taker delay, they finish immediately and cannot be cancelled.
The endpoint distinguishes:
* `404 Order not found` when the internal ID does not exist;
* `401 Unauthorized` when the order belongs to another profile;
* `400 Market has already been resolved` for a resolved market; and
* `400 Order not found or already canceled` when the order can no longer be cancelled; and
* `409 ORDER_RELEASE_IN_PROGRESS` when the delayed order's release is in progress. The response includes `retryable: true`.
# Cancel and Replace Order
Source: https://docs.limitless.exchange/api-reference/trading/cancel-replace
POST /orders/cancel-replace
Cancel one order and submit its replacement in one request. The two actions are **not atomic**: cancellation and replacement have independent outcomes, and a successful cancellation does not guarantee a successful replacement.
## Request
| Field | Type | Description |
| - | - | - |
| `cancel` | `object` | Identifies the order to cancel. Provide exactly one of `orderId` or `clientOrderId`. |
| `replacement` | `object` | A standard signed order request. It accepts the same fields as [`POST /orders`](/api-reference/trading/create-order), except `onBehalfOf`. |
| `mode` | `string` | `STOP_ON_FAILURE` or `ALLOW_FAILURE`. Controls whether replacement is attempted after cancellation fails. |
| `onBehalfOf` | `number` | Optional profile ID for an authorized partner sub-account. Applies to both cancellation and replacement. |
`replacement.ownerId` must own the order being cancelled. Put `onBehalfOf` only at the operation level, not inside `replacement`.
The `x-on-behalf-of` header is not supported by this endpoint.
### Failure modes
| Mode | Cancellation fails | Replacement result |
| - | - | - |
| `STOP_ON_FAILURE` | Stops this operation | `NOT_ATTEMPTED` |
| `ALLOW_FAILURE` | Continues this operation | Placement is attempted and reports `SUCCESS`, `FAILURE`, or `UNKNOWN` independently |
## Authentication and signing
Use a scoped API token with the `trading` scope and HMAC-sign the exact request body and path `/orders/cancel-replace`; see [Authentication](/developers/authentication#hmac-request-signing).
The cancellation itself is not EIP-712 signed. For a direct request, the replacement is a normal order and must contain an [EIP-712 signature](/developers/eip712-signing). The authenticated profile must be a valid signer of the order being cancelled.
For an authorized partner sub-account, set the operation-level `onBehalfOf` to the sub-account profile ID and set `replacement.ownerId` to the same ID. This path requires `delegated_signing` in addition to `trading`. If the replacement omits its order signature, the server signs it only for a managed server-wallet sub-account under the existing [delegated signing](/developers/authentication#delegated-signing) rules.
## Example: stop after cancellation failure
The order values below illustrate the request shape. Generate `order.signature` from the complete replacement payload instead of copying the placeholder signature.
```json theme={null}
{
"cancel": {
"clientOrderId": "old-order-001"
},
"replacement": {
"ownerId": 12345,
"orderType": "GTC",
"marketSlug": "btc-100k",
"clientOrderId": "replacement-001",
"order": {
"salt": "1778155025318314496",
"maker": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"signer": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"taker": "0x0000000000000000000000000000000000000000",
"tokenId": "19633204485790857949828516737993423758628930235371629943999544859324645414627",
"makerAmount": 5000000,
"takerAmount": 10000000,
"nonce": 0,
"price": 0.5,
"feeRateBps": 0,
"side": 0,
"signature": "0x1234",
"signatureType": 0
}
},
"mode": "STOP_ON_FAILURE"
}
```
If `old-order-001` is not found, the response is `409 Conflict`:
```json theme={null}
{
"cancel": {
"status": "FAILURE",
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order not found"
}
},
"replacement": {
"status": "NOT_ATTEMPTED"
}
}
```
## Example: allow replacement after cancellation failure
Changing `mode` to `ALLOW_FAILURE` attempts the replacement even if cancellation fails. The replacement reports its own result. The response remains `409 Conflict` because the complete cancel-and-replace operation did not succeed:
```json theme={null}
{
"cancel": {
"status": "FAILURE",
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order not found"
}
},
"replacement": {
"status": "FAILURE",
"error": {
"code": "400",
"message": "Insufficient balance"
}
}
}
```
## Validation and outcomes
Request-level authentication, authorization, validation, market, signature, maintenance-mode, and receive-window failures return their ordinary HTTP status without a cancel/replace result. Maintenance-mode and receive-window rejections use `425 Too Early`.
A successful cancellation does not guarantee replacement success. Insufficient balance or allowance can produce `cancel.status: "SUCCESS"` with `replacement.status: "FAILURE"` and an HTTP-status error code such as `"400"`; the endpoint returns `409 Conflict`.
A receive-window or trading-availability rejection can likewise produce a successful cancellation with `replacement.status: "FAILURE"` and `error.code: "425"`.
```json theme={null}
{
"cancel": {
"status": "SUCCESS",
"orderId": "6f52b6d2-6c9e-4a5c-8a4f-28ab4b7ff203"
},
"replacement": {
"status": "FAILURE",
"error": {
"code": "425",
"message": "order receive window expired: elapsed=2500ms, window=1500ms"
}
}
}
```
## Response semantics
`cancel.status` can be `SUCCESS`, `FAILURE`, or `UNKNOWN`. `replacement.status` can be `SUCCESS`, `FAILURE`, `UNKNOWN`, or `NOT_ATTEMPTED`.
| Outcome | HTTP status |
| - | - |
| Cancellation and replacement are both `SUCCESS` | `200 OK` |
| Cancellation fails under `STOP_ON_FAILURE`; replacement is `NOT_ATTEMPTED` | `409 Conflict` |
| Cancellation fails under `ALLOW_FAILURE`; replacement is attempted | `409 Conflict` |
| The service returns any other non-success top-level result | `409 Conflict` |
Operation error codes are strings, not a closed enum. A missing cancellation target uses `ORDER_NOT_FOUND`; request failures use their decimal HTTP status such as `400`, `404`, `409`, `425`, or `500`; other failures use `INTERNAL_ERROR`. `FAILURE` identifies a known failure and `UNKNOWN` an inconclusive outcome. Cancellation authorization failures remain request-level `401` or `403` responses.
`replacement.status: "SUCCESS"` means the placement was accepted. It does not guarantee on-chain settlement. A matched replacement remains top-level `SUCCESS` when `replacement.data.execution.settlementStatus` is `FAILED`; the settlement failure and `reason` remain nested under `execution`. There is no top-level `SETTLEMENT_FAILED` replacement status.
# Cancel and Replace Orders (Batch)
Source: https://docs.limitless.exchange/api-reference/trading/cancel-replace-batch
POST /orders/cancel-replace/batch
Submit multiple cancel-and-replace operations in one request. Each executed operation has the same non-atomic behavior as [`POST /orders/cancel-replace`](/api-reference/trading/cancel-replace), and one operation result does not stop later operations.
## Request
Send `operations` as an array of 1 to 4 cancel-and-replace operations. Each item contains `cancel`, `replacement`, `mode`, and optional `onBehalfOf` fields as described in the [single-operation reference](/api-reference/trading/cancel-replace#request).
Use HMAC authentication with the `trading` scope and sign the exact JSON body with the path `/orders/cancel-replace/batch`. Each signed replacement remains a normal EIP-712 order; delegated body `onBehalfOf` operations follow the same authorized-owner and delegated-signing rules. The `x-on-behalf-of` header is not supported.
A request-level validation, authorization, market, signature, maintenance-mode, or receive-window error rejects the batch without a `results` array; maintenance-mode and receive-window failures return `425 Too Early`. Duplicate cancellation targets and duplicate normalized replacement `clientOrderId` values also reject the entire request. Accepted operations then report independent cancellation and replacement outcomes.
The order values below illustrate the request shape. Generate each `order.signature` from its complete replacement payload instead of copying the placeholder signatures.
```json theme={null}
{
"operations": [
{
"cancel": {
"orderId": "6f52b6d2-6c9e-4a5c-8a4f-28ab4b7ff203"
},
"replacement": {
"ownerId": 12345,
"orderType": "GTC",
"marketSlug": "btc-100k",
"clientOrderId": "replacement-101",
"order": {
"salt": "1778155025318314496",
"maker": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"signer": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"taker": "0x0000000000000000000000000000000000000000",
"tokenId": "19633204485790857949828516737993423758628930235371629943999544859324645414627",
"makerAmount": 5000000,
"takerAmount": 10000000,
"nonce": 0,
"price": 0.5,
"feeRateBps": 0,
"side": 0,
"signature": "0x1234",
"signatureType": 0
}
},
"mode": "STOP_ON_FAILURE"
},
{
"cancel": {
"clientOrderId": "missing-order-102"
},
"replacement": {
"ownerId": 12345,
"orderType": "GTC",
"marketSlug": "btc-100k",
"clientOrderId": "replacement-102",
"order": {
"salt": "1778155025318314497",
"maker": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"signer": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"taker": "0x0000000000000000000000000000000000000000",
"tokenId": "19633204485790857949828516737993423758628930235371629943999544859324645414627",
"makerAmount": 5500000,
"takerAmount": 10000000,
"nonce": 0,
"price": 0.55,
"feeRateBps": 0,
"side": 0,
"signature": "0x5678",
"signatureType": 0
}
},
"mode": "ALLOW_FAILURE"
}
]
}
```
## Ordering and partial failures
Accepted operations execute sequentially and the response preserves request order: every result has the zero-based `index` of its input operation. Operations produce independent outcomes, so inspect both `cancel` and `replacement` for every index rather than treating the HTTP status as an item-level result. A failed operation does not stop later operations.
If all items have top-level `cancel.status: "SUCCESS"` and `replacement.status: "SUCCESS"`, the endpoint returns `200 OK`. If any item has another top-level status, it returns `207 Multi-Status` with every result, including successful items.
```json theme={null}
{
"results": [
{
"index": 0,
"cancel": {
"status": "SUCCESS",
"orderId": "6f52b6d2-6c9e-4a5c-8a4f-28ab4b7ff203"
},
"replacement": {
"status": "FAILURE",
"error": {
"code": "425",
"message": "order receive window expired: elapsed=2500ms, window=1500ms"
}
}
},
{
"index": 1,
"cancel": {
"status": "FAILURE",
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order not found"
}
},
"replacement": {
"status": "FAILURE",
"error": {
"code": "400",
"message": "Insufficient balance"
}
}
}
]
}
```
`STOP_ON_FAILURE` produces `NOT_ATTEMPTED` only for the affected operation. `ALLOW_FAILURE` still attempts that operation's replacement. Cancellation and replacement outcomes can also be `UNKNOWN` when the final result cannot be determined.
Insufficient balance or allowance can produce a successful cancellation with an item-level replacement `FAILURE` and an HTTP-status error code such as `"400"`. The batch continues and returns `207 Multi-Status` with all results.
A receive-window or trading-availability rejection can likewise produce a successful cancellation with an item-level replacement `FAILURE` and `error.code: "425"`.
Operation error codes are strings, not a closed enum. A missing cancellation target uses `ORDER_NOT_FOUND`; request failures use decimal HTTP status strings such as `400`, `404`, `409`, `425`, or `500`; other failures use `INTERNAL_ERROR`. `FAILURE` identifies a known failure and `UNKNOWN` an inconclusive outcome. Cancellation authorization failures remain request-level `401` or `403` responses.
Request-level errors use their ordinary HTTP status instead of `207 Multi-Status`. A `207` is returned when at least one operation has a non-success top-level result.
HTTP `200` versus `207` follows the top-level cancellation and placement statuses. An accepted replacement stays top-level `SUCCESS` even if its nested `execution.settlementStatus` is `FAILED`; the nested `reason` carries the settlement failure.
# 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, including the USDC approval to `venue.feeModule` on [new trading venues](/developers/venue-system#new-trading-venues)
Order creation can be temporarily restricted during maintenance. In `post_only`, only orders with `postOnly: true` are accepted. In `cancel_only` or `disabled`, new orders return `425 Too Early` with a trading-mode `code`. Check [Maintenance Mode](/developers/maintenance-mode) before sending orders.
## Authentication
Use a scoped API token with HMAC signing and the `trading` scope.
## Signed order rules
For client-signed orders, `signature` and `signatureType` are both required. They must either both be present or both be omitted. Omission is accepted only for [delegated server signing](#delegated-signing) with an HMAC token carrying `delegated_signing`.
`nonce` is required and must be the numeric value `0`. `expiration` may be omitted; omission is signed as zero. If supplied, it must represent zero, for example `"0"`. Non-zero expiration is not supported.
Amounts are integer raw units with 6 decimals. `makerAmount` must be at least `100` raw units; this is the API's only order-size floor and exists to reject dust. The web app applies its own, larger minimum order value before submitting, but that is a web-app rule and does not apply to orders placed through the API or the SDKs. For GTC and FAK orders, `price` is required and may have at most three decimal places, so prices run from `0.001` to `0.999`. The collateral equation must be exact. `price × contracts` must be an integer raw-unit amount, without rounding.
| Type | BUY amounts | SELL amounts | Price |
| - | - | - | - |
| `GTC` / `FAK` | `makerAmount = price × contracts`; `takerAmount = contracts` | `makerAmount = contracts`; `takerAmount = price × contracts` | Required |
| `FOK` | `makerAmount = maximum collateral to spend`; `takerAmount = 1` | `makerAmount = shares to sell`; `takerAmount = 1` | Must be omitted |
`postOnly` is supported only for GTC orders.
### Optional fields
| Field | Type | Description |
| - | - | - |
| `clientOrderId` | `string` | Optional uniqueness and deduplication key. The submitted value may contain at most 128 characters; it is then trimmed and must not be blank. Reuse returns `409 Conflict`; the API does not replay the earlier response. |
| `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`. |
| `timestamp` | `number` | Optional client-stamped Unix time in milliseconds for receive-window checks. Top-level request field; not part of the signed `order`. |
| `recvWindow` | `number` | Optional maximum accepted order age in milliseconds, up to `10000`. If omitted, the API default applies. Top-level request field; not part of the signed `order`. |
| `stpPolicy` | `string` | Self-trade prevention policy. Values: `cancel_maker`, `cancel_taker`, `cancel_both`. Default `cancel_maker`. Top-level request field; not part of the signed `order`. |
### Receive window
`POST /orders` supports optional receive-window controls for clients that want freshness checks on order submission. Send these fields at the top level of the request body, next to `order`, `orderType`, and `marketSlug`; never put them inside the EIP-712 signed `order` object.
```json theme={null}
{
"order": {
"...": "signed order fields"
},
"orderType": "GTC",
"marketSlug": "btc-100k",
"timestamp": 1779870000000,
"recvWindow": 1500
}
```
| Field | Description |
| - | - |
| `timestamp` | Client order creation time as Unix milliseconds. Send it with a nonzero effective `recvWindow` to enforce client-to-server freshness. If omitted while a window applies, the server uses its receive time. |
| `recvWindow` | Maximum accepted age, in milliseconds, between the effective timestamp and server receive time, up to `10000`. If omitted, the API default applies. A timestamp alone has no freshness effect when the effective window is zero. |
Keep trading hosts NTP-synced. If the timestamp is too far in the future or older than the receive window, the API returns `425 Too Early`. Do not retry the same signed payload after a receive-window `425`; build and sign a fresh order. If the `425` body has a trading-mode `code`, refresh maintenance status instead.
### Self-trade prevention
`POST /orders` can reject or cancel orders that would match against your own resting order on the same token.
```json theme={null}
{
"order": {
"...": "signed order fields"
},
"orderType": "GTC",
"marketSlug": "btc-100k",
"stpPolicy": "cancel_taker"
}
```
| Policy | Result |
| - | - |
| `cancel_maker` | Default. Cancel your conflicting resting order and continue with the incoming order. |
| `cancel_taker` | Reject the incoming order before it self-trades. |
| `cancel_both` | Cancel your conflicting resting order and reject the incoming order. |
With `cancel_maker`, matching continues against the remaining non-self liquidity, and limit orders still never execute past their limit price.
**Outcomes on the placement response:**
* A rejected taker (`cancel_taker` / `cancel_both`) returns `settlementStatus: "CANCELED"` with `reason: "STP_TAKER_REJECTED"`.
* A `cancel_maker` taker lists the cancelled resting order IDs in `stpMakerCancels` and keeps its normal status (`UNMATCHED` when nothing else fills it); no `reason` is set.
`stpPolicy` is a top-level request field. Do not include it inside the EIP-712 signed `order` object.
**SDK support.** Self-trade prevention is available in the TypeScript, Python, Rust, and Go SDKs. Pass the policy on order creation and read the outcome from the `execution` object on the response. See [TypeScript](/developers/sdk/typescript/orders#self-trade-prevention), [Python](/developers/sdk/python/orders#self-trade-prevention), [Rust](/developers/sdk/rust/orders#self-trade-prevention), and [Go](/developers/sdk/go/orders#self-trade-prevention).
**Where does `ownerId` come from?** It is the `id` field returned by [`GET /profiles/me`](/api-reference/portfolio/get-current-profile) or [`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), or recovered with [`GET /profiles/partner-accounts`](/api-reference/partner-accounts/list-partner-accounts).
**`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` with the message `"Profile ID does not match the order owner"`.
* **Delegated signing** (unsigned `order`, `delegated_signing` scope, server wallet sub-account): send the target profile ID as both `onBehalfOf` and `ownerId`. The server normalizes `ownerId` to that target 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).
* **`Signer does not match - you should use embedded address for smart wallet`**: your profile is in smart-wallet trading mode, which self-signed API orders can't satisfy. Switch to EOA mode. See [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
### Delegated signing
Partners place orders for a managed sub-account by setting body `onBehalfOf` to its profile ID. A client-signed order requires `trading`; the target must belong to the authenticated partner.
With both `trading` and `delegated_signing`, partners may instead omit both `signature` and `signatureType`. The request must still include the target profile ID as both `onBehalfOf` and `ownerId`. The server signs using a Privy server wallet linked to that profile, replaces `maker` and `signer` with that wallet address, and normalizes `ownerId` to the target. See [Authentication](/developers/authentication#delegated-signing).
### Fee rate
On fee-enabled markets (`metadata.fee: true`), a signed order's `feeRateBps` must equal the order owner's `effectiveFeeRateBps`. Read your own rate from [`GET /profiles/me`](/api-reference/portfolio/get-current-profile). A partner cannot read a sub-account's private profile with its own credentials; use the sub-account's rate when signing for it. On fee-disabled markets, sign with `feeRateBps: 0`. Unsigned delegated orders are signed by the server with the sub-account's effective rate.
On [new trading venues](/developers/venue-system#new-trading-venues) (`venue.feeModuleType: "COLLATERAL"`), sign with `feeRateBps: 0` on every order, whatever the market's `metadata.fee` flag or your profile's rate. Any other value is rejected with `FEE_RATE_MISMATCH`. Unsigned delegated orders on these venues are signed by the server with `0`.
### 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`, `FAILED`, `DELAYED`, or `CANCELED` |
| `reason` | `string` | Free-form terminal detail. For example, `STP_TAKER_REJECTED` accompanies an STP-cancelled taker; settlement or recovery failures can return other text with `FAILED`. |
| `eligibleAt` | `string` | ISO-8601 time the order is released to the matching engine. Present only when `settlementStatus` is `DELAYED` (taker delay). |
| `tradeEventId` | `string` | Trade event ID (present when matched) |
| `txHash` | `string` | On-chain transaction hash (present when mined) |
| `stpMakerCancels` | `string[]` | Resting order IDs cancelled by self-trade prevention (present when `cancel_maker` / `cancel_both` cancelled one or more of your resting orders) |
| `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`) |
**`DELAYED` (taker delay).** Some markets apply a short hold to marketable (taker) orders before the matching engine fills them. On such a market, a marketable order's response returns immediately with `settlementStatus: "DELAYED"`, `matched: false`, zero `totalsRaw`, and an `eligibleAt` timestamp. It does **not** block until the trade is mined. Track the outcome over [`subscribe_order_events`](/developers/websocket/order-events#subscribing-to-order-events) (provisional `MATCHED` → terminal `MINED` / `FAILED`), correlating by `order.id` and `tradeEventId`; `MATCHED` does not carry `clientOrderId`. A released FAK that fills partially gets one `MATCHED` and one terminal frame for the whole fill, plus an `OME` `EXECUTION` frame with `status: "PARTIALLY_FILLED"`. A FAK that matches nothing at release emits only an `OME` `EXECUTION` frame with `status: "KILLED"`, and `POST /orders/status/batch` reports it as `settlementStatus: "UNMATCHED"`. `postOnly` orders are never delayed.
Which orders are held: `FOK` and `FAK` orders are always held for the delay. A `GTC` order is submitted immediately and rests at once if it rests; only a GTC that would cross the book is held instead, returning the same `DELAYED` response, so a GTC is delayed only when it would have taken. `postOnly` orders are never held. The hold length is the market's `settings.takerDelayMs`.
During the hold the order is not on the book and emits no order or orderbook events; it first appears at `eligibleAt`. Cancellation is never delayed: cancelling a held order before `eligibleAt` removes it before it reaches the matching engine.
## Errors
| Status | Meaning |
| - | - |
| `400` | Structural validation, invalid signature/owner/token/amounts, insufficient balance or allowance, unavailable/resolved/expired market, delegated-signing failure, or matching-engine rejection. Validation failures use a `message` array; business failures usually use a string. Missing markets and profiles are normalized to `400` on this endpoint. |
| `401` | Authentication failed. |
| `403` | The HMAC token lacks `trading`, or `onBehalfOf` is not an authorized partner target. |
| `409` | `clientOrderId` or the signed order hash already exists or is being processed. |
| `425` | Receive-window rejection or a maintenance trading mode that blocks creation. Receive-window bodies contain a message; maintenance bodies contain a trading-mode `code`. |
| `500` | Unexpected order-creation failure. |
If the signed fee is incorrect, the API returns `400`:
```json theme={null}
{ "code": "FEE_RATE_MISMATCH", "expectedFeeRateBps": 150 }
```
Build and sign a new order using `expectedFeeRateBps` before resubmitting. Changing only the JSON does not update the EIP-712 signature.
On [new trading venues](/developers/venue-system#new-trading-venues), an order that is not `postOnly` (BUY or SELL) also needs a USDC approval to `venue.feeModule` from the signing wallet. Without it, the API returns `400`:
```
Insufficient collateral allowance for the order fee.
```
Approve USDC to `venue.feeModule` once, then resubmit.
# Market Maker Heartbeat
Source: https://docs.limitless.exchange/api-reference/trading/heartbeats
POST /heartbeats
A safety timer for your orders. Set a deadline, then keep extending it while your trading service runs. If updates stop and the deadline passes, Limitless starts canceling your open orders across all markets. This does not block new orders, so stop placing them if an update fails.
## Set or update a deadline
Send `cancelAt` as a Unix timestamp in **milliseconds**. It must be a whole number at least five seconds later than server time. Each successful request replaces your previous deadline. The timestamp below is an example; calculate a new one for each request.
```json theme={null}
{
"cancelAt": 1800000010000
}
```
Update the deadline early enough to allow for network delays and clock differences. If an update fails, **stop placing new orders** until an update succeeds. The API does not block new orders when the deadline passes.
## Turn off the timer
Send an empty JSON object to remove your deadline:
```json theme={null}
{}
```
This also succeeds if no deadline is set. Once the deadline passes and cancellation starts, updating or removing the deadline cannot stop it.
## Authentication and results
Authenticate as the profile whose orders you want to cover. If you use a scoped API token, it needs the `trading` scope. Sign the exact JSON body and `/heartbeats` path; see [HMAC request signing](/developers/authentication#hmac-request-signing). This endpoint does not accept `onBehalfOf`: a partner token cannot set a deadline for a sub-account.
* `200 OK`: deadline set, updated, or removed. No response data is returned.
* `400 Bad Request`: `cancelAt` is invalid or too close to server time.
* `401 Unauthorized`: authentication or profile is missing.
* `403 Forbidden`: the API token lacks the `trading` scope.
* `409 Conflict` with `errorCode: "cancellation_in_progress"`: the deadline passed and cancellation has started. Stop placing orders. You can set a new deadline after cancellation finishes.
Cancellation may take time, and some orders may remain open, especially orders submitted after the deadline. Do not assume every order was canceled. [Check your orders](/api-reference/trading/user-orders) in each market to see which are still open.
# Get Historical Prices
Source: https://docs.limitless.exchange/api-reference/trading/historical-price
GET /markets/{slug}/historical-price
Retrieve newest-first historical YES-price data for an exact market slug or all visible children of a group slug. Interval values are lookback presets, not custom date bounds.
Historical price data is **scoped to each market's lifespan**. Each market on
Limitless is a distinct instance with a fixed start and end time, so this
endpoint clips the selected lookback to market creation and the earlier of the
market deadline or the current time.
Short-lived markets (for example, hourly or 15-minute crypto markets) therefore
return only a small window of history, typically no longer than the market's
duration itself. To reconstruct a longer time series for a recurring product,
query across the multiple markets that made up that period (each with its own
slug).
## Slug and response variants
Slug resolution checks for an exact market first, including a standalone CLOB market, an AMM market, or one child of a multi-outcome group. An exact market returns one object with `title`, `marketStatus`, and `prices`.
If no exact market matches, a visible group slug returns an array containing one object per visible child. Each series reports the group's status as `marketStatus`; child order is not guaranteed. A hidden group is treated as missing, while an exact hidden market slug is still resolved.
Every price point is `{ price, timestamp }`. `price` is a JSON number and `timestamp` is a decimal Unix epoch-millisecond string, such as `"1736944200000"`. Points within each series are newest first. A resolved CLOB series with retained history starts with its final payout price, timestamped one second after the newest retained point.
## Lookback presets
`interval` selects a lookback preset, not merely the spacing between points. It defaults to `1d`. The API does not accept custom `from` or `to` bounds.
| Preset | Requested lookback | Normal live bucket | Bucket limit |
| - | -: | -: | -: |
| `5m` | 5 minutes | 1 minute | 5 |
| `1h` | 1 hour | 1 minute | 60 |
| `6h` | 6 hours | 5 minutes | 72 |
| `1d` | 1 day | 15 minutes | 96 |
| `1w` | 1 week | 30 minutes | 336 |
| `1m` | 30 days (one month) | 4 hours | 180 |
| `all` | Entire market lifespan | 12 hours | 1,000 |
For a young live market, the service may use a finer preset so the returned series fits the market's age. Resolved CLOB markets use the finest retained bucket available for their lifespan. A live CLOB series may add its latest event after applying the bucket limit, and a resolved CLOB series may add its final payout point. These choices do not change the requested lookback boundary.
## Empty data and freshness
* A CLOB market with no retained price events returns `prices: []`.
* AMM buckets without trades use a `0.5` fallback, so an AMM no-data response contains synthesized points rather than an empty series.
* A visible group with no visible children returns `[]`.
Successful live responses are cacheable for 15 seconds for standalone CLOB, 30 seconds for NegRisk children and groups, and 60 seconds for AMM. Resolved responses are cacheable for 12 hours with a 30-minute stale-while-revalidate window. These HTTP cache boundaries do not guarantee data freshness; no freshness bound is promised.
## Errors
| Status | Meaning |
| - | - |
| `404` | Neither an exact market nor a visible group has the slug. The response message is `No entity found`. |
| `500` | A historical-price or interval lookup failed unexpectedly. Unsupported interval values are not currently validated into a stable `400` contract. |
# Get Market Events
Source: https://docs.limitless.exchange/api-reference/trading/market-events
GET /markets/{slug}/events
Returns public, finalized MINED CLOB trades newest first. An exact market slug selects that market; if no market matches, an exact group slug combines all group markets. Stable-slug aliases are not resolved. No authentication is required.
Returns public, finalized CLOB trade activity. Each event represents a `MINED` trade; this endpoint does not report order placement, cancellation, or liquidity-change events. No authentication is required.
## Slug scope
The path accepts either an exact market slug or an exact market-group slug. Market lookup runs first. A market slug returns trades for that market; a group slug combines trades from all markets in the group. Stable-slug aliases are not resolved.
The feed is backed only by finalized CLOB trades. Resolving a market that has no such activity returns an empty page.
## Trade fields and units
Events are ordered newest first. `side` is the taker side and uses the numeric mapping `0 = BUY`, `1 = SELL`.
`matchedSize` is the taker's filled position-token amount in raw 6-decimal units. `makerAmount` and `takerAmount` currently both contain the trade's collateral cost in raw 6-decimal units. These three values are decimal strings; divide them by `1e6` for display amounts. `price` is the weighted average fill price as a JSON number.
When available, `profile` publicly attributes the taker by username, display name, profile-picture URL, social URL, and rank. The field is omitted when no public profile can be resolved.
## Pagination
`page` defaults to `1` and `limit` defaults to `100`. Both must be integers greater than or equal to `1`, and `limit` cannot exceed `100`.
A populated page returns `events`, `limit`, `page`, `totalPages`, and `totalRows`. The current runtime returns the reduced shape below whenever a page has no events, including an out-of-range page:
```json theme={null}
{
"events": [],
"totalPages": 0
}
```
The reduced empty-page shape intentionally omits `limit`, `page`, and `totalRows`.
## Freshness and caching
Successful responses use `Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=30`. Newly mined trades may therefore not appear immediately. A `404` response uses a 5-minute cache and a further 5-minute stale-while-revalidate window.
## Errors
| Status | Meaning |
| - | - |
| `400` | `page` or `limit` is invalid, or an unknown query field was supplied. |
| `404` | The slug matches neither an exact market nor an exact market group. |
| `500` | The trade activity lookup failed unexpectedly. |
# 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. Partner API tokens with `delegated_signing` scope may read a sub-account by sending the `x-on-behalf-of: ` header.
Look up orders by either `orderId` (internal) or `clientOrderId` (your identifier). Provide exactly one per item, not both.
## Authentication and delegation
Use HMAC authentication. Looking up your own orders does not require a specific token scope.
To look up a managed sub-account, send `x-on-behalf-of: `. The HMAC token must carry `delegated_signing`, and the target profile must belong to the authenticated partner.
## Validation behavior
The request accepts 1–50 items and preserves their input order in `results` through the `index` field.
* An item with both identifiers, neither identifier, or a blank `clientOrderId` returns an item-level `status: "invalid"`; the rest of the batch is processed.
* An unknown identifier, including an order owned by another profile, returns item-level `status: "not_found"`.
* Malformed UUIDs or field types, client IDs longer than 128 characters, unknown fields, malformed nesting, and batch-size violations reject the whole request with `400 Bad Request`.
For `status: "found"`, `data` directly contains `order`, optional `makerMatches`, and `execution`. There is no additional response envelope inside `data.order`.
Historical status reports `UNMATCHED`, `DELAYED`, `MATCHED`, `MINED`, `CONFIRMED`, `RETRYING`, or `FAILED`. It returns the latest known execution state; it does not include placement-time STP reasons, STP maker-cancel IDs, or a historical `CANCELED` state.
## Errors
| Status | Meaning |
| - | - |
| `400` | Whole-request validation failed, the delegated header is malformed, or the authenticated profile cannot be resolved. |
| `401` | Authentication failed. |
| `403` | Delegation lacks `delegated_signing` or the target is not owned by the partner. |
| `500` | Historical status lookup failed unexpectedly. |
# Get Orderbook
Source: https://docs.limitless.exchange/api-reference/trading/orderbook
GET /markets/{slug}/orderbook
Retrieve the current LIVE GTC YES-side order book for one exact active CLOB market slug. The separately produced last mined trade price may be null without failing the request.
For real-time orderbook updates, use the [WebSocket API](/developers/websocket/market-data) instead of polling this endpoint. Subscribe to `subscribe_market_prices` with `marketSlugs`. Subscribing also pushes this book once per slug as an initial `orderbookUpdate`, so you do not need this endpoint to seed local state.
**Current snapshot only, one market per request.** This endpoint returns the current LIVE GTC orderbook for one exact CLOB market slug. There is no batched form that accepts multiple slugs; to follow several markets at once use the WebSocket subscription above. It does not aggregate a group slug, and AMM markets do not have an orderbook. CLOB markets are available only while their status is `CREATED` or `FUNDED`.
There is no historical orderbook endpoint. For past activity on a market, combine three sources:
* [Get Historical Prices](/api-reference/trading/historical-price) for price series.
* [Get Feed Events](/api-reference/markets/feed-events) for trade events.
* [Get History](/api-reference/portfolio/history) for your own CLOB fills (filter `strategy` on `Limit Buy` / `Limit Sell` / `Market Buy` / `Market Sell`).
## Units
Each level's `size` is in raw 6-decimal position-token units: 1 share = 1,000,000, so `"size": 150000000` is 150 shares. Divide by `1e6` to get shares. `minSize` uses the same units. `price` is a plain decimal between 0 and 1 and is not scaled.
`minSize` and `maxSpread` are returned as decimal strings. The WebSocket [`orderbookUpdate`](/developers/websocket/market-data#orderbookupdate) frames, including the initial snapshot, use the same size units, but `minSize` and `maxSpread` can arrive there as JSON numbers, so accept either form.
## Snapshot and price freshness
`lastTradePrice` is a nullable YES-equivalent price and is not guaranteed to represent the same point in time as the order-book levels. No bound on response freshness is guaranteed.
`midpoint` uses the best displayed bid and ask. `adjustedMidpoint` excludes individual orders smaller than `minSize` before price-level aggregation and midpoint calculation. Every bid level has `side: "BUY"`; every ask level has `side: "SELL"`.
## Complementary token prices (deriving the NO book)
This endpoint returns a **single, YES-side book**. The `tokenId` in the response is the market's YES position ID, and `bids`/`asks` are quoted in YES-token terms. The two outcome tokens are complementary: a YES share and a NO share always redeem together for exactly \$1. Their prices therefore satisfy:
```
price(YES) + price(NO) = 1
```
The book you get back already merges **all** liquidity for the market: native NO orders are converted into their YES-side equivalent before aggregation, using the same identity:
```
NO bid @ P ≡ YES ask @ (1 - P) (someone bidding for NO is offering YES)
NO ask @ P ≡ YES bid @ (1 - P) (someone offering NO is bidding for YES)
```
So you never lose NO liquidity by reading the YES book. It's all there, expressed in YES prices.
### Deriving the NO orderbook
To quote or trade the **NO** token, mirror the returned YES book: flip bids ↔ asks and replace every price `p` with `1 - p` (sizes are unchanged).
| NO book | Derived from | Best level |
| - | - | - |
| **NO bids** (orders to buy NO) | YES **asks** | best NO bid = `1 - bestYesAsk` |
| **NO asks** (orders to sell NO) | YES **bids** | best NO ask = `1 - bestYesBid` |
The midpoint and spread carry over directly: `noMidpoint = 1 - yesMidpoint`, and the spread is identical.
```typescript TypeScript theme={null}
type Level = { price: number; size: number; side: "BUY" | "SELL" };
type Book = { bids: Level[]; asks: Level[] };
// Mirror the YES-side book into the NO-side book.
function deriveNoBook(yes: Book): Book {
const invert = (l: Level): Level => ({
price: 1 - l.price,
size: l.size,
side: l.side === "BUY" ? "SELL" : "BUY",
});
return {
bids: yes.asks.map(invert).sort((a, b) => b.price - a.price), // best (highest) first
asks: yes.bids.map(invert).sort((a, b) => a.price - b.price), // best (lowest) first
};
}
// To SELL NO, hit the best NO bid: 1 - bestYesAsk
// To BUY NO, lift the best NO ask: 1 - bestYesBid
```
```python Python theme={null}
def derive_no_book(yes: dict) -> dict:
"""Mirror the YES-side book into the NO-side book."""
invert = lambda l: {
"price": 1 - l["price"],
"size": l["size"],
"side": "SELL" if l["side"] == "BUY" else "BUY",
}
return {
"bids": sorted((invert(a) for a in yes["asks"]), key=lambda l: -l["price"]), # best first
"asks": sorted((invert(b) for b in yes["bids"]), key=lambda l: l["price"]), # best first
}
# To SELL NO, hit the best NO bid: 1 - best_yes_ask
# To BUY NO, lift the best NO ask: 1 - best_yes_bid
```
When you then place a NO order, sign it against the NO `tokenId` (`noPositionId` from [Get Market](/api-reference/markets/get-market)) at the derived price. The price inversion only affects how you *read* the book, not how the order is signed.
## Multi-outcome (NegRisk) markets
In a [NegRisk](/user-guide/negrisk-overview) multi-outcome market, **each outcome is its own market** with its own slug, its own YES/NO tokens, and its own orderbook. Fetch each outcome's book by its slug and derive that outcome's NO book with the same inversion above. There is no single cross-outcome book to invert.
What links the outcomes is a soft pricing constraint, not a shared book. Across the *N* outcomes, the YES prices tend toward summing to 1 (exactly one outcome resolves YES). All the NO contracts are linked for [share conversion](/user-guide/converting-shares). To assemble a full picture of a multi-outcome market, request the orderbook for each outcome slug and mirror each one independently.
## Errors
| Status | Meaning |
| - | - |
| `400` | The exact market is an AMM (`Market does not support orderbook (AMM market)`) or its CLOB status is not `CREATED` or `FUNDED` (`Market is not active`). |
| `404` | No exact market has the slug (`Market not found`). A group slug also produces this response. |
| `500` | The orderbook could not be read unexpectedly. |
# Get User Orders
Source: https://docs.limitless.exchange/api-reference/trading/user-orders
GET /markets/{slug}/user-orders
Returns a bare, newest-first array of the effective profile's orders for one exact market slug. Omitted statuses default to LIVE. Partner HMAC tokens with `delegated_signing` may select an owned sub-account with `x-on-behalf-of`.
Returns a bare JSON array of orders for the effective profile and one exact market slug. Group slugs and stable-slug aliases are not accepted. Results are ordered newest first.
This is the read for a market's cancelled and unfilled orders as well as live ones. Pass `statuses` for each of `LIVE`, `MATCHED`, `CANCELED`, `UNMATCHED` to get everything; omitted, it returns `LIVE` only. [Get History](/api-reference/portfolio/history) lists mined trades and never includes orders that did not fill. There is no cross-market equivalent; call this per market, or keep the `orderEvent` stream, which emits one frame per change.
## Authentication and delegation
Use HMAC authentication. Looking up your own orders does not require a specific token scope.
To read a managed sub-account, send `x-on-behalf-of: `. The HMAC token must carry `delegated_signing`, and the target profile must belong to the authenticated partner.
### Status filters
If `statuses` is omitted, the API returns live orders.
Supported status filters are `LIVE`, `MATCHED`, `CANCELED`, and `UNMATCHED`. Repeat the query parameter to request multiple statuses. Values are case-insensitive.
A taker rejected by self-trade prevention terminates as `CANCELED` (the placement response carries `execution.reason: "STP_TAKER_REJECTED"`); filter for it with `CANCELED`.
## Limit and response
When supplied, `limit` must be an integer from 1 through 200. If omitted, the result is unbounded. The endpoint has no cursor, page, total, or other pagination metadata.
The response is the array itself, not an `{ "orders": [...] }` envelope. Each item contains the internal order and owner/market/token identifiers, order type, status, side, maker and taker amounts, price, original and remaining size, and creation time. Numeric values are serialized as decimal strings. `clientOrderId` is present only when the order was placed with one.
An existing market with no matching orders returns `[]`.
## Errors
| Status | Meaning |
| - | - |
| `400` | A path/query value or the delegated header is invalid, or an unknown query field was supplied. |
| `401` | Authentication failed. |
| `403` | Delegation lacks `delegated_signing` or the target is not owned by the partner. |
| `404` | The exact market slug or effective profile was not found. |
| `500` | The user-order lookup failed unexpectedly. |
# Changelog
Source: https://docs.limitless.exchange/changelog
Notable changes to the Limitless Exchange platform, API, and SDKs
Welcome to the Limitless Changelog. Here you'll find important changes to the Limitless Exchange platform including API updates, SDK releases, and new features.
## Oct 14, 2026
### New markets launch on new trading venues
New markets now launch on new trading venues. On these venues the trading fee is paid in USDC from your wallet instead of being part of the signed order. Markets created before the switch stay on their original venue and work exactly as before.
* A market is on a new venue when `venue.feeModuleType` is `"COLLATERAL"` on `GET /markets/{slug}`. The venue then also has a `feeModule` address.
* From the wallet that signs your orders, approve USDC to `venue.feeModule`, once per fee module. Every order except `postOnly` needs it. Without it, orders are rejected with `Insufficient collateral allowance for the order fee.`
* Sign `feeRateBps` as `0` on new-venue orders. Any other value returns `FEE_RATE_MISMATCH`. Legacy venues keep the existing fee rate rules.
* The exchange and adapter contracts, your existing approvals and the EIP-712 domain are unchanged. Server wallets are approved for you.
* All four SDKs support the new venues from 1.2.0: `@limitless-exchange/sdk` on npm, `limitless-sdk` on PyPI, the Go SDK at `v1.2.0` and the `limitless-exchange-rust-sdk` crate. They sign the fee rate for you; the fee module approval is still yours to send.
**Documentation:** [Migrate to the new trading venues](/developers/migrate-to-new-trading-venues) | [Venue system](/developers/venue-system#new-trading-venues) | [Smart Contracts](/user-guide/smart-contracts)
## Oct 13, 2026
### Crypto dailies in packs
BTC, ETH, Solana, and XRP Up or Down Daily markets can now be pack legs alongside sports markets.
* `GET /parlay/markets` lists them with `kind: "price"`; sports entries carry `kind: "sport"`. Route on `kind` before drawing a card: a price entry is `isMatch: true` with `outcomeLabels: ["Up", "Down"]`, a `null` `leagueName`, and no kickoff.
* `tradeCutoffAt` is when a leg stops being buyable in packs; it is buyable while the current time is before it. A daily leg stops 60 minutes before it closes. Read the field rather than computing the cutoff yourself.
* `closesAt` is a price leg's close time, for display. It is `null` on sport legs.
* Pack legs from `GET /parlay/packs` carry `kind` and `tradeCutoffAt` as well. Additive fields only.
**Documentation:** [Packs](/user-guide/packs)
## Oct 5, 2026
### Positions report when they were opened
`GET /portfolio/positions` and the public `GET /portfolio/{account}/positions` include `openedAt`, an ISO timestamp for when each holding was opened.
* It appears per outcome at `clob[].positions.yes` / `.no` and `group[].markets[].positions.yes` / `.no`, and on each `amm[]` entry.
* `null` means the open time is unknown or the position is no longer held.
* AMM WebSocket position updates carry it as well. CLOB WebSocket items do not, so read it over REST for CLOB positions.
* Additive only. Balances, cost basis, average fill price, and PnL are unchanged.
**Documentation:** [Positions](/api-reference/portfolio/positions)
### Filter pack positions to the ones still held
`GET /parlay/{account}/positions` and the authenticated `GET /parlay/positions` accept `open=true`, which returns only the packs a user still holds: `ACTIVE`, or `WON` and not yet claimed.
* Without `open`, both endpoints behave exactly as before.
* `open` cannot be combined with `status`; that returns `400`, as does a non-boolean value such as `open=yes`.
**Documentation:** [Pack positions](/api-reference/public-portfolio/pack-positions)
## Oct 2, 2026
### Orderbook sizes are raw 6-decimal units
This is a documentation correction. API behavior has not changed. Several pages described orderbook level sizes as shares. They have always been raw 6-decimal units.
* `bids[].size` and `asks[].size` are raw 6-decimal units (1 share = 1,000,000) on `GET /markets/{slug}/orderbook`, on the initial `orderbookUpdate` after `subscribe_market_prices`, and on every later `orderbookUpdate`. Divide by `1e6` for shares. `price` is not scaled.
* `minSize` uses the same units. REST returns `minSize` and `maxSpread` as decimal strings; WebSocket frames can send them as JSON numbers.
* The Python markets example now reads `OrderBook` fields as attributes. Dictionary-style access raised `TypeError`.
* The order-events and create-order pages now state that a released taker-delay order follows the same rule as any other order: what matches on arrival is one trade, with one `MATCHED` and one terminal frame, even on a partial fill across several makers.
**Documentation:** [Get Orderbook](/api-reference/trading/orderbook) | [Market data](/developers/websocket/market-data) | [Order events](/developers/websocket/order-events)
## Sep 30, 2026
### Trading feed entries show when a position was opened
`GET /feed/trading` entries carry three new fields.
* `openedAt` on `SOLD` and `RESOLVED` entries: when the position being closed was first opened.
* `profile.discordConnected`, a boolean, alongside the existing `xHandle`.
* `parlay.sourcePackId` on pack entries: the catalog pack a purchase came from, kept even if that pack was later removed. It is `null` for a custom pack.
* Additive fields only. No action required.
**Documentation:** [Trading feed](/api-reference/feed/trading-feed)
## Sep 25, 2026
### Market maker heartbeat
`POST /heartbeats` arms a profile-wide cancellation deadline, so a market maker that disconnects or wedges does not leave quotes resting on the book.
* Send `cancelAt` as a Unix timestamp in milliseconds, at least five seconds after server time. Each request replaces the previous deadline. An empty body `{}` disarms it.
* One deadline per profile, covering every market that profile trades. There is no per-market state and no heartbeat id.
* When the deadline passes, the orders you had resting at that moment are cancelled. Orders placed after it are not covered, which is why you should stop placing orders as soon as a renewal fails and resume only after one succeeds.
* API tokens need the `trading` scope; without it the call returns `403`.
* Once the deadline passes and cancellation starts, renewing or disarming returns `409`. Cancellation cannot be called off.
**Documentation:** [Market Maker Heartbeat](/api-reference/trading/heartbeats)
### Terminal order events are emitted again
Terminal `OME` `EXECUTION` frames for immediate-or-cancel orders stopped reaching `subscribe_order_events` on Aug 27 and are flowing again as of Sep 25. If you added polling to cover their absence, the frames are reliable again.
* A FAK always terminates and now emits its terminal frame, including a zero-fill FAK, which emits `status: "KILLED"` and no settlement frames. A FOK emits one only when it filled.
* Terminal frames are shaped differently from lifecycle frames: `eventId` is the string `terminal:`, the frame carries a `status` label, and `price` and `remainingSize` are strings, with `remainingSize` in raw 6-decimal units.
* `POST /orders/status/batch` reports the same zero-fill FAK as `settlementStatus: "UNMATCHED"`. REST and the stream use different vocabulary for one outcome.
* Frames are delivered only to the order owner.
**Documentation:** [Order events](/developers/websocket/order-events)
## Sep 23, 2026
### Sign orders with your effective fee rate
On fee-enabled markets the signed `feeRateBps` must now equal the order owner's current effective fee rate. A profile-specific cap can make that lower than the rank rate, so signing with `rank.feeRateBps` alone is no longer safe.
* Read `effectiveFeeRateBps` from your own private profile (`GET /profiles/{account}`, authenticated) and sign with it. On fee-disabled markets sign with `0`.
* A stale or incorrect rate is rejected with `400` and `{ "code": "FEE_RATE_MISMATCH", "expectedFeeRateBps": }`. Build a new order with that rate, sign it again, and resubmit.
* `feeRateBps` is part of the EIP-712 payload. Editing the submitted JSON without re-signing does not work.
* Partner credentials cannot read a sub-account's private profile, so for a signed sub-account order take the rate from the mismatch response. Unsigned delegated orders are signed server side with the correct rate.
* An `effectiveFeeRateBps` of `null` means the profile has no rank and cannot supply a fee rate for a fee-enabled order.
## Sep 21, 2026
### NFL games in packs
NFL moneyline games can now be legs in a pack, alongside football, tennis, cricket, and esports.
* `GET /parlay/markets` lists eligible NFL games. Each carries `leagueName: "NFL"`, team names in `outcomeLabels`, and team logos in `outcomeImageUrls`.
* NFL games carry an `nfl-league` property, the same way other sports carry their league property.
* `GET /markets/{slug}/packs` now resolves a standalone match slug, so an NFL, tennis, or cricket match page returns the packs holding a leg on that market instead of an empty list.
* Response shapes are unchanged. No action required.
**Documentation:** [Packs](/user-guide/packs)
## Sep 19, 2026
### Book price on pack legs
Pack and quote responses now expose each leg's order-book price alongside the price the leg is charged.
* `POST /parlay/quote`: every entry in `legs[]` carries `bookPrice` (number, 0-1), always present, the chosen side's price on the market's order book. `price` still reports what the leg is charged.
* `GET /parlay/packs`, `GET /parlay/packs/search`, and `GET /parlay/packs/{id}`: every entry in `legs[]` carries `bookPrice` too. It is `null` when no live or cached pricing source is available, for example on ineligible listings or settled markets.
* Additive response fields only. Pricing, payouts, `livePrice`, and `priceSnapshot` are unchanged. No action required.
**Documentation:** [Search packs](/api-reference/packs/search-packs)
## Sep 18, 2026
### Clearer failures when cancelling a delayed order
A cancellation that arrives while a taker-delayed order is already being released now says so explicitly, instead of failing without a usable reason.
* `POST /orders/cancel` returns `409 Conflict` with `reason: "ORDER_RELEASE_IN_PROGRESS"` and `retryable: true` when the release is already under way. Retry the cancel.
* Batch and client-order-id cancellations carry the same `reason` field: `ORDER_NOT_FOUND`, `ORDER_RELEASE_IN_PROGRESS`, or `UNKNOWN_ERROR`, with `retryable` set where it applies.
* Cancellation itself is never subject to the taker delay.
* FAK and FOK orders can only be cancelled while they wait out a taker delay. On markets without one they complete immediately and cannot be cancelled.
**Documentation:** [Cancel order](/api-reference/trading/cancel)
## Sep 17, 2026
### Python SDK 1.1.1 recovers subscriptions after a reconnect
`limitless-sdk` 1.1.1 on PyPI fixes the WebSocket client's reconnect path. Upgrade if you hold long-running connections: `pip install -U limitless-sdk`.
* After an automatic reconnect, the client now re-sends every active subscription itself. 1.1.0 did not restore subscriptions on its own.
* Every connection attempt, including automatic reconnects, is signed with a fresh HMAC timestamp. 1.1.0 re-sent the original handshake headers, so authenticated subscriptions such as `subscribe_positions` and `subscribe_order_events` failed with `Invalid HMAC authentication` once the 30-second window had passed.
* WebSocket connections are closed after 24 hours, so every long-running client goes through this path at least once a day.
**Documentation:** [Python SDK WebSocket](/developers/sdk/python/websocket) | [Connection limits](/developers/websocket/overview#connection)
## Sep 11, 2026
### Username search matches anywhere in a name
`GET /profiles/search` now finds usernames that contain your query, not only ones that start with it. Searching `are` returns `WhyAreYouGay` alongside names beginning with "are".
* Exact and prefix matches still rank first; mid-string matches fill the page when those come up short.
* Mid-string matching applies to queries with at least three consecutive letters or digits.
* No parameter or response changes. No action required.
### `include` now works on the stable-slug market endpoint
`GET /markets/stable/{slug}` accepts the same `include` query parameter as `GET /markets/{addressOrSlug}`, so a stable slug returns the same envelope as an instance slug.
* Pass `include=timelinePreview,nextMarket` to get the timeline preview and successor-round data on a stable slug.
* Without `include`, the response is the bare market DTO exactly as before.
* No action required for callers that do not send `include`.
**Documentation:** [Get market by stable slug](/api-reference/markets/resolve-stable-slug)
### Lifetime realized PnL on public profiles
`GET /portfolio/{account}/realized-pnl` accepts `timeframe=all`, returning a wallet's full realized-PnL history instead of only the last day, week, or month.
* `all` joins the existing `1d`, `1w`, and `1m` values; the default is unchanged.
* The response shape is the same as the other timeframes.
* No action required.
**Documentation:** [Realized PnL (Public)](/api-reference/public-portfolio/realized-pnl)
### Bug fix: top holders missing on funded custom markets
Top-holder reads could return `404` for funded user-generated custom markets after a split or merge, even though the market was live and had holders.
* Affected custom markets now return their holder list as expected.
* Ordinary hidden markets are still excluded from holder reads, as before.
* Display only; positions, funds, and settlement were never affected. No action required.
## Sep 7, 2026
### Points no longer accrue on user-generated markets
Trading on user-generated markets (the ones with a Community badge) is now excluded from the points program.
* Trade, realized PnL, and hold-to-resolution rewards skip user-generated markets.
* Volume on user-generated markets no longer counts toward daily-activity thresholds.
* All other markets are unaffected. No action required.
**Documentation:** [User-generated markets](/user-guide/user-generated-markets)
### Bug fix: pack picker listed outcomes packs cannot accept
The pack-eligible markets list could include outcomes priced outside the range pack creation accepts, so a pick made from the list was rejected when quoted or bought.
* `GET /parlay/markets` now hides markets whose current price falls outside the leg price range that `POST /parlay/create` enforces (10 to 90 cents by default).
* A grouped event is hidden entirely while any of its outcomes is out of range.
* No funds were affected. No action required.
**Documentation:** [Packs](/user-guide/packs)
## Sep 4, 2026
### Apply your X handle and picture to your profile
After connecting an X account, you can now copy its handle and profile picture onto your Limitless profile in one call.
* `POST /profiles/x/connection/social-data` (authenticated) takes `{ "username": true, "avatar": true }`, each field optional, and returns `usernameApplied`, `avatarApplied`, and `usernameSkippedReason`.
* If the handle is already taken as a Limitless username, only the username is skipped (`usernameSkippedReason: "taken"`). The picture still applies.
* Disconnecting X reverts your username to a generated one when it still matches the X handle. The profile picture stays and can be changed in profile settings like any other picture.
### AMM markets can be bookmarked
Market bookmarks now cover standalone AMM markets in addition to orderbook markets and groups.
* `PUT /market-bookmarks/amm/{targetId}` saves an AMM market; `amm` joins `clob`, `negrisk`, and `ladder` as accepted kinds.
* Bookmark lists hydrate AMM market data the same way as other kinds. No action required.
### Resolution time on trading feed entries
Market subjects on `GET /feed/trading` now report when the market resolved.
* `subject.resolvedAt` holds the best available resolution time and is `null` until the market resolves.
* Group subjects carry the field as `null`. Additive change, no action required.
**Documentation:** [Trading feed](/api-reference/feed/trading-feed)
### Bug fix: cricket over and ball counters disappearing at zero
Live cricket data dropped over and ball counters when their value was zero, so the counters vanished during the first over of an innings and right after an over ended.
* `currentOver` and `ballInOver` on live cricket entries now keep zero values. `currentOver` counts completed overs (0 during the first over), and `ballInOver` counts legal balls bowled in the over in progress (0 right after an over ends).
* Display only; scores and market resolution were unaffected. No action required.
## Sep 3, 2026
### Pack vig raised to 10 percent
Pack pricing now applies a 10 percent vig, up from 4 percent. Displayed multipliers stay net of vig, so the same legs quote a lower multiplier than before.
* `POST /parlay/quote` responses report `vigPercent` as `0.1`; the displayed `multiplier` is `rawMultiplier` with the vig taken out.
* The net multiplier ceiling is now 20x. The pre-vig cap moved to about 22.2x so the 20x net ceiling holds.
* Stake limits are unchanged. Packs already bought keep the vig they were sold at, along with their locked-in multiplier and payout.
**Documentation:** [Packs](/user-guide/packs)
### Market bookmarks
Save markets to a personal bookmark list and optionally share it from your profile.
* `PUT /market-bookmarks/{targetKind}/{targetId}` (authenticated) saves a market or group; kinds are `clob`, `negrisk`, and `ladder`. A profile holds up to 100 bookmarks; adding beyond that returns `409`.
* `GET /market-bookmarks/me` returns your bookmarks with hydrated market data and an `availability` of `active`, `resolved`, or `unavailable`. A bookmark on a recurring market resolves to the series' current market.
* `DELETE /market-bookmarks/{bookmarkId}` removes one bookmark; `DELETE /market-bookmarks` clears the list.
* Bookmarks are private by default. `PATCH /market-bookmarks/visibility` with `{ "isPublic": true }` opts in, after which `GET /market-bookmarks/public/{account}` serves the list without authentication; while private, that endpoint returns `404`.
### Live cricket scores
Live score data now covers cricket matches alongside football and tennis.
* `GET /sports/live` and the `live_sports_update` WebSocket event include cricket entries, discriminated by `sportType: "cricket"`.
* Each entry carries per-side `runs`, `wickets`, and a per-over run breakdown (`overRuns`), plus match `status` and `isFinished`.
* `battingTeam`, `currentOver`, and `ballInOver` are optional fields and can be absent when the data source does not report them.
* Additive change. No action required.
### Market lifecycle and prices on trading feed entries
Feed cards from `GET /feed/trading` now carry the subject market's lifecycle and current prices, and no longer carry a per-trader live position block.
* Market subjects gain `createdAt`, `startAt`, `deadline`, `prices` (`[yes, no]` as fractions of 1), `payoutNumerators`, and `winningOutcomeIndex`.
* `prices` holds a resolved market's final resolution or the latest stored midpoint for an open orderbook market, and is `null` whenever no reliable price exists.
* Group subjects carry the new keys as `null`; a card for a group entry uses its child market's own data.
* Feed entries no longer include the `positionNow` block. Value an entry's contracts with the subject's `prices` instead.
**Documentation:** [Trading feed](/api-reference/feed/trading-feed)
## Sep 2, 2026
### Trading activity feed
`GET /feed/trading` returns a feed of trading activity across the platform from the last 24 hours.
* `audience` selects `all` (the default), `featured`, or `following`. The following feed requires authentication and covers the profiles you follow.
* Entry types are `BOUGHT`, `SOLD`, `RESOLVED`, `PARLAY_PLACED`, and `PARLAY_RESOLVED`, covering orderbook and AMM trades, settled positions, and pack buys and settlements.
* Each entry carries the trader's public profile, the market or group it concerns, and the trader's live position state when one is open and a valuation is available.
* Pages hold up to 30 entries (`limit`, default 30) with an opaque `cursor` for continuation.
* The `all` and `featured` audiences are CDN-cached for up to 60 seconds, so public entries can trail the newest trades slightly.
**Documentation:** [Trading feed](/api-reference/feed/trading-feed)
### Display details on trading feed entries
Entries on `GET /feed/trading` carry everything a card needs to render without extra lookups.
* `profile.xHandle` holds the trader's connected X handle, or `null` when no X account is connected.
* `subject.imageUrl` holds the market or group artwork.
* Pack entries carry an `entry.parlay` block: the source pack name plus per-leg team crests, named outcome labels, league, teams, live score, and kickoff time.
* Additive fields. No action required.
### Cricket match-winner markets
Cricket joins the sports lineup with match-winner markets, starting with the Caribbean Premier League.
* Each match is a standalone binary market with the two teams as its named outcomes.
* Markets resolve to the officially declared winner. A tie decided by an on-field tiebreak such as a Super Over resolves to that winner; a tie with no tiebreak, or a permanently cancelled or abandoned match, resolves 50-50.
* Postponed or rescheduled matches stay open until the listed fixture completes.
* Cricket markets carry a `sport-type` property of `cricket` and a `cricket-league` property for filtering.
### Automatic resolution for markets mirroring external venues
Markets that replicate a listing on another prediction venue now resolve automatically once that venue settles the outcome.
* Resolution can land before the market's own deadline once the source venue settles.
* Only clear-cut outcomes resolve automatically. Split or ambiguous venue results are never auto-resolved.
* Sports and price-automated markets are unaffected and keep their existing resolution paths.
* No action required. Positions redeem as usual once the market resolves.
### Higher Gold and Diamond referral tier thresholds
The top two referral tiers now require more of your own trading volume.
* Gold now requires $3,000,000 of your own CLOB trading volume, up from $1,000,000. Diamond now requires $20,000,000, up from $10,000,000.
* Existing tier floors assigned by Limitless still apply as a minimum.
**Documentation:** [Referral program](/user-guide/referral-program)
### MCP server: price history tool and market volume
The MCP server at `https://api.limitless.exchange/mcp` gains a price history tool and now reports trading volume on market reads.
* New `get_market_price_history` tool returns newest-first historical YES midpoint observations for one exact market slug. Interval presets are `5m`, `1h`, `6h`, `1d` (default), `1w`, `1m`, and `all`; longer lookbacks automatically use coarser buckets. For a grouped market, pass a child market slug.
* Returned prices are midpoint observations, not executable prices. Use `get_orderbook` for current liquidity.
* `search_markets`, `list_markets`, `get_market`, and `get_market_group` child entries now include a `volume` object: the raw integer value, a `formatted` amount, the token `decimals`, and explicit `unit` (`contracts`) and `scope` (`cumulative`) fields.
* Additive changes. The new tool follows the same authenticated-connection requirement as every other tool.
**Documentation:** [MCP Server](/developers/mcp-server)
## Sep 1, 2026
### Username on referral leaderboard and referral list entries
Entries on `GET /referral/usdc/leaderboard` (including the pinned `me` block), `GET /referral/usdc/leaderboard/friends`, and `GET /referral/usdc/referrals` now include `username: string | null` alongside `displayName`.
* For most accounts `displayName` holds a wallet address. Prefer `username` when labeling users and fall back to `displayName` when `username` is `null`.
* Additive field. `displayName` is unchanged, so existing consumers keep working.
**Documentation:** [Referral leaderboard](/api-reference/referral/leaderboard), [My referrals](/api-reference/referral/my-referrals)
### Generated usernames for new profiles
New profiles now receive a readable generated username at signup instead of a `null` username.
* The generated name is a random Adjective+Adjective+Noun combination, for example `SwiftQuickHorse`. Limitless checks that the name is free before assigning it.
* Users can still set their own username. Create and update requests cap usernames at 44 characters; existing usernames longer than that remain valid.
* Expect `username` to be populated for newly created profiles. Profiles created before this change may still have `null` until Limitless backfills them.
## Aug 31, 2026
### Usernames on leaderboard, referral, and live-offer payloads
Payloads that previously carried only `displayName` (often a wallet address) now also include the user's `username`, so clients can show readable names.
* Points and volume leaderboard entries on `GET /leaderboard/:type` gain `username`.
* Referral payloads gain `username` per entry: `GET /referral/usdc/referrals`, `GET /referral/usdc/leaderboard`, and `GET /referral/usdc/leaderboard/friends`.
* Live-offer WebSocket frames gain `username` on the `creator` object. It is nullable for frames built before the profile carried a username.
* `displayName` is unchanged everywhere. Additive fields, no action required.
**Documentation:** [My Referrals](/api-reference/referral/my-referrals) | [Referral Leaderboard](/api-reference/referral/leaderboard)
### Bug fix: NO picks rejected as thin on liquid books
Pack quoting measured NO picks in the counterparty's dollars, so a NO leg on a genuinely liquid book could be rejected as "top-of-book too thin" while the book showed ample NO liquidity.
* `POST /parlay/quote` and pack leg pricing now measure each outcome in the dollars its buyer pays. Liquid NO legs that previously came back ineligible now quote.
* NO leg prices moved by single-digit basis points; no visible price jumps.
* When a leg is ineligible, `ineligibleReasons` now lists every failed check instead of only the first.
* No action required.
**Documentation:** [Packs](/user-guide/packs)
### Bug fix: pack quotes ignore liquidity far above the best price
Pack leg pricing swept the entire order book, so orders resting far above the best price could distort a quoted leg price. A leg with only a few dollars of real asks could quote at a badly wrong price.
* `POST /parlay/quote` and pack leg pricing now only count liquidity within 10 cents of the best usable price.
* Books that are genuinely thin near the best price now reject with an explicit thin-book reason in `ineligibleReasons` instead of quoting a distorted price.
* Liquidity resting more than 10 cents above the best price no longer counts toward depth, so a small number of previously quotable legs now reject.
* No action required.
**Documentation:** [Packs](/user-guide/packs)
### Bug fix: missing tennis player photos on rescheduled matches
Tennis moneyline markets created after a reschedule could permanently show placeholder avatars and missing player names on match cards, pack legs, and homepage portraits. The event detail page showed the players correctly.
* Affected markets now pick up player photos and names on the next refresh.
* Display only; markets traded and resolved normally throughout. No action required.
## Aug 26, 2026
### Public pack positions endpoint
`GET /parlay/{account}/positions` returns a wallet's pack (parlay) positions for public profile views, newest first. No authentication required.
* Only `ACTIVE`, `WON`, and `LOST` positions appear. An optional `status` query parameter narrows the list to one of those three; other values return `400`.
* Each position includes its stake, multiplier, claim state (`isClaimed`), full leg details, and the source pack it was bought from. The public payload deliberately excludes wallet, token, and transaction fields.
* Pagination uses `limit` (1-50, default 20) and `offset` (default 0, capped at 10,000). `totalRows` reports the full matching count while `totalPages` is capped at the last browsable page.
* Responses are CDN-cached with `max-age=5, s-maxage=15`, so public status and claim state can trail the database by roughly 30 seconds.
**Documentation:** [Get User Pack Positions (Public)](/api-reference/public-portfolio/pack-positions)
### Profile ids on PnL leaderboard entries
Entries on `GET /leaderboard/pnl`, `GET /leaderboard/pnl/lifetime`, and `GET /leaderboard/pnl/me` now include `profileId`.
* It is the same id the follow endpoints use. Clients can resolve follow state for leaderboard rows from `GET /follow/ids` in one call instead of one lookup per row.
* Additive field. No action required.
### ATP and WTA rankings on tennis event views
Tennis event-view responses from `GET /sports/events` now carry the players' singles rankings in the match header.
* `generalInfo.teams.home` and `generalInfo.teams.away` gain a nullable `rank` field: the player's current-season ATP or WTA singles ranking.
* `rank` is `null` when no current-season ranking exists; a stale prior-season ranking is never shown.
* Football and esports event views carry the field as `null`. Additive change, no action required.
### Bug fix: grouped market titles on biggest positions
Biggest-positions entries for markets inside a group previously showed only the child market title with no outcome label, so rows like a team-total market read ambiguously.
* `GET /leaderboard/pnl/unrealized/biggest-positions` now returns the group title as the market title, with the child market shown as the entry's `outcomeLabel`.
* Named-outcome markets now carry their proper outcome labels instead of `null`.
* Display only. No action required.
**Documentation:** [Biggest open positions](/api-reference/leaderboard/biggest-positions)
### Bug fix: AMM volume in market search
AMM markets returned by `GET /markets/search` were missing their volume figures, so search results showed AMM markets without activity numbers alongside fully populated orderbook markets.
* Search results now include volume figures for AMM markets again.
* No parameter or shape changes. No action required.
**Documentation:** [Search markets](/api-reference/markets/search)
### Bug fix: CS2 final-map round scores after settlement
On CS2 event views, a match that settled within minutes of its final map could show the decisive map with a missing round score. In the worst case, the gap lasted up to 7 days.
* `GET /sports/events` now keeps re-reading a finished CS2 map until its round score lands. Settled matches with an incomplete final-map score refresh within about an hour instead of staying stale for days.
* Display only; market settlement was never affected. No action required.
## Aug 25, 2026
### Category leaderboards and PnL-ranked biggest positions
Leaderboards can now be scoped to a category, and the biggest-positions board can rank by unrealized PnL instead of position size.
* `GET /leaderboard/pnl` accepts `scope=category` with a required `marketPageId` (the category's page id); category scope supports the `24h` window.
* `GET /leaderboard/pnl/unrealized/biggest-positions` gains a `metric` param: `position_size` (default, unchanged behavior) or `pnl`, plus an optional `marketPageId` to rank within one category when using `pnl`.
* Responses echo `metric` and `marketPageId`, entries gain `avgEntry`, and each entry's market carries an `imageUrl` for linking.
* Existing calls keep working unchanged.
**Documentation:** [Biggest open positions](/api-reference/leaderboard/biggest-positions)
### Higher parlay limits
Parlays now allow larger stakes, more legs, and higher multipliers.
* The maximum stake on `POST /parlay/quote` and `POST /parlay/create` is now $70, up from $50. The maker-funded flow keeps its separate \$100 ceiling.
* Parlays now accept 2 to 5 legs, up from a maximum of 3. Packs can also carry 4 or 5 legs.
* The maximum offered multiplier is now about 20x, up from about 14.4x. Baskets previously rejected for exceeding the multiplier cap can now quote.
* Clients hard-coding the old \$50, 3-leg, or multiplier limits should update.
**Documentation:** [Packs](/user-guide/packs)
### Match and prop markers on parlay outcomes
Parlay surfaces now say explicitly whether an outcome is the match itself or a prop, so clients no longer render esports map-winner props as duplicate match cards.
* Each outcome on `GET /parlay/markets` gains `isMatch`: `true` for the event or match market, `false` for a typed prop.
* Parlay position legs and pack legs gain a nullable `esportsPropType`: `map_winner`, `map_handicap`, or `total_maps`; `null` for match winners and non-esports markets.
* Additive fields. No action required.
**Documentation:** [Packs](/user-guide/packs)
### Daily top-trades digest in Telegram and Discord
Group chats subscribed to a category via `/topics` now receive one digest a day listing the biggest positions opened in that category on Limitless.
* Sent daily at 16:00 UTC, covering positions opened in the last 24 hours.
* Up to 10 positions per category, each at least \$25, with the market, side, amount, and payout multiplier.
* The digest names each trader by their public username, or a shortened address when they have none.
* Uses existing `/topics` subscriptions; no new commands.
### Bug fix: manage your orders from esports match pages without a market group
`GET /sports-markets/:slug/user-orders` and `DELETE /sports-markets/:slug/user-orders` now work for esports matches that have a standalone match-winner market instead of a NegRisk group (odd best-of and first-to formats). Both routes previously returned `404` for these slugs, and cancelling per market could leave prop orders live.
* Pass the standalone match-winner market slug. Your match-winner orders fill the `groups` section and your prop orders (map winner, total maps, handicaps) fill `props`; `offThePitch` stays empty for esports.
* `DELETE` cancels every open order you have across all markets of that match in one call, matching the football and tennis flow.
* Even best-of matches keep resolving through their group slug. This mirrors the Aug 20 tennis fix. Authentication required, as before.
### Market description limit raised to 10,000 characters
Market descriptions now accept up to 10,000 plain-text characters, up from 6,000.
## Aug 24, 2026
### Esports payload on the unified event view
`GET /sports/events` now serves the full esports match-page payload for all four videogames: `cs-go`, `dota-2`, `league-of-legends`, and `valorant`. Query with `esportType` (a videogame slug) plus the provider event id from the market's metadata, as introduced in the Aug 14 unified event view. The response echoes the provider's canonical videogame slug, not the request string.
* **Score** uses an esports shape: series score (maps won per side), a `games` array with per-game status, winner, length, and current game position. CS2 games also carry the map name and finished-map round scores, updated live each minute. League of Legends games carry the final kill score shortly after each game ends.
* **Streams** is a new top-level response section: broadcast embeds (for example Twitch and Kick) with `embedUrl`, `rawUrl`, language, and `main`/`official` flags. The main and official streams are listed first. The section is `null` for football and tennis.
* **General info** gains three fields: `serieName` (the tournament serie label for the page header), `tier` (`s` | `a` | `b` | `c` | `d`), and `eventKind` (`online` | `offline` | `online/offline`). `bestOf` is now populated for esports as the best-of map count. All three new fields are `null` for other sports.
* **Lineups** carry team rosters with per-title player roles.
* **Match stats** (League of Legends only) list per-team rows for each finished game: kills, gold, towers, dragons, and barons.
* Head-to-head history, per-team recent form, and team stats (series and map win rates) are populated for all four titles.
### Public accuracy endpoint
`GET /user-stats/accuracy/{account}` returns a wallet's accuracy score for public profile views: markets `won`, `lost`, `total` resolved, and `since` (the earliest resolved date tracked). No authentication required.
* The `200` body is nullable: expect `null` when no accuracy data is available for the address yet.
* The address is normalized server-side (EIP-55 or lowercase both work). A malformed address returns `400`.
* Successful responses are CDN-cached for up to 15 seconds; errors are not cached.
* The endpoint is feature-flagged and returns `404` while accuracy scoring is disabled.
**Documentation:** [Get User Accuracy (Public)](/api-reference/public-portfolio/accuracy)
### Bug fix: filter tab counts on market pages
Market page listings previously omitted the `counts` object when an active filter matched zero markets, so clients fell back to unscoped page totals next to an empty list.
* `GET /market-pages/:id/markets` and the by-path variant now always include `counts` while a filter is active, even when every value is `0`.
* An absent `counts` now reliably means no filter is active.
* No new fields or parameters. No action required.
**Documentation:** [List page markets](/api-reference/navigation/list-page-markets)
### Bug fix: selecting a whole filter group respects its configured options
Selecting a filter group's parent row (the `__all__` value) on market pages previously matched every market carrying that property, surfacing values outside the curated set for that group.
* `__all__` now expands only over the options configured for the group, in both the market list and the tab counters, so lists and counts agree.
* It falls back to matching any value only when the group has no configured option subset.
* Same parameters and response shape; only the returned set changes.
**Documentation:** [List page markets](/api-reference/navigation/list-page-markets)
### Bug fix: your own PnL chart refreshes when you view it
Viewing your own PnL chart previously never triggered a data catch-up, so your chart could lag behind recent trading activity.
* `GET /portfolio/:account/pnl-chart` now accepts optional authentication. When the caller and the requested address resolve to the same profile, the chart data refreshes on read.
* Owner responses are returned uncached.
* Anonymous and non-owner requests are unchanged, including caching. No action required.
**Documentation:** [PnL chart](/api-reference/public-portfolio/pnl-chart)
## Aug 22, 2026
### Bug fix: settlement failures and wrongful blocks for smart-contract wallets
Some matched trades that delivered outcome tokens to smart-contract wallets could fail to settle. A wallet that hit this failure could also be wrongly auto-blocked, which cancelled its resting orders and stopped it from trading.
* Trades of the affected shape now settle successfully.
* This settlement failure mode no longer triggers an automatic block; only genuine rejection by the receiving wallet does.
* A small number of trades and wallets were affected in production.
## Aug 21, 2026
### Following feed
`GET /feed/following` returns recent activity from the profiles you follow, covering the last 24 hours. Authentication required.
* Query params: `limit` (1-100, default 30) and `cursor` (opaque string from the previous page).
* `400` for a malformed cursor; `409` when your follow list changed since the cursor was issued (restart pagination); `503` when the feed is temporarily unavailable, never a silently empty response.
* Responses are per-viewer and not cached.
* The public `GET /feed` is unchanged.
### Browser alerts for a trader's trades
New endpoints under `/profile-notifications` let you receive browser push alerts when a chosen trader's trades finalize, covering both AMM and orderbook trades.
* `GET /profile-notifications/vapid-public-key` is public; registering or removing a browser subscription (`POST` / `DELETE /profile-notifications/browser-subscriptions`) requires authentication, with up to 5 browsers per profile.
* Manage per-trader subscriptions via `GET | PUT | DELETE /profile-notifications/users/:account`, with a `tradeAmountFilter` of `ALL` or `GTE_100_USD`.
* Alerts deep-link to the market and include the trader, market, side, and outcome. Trades on hidden markets are excluded.
* Alerts are sampled: subscribers receive a portion of a trader's finalized trades, not every one.
* Alert subscriptions are independent of the social follow feature.
### Tennis parlays
Tennis matchups and props are now first-class parlay legs on `GET /parlay/markets`.
* Tennis cards carry the tournament label; previously they showed no league.
* Player headshots now populate `outcomeImageUrls` on tennis matchup cards instead of a placeholder avatar.
* A match's moneyline and prop cards share one event grouping, so they cluster together instead of appearing unrelated.
* Tennis prop markets (total sets, total games) are offered alongside the match winner.
**Documentation:** [Packs](/user-guide/packs)
### Esports parlay league labels and pre-kickoff cutoff
Esports parlay cards now show the actual league (for example "EWC 2026") instead of the game title, and matches that have already started no longer appear as offerable legs.
* League labels resolve to the most specific league tag first, with the game discipline as last fallback.
### Bug fix: Telegram account-link confirmation in DMs
After you completed account linking on limitless.exchange from a Telegram group, the bot's DM conversation went silent. The confirmation posted only in the group chat, making the flow look stalled.
* The bot now sends you a private confirmation naming the group you linked for.
* Discord behavior is unchanged.
## Aug 20, 2026
### Tennis props and tournament matches on match-page routes
The match-page routes now work for tennis market slugs. Previously they only resolved via market group slugs, which tennis markets don't have, so tennis pages returned `404`.
* `GET /sports-markets/:slug/props` returns tennis ladders keyed by market type: `total_sets` and `total_games`, each sorted by threshold, including open and resolved markets.
* `GET /sports-markets/:slug/league-matches` returns other open match-winner markets from the same tournament, sorted by volume descending, with the tournament name as the label.
* Both routes are public with a 60-second shared cache. Football and esports behavior is unchanged.
### Manage your orders from tennis match pages
`GET /sports-markets/:slug/user-orders` and `DELETE /sports-markets/:slug/user-orders` now work for tennis market slugs; both previously returned `404` for tennis.
* The response groups your match-winner orders and totals-market orders for the whole match.
* `DELETE` cancels every open order you have across all markets of that tennis match in one call, matching the football flow.
* Authentication required, as before.
### Tennis round metadata
Tennis market metadata gains a `roundShort` field, and the tournament round now fills in reliably on match pages.
* `roundShort` uses a closed vocabulary: `1/64`, `1/32`, `1/16`, `1/8`, `1/4`, `1/2`, `final`; unrecognized round wording (qualifying, round-robin) stays `null`.
* The round is re-checked until the tournament annotates it, so `league.round` no longer stays `null` forever when the data arrives late.
### Parlay card metadata for outcomes, artwork, and classification
Parlay market cards, pack legs, and position legs now carry server-owned display metadata, so clients no longer derive labels and artwork locally.
* `outcomeLabels`: outcome button labels for named-outcome markets (home/away team names); `null` for plain Yes/No markets.
* `outcomeImageUrls`: team or side artwork as a home/away pair; `null` when unavailable.
* `properties`: league and discipline tags for classifying cards, plus `marketType` (`single` or `group`) and raw prop facts (`fixtureKey`, `propType`, `propLine`) for grouping fixture cards.
* Bug fix: confirming a claim on a parlay bought from a pack now returns the parlay with its `sourcePack` populated.
**Documentation:** [Packs](/user-guide/packs)
### Market description limit raised to 6,000 characters
Market descriptions now accept up to 6,000 plain-text characters (previously 4,000). The title limit is unchanged at 120 characters.
* Applies to all market, group, and ladder create and update paths.
* Description length is measured against visible text; HTML markup and entities are stripped before counting.
* No action required.
## Aug 18, 2026
### Public profile realized PnL endpoint
`GET /portfolio/{account}/realized-pnl` returns a wallet's realized PnL timeline for public profile views, with a category breakdown grouped by navigation section (e.g. sport, crypto) and a cumulative timeline series.
* Query with an optional `timeframe` of `1d`, `1w`, or `1m` (default `1w`). Unknown values return `400`.
* Response includes `windowStart`, a `current` snapshot, `categories` (sorted by realized PnL descending, with a synthetic `other` bucket for uncategorized markets), and `data` (per-point cumulative realized PnL).
* Figures are best-effort for the most recent minutes until the current aggregation window closes.
* The legacy `GET /portfolio/{account}/pnl-chart` is now marked deprecated. It still responds; migrate to `/realized-pnl` when convenient.
**Documentation:** [Realized PnL (Public)](/api-reference/public-portfolio/realized-pnl)
### Current season points on `/portfolio/points`
The `/portfolio/points` response gains a `currentSeason` field alongside the existing `currentMonth`. Points months are 28-day windows, so `currentMonth` reads as zero for most accounts immediately after a month rollover. Use `currentSeason` for a live running total across the season in progress.
* New field: `currentSeason` on `GET /portfolio/points`.
* `currentMonth` is unchanged and still returned.
* No action required.
**Documentation:** [Get Points Breakdown](/api-reference/portfolio/points)
### Larger market title and description limits
Market titles now accept up to 120 characters (previously 70) and descriptions up to 4,000 plain-text characters (previously 3,000). The limits are enforced consistently across markets, groups, and ladders.
* Applies to all market, group, and ladder create and update paths.
* Description length is measured against visible text; HTML markup and entities are stripped before counting.
* Duplicating a title at the new maximum no longer silently truncates it.
* No action required.
### Tennis fixtures on `GET /sports/events`
Tennis fixtures now return a full tennis-shaped payload from `GET /sports/events`. Football and esports responses are unchanged.
* `generalInfo` carries players, tournament, series, `bestOf`, and a strict `clay | grass | hard` surface parse with a fallback to the raw tournament name when unset.
* Top-level `score` uses a tennis shape: sets, games, points, and server.
* Head-to-head, per-player recent form, and per-player season averages (with latest-season surface win rates) are populated from the stats provider.
* Player photos are populated when available and omitted otherwise.
### Football postponed and never-played match copy
Football Result (1X2), BTTS, and total-goals markets now carry updated description copy that matches how these markets actually resolve.
* Postponed matches are waited out with no fixed deadline. An officially recognized result (abandoned, cancelled, awarded) is honored when returned.
* For a match that will never be played: team legs resolve NO, the Draw leg resolves YES (preserving the one-winner invariant), and the CLOB props resolve 50-50.
* Corners, cards, spreads, player props, and user-created templates keep their existing copy.
### NYSE-aware scheduling for equity and FX markets
Equity and FX market scheduling now follows the NYSE trading calendar instead of pure arithmetic, closing gaps where markets could appear outside trading sessions.
* Equity markets are only scheduled for whole NYSE sessions; intraday markets align to the top-of-hour grid anchored at midnight ET, and daily equity markets run close-to-close.
* Weekly equity markets resolve on the last session of the week, with Thursday used when the following Friday is an NYSE holiday.
* FX markets whose deadline falls on a UTC Saturday or Sunday are not created; a Sunday 23:00 UTC market survives because it resolves on Monday.
* Crypto and commodities keep the round-the-clock grid.
* No trades or funds were affected. No action required.
## Aug 17, 2026
### TWAP resolution coming to 5-minute and 15-minute crypto markets
Scheduled for **Wednesday, August 19**: short-cadence crypto up/down markets begin resolving on a **Chainlink time-weighted average price (TWAP)** instead of a single instantaneous price. Advance notice for builders: if you trade or quote these markets programmatically, review your assumptions about how the boundary price is determined.
* **What changes:** both the Price to Beat (captured at open) and the closing value come from the same Chainlink TWAP stream — an average over a fixed window — rather than one price tick. The closing TWAP greater than or equal to the Price to Beat resolves **Up**; otherwise **Down**.
* **Why:** an average over the window means a momentary spike at the market boundary does not decide the outcome on its own.
* **Detection:** TWAP markets carry `streamType: "twap"` and `twapWindowSeconds` on `metadata.chainlinkDataStream` in market responses; existing spot-resolved markets carry `streamType: "spot"`. Each market's description names the exact feed, window, and tolerance.
* **Unchanged:** market cadence, slugs, trading, order flow, and settlement. This is a resolution-source change only, applied per market. Read the market's description and `metadata.chainlinkDataStream` to see which rule applies.
**Documentation:** [Market resolution — Chainlink TWAP](/user-guide/market-resolution#chainlink-twap-resolution) | [Get Market](/api-reference/markets/get-market)
## Aug 14, 2026
### Unified event view for sports and esports match pages
`GET /sports/events` returns the match-page context for a fixture in one call: the event header, head-to-head history, and recent team form, for football, tennis, and esports fixtures.
* Query with `sportType` or `esportType` (a videogame slug such as `cs-go`), which are mutually exclusive, plus the provider event id carried on the market's metadata.
* One response replaces the separate lookups clients previously stitched together for match pages.
* No changes to market, orderbook, or trading endpoints.
## Aug 13, 2026
### Market properties on public responses
Public market responses now include the market's property assignments as `properties` — pairs of `propertyKeySlug` and `value`. The field appears on market details, group details (including child markets), and the feed and navigation listings that power cards and market lists.
* Also present on sports and esports props, specials, and tournament bracket responses that can initiate trades.
* The field is empty when a market has no properties, and responses degrade to empty properties rather than failing when enrichment is unavailable.
* No action required.
**Documentation:** [Get Market](/api-reference/markets/get-market)
## Aug 12, 2026
### All four SDKs: v1.1.0
The TypeScript, Python, Go, and Rust SDKs are released as v1.1.0. Non-breaking upgrades across the board; existing method signatures are unchanged.
* AMM trading: typed buy and sell plus allowance check/approve workflows, for direct wallets and partner-owned server wallets (the programmatic AMM surface shipped Aug 3).
* Cancel-replace: single and batch variants with typed request and response models, including the delegated partner path.
* Opt-in raw HTTP responses: every API-backed service method gains a variant returning the status, headers, and original body alongside the typed result.
* Plus per-SDK fixes and enhancements. See each SDK's changelog for the full list.
* Packages: `@limitless-exchange/sdk` 1.1.0 on npm, `limitless-sdk` 1.1.0 on PyPI, Go and Rust at v1.1.0.
**Documentation:** [SDK orders](/developers/sdk/typescript/orders), [AMM Trading guide](/developers/amm-trading), [Cancel and Replace Order](/api-reference/trading/cancel-replace)
## Aug 11, 2026
### Filter portfolio history by market
`GET /portfolio/history` now accepts an optional `market=` query parameter. When set, the response only includes activity for that market across every source: CLOB and AMM trades, splits/merges, NegRisk conversions, claims, and resolutions. Use it to verify a single market's activity without walking your full account history.
* An unknown `market` slug returns `404`.
* Cursors are market-scoped: a `nextCursor` issued for one `market` value is only valid when replayed with the same `market` value on the next page. Mixing a filtered cursor with a different or missing `market` produces incorrect pagination.
* Requests without `market` behave exactly as before.
**Documentation:** [Get History](/api-reference/portfolio/history)
## Aug 10, 2026
### Richer identifiers on `/portfolio/history` rows
CLOB rows in `GET /portfolio/history` now carry the identifiers needed to reconcile a history entry back to the trade and order that produced it.
* Response items may now include `tradeEventId`, `orderId`, and `makerMatchId` on CLOB-sourced rows.
* Existing fields (`operation`, `blockTimestamp`, `collateralAmount`, and the rest) are unchanged.
* All three new fields are optional. No action required.
**Documentation:** [Get History](/api-reference/portfolio/history)
### Bug fix: `/portfolio/history` pages past source limits
Long account histories now paginate cleanly across every data source that backs `/portfolio/history`. Previously a single `nextCursor` could stop advancing once one underlying source hit its internal window, leaving the reader with an incomplete history and no way to resume.
* Affects `GET /portfolio/history` for accounts with activity spanning long time ranges.
* Cursor walks now continue until all matching rows are returned.
* No trades, positions, or funds were affected. Re-page from the first page to backfill any gap that was previously unreachable.
**Documentation:** [Get History](/api-reference/portfolio/history)
## Aug 7, 2026
### Public trading history endpoint
A new public `GET /portfolio/{account}/history` endpoint returns the trading history of any wallet address with the same cursor-based pagination as the authenticated `/portfolio/history` route. Profile pages and third-party read integrations can now surface a wallet's history without an authenticated session.
* Path: `GET /portfolio/{account}/history`. Accepts `cursor` and `limit` query parameters; default `limit` is `20`.
* Address is normalized to EIP-55 checksum form. Invalid addresses return `400`; addresses with no associated Limitless profile return `404`.
* Response shape matches the authenticated `/portfolio/history` route, including cursors and the new CLOB identifier fields.
**Documentation:** [Get User History (Public)](/api-reference/public-portfolio/history)
### Clearer resolution rules on esports match-winner markets
Standalone CLOB match-winner descriptions on esports markets now spell out the fallback rules that were previously implicit. Traders can see exactly how mid-match forfeits, walkovers, and late cancellations resolve before entering a position.
* Applies to standalone CLOB `MATCH_WINNER` markets on esports fixtures. NegRisk 3-way markets and prop descriptions are unchanged.
* Descriptions now spell out: match not played or delayed beyond 7 days resolves 50-50; mid-match forfeit, disqualification, or walkover resolves to the team that wins; a forfeit before the start resolves 50-50.
* No API or payload changes. No action required.
## Aug 6, 2026
### Cancel and replace an order in one call
New `POST /orders/cancel-replace` and `POST /orders/cancel-replace/batch` endpoints let a trader cancel a resting order and submit its replacement in a single request. Useful for repricing quotes without maintaining two client-side steps.
* The two actions are non-atomic. Each of the cancellation and replacement returns its own outcome (`SUCCESS`, `FAILURE`, or `UNKNOWN`; the replacement can also be `NOT_ATTEMPTED`).
* Choose behavior with `mode`: `STOP_ON_FAILURE` skips the replacement when the cancel fails; `ALLOW_FAILURE` attempts the replacement regardless.
* Batch endpoint runs up to the standard batch size sequentially, returning per-operation results with their original `index`. Full success returns `200`; partial success returns `207` (batch) or `409` (single).
* Cancellation identifies the target by `orderId` or `clientOrderId`. Replacement uses the standard signed order body and must be EIP-712 signed by the profile that owns the cancelled order.
* Authentication matches the existing cancel route: HMAC (`apiToken`), Privy, and session are accepted. Partners can target a sub-account with `onBehalfOf` at the operation level (not inside `replacement`).
**Documentation:** [SDK orders](/developers/sdk/typescript/orders)
## Aug 5, 2026
### Embed Limitless markets on any site
You can now embed a live Limitless market widget on external sites. The builder at [embed.limitless.exchange](https://embed.limitless.exchange) turns any market or category link into a copy-paste snippet. Each snippet renders real-time odds, a price chart, and buy buttons in a sandboxed iframe, with layout, theme, and size controls.
* Three snippet formats: Default (styled iframe with SEO markup), Script (auto-sizing height), and Minimal (bare iframe for platforms like Notion).
* Category embeds rotate through the category's top live markets and replace resolved markets automatically; rotation speed, pool size, and sort are configurable.
* Crypto price markets show a live asset-price line with an Odds ↔ Price toggle; sports fixtures render as a live scoreboard.
* Every snippet carries a referral code. Use the code from your Limitless account to earn a share of referred users' trading fees (10–40% by tier, in USDC, paid daily) through the referral program.
* Embedding is free and requires no account; a Limitless account is needed only to collect referral rewards.
**Documentation:** [Embed Limitless Markets](/user-guide/embed-markets), [Referral Program](/user-guide/referral-program)
### Faster automated resolution for football score markets
Football markets that resolve from the final score are now eligible for automated resolution at kickoff plus 105 minutes, roughly regulation full time. This covers 1X2 group markets and BTTS, TOTAL\_GOALS, and SPREADS solo markets. Previously these markets waited until deadline minus 21 hours, about kickoff plus 3 hours. Stats-based props and team-to-advance markets still use the earlier gate.
* Applies to score-derived football markets only. Other sports and market types are unchanged.
* Resolution never fires before the fixture reaches FT, AET, or PEN. The earlier window only starts polling.
* No API or payload changes. No action required.
**Documentation:** [Market resolution](/user-guide/market-resolution)
### Bug fix: `subscribe_positions` ignores empty payloads
Emitting `subscribe_positions` over the WebSocket API with no `marketAddresses` and no `marketSlugs` now returns an empty stream without touching any existing subscription. Previously, the server accepted the empty payload, cancelled the client's prior position subscription, and then returned nothing, so updates stopped silently.
* Affects the WebSocket `subscribe_positions` event only.
* A previous non-empty subscription on the same connection continues to receive updates.
* Include at least one market address or slug to open a subscription. The same payload shape and replace-on-resubscribe behavior otherwise apply.
**Documentation:** [WebSocket events](/developers/websocket/overview)
### Bug fix: positions refresh immediately when an order rests
Placing a limit order that rests on the orderbook now refreshes your cached portfolio positions right away. A follow-up read reflects the new open exposure without waiting for a fill. Previously, positions could only refresh when an order matched, so a resting-only order left stale values until the next trade.
* Applies to `GET /portfolio/positions` and to the positions view in the app.
* No positions, orders, or funds were affected. No action required.
**Documentation:** [Positions](/api-reference/portfolio/positions), [Create Order](/api-reference/trading/create-order)
## Aug 4, 2026
### Paginated Unrealized PnL market leaderboard
The market-scoped Unrealized PnL leaderboard now supports page-based pagination, matching the realized-PnL leaderboard convention. Existing clients keep working unchanged: an omitted `page` is treated as `page=1` and the `limit` default remains `100`.
* `GET /leaderboard/pnl/unrealized/markets/{marketId}` accepts a new `page` query parameter (integer, `1..100`, defaults to `1`); `limit` is unchanged (`1..100`, defaults to `100`).
* Responses now include `page`, `totalRows`, and `totalPages`. `totalRows` reflects the committed Top-100 generation for that market.
* `rank` remains absolute across pages — with `limit=10`, page 2 starts at rank `11`.
* The `ETag` is page-scoped, so `If-None-Match` validators cannot cross pages.
* The `biggest-positions` route is unchanged.
**Documentation:** [Unrealized PnL leaderboard by market](/api-reference/leaderboard/unrealized-pnl-market)
### More packs stay quotable in thin orderbooks
Pack quoting now applies a consistent depth requirement across every leg side, so more packs price and stay available when orderbook depth is thinner.
* The quote response contract is unchanged (`eligible`, `ineligibleReasons`, `multiplier`, `potentialPayout`, `collateralRequired`, `legs`); only which quotes come back as `eligible: true` changes.
* No action required for existing integrations.
**Documentation:** [Packs](/user-guide/packs)
### Bug fix: Unrealized PnL leaderboard omits entries without a valuation
Live Unrealized PnL leaderboard responses now skip entries whose mark is temporarily unavailable, so ranking pages return only rows with a computable Unrealized PnL. Previously, a missing mark could leave a row in the response with an unusable valuation.
* Affects `GET /leaderboard/pnl/unrealized/markets/{marketId}` and `GET /leaderboard/pnl/unrealized/biggest-positions`.
* Rows with a valid `mark`, `marketValue`, and `unrealizedPnl` are returned as before; rows without an available valuation are excluded from that snapshot and reappear once a mark is available.
* Ranks remain absolute across the returned page. No positions, orders, or funds were affected. No action required.
**Documentation:** [Unrealized PnL leaderboard by market](/api-reference/leaderboard/unrealized-pnl-market), [biggest open positions](/api-reference/leaderboard/biggest-positions)
## Aug 3, 2026
### Programmatic AMM trading for server wallets
Partners can now execute AMM buy and sell trades — and manage the underlying token approvals — through the Programmatic API using a Privy server-managed wallet. This closes the gap between CLOB (already supported) and AMM markets for delegated trading flows.
* New endpoints: `POST /amm/buy`, `POST /amm/sell`, `POST /amm/allowances/check`, `POST /amm/allowances/approve`.
* Requires an HMAC-signed API token with both the `trading` and `delegated_signing` scopes. Legacy API keys are not accepted.
* Partners target an owned sub-account with `onBehalfOf` in the request body, matching the existing portfolio write endpoints.
* Only AMM markets in `FUNDED` state with a future deadline are tradable. Amounts are positive integer strings in the market's collateral base units; buy takes `collateralAmount`, sell takes `collateralReturnAmount`. Outcome `0` is YES, `1` is NO.
* Slippage defaults to 100 bps and is capped at 1000 bps. Execution bounds are computed from a fresh on-chain quote at submission time.
* Allowances are explicit and one-time per wallet/market/side: buy needs a collateral ERC-20 approval to the market, sell needs a CTF `setApprovalForAll`. `approve` returns `200` when already ready or `202` immediately after submission; poll `check` until confirmed. Buy and sell do not auto-approve.
* Each trade requires a partner `idempotencyKey` bound to the trade fingerprint for 24 hours. Replays of the same key return the original submission; mismatched reuse returns `409`.
* Successful submissions return Privy `transactionId`, `userOperationHash`, and/or `txHash` (sponsored user operations may not have a synchronous `txHash`).
* Rate limit: 10 requests per 10 seconds per caller.
**Documentation:** [AMM Trading guide](/developers/amm-trading), [Programmatic API](/developers/programmatic-api), [Derive API token](/api-reference/api-tokens/derive-token)
### Bug fix: manual LP reward distribution records paid rewards correctly
Manual LP reward distribution now records paid rewards and the linked on-chain payout after the transaction confirms. Previously, the API returned an error and marked the payout as failed even after the Base transaction had confirmed, leaving reward rows without their payout link.
* Affects the manual LP reward distribution endpoint only.
* After a payout transaction confirms, the associated reward rows are now marked paid and linked to the payout in the same request.
* If the payout update does not affect exactly one row, distribution stops before rewards are marked paid and before any success event is published, so state stays consistent.
* No funds were lost. On-chain payouts that already confirmed prior to this fix are reconciled separately.
* No action required from traders or integrators.
**Documentation:** [LP Rewards](/user-guide/lp-rewards)
### Bug fix: LP reward distribution reliability restored
Daily LP reward distribution runs are reliable again. The scheduled distribution job had started failing on backlogs of unpaid rewards because a single aggregation query could time out against the database. The job now processes the same rewards across smaller bounded pages, so distributions complete within the request timeout even with large backlogs.
* Affects the scheduled LP reward payout job; no API contract, authentication, or payout-math changes.
* No funds were lost — unpaid rewards remained queued and are paid out on the next successful run.
* No action required from traders or integrators.
**Documentation:** [LP Rewards](/user-guide/lp-rewards)
## Aug 1, 2026
### Bug fix: Unrealized PnL leaderboard degrades gracefully under backpressure
The live Unrealized PnL leaderboard now isolates backpressure incidents to the leaderboard itself, so other endpoints stay unaffected. Clients continue to receive a response for `GET /leaderboard/pnl/unrealized/markets/{marketId}` and `GET /leaderboard/pnl/unrealized/biggest-positions`; the snapshot reports its recovery state until fresh data is available.
* Under pressure, incoming mark and position updates are dropped rather than written as partial state, so ranking data can go stale but never corrupt.
* Snapshots that haven't fully rebuilt continue to return `state: DEGRADED` with a `staleReason` (or `BUILDING` when there is no snapshot yet) instead of being promoted to `READY`. Existing clients that already branch on `state` need no changes.
* No positions, orders, or funds were affected. No action required.
**Documentation:** [Unrealized PnL leaderboard by market](/api-reference/leaderboard/unrealized-pnl-market), [biggest open positions](/api-reference/leaderboard/biggest-positions)
## Jul 31, 2026
### Live unrealized PnL leaderboard
A new live leaderboard ranks traders by unrealized PnL across their open positions, updating in real time as marks move.
* A dedicated live projection, fed by finalized CLOB, AMM, split/merge, conversion, and resolution events, maintains open-position state and leaderboard generations
* The API exposes new leaderboard read routes (market-scoped rankings and biggest positions), and the WebSocket layer sends lightweight invalidation hints so clients know when to refetch
### Live markets on a whole team
User-created live markets can now target a **team**, not only the match or a single named player — for example "any Chelsea player to score, get a yellow, or get a red in the next 5, 10, or 15 minutes." Another user takes the other side, and the market settles from the same match events as existing live windows.
* Market types now form a grid: four event kinds (goal, card, yellow card, red card) across three scopes (match, team, player) — twelve types in total
* Own goals are excluded on every scope
### Live windows settle after full time
Live-window markets now settle once the match is **over**, not when the window's half ends. A market like "goal between 24' and 28'" previously settled at half time; it now settles roughly an hour after the final whistle.
* Why: the data provider keeps amending match events after the whistle (e.g. a late VAR reversal). Settling at the half could pay out on events that later changed; waiting for full time plus a delay means settlement reads the final record
* Suspended or interrupted matches retry until play resumes; matches that end with no result (postponed, cancelled, abandoned, awarded) settle on the events recorded in-window, after the same post-match delay
### Esports: `videogameSlug` on every market response
Esports market responses now expose the videogame slug (`cs-go`, `league-of-legends`, …) under a single key, `videogameSlug`, in the public market metadata — for both match groups and solo props. Previously groups carried the value only as `esportTitle` while solo props used `videogameSlug`, forcing consumers to branch on market kind. Additive change; `esportTitle` is untouched.
### Bug fix: fee-module wallet excluded from holder lists
Market holder lists and top-holder queries no longer include the venue's fee-module contract wallet, which could previously appear as a large "holder" on fee-bearing markets. Display-only fix; no positions or funds were affected.
## Jul 30, 2026
### Tennis: total sets and total games markets
Tennis matches now get automatically created solo CLOB prop markets, following the same pattern as the football props:
* **Total Sets** — over/under (e.g. 2.5)
* **Total Games** — over/under lines (e.g. 21.5 / 22.5 / 23.5)
* Created automatically per match and resolved from official settlement data; carry `metadata.externalSlug` where a matching Polymarket market exists
### Bug fix: league-scoped tab counts on sport pages
On sport pages, the Matches/Props tab counts now reflect the league filter the visitor picked instead of whole-sport totals (e.g. filtering to one tennis league previously kept showing all-tennis counts).
### X account connections now brokered through the platform
The X-account connect flow now runs on the platform's own OAuth handshake instead of being brokered through Privy. This removes a case where users who had signed in with X on any other Limitless account could not connect that same X account to a new profile.
* Connecting an X account that is already active on another profile now transfers it. The previous profile is marked disconnected in the same operation, and the new profile owns the connection.
* One active X connection per profile and one active profile per X account are still enforced. Simultaneous connect attempts return `409`; the losing caller is asked to retry.
* The connect and disconnect endpoints require a Privy or session-authenticated user. Machine API tokens cannot manage the X identity.
* Disconnect immediately clears stored credentials and best-effort revokes the refresh token at X; the connection row is retained so the account can be reconnected later.
## Jul 29, 2026
### Connect an X account to your Limitless profile
Profiles can now be linked to an X account. Once connected, the handle is exposed on the public profile connection resource so anyone can jump from a Limitless profile to the linked X profile.
* New endpoints on `/profiles`: `POST /x/connection/authorize` returns an `x.com/i/oauth2/authorize` URL to redirect the user to, `POST /x/connection` exchanges the returned `{ code, state }` for a stored connection, and `DELETE /x/connection` disconnects.
* Public read: `GET /profiles/:account/x-connection` returns `{ connected, xHandle }` for any caller, including logged-out visitors, and is CDN-cached.
* Owner-only fresh read: `GET /profiles/:account/x-connection/handle` returns the handle for the profile owner without CDN caching, so a user sees their own connect/disconnect immediately.
* Rate limits: 5 mutations per minute per profile, 120 public reads per minute per IP, 60 owner reads per minute per account.
### Partner-restricted market pages
You can now gate a market page to a specific partner and their sub-accounts, so a partner can preview hidden markets on a dedicated page before announcing them publicly. Public pages are unchanged.
* New `restrictedToProfileIds` allowlist on market page create and update. `null` or empty means the page stays public.
* A caller gets access when their own profile ID or `partnerId` appears in the allowlist. Listing the partner root admits existing and future sub-accounts automatically.
* Works for any authenticated caller, including HMAC API tokens and app sessions.
* Unauthorized or anonymous callers get a `404`, the same response as a nonexistent page.
* Restricted pages must be active, `navPlacement=hidden`, root-level, and childless, and list standalone hidden markets only (no groups, no `includeNextMarket=true`).
* Authorized responses are served with `private, no-store`; public pages keep their existing CDN caching.
**Documentation:** [Get Market Page by Path](/api-reference/navigation/get-page-by-path), [List Page Markets](/api-reference/navigation/list-page-markets)
### Direct-link pages for hidden partner markets
The market detail and historical-prices endpoints now return hidden markets when you request them by exact slug. Partners can share deep links to markets that aren't listed in public feeds (private launches, embedded surfaces, previews) without first flipping them to public.
* Applies to both AMM markets and CLOB markets fetched by slug.
* Slugs must match exactly; hidden markets still don't appear in search, category, or navigation feeds.
* Group-level pages continue to hide their non-visible child markets.
* This is separate from partner-restricted pages above: fetching a hidden market by slug works for any caller who has the exact slug. A restricted page, by contrast, gates a whole curated page to an allowlist.
**Documentation:** [Get market details](/api-reference/markets/get-market)
### AMM market stats degrade gracefully during indexer downtime
Market detail and page listings no longer fail when upstream AMM indexing data is temporarily unavailable. Affected AMM markets return zero volume, liquidity, and open interest instead of an error, and real values come back automatically once indexing recovers.
* Applies to both direct market lookups and page-market listings.
* Failed enrichment reads are not cached, so recovery propagates as soon as the response cache expires.
### Split market resolution now supports manual AMM markets
Binary split resolution now works for manual standalone binary AMM markets, in addition to CLOB binary markets. A split resolves the market at a payout ratio (for example 70/30) instead of winner-take-all, and both sides remain redeemable at the split ratio in a single call. Existing integrations keep working.
* On a `RESOLVED` manual AMM market, a split is signaled the same way as on CLOB: `winningOutcomeIndex: null` with the ratio in `payoutNumerators`.
* Detect a split by reading `payoutNumerators` on resolved markets and redeem as usual — the redeem endpoint handles both sides at the ratio.
* Oracle-automated AMM markets continue to resolve winner-take-all and reject split payloads.
* Unrealized and realized PnL account for the split ratio automatically on both sides.
**Documentation:** [Get Market](/api-reference/markets/get-market) | [Positions](/api-reference/portfolio/positions) | [Redeem](/api-reference/portfolio/redeem)
### More resilient real-time streams during upstream latency
Ordered real-time publishes now ride out transient upstream latency instead of failing fast. Short latency blips on the messaging backbone briefly pause per-market ordering rather than dropping the handoff and forcing WebSocket clients into a reconnect/replay loop. Order and orderbook events keep their exactly-once, per-market ordering guarantees.
* No API or payload changes — the fix improves reliability under load only.
* Per-message send deadlines are unchanged; only the retry window around a stalled backbone is extended.
* Unordered publishes are unaffected.
**Documentation:** [WebSocket Events](/developers/websocket/overview)
### Faster live order and orderbook updates under load
The real-time pipeline that drives order events and orderbook updates now scales further before it queues. Under heavy trading, live-order and orderbook WebSocket messages arrive with less delay, and per-market updates no longer stall behind a single busy market.
* Order and orderbook events flow through the same WebSocket channels — no API or payload changes.
* Bursts on one market no longer slow updates for other markets: each market has its own update lane.
* Order events keep exactly-once delivery.
**Documentation:** [WebSocket Events](/developers/websocket/overview)
### Bug fix: per-parlay PnL attribution on shared positions
When two or more parlays in the same pack shared a position and one redeemed first, the remaining parlays could be recorded at \$0, under-reporting their charted PnL. Each parlay's share is now credited only after the shared redemption is confirmed on-chain; if confirmation hasn't landed yet, attribution is deferred and retried automatically instead of being finalized too early.
* Display-level accounting only: pack-level payouts were always correct, and no funds were affected.
* Extends the earlier fix that covered NegRisk parlays to CLOB parlays, which now behave the same way.
* No API or client changes, and no action required.
## Jul 28, 2026
### Esports markets: Polymarket slug auto-sync for MM and arb
Esports markets now carry the matching Polymarket market slug on `metadata.externalSlug`, and Limitless populates it automatically. Previously the field was populated for football and tennis markets only.
* Applies to esports match-winner groups (both team markets share the event slug) and the new solo prop markets — map winner, total maps, and map handicap.
* Handicap markets are only synced when the favored side can be unambiguously aligned with Polymarket's team ordering; anything ambiguous is skipped rather than guessed.
* Live in v1 for CS2. Other titles are skipped until their Polymarket slug convention is verified against a live event.
* Sync runs hourly and only writes when the corresponding Polymarket event actually lists the expected market — no unverified slugs are emitted.
* Cross-venue market makers and arb integrations can read `metadata.externalSlug` on esports markets to resolve the Polymarket counterpart without extra lookups.
### Esports slug sync: Dota 2, League of Legends, and Valorant match moneylines
The Polymarket `externalSlug` sync for esports now covers Dota 2, League of Legends, and Valorant match moneylines, in addition to CS. Cross-venue market makers and arb integrations can read `metadata.externalSlug` on these markets to resolve the Polymarket counterpart without extra lookups.
* Applies to match moneylines only. Prop-market sync (map winner, total maps, map handicap) stays CS-only.
* Dota 3-way moneylines resolve to their per-team Polymarket sub-markets; the CS/LoL/Valorant binary path is unchanged.
* The matcher now handles Polymarket's trailing instance digits (`100t1`, `g21`, `kru1`) and diacritics (e.g. `KRÜ`) when aligning team abbreviations.
* Ambiguous matches are skipped rather than guessed. Existence-gated: no slug is written unless the Polymarket event actually lists the expected market.
### Esports solo prop markets: map winner, total maps, map handicap
Esports match pages now surface standalone binary prop markets alongside the main match-winner group. Limitless derives these props from the match structure rather than mirroring third-party betting feeds, so coverage is uniform across every supported league. Live in v1 for CS2.
* **Map winner** — one market per guaranteed-played map in odd-format series (a best-of-3 spawns maps 1 and 2), so every market is decidable with no void path.
* **Total maps** — over/under lines strictly inside the achievable series total (e.g. 2.5 for a best-of-3).
* **Map handicap** — margin lines strictly inside the achievable margin range (e.g. 1.5 for a best-of-3).
* Props trade through the same CLOB stack as the match-winner group, with a 24-hour trading window from match start. Live updates flow over the existing WebSocket streams.
* Settlement uses per-game results from the match data provider, and markets only resolve once the provider marks the match fully settled — interim finished states are not trusted.
**Documentation:** [WebSocket Events](/developers/websocket/overview)
### Realized PnL: split/merge accounting and partition retries
Realized PnL now records qualifying split and merge activity that was previously skipped. The values returned by the public PnL endpoint reflect that activity going forward. Distant future partition maintenance timeouts now defer to the next hourly run instead of failing.
* No API changes — the fix affects data completeness only.
* Applies to new activity going forward; historical replay is not included.
**Documentation:** [Get PnL Chart (Public)](/api-reference/public-portfolio/pnl-chart)
### Unrealized PnL: fee-aware accounting
Unrealized PnL now consistently accounts for fees across the platform, so the open-position value you see reflects what you would actually walk away with. Portfolio and public-portfolio positions responses use the same shared calculation, keeping the app, API, and downstream analytics aligned.
* No response-shape changes — the same `unrealizedPnl` field is returned; only its value is adjusted to include fees.
* Applies going forward to all open positions across binary, split, and grouped markets.
**Documentation:** [Positions](/api-reference/portfolio/positions), [Public Positions](/api-reference/public-portfolio/positions)
## Jul 27, 2026
### Referral tier floors: `customTier` field
A referrer's USDC referral tier can now include a minimum floor assigned by Limitless. The floor acts as a minimum: if the referrer's trading volume later clears a higher tier, volume wins and they are promoted; if it falls back, the floor holds.
* `GET /referral/usdc/me` now returns an optional `customTier` field with the floor's ladder name when one is active. The response shape is unchanged when no floor exists, so existing clients keep working.
* Ladder widgets can read `customTier` to render the floor above the volume-derived tier.
* There is no public endpoint to set or remove a tier floor.
### Split market resolution for binary markets
Binary markets can now resolve to a payout split (for example 50/50) instead of a single winner. Both sides remain redeemable at the split ratio in a single call — no new endpoint, and existing integrations keep working.
* On a `RESOLVED` market, `winningOutcomeIndex: null` no longer means "not yet resolved" — it can also indicate a split, with the ratio carried in `payoutNumerators`.
* Integrators should detect a split by reading `payoutNumerators` on resolved markets and redeem as usual; the redeem endpoint handles both sides at the ratio.
* Applies to CLOB binary markets. AMM and NegRisk flows are unchanged.
**Documentation:** [Get Market](/api-reference/markets/get-market) | [Positions](/api-reference/portfolio/positions) | [Redeem](/api-reference/portfolio/redeem) | [Programmatic API](/developers/programmatic-api)
### WebSocket orderEvent: new timestamp fields
The WebSocket `orderEvent` payload now carries three additive timestamp fields — `matchedAt`, `occurredAt`, and `publishedAt` — across the EXECUTION, MATCHED, and MINED/FAILED lifecycle stages. The legacy `timestamp` field still ships on every event but is now marked deprecated.
* `matchedAt`: when the match occurred in the order matching engine.
* `occurredAt`: when the lifecycle event (execution, mine, fail) was produced.
* `publishedAt`: when the event was published to the WebSocket stream, useful for measuring stream latency.
* No changes required — new fields are additive. Migrate off `timestamp` at your own pace.
**Documentation:** [WebSocket Events](/developers/websocket/overview)
### Maker rebates: current rates now on the rewards page
The Maker Rebates docs no longer hardcode a "current program parameters" table. Rates are per-market and change over time (recent example: BTC/ETH 15m and 5m tuned separately). The docs now defer to the rewards dashboard as the source of truth and explain the mechanism only.
* Current rates: [limitless.exchange/rewards?view=rebates](https://limitless.exchange/rewards?view=rebates).
* The illustrative 100 percent example on the page is clearly labeled as an assumption for the walkthrough, not a live rate.
**Documentation:** [Maker Rebates](/user-guide/maker-rebates)
### Starter referral rate returns to 10 percent
The one-week Starter tier promo has ended. The Starter referral rate is back to 10 percent, and the tier ladder is now 10/18/25/32/40. Higher tiers are unchanged.
* New referral fills on the Starter tier earn 10 percent from the deploy onward.
* Promo-week fills that are still unresolved will be restamped at 10 percent when their markets resolve, so pending referral earnings on those positions will shrink. Already-resolved fills are not affected.
* No action is required from referrers or referred users.
### Settlement hardening against ERC-1155 receiver-griefing wallets
Wallets that deliberately reject settlement transfers are now identified and blocked automatically, keeping CLOB settlement reliable for everyone else.
* No action is required from legitimate traders — safe wallets are never affected.
### Bug fix: Realized PnL stalled on standalone AMM markets
Realized-PnL updates for standalone AMM (binary) markets could stall when outcome-token mapping was incomplete, causing the PnL chart to fall behind for affected accounts. New events now flow through as expected.
* Applies to standalone AMM binary markets only. CLOB, NegRisk, and grouped markets are unaffected.
* The fix restores forward progress from the point of deploy; it does not automatically replay historical events that stalled before the fix.
**Documentation:** [PnL Chart](/api-reference/portfolio/pnl-chart)
### Bug fix: extra-time corners could flip sports YES resolutions
Sports corners markets on fixtures that went to extra time or penalties could resolve YES using inflated regular-time counts. This happened when the stats provider had not yet split extra-time corners out of the regular-time bucket. Affected YES resolutions on such fixtures now route to the Limi Oracle for verification against sources like FIFA and ESPN.
* Only YES outcomes on corners markets for fixtures ending in AET or PEN are re-verified. NO outcomes and full-time fixtures settle as before.
* Cards markets are unaffected — the provider splits cards per period correctly.
* Expected volume of extra Oracle checks is on the order of one per month.
## Jul 23, 2026
### Time-based LP reward schedules
A market's daily LP rewards budget can now be scaled up or down at specific hours or days of the week, evaluated in Eastern Time. Defaults are unchanged — markets without a schedule still pay a flat rate.
* Per-market boosts are applied within the market's daily cap, so total daily payout is bounded.
* Platform-wide dimming (a global multiplier) can also apply on top of the per-market schedule.
* The applied multiplier is recorded per minute for auditability.
**Documentation:** [LP Rewards](/user-guide/lp-rewards)
### Market listing endpoints: `includeNextMarket` for upfront-scheduled rounds
Market listing and search endpoints now accept an opt-in `includeNextMarket=true` query parameter. When set, each upfront-scheduled market includes a `navigation.nextMarket` object describing the successor round. Clients can then prefetch or subscribe to it before the current round expires.
* Response gains a `navigation.nextMarket` field with the next round's slug and metadata. Cursor pagination still walks the primary result set — `nextMarket` is attached to each item, not injected as extra rows.
* The `tradable` flag on `nextMarket` is intentionally conservative: it stays `false` until the successor is actually live.
* Opt-in only — omit the parameter and responses are unchanged.
**Documentation:** [Browse Active Markets](/api-reference/markets/browse-active) | [Browse Active by Category](/api-reference/markets/browse-active-category) | [Search Markets](/api-reference/markets/search) | [List Page Markets](/api-reference/navigation/list-page-markets)
## Jul 16, 2026
### SDK authentication: scoped API tokens (HMAC) are now the default path
Every SDK getting-started flow — TypeScript, Python, Go, and Rust — now leads with scoped API tokens (HMAC) as the recommended authentication path. The legacy `X-API-Key` / `LIMITLESS_API_KEY` path still works, but the docs now mark it as legacy and not recommended for new integrations.
* New integrations should authenticate with a scoped API token, configured on the SDK client via `hmacCredentials` / `hmac_credentials` / `WithHMACCredentials` / `HmacCredentials`, typically loaded from `LMTS_TOKEN_ID` and `LMTS_TOKEN_SECRET`.
* Existing API-key integrations are unaffected — no forced migration, no removal timeline.
* Scoped tokens give you granular scopes (`trading`, `account_creation`, `delegated_signing`, `withdrawal`) and are required for partner flows like delegated signing and sub-accounts.
**Documentation:** [Authentication](/developers/authentication) | [TypeScript SDK](/developers/sdk/typescript/getting-started) | [Python SDK](/developers/sdk/python/getting-started) | [Go SDK](/developers/sdk/go/getting-started) | [Rust SDK](/developers/sdk/rust/getting-started)
## Jul 13, 2026
### Trading restricted for Ontario and Alberta
Users in the Canadian provinces of Ontario (ON) and Alberta (AB) can no longer trade on the Platform. Ontario and Alberta now join the trading blocklist alongside the United States and the Republic of China. You can still browse markets, but you cannot place or fill orders.
* If you're located in ON or AB, you'll see a restriction when attempting to trade.
* Fully blocked jurisdictions (Russia, Belarus, Cuba, Iran, North Korea, Syria, and the Crimea, Donetsk, and Luhansk regions) are unchanged.
**Documentation:** [Terms of Service](/user-guide/terms-of-service)
## Jul 7, 2026
### Get market details: full response schema in the API reference
`GET /markets/{addressOrSlug}` now publishes its complete response schema, so the API playground renders a real example (instead of ``) and integrators can generate clients directly from the spec.
* Covers all three market shapes returned by the endpoint — CLOB markets, NegRisk groups, and AMM markets — plus every nested object (venue, settings, tokens, resolution, creator, and more).
* No behavior change on the endpoint itself: fields returned in production are unchanged; this is a spec-completeness fix.
* If you generate types or clients from `openapi.json`, regenerate to pick up the concrete response types.
**Documentation:** [Get Market Details](/api-reference/markets/get-market)
## Jul 1, 2026
### Predictable, second-precision slugs for scheduled and recurring markets
Going live Wednesday, July 1. Recurring and upfront-scheduled CLOB market slugs become predictable, so market makers can precompute a slug before its market exists. This aligns the slug timestamp with Polymarket's convention and was driven by multiple MM requests.
* **Format change:** on recurring and scheduled markets, the slug timestamp suffix moves from a 13-digit millisecond value (set at creation time, carrying no real information) to a 10-digit Unix-second value taken from the slot's scheduled open time.
* Before: `eth-up-or-down-5-min-1782716420095`
* After: `eth-up-or-down-5-min-1782717300` (Polymarket parity: `eth-updown-5m-1782717300`)
* **Cadences:** applies to every recurring up/down series — 5-min, 15-min, hourly, daily, and weekly — plus upfront-scheduled markets. The suffix is the slot start in Unix seconds, so it aligns to the slot boundary (e.g. a multiple of `300` for 5-min, `3600` for hourly, `86400` for daily).
* **Ladder markets:** same second-precision suffix, taken from the market's start point (`openPriceAt`).
* **UGM markets:** still use a full millisecond timestamp (not precomputable), but now carry a dedicated `-ugm-` marker so they can be identified, e.g. `btc-above-dollar6024331-on-jun-28-1218-utc-ugm-1782648136065`.
* **Unchanged:** manually-launched markets and AMM markets keep the millisecond suffix.
* **Action:** if you parse or hardcode slug timestamps, switch to the seconds-based format. To fetch upcoming slot slugs without computing them, use the timeline endpoints.
**Documentation:** [Market Timeline](/api-reference/markets/timeline) | [Get Active Market Slugs](/api-reference/markets/active-slugs)
## Jun 29, 2026
### Market status values and `orderbookUpdate` payload documented
Two reference gaps closed for integrators reading market state and real-time orderbooks.
* `GET /markets/{slug}`: the market `status` enum is now documented — `FUNDED` (live and tradeable), `LOCKED` (a legacy value that appears only if an operator manually overrides a market's status; not tradeable), `RESOLVED` (outcome known, winners can redeem), and `CREATED` / `DRAFTED` (not yet funded, not tradeable). Treat any unrecognized future value defensively rather than assuming the market is tradeable.
* WebSocket `orderbookUpdate`: the payload is now spelled out with the concrete `bids`/`asks` schema and a field table, plus a precision/snapshot note pointing at `GET /markets/{slug}/orderbook`.
**Documentation:** [Get Market](/api-reference/markets/get-market) | [WebSocket Events](/developers/websocket/overview)
## Jun 25, 2026
### Maintenance mode: detecting trading pauses
Production integrations can now detect and handle platform maintenance windows. During maintenance, trading can become post-only, cancel-only, or fully disabled, and trading endpoints may return `425 Too Early` with a trading-mode `code`.
* New `GET /maintenance/status` endpoint to check whether trading is currently restricted or a future maintenance notice is scheduled
* New **Maintenance Mode** developer guide covering when to check status, the trading modes, and `425` handling (do not retry a blocked action in a tight loop — refresh status and wait until the mode allows it)
**Documentation:** [Maintenance Mode](/developers/maintenance-mode) | [Maintenance Status](/api-reference/system/maintenance-status)
### Public portfolio: PnL chart endpoint
A public, read-only endpoint exposes an account's realized PnL over a timeframe — no authentication required for the read.
* `GET /portfolio/{account}/pnl-chart` returns a realized PnL series plus the current realized snapshot for the given account address, selected by a `timeframe` query (e.g. `1w`)
* Part of the public-portfolio read surface (alongside public positions and traded-volume); served read-only with caching, and the `account` address is normalized
**Documentation:** [Get PnL Chart (Public)](/api-reference/public-portfolio/pnl-chart)
### `/portfolio/orders`: cursor pagination
The authenticated `/portfolio/orders` read now supports keyset (cursor) pagination.
* Responses carry `nextCursor` (`null` on the last page) and an exact `total`
* Pass the returned cursor back along with `limit` to page forward — the cursor is opaque and resumes exactly where the previous page ended, with no duplicates across pages even when many orders share the same timestamp
* The legacy non-projection fallback is not paginated (returns the first `limit` orders with `nextCursor: null`) and is being retired
### Market creator: `username` and trade wallet
The `creator` object on market and group responses now includes the creator's `username` (alongside display name, image, and social link). The creator `address` resolves to the creator's trade wallet (their smart wallet, or the signer EOA when the trade wallet is set to EOA).
**Documentation:** [Get Market](/api-reference/markets/get-market)
## Jun 24, 2026
### Packs: on-chain mechanics documented
The Packs guide now explains how a pack works on-chain. Each pack is a single Conditional Token (CTF) held in the user's own wallet, redeemable only if every leg resolves their way (otherwise it expires worthless). Buying a pack signs an EIP-712 intent covering the chosen pack, stake, and deadline, and winners claim permissionlessly on Base — there is no dependency on Limitless to release winnings. Risk is fixed to the stake.
**Documentation:** [Packs](/user-guide/packs#under-the-hood)
## Jun 22, 2026
### Market data clarifications and market pages discoverability
* Confirmed there is **no testnet or sandbox** — all integrations run against Base mainnet with real USDC. Rehearse a flow with small live orders on a low-volume market.
* Clarified that reported user volume is **lifetime** volume, and the fee difference between **claiming** a resolved win and **selling** a position before resolution.
* The **Market Pages** endpoints (category- and property-based filtering) are now surfaced in the API Reference navigation, and `GET /markets/active` points to them for structured filtering.
**Documentation:** [For Developers](/developers/introduction) | [Market Resolution](/user-guide/market-resolution)
## Jun 17, 2026
### Orderbook: deriving the NO book
`GET /markets/{slug}/orderbook` returns a single, YES-side book. The reference now documents the complementary-pricing identity — `price(YES) + price(NO) = 1` — so integrators can mirror the YES book into the NO book (flip bids and asks, replace each price `p` with `1 - p`) instead of assuming NO liquidity is missing. Native NO orders are already merged into the returned YES book. Multi-outcome (NegRisk) markets are covered too: each outcome has its own book and is derived independently.
**Documentation:** [Get Orderbook](/api-reference/trading/orderbook)
## Jun 14, 2026
### SDK WebSocket: authenticated subscriptions on HMAC
All four SDK WebSocket pages (TypeScript, Python, Go, Rust) now document authenticated subscriptions — positions, order events, and transactions — via `hmacCredentials` passed to the WebSocket client, which signs the handshake automatically. The Python page gained full authenticated-subscription examples to match the others. No `X-API-Key` is used on the WebSocket handshake.
**Documentation:** [TypeScript](/developers/sdk/typescript/websocket) | [Python](/developers/sdk/python/websocket) | [Go](/developers/sdk/go/websocket) | [Rust](/developers/sdk/rust/websocket)
## Jun 12, 2026
### Authentication docs standardized on HMAC
Active code examples across the developer docs now standardize on scoped API tokens with HMAC-SHA256 request signing (`lmts-api-key`, `lmts-timestamp`, `lmts-signature`). The legacy `X-API-Key` header has been removed from active examples. Existing API-key integrations continue to work, but new integrations should use HMAC scoped tokens.
**Documentation:** [Authentication](/developers/authentication)
## Jun 10, 2026
### Documentation accuracy pass
A sweep fixed copy-paste-breaking inaccuracies across the API and SDK references: scoped-token field names, SDK retry-timing units, WebSocket symbol and event names, and signing-payload details. It also clarified that `expiration` and `nonce` must both be `0` for standard CTF orders.
**Documentation:** [EIP-712 Signing](/developers/eip712-signing) | [Authentication](/developers/authentication)
## Jun 9, 2026
### Self-trade prevention on `POST /orders`
`POST /orders` now accepts an optional top-level `stpPolicy` for clients that want explicit self-trade handling. Available across the TypeScript, Python, Rust, and Go SDKs.
* Existing integrations keep the default `cancel_maker` behavior when `stpPolicy` is omitted.
* Supported values: `cancel_maker`, `cancel_taker`, `cancel_both`.
* `stpPolicy` sits outside the EIP-712 signed order payload (top-level request field).
* `cancel_taker` / `cancel_both` reject the incoming order with `execution.settlementStatus: "CANCELED"` and `execution.reason: "STP_TAKER_REJECTED"` on the `POST /orders` response.
* `cancel_maker` / `cancel_both` list the cancelled resting order IDs in `execution.stpMakerCancels`; each cancelled maker emits a `CANCELLATION` order-event with `reason: "STP_MAKER_CANCELLED"`.
* Scope: blocks same-profile self-matches on the same token only; cross-token orders on the same profile are unaffected.
**Documentation:** [Create Order — Self-Trade Prevention](/api-reference/trading/create-order#self-trade-prevention)
## Jun 8, 2026
### Taker delay: async submission for marketable orders
Markets can now apply a **taker delay** — a short, per-market hold on marketable (taker) orders before the matching engine fills them. When a market has it enabled, order submission becomes asynchronous.
**What changes for integrators on a delay-enabled market:**
* A marketable order's `POST /orders` returns immediately with `execution.settlementStatus: "DELAYED"` and an `eligibleAt` timestamp — it no longer blocks until the trade is mined. Track the fill asynchronously over `subscribe_order_events` (provisional `MATCHED` → terminal `MINED` / `FAILED`), correlating by `clientOrderId` / `tradeEventId`.
* **`postOnly` orders are not affected** — maker quotes rest immediately, as today.
* Read a market's current delay from `settings.takerDelayMs` (milliseconds; `0` = none) on the market response.
**Documentation:** [WebSocket Events](/developers/websocket/order-events#subscribing-to-order-events)
## Jun 7, 2026
### Packs
A new product on Limitless. **Packs** are curated bundles of 2 to 3 predictions from different markets, presented as ready-made offers with a clear multiplier and a single buy-in window. A user wins only if every leg lands.
**What's live:**
* USDC collateral on Base, stake range \$1 to \$10, multiplier capped at 15x before vig
* 4% vig on crypto and sports packs, already baked into the displayed multiplier
* Pack listing states: Active, Hidden (no new buys; existing positions continue to resolve), Closed (terminal)
* User-held pack status: Active, Won, Lost; Won packs expose a Claim button until winnings are collected
* External wallets sign a one-time USDC approval for \$100 (covers up to 10 max-stake packs); each pack purchase needs a confirmation
* Limitless Safe wallets do not need ETH for gas; external EOA wallets need a small Base ETH balance when claiming
**Documentation:** [Packs](/user-guide/packs)
## Jun 5, 2026
### WebSocket order events: FAK/FOK terminal (`EXECUTION`) frame
Authenticated `subscribe_order_events` clients now receive a terminal `EXECUTION` frame the moment an immediate-or-cancel (FAK/FOK) order reaches its final state — no polling needed.
**What changed:**
* `source: "OME"` now carries a fourth `type`, `EXECUTION`, with a `status` of `FILLED`, `PARTIALLY_FILLED`, or `KILLED`
* `eventId` uses the string form `terminal:`
* Delivered once, to the order owner; it carries no fee fields — read the `MINED` settlement frame or the `POST /orders` response for the realized fee
* A FOK that cannot fill is rejected with HTTP 400 and emits no terminal frame, so a FOK only ever appears as `FILLED`
**Documentation:** [WebSocket Events](/developers/websocket/order-events#subscribing-to-order-events)
## Jun 3, 2026
### WebSocket order events: pre-settlement `MATCHED` frame
Authenticated `subscribe_order_events` clients now receive a provisional `MATCHED` settlement frame the moment the matching engine fills an order — **before** the on-chain settlement transaction. This is the earliest "your order will be matched for N" signal. It is emitted per participant (the taker and each maker), each carrying its own side, token, fill size, and a fee **estimate** (`isEstimate: true`).
**What changed:**
* `source: "SETTLEMENT"` now carries three `type`s: provisional `MATCHED`, then terminal `MINED` or `FAILED`
* `MATCHED` uses its own `eventId` namespace `matched::`, distinct from the terminal `settlement::`, so the two never dedup-collide
* Correlate provisional → terminal by `tradeEventId` + `orderId`; settle accounting on `MINED` (realized fee + `txHash`), never on the `MATCHED` estimate
**Documentation:** [WebSocket Events](/developers/websocket/order-events#subscribing-to-order-events)
### Polymarket migration guide: updated for CLOB V2
Polymarket cut over to CLOB V2 around May 2026, deprecating the V1 SDKs and endpoints the original migration guide was built on. The entire **Migrate from Polymarket** page has been reframed against V2.
**What changed in the guide:**
* Polymarket collateral updated from USDC.e to pUSD
* New host `data-api.polymarket.com` documented for positions, trades, activity, and price history
* Cancel semantics, `/data/*` order endpoints, public search, and slug paths corrected to V2 conventions
* The Limitless side of every comparison table remains classic CTF (taker, expiration, nonce, feeRateBps)
* Getting Started navigation reordered to surface the migration guide earlier
**Documentation:** [Migrate from Polymarket](/developers/migrate-from-polymarket)
## Jun 2, 2026
### User-generated markets
Limitless now lets users propose and fund their own markets. A new user-guide page covers the flow end-to-end, and the Maker Rebates page has been updated alongside to reflect the current rebate eligibility surface.
**Documentation:** [User-Generated Markets](/user-guide/user-generated-markets) | [Maker Rebates](/user-guide/maker-rebates)
### Authentication: UI flow for scoped API tokens
The "Getting a Scoped API Token" section now documents the UI derivation flow (connect wallet, open the API token modal, derive) alongside the existing programmatic `POST /auth/api-tokens/derive` path. Most builders should use the UI; the programmatic path is for fully automated partner integrations that need to mint tokens on behalf of end users.
**Documentation:** [Authentication](/developers/authentication)
## Jun 1, 2026
### Markets: `winningOutcomeIndex` outcome mapping
The `winningOutcomeIndex` field on resolved markets is now explicitly documented as `0` = YES, `1` = NO. Integrators previously had to verify this convention empirically. The mapping is added to the `GET /markets/{slug}` reference, the Programmatic API guide, and the OpenAPI description for `winning_index`.
**Documentation:** [Get Market](/api-reference/markets/get-market) | [Programmatic API](/developers/programmatic-api)
## May 29, 2026
### Cross-market market making quickstart
A new dedicated quickstart for the agents-starter `cross-market-mm` strategy: quote on Limitless, hedge fills on Polymarket to stay delta-neutral. The page covers the one-key wallet model (EOA plus key-less ERC-1271 deposit wallet, pUSD held in it), a no-money dry-run flow that takes under 10 minutes, an honest 20 to 30 minute go-live path, the lifecycle commands, safety rails, and the economics.
**Economics callout:** Limitless charges takers only, so the maker bot pays no Limitless fee. Revenue is the cross-venue spread plus maker rebates (100% of eligible taker fees on Daily/Hourly Crypto and 15-min Crypto, fill-gated) plus LP rewards for resting orders near midpoint. Cost is the Polymarket hedge taker fee.
**Documentation:** [Cross-Market Market Making](/developers/cross-market-market-making) | [Build a Trading Agent](/developers/build-a-trading-agent)
### SDK retirement: Java quickstart removed
The Java quickstart has been removed from the developer documentation. It was unmaintained and out of line with current API behavior. The four supported developer quickstarts are now TypeScript, Python, Go, and WebSocket.
### Small audit fixes
* `GET /auth/api-tokens/capabilities`: profiles with no capability row return `tokenManagementEnabled: true` and `allowedScopes: ["trading"]`, not `false` and an empty array. The docs now match the backend handler.
* TypeScript SDK markets reference: `group.submarkets` corrected to `group.markets` (the field is `markets?: Market[]`). The previous samples would throw at runtime.
## May 28, 2026
### Market timeline endpoints
Two new endpoints let integrators pre-fetch upcoming recurring-market slots (5-minute crypto, hourly, etc.) before they open:
* `GET /markets/{slug}/timeline` for the schedule on a single recurring market
* `GET /markets/timeline` for the global upcoming schedule
**Documentation:** [Market Timeline](/api-reference/markets/timeline)
### Cancel authentication clarifications
`POST /orders/cancel` is not EIP-712 signed. It requires the `trading` scope. Cancels on behalf of sub-accounts via `onBehalfOf` additionally require the `delegated_signing` scope. The reference page now spells this out so partner integrations can scope their tokens correctly the first time.
**Documentation:** [Cancel Order](/api-reference/trading/cancel)
## May 27, 2026
### SDK update: current profile and partner account listing
All four official SDKs now expose helpers for authenticated current-profile reads and partner-owned sub-account listing/recovery.
**Current profile:**
* `GET /profiles/me` returns the authenticated caller's private profile without passing a wallet address
* SDK helpers:
* TypeScript: `client.portfolio.getProfile()`
* Python: `client.portfolio.get_current_profile()`
* Go: `client.Portfolio.GetCurrentProfile(ctx)`
* Rust: `client.portfolio.get_current_profile()`
**Partner account listing:**
* `GET /profiles/partner-accounts` lists child accounts owned by the authenticated partner or recovers one by `account`
* Requires HMAC authentication with the `account_creation` scope
* Do not send `x-on-behalf-of`; results are always scoped to the partner behind the HMAC token
* SDK helpers:
* TypeScript: `client.partnerAccounts.listAccounts()`
* Python: `client.partner_accounts.list_accounts()`
* Go: `client.PartnerAccounts.ListAccounts(ctx, ...)`
* Rust: `client.partner_accounts.list_accounts(...)`
**Documentation:** [Get Current Profile](/api-reference/portfolio/get-current-profile) | [List Partner Sub-Accounts](/api-reference/partner-accounts/list-partner-accounts) | [Programmatic API](/developers/programmatic-api) | [TypeScript SDK](/developers/sdk/typescript/partner-accounts) | [Python SDK](/developers/sdk/python/partner-accounts) | [Go SDK](/developers/sdk/go/partner-accounts) | [Rust SDK](/developers/sdk/rust/partner-accounts)
## May 25, 2026
### Pyth markets: full-precision open price and descriptions
New Pyth-resolved markets now expose the full oracle precision in `metadata.openPrice` and the auto-generated market description text. Chainlink-resolved markets are not affected.
* `metadata.openPrice` for new Pyth markets returns the full decimal value (e.g. `2132.21000000` instead of `2132.21`). Pyth crypto feeds typically use 8 decimal places.
* The strike price quoted inside the auto-generated market `description` text uses the same full-precision value.
* Chainlink markets continue to return the 18-decimal padded string in both `metadata.openPrice` and the description.
* No backfill — only Pyth markets created or activated after the cutover get the new shape. Existing markets remain as-is.
**Action for partners:** if you parse `openPrice` as a number, no change is needed. If you display it verbatim or regex-match against the rounded value inside descriptions, expect longer strings for new Pyth markets.
**Documentation:** [Market Resolution](/user-guide/market-resolution)
### Sports markets: updated titles and resolution wording
Football match markets get cleaner titles and more explicit resolution language. Existing markets are not affected; the changes apply to newly created markets.
* **Group titles** no longer include the match date. The scheduled date and time appears in the description body.
* **Solo market titles** are reformatted (e.g. "Both Home and Away score on Feb 17?" → "Home and Away both to score?"). Affects BTTS, TOTAL\_GOALS, CORNERS\_MATCH, CARDS\_MATCH, and SPREADS.
* **Descriptions** are expanded to a 3-paragraph format with explicit branches for: match completed on the pitch, match abandoned after kickoff (solo markets resolve based on official data recorded up to that point), and match cancelled before kickoff or postponed beyond the resolution window.
* **Team-outcome markets** now explicitly recognize wins awarded by the competition authority when the match itself is not completed.
**Action for partners:** if you parse market titles for the match date, switch to the `deadline` field on the market object. If you regex-match descriptions for resolution wording, expect the new 3-paragraph format on newly created markets.
## May 20, 2026
### Receive window on `POST /orders`
`POST /orders` now accepts optional top-level `timestamp` (Unix ms) and `recvWindow` (ms) fields for clients that want order freshness checks. Both fields sit outside the EIP-712 signed order payload.
* Existing integrations are unchanged unless they send `recvWindow`.
* `recvWindow` must be between `1` and `10000` ms.
* A stale or future-dated timestamp returns HTTP `425 Too Early`. Build and sign a fresh order before retrying.
**Documentation:** [Create Order — Receive Window](/api-reference/trading/create-order#receive-window)
## May 7, 2026
### SDK update: partner withdrawal allowlist and expanded WebSocket coverage
All four official SDKs (TypeScript, Python, Go, Rust) now expose helpers for the partner withdrawal-address allowlist and a typed WebSocket subscription surface for the public real-time channels.
**Partner withdrawal-address allowlist:**
* `PartnerAccountService.addWithdrawalAddress()` — `POST /portfolio/withdrawal-addresses`
* `PartnerAccountService.deleteWithdrawalAddress()` — `DELETE /portfolio/withdrawal-addresses/:address`
* Both use a Privy identity token (`identity: Bearer `), not API-token/HMAC auth
* `HttpClient.deleteWithIdentity()` exposed for identity-token authenticated DELETE requests
* `POST /portfolio/withdraw` continues to use HMAC auth with the `withdrawal` scope; `destination` is optional for child server-wallet withdrawals and defaults to the partner smart wallet (or partner account)
* Unit coverage added for identity-auth allowlist calls and explicit server-wallet withdrawal destinations
**WebSocket subscription / event surface:**
* Typed subscriptions for order events, live sports/esports, market lifecycle, oracle price data, and system messages
* Mirrors the `subscribe_*` channels documented on the WebSocket reference
**Documentation:** [TypeScript Partner Accounts](/developers/sdk/typescript/partner-accounts#withdrawal-address-allowlist) | [Python Partner Accounts](/developers/sdk/python/partner-accounts) | [Go Partner Accounts](/developers/sdk/go/partner-accounts) | [Rust Partner Accounts](/developers/sdk/rust/partner-accounts) | [Add Withdrawal Address](/api-reference/portfolio/add-withdrawal-address) | [Delete Withdrawal Address](/api-reference/portfolio/delete-withdrawal-address) | [WebSocket Events](/developers/websocket/overview)
## May 5, 2026
### Combined cancel endpoints (orderIds + clientOrderIds)
A new `POST /orders/batch-cancel` endpoint accepts either internal `orderIds` or `clientOrderIds` (the value supplied at order creation time) in a single call. Provide exactly one identifier array — requests with both, or with neither, return `400 Bad Request`.
* `POST /orders/cancel-batch` remains supported for existing integrations and continues to accept `orderIds` only
* `clientOrderId` is also surfaced on the `subscribe_order_events` payload so partners can correlate WebSocket events to their own client-side IDs
**Documentation:** [Batch Cancel Orders (Combined)](/api-reference/trading/batch-cancel) | [Cancel Orders (Batch)](/api-reference/trading/cancel-batch) | [WebSocket Events](/developers/websocket/order-events#subscribing-to-order-events)
## May 4, 2026
### Withdrawal address whitelisting
Explicit treasury destinations for server-wallet withdrawals must now be allowlisted on the authenticated partner profile. Withdrawals to the partner account or partner smart wallet do not require allowlisting.
* `POST /portfolio/withdrawal-addresses` — add an allowlisted destination (Privy identity token)
* `DELETE /portfolio/withdrawal-addresses/:address` — remove an allowlisted destination (Privy identity token)
* `POST /portfolio/withdraw` — `destination` field now references allowlisted addresses for child server-wallet withdrawals; HMAC auth with the `withdrawal` scope unchanged
**Documentation:** [Add Withdrawal Address](/api-reference/portfolio/add-withdrawal-address) | [Delete Withdrawal Address](/api-reference/portfolio/delete-withdrawal-address) | [Withdraw](/api-reference/portfolio/withdraw)
## Apr 30, 2026
### Partner account allowance helpers
Server-wallet sub-accounts need delegated-trading approvals before they can trade. Two new endpoints (and matching helpers across all four SDKs) make the recovery flow explicit:
* `GET /profiles/partner-accounts/:profileId/allowances` — inspect approval state per target
* `POST /profiles/partner-accounts/:profileId/allowances/retry` — re-run any `missing` or `failed` retryable targets
* Both require HMAC credentials with `account_creation` and `delegated_signing`
* `409 Conflict` indicates a retry is already running; `429` returns `retryAfterSeconds`
Recommended flow: poll `checkAllowances` → if not ready and any target is retryable, call `retryAllowances` → on `submitted`, poll again after a short delay.
**Documentation:** [Check Allowances](/api-reference/partner-accounts/check-allowances) | [Retry Allowances](/api-reference/partner-accounts/retry-allowances) | [Programmatic API](/developers/programmatic-api)
### Restricted regions update
Russia and Belarus added to the list of restricted jurisdictions in the Terms of Service.
**Documentation:** [Terms of Service](/user-guide/terms-of-service)
## Apr 27, 2026
### Rust SDK v1.0.7: initial stable release
The official Rust SDK is now documented with full coverage for individual traders, bot builders, and partner integrations.
**Highlights:**
* Typed async client for CLOB and NegRisk markets
* Root `Client` entrypoint for markets, portfolio, market pages, API tokens, partner accounts, delegated orders, server wallets, and WebSockets
* Built-in EIP-712 order building and signing on Base
* API key and HMAC authentication support
* Async WebSocket streaming with reconnect handling
* Pluggable logging, retry helpers, and builder-style HTTP configuration
**Documentation:** [Rust SDK](/developers/sdk/rust/getting-started)
### Order execution: FAK orders added to signing and SDK docs
Fill-and-kill (FAK) orders are now documented across the developer guides. FAK orders use the same `price` and `size` inputs as GTC limit orders, match immediately against available liquidity, and cancel any unmatched remainder instead of resting on the book.
**Updated docs:**
* EIP-712 signing now includes FAK in the order type reference and amount-calculation guidance
* TypeScript SDK order docs include FAK examples and parameter reference
* Programmatic API delegated orders now show GTC, FAK, and FOK flows
**Documentation:** [EIP-712 Signing](/developers/eip712-signing) | [TypeScript SDK Orders](/developers/sdk/typescript/orders) | [Programmatic API](/developers/programmatic-api)
### WebSocket order events
Authenticated WebSocket clients can now subscribe to the CLOB order lifecycle with `subscribe_order_events`.
**New real-time stream:**
* Emits `orderEvent` messages for both off-chain matching-engine updates and on-chain settlement results
* OME events cover placement, updates, and cancellations
* Settlement events cover mined and failed transaction outcomes
* Auth failures are surfaced on the `exception` channel
* Clients should resubscribe on reconnect and treat OME and settlement events as independent streams
**Documentation:** [WebSocket Events](/developers/websocket/order-events#subscribing-to-order-events)
### Portfolio history cursor contract and partner position headers
Portfolio and partner account docs were updated for integrations that page through account activity or query positions on behalf of managed accounts.
**API documentation updates:**
* `GET /portfolio/history` now reflects the cursor-based pagination contract
* Position reads now document partner `onBehalfOf` usage for managed account flows
* Server-wallet redemption docs clarify the difference between API resolution state and on-chain conditional-token settlement
**Documentation:** [Portfolio History](/api-reference/portfolio/history) | [Get Positions](/api-reference/portfolio/positions) | [Programmatic API](/developers/programmatic-api#lifecycle-after-a-trade)
### Developer onboarding and partner flow updates
Developer onboarding now more clearly separates the individual trader/bot path from the platform/partner path.
**Updated guidance:**
* Individual traders and bot builders can use API keys or self-service scoped trading tokens without applying for partner access
* Platform partners should use backend-held HMAC credentials, sub-accounts, and delegated signing
* Programmatic API docs now include the partner application link and updated signing guidance
**Documentation:** [For Developers](/developers/introduction) | [Authentication](/developers/authentication) | [Programmatic API](/developers/programmatic-api)
### Brand assets and support contact
Brand assets were refreshed and the docs support link now points to `help@limitless.network`.
## Apr 14, 2026
### SDK v1.0.6: server wallet claim and full maker rebates
**Server wallet claim (all SDKs):**
* All three SDKs (TypeScript, Python, Go) now include helper methods for server wallet redemption and withdrawal
* `redeem` — claim resolved conditional-token positions from managed sub-accounts
* `withdraw` — transfer ERC20 funds from managed sub-accounts to partner wallets
* Previously these operations required direct REST calls; now fully supported in the SDK
**Maker Rebates program update:**
* Rebate rate increased to **100%** across all market types (daily, hourly crypto, 15-minute crypto)
**Documentation:** [Programmatic API](/developers/programmatic-api) | [TypeScript SDK](/developers/sdk/typescript/getting-started) | [Python SDK](/developers/sdk/python/getting-started) | [Go SDK](/developers/sdk/go/getting-started) | [Maker Rebates](/user-guide/maker-rebates)
## Apr 2, 2026
### Programmatic API: partner integrations, delegated signing, and FOK orders
Major new capability for partners building on Limitless. The Programmatic API enables platform integrations with sub-account management, server-side order signing, and both GTC and FOK order types — all through HMAC-authenticated scoped tokens.
**New API endpoints:**
* `POST /auth/api-tokens/derive` — Create scoped HMAC tokens with granular permissions (`trading`, `account_creation`, `delegated_signing`)
* `GET /auth/api-tokens/capabilities` — Check partner capability configuration
* `GET /auth/api-tokens` — List active tokens
* `DELETE /auth/api-tokens/:tokenId` — Revoke a token
* `POST /profiles/partner-accounts` — Create sub-accounts with server-managed wallets or EOA verification
**Delegated signing:**
* Partners with the `delegated_signing` scope can submit unsigned orders on behalf of sub-accounts
* The server signs orders using Privy-managed wallets — no private key management needed on the partner side
* Both **GTC** (limit) and **FOK** (market) order types are supported for delegated orders
* FOK orders use `makerAmount` (USDC to spend for BUY, shares to sell for SELL) instead of `price` + `size`
**Recommended architecture:**
* 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
**Documentation:** [Programmatic API guide](/developers/programmatic-api) | [Authentication](/developers/authentication#scoped-api-tokens-hmac) | [API Tokens reference](/api-reference/api-tokens/derive-token)
### SDK v1.0.4–1.0.5: programmatic API support (TypeScript, Python, Go)
All three official SDKs now include full support for the Programmatic API:
**New services across all SDKs:**
* `ApiTokenService` — Derive, list, and revoke scoped HMAC tokens
* `PartnerAccountService` — Create sub-accounts with server wallets or EOA verification
* `DelegatedOrderService` — Place GTC and FOK orders on behalf of sub-accounts, cancel orders
**New root `Client` entrypoint:**
* Composes all domain services (markets, portfolio, orders, API tokens, partner accounts, delegated orders) behind a single constructor
* HMAC credentials are configured once and applied to all requests automatically
**TypeScript SDK:**
* `Client` with `hmacCredentials` option
* `client.apiTokens`, `client.partnerAccounts`, `client.delegatedOrders`
* WebSocket HMAC authentication support
* [TypeScript SDK docs](/developers/sdk/typescript/getting-started)
**Python SDK:**
* `Client` with `hmac_credentials` option
* `client.api_tokens`, `client.partner_accounts`, `client.delegated_orders`
* WebSocket HMAC handshake support
* [Python SDK docs](/developers/sdk/python/getting-started)
**Go SDK:**
* `NewClient(limitless.WithHMACCredentials(...))`
* `client.ApiTokens`, `client.PartnerAccounts`, `client.DelegatedOrders`
* WebSocket HMAC authentication support
* [Go SDK docs](/developers/sdk/go/getting-started)
## Mar 23, 2026
### SDK v1.0.3: market pages navigation
All three SDKs now support the market pages navigation API for browsing markets by category.
**New capabilities:**
* Navigation tree discovery
* Market page resolution by path with automatic redirect handling
* Market listing with filters, pagination, and cursor support
* Property keys and options for dynamic filtering
**TypeScript:** [Market Pages](/developers/sdk/typescript/market-pages) | **Python:** [Market Pages](/developers/sdk/python/market-pages) | **Go:** [Market Pages](/developers/sdk/go/market-pages)
### Go SDK v1.0.3: initial stable release
First stable, production-ready release of the Go SDK with full feature parity.
**Highlights:**
* EIP-712 order signing with private key (Base mainnet)
* GTC and FOK order support
* WebSocket streaming with Socket.IO protocol, auto-reconnect, and typed event handlers
* Configurable retry with exponential backoff
* Functional options pattern throughout (`WithBaseURL`, `WithAPIKey`, `WithLogger`, etc.)
* Zero external dependencies beyond `go-ethereum` and `gorilla/websocket`
**Documentation:** [Go SDK](/developers/sdk/go/getting-started)
## Mar 2026
### WebSocket market lifecycle events
New WebSocket events for market creation and resolution, enabling real-time monitoring of market lifecycle changes.
* `market.created` — Fires when a new market is created
* `market.resolved` — Fires when a market resolves
**Documentation:** [WebSocket Events](/developers/websocket/overview)
## Feb 2026
### SDK v1.0.2: stable LTS release (TypeScript and Python)
First stable, long-term support releases of the TypeScript and Python SDKs.
**Core features across both SDKs:**
* API key authentication with `X-API-Key` header
* EIP-712 order signing with venue-aware contract resolution
* GTC and FOK order creation with automatic signing
* Market data fetching with intelligent venue caching
* Portfolio and position tracking
* WebSocket streaming with auto-reconnect
* Retry mechanisms with configurable backoff strategies
* NegRisk market support with dual approval system
**TypeScript SDK highlights:**
* Full TypeScript type safety with comprehensive type definitions
* 17 production-ready code samples
* `@retryOnErrors` decorator and `withRetry` wrapper
* [TypeScript SDK docs](/developers/sdk/typescript/getting-started)
**Python SDK highlights:**
* Async-first architecture built on aiohttp
* Pydantic models for type-safe request/response handling
* `@retry_on_errors` decorator
* [Python SDK docs](/developers/sdk/python/getting-started)
### Python SDK v1.0.2: float precision fix
Fixed IEEE 754 float precision issues in order pricing that could cause incorrect `makerAmount`/`takerAmount` calculations (e.g., `0.95` scaling to `949999` instead of `950000`).
## Dec 2025
### API: cookie-based authentication removed
Cookie-based session authentication has been removed. All programmatic access now requires API keys (`X-API-Key` header) or scoped API tokens (HMAC).
**Action required:** If you were using cookie-based authentication, migrate to API key authentication. Generate keys at [limitless.exchange](https://limitless.exchange) under profile menu → API keys.
**Documentation:** [Authentication](/developers/authentication)
## Nov 2025
### Initial SDK releases (pre-release)
Pre-release versions of the TypeScript (v0.0.1) and Python (v0.1.0) SDKs launched with core trading functionality including market data access, GTC/FOK order creation, and portfolio tracking.
# AMM Trading (Server Wallets)
Source: https://docs.limitless.exchange/developers/amm-trading
Buy and sell outcome shares on AMM markets from a server-managed wallet using scoped API tokens
Limitless exposes intent-level HTTP endpoints for trading AMM (fixed-product market maker) markets from a Privy-backed server wallet. You can execute buys and sells for your own server-wallet sub-accounts without holding user keys, signing calldata, or managing gas.
**Who this is for.** Programmatic API partners with a server-wallet sub-account. Endpoints require a scoped API token with the `trading` and `delegated_signing` scopes. See [Programmatic API](/developers/programmatic-api) to get access and create a sub-account.
These endpoints reject legacy `x-api-key` credentials. Trades submit on-chain from the sub-account's Privy server wallet. EOA sub-accounts and non-server-wallet profiles are not supported.
## When to use these endpoints
Use AMM endpoints when the market is an FPMM AMM market (single-outcome buy/sell against a pool). Use the [CLOB order endpoints](/api-reference/trading/create-order) for order-book markets. If you're unsure which type a market uses, fetch it with [`GET /markets/{slug}`](/api-reference/markets/get-market) and inspect the market metadata.
| Endpoint | Purpose |
| - | - |
| `POST /amm/buy` | Spend collateral to acquire outcome shares |
| `POST /amm/sell` | Return an exact amount of collateral by selling outcome shares |
| `POST /amm/allowances/check` | Read the on-chain approval state for a market and side |
| `POST /amm/allowances/approve` | Submit a fresh approval from the server wallet |
All four endpoints are HMAC-authenticated. See [HMAC request signing](/developers/authentication#hmac-request-signing) for the signing protocol.
## Prerequisites
* A scoped API token with **both** `trading` and `delegated_signing` scopes. Derive one via [`POST /auth/api-tokens/derive`](/api-reference/api-tokens/derive-token).
* A server-wallet sub-account. Create one with `createServerWallet: true` via [`POST /profiles/partner-accounts`](/api-reference/partner-accounts/create-partner-account).
* Enough collateral (for buys) or outcome-share balance (for sells) on the server wallet.
* A one-time approval per market and side (see [Allowances](#allowances)).
## Amount conventions
All amounts are **positive integer strings** in the market collateral token's base units (for USDC this means 6 decimals: `"1000000"` = 1 USDC).
Slippage is expressed in **basis points**:
* Default `slippageBps`: `100` (1%)
* Maximum: `1000` (10%)
* Applied against the on-chain quote at request time. The server rejects trades whose quoted price moves beyond the bound.
## Buying outcome shares
`POST /amm/buy` spends a fixed amount of collateral on a chosen outcome. The server fetches a fresh on-chain quote, applies your slippage bound, and submits a sponsored user operation from the sub-account's server wallet.
### Request
| Field | Type | Required | Description |
| - | - | - | - |
| `market` | `string` | Yes | Market slug or checksummed FPMM address |
| `outcomeIndex` | `0 \| 1` | Yes | `0` = YES, `1` = NO |
| `collateralAmount` | `string` | Yes | Collateral base units to spend (positive integer) |
| `slippageBps` | `number` | No | Slippage tolerance in basis points. Default `100`, max `1000` |
| `idempotencyKey` | `string` | Yes | Partner-provided key, up to 128 chars. Required for safe retries |
| `onBehalfOf` | `number` | No | Server-wallet sub-account profile ID. Omit when trading from your own server wallet |
### Example
```bash theme={null}
curl -X POST https://api.limitless.exchange/amm/buy \
-H "lmts-api-key: $TOKEN_ID" \
-H "lmts-timestamp: $TIMESTAMP" \
-H "lmts-signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{
"market": "will-btc-hit-150k-2026",
"outcomeIndex": 0,
"collateralAmount": "1000000",
"slippageBps": 100,
"idempotencyKey": "buy-2026-03-06-001",
"onBehalfOf": 12345
}'
```
### Response — `201 Created`
```json theme={null}
{
"status": "SUBMITTED",
"market": "will-btc-hit-150k-2026",
"outcomeIndex": 0,
"collateralAmount": "1000000",
"expectedShares": "1763995",
"minShares": "1746355",
"transactionId": "…",
"userOperationHash": "0x…",
"txHash": "0x…"
}
```
* `expectedShares` — shares expected at the current quote
* `minShares` — floor after applying `slippageBps`
* `txHash` may be absent immediately after submission for sponsored user operations; use `transactionId` or `userOperationHash` to track completion
## Selling outcome shares
`POST /amm/sell` targets an **exact collateral return**, bounded by the maximum shares you're willing to spend to achieve it. The server quotes the required shares, applies your slippage tolerance in the opposite direction, and submits.
### Request
| Field | Type | Required | Description |
| - | - | - | - |
| `market` | `string` | Yes | Market slug or checksummed FPMM address |
| `outcomeIndex` | `0 \| 1` | Yes | `0` = YES, `1` = NO |
| `collateralReturnAmount` | `string` | Yes | Collateral base units you want to receive (positive integer) |
| `slippageBps` | `number` | No | Slippage tolerance in basis points. Default `100`, max `1000` |
| `idempotencyKey` | `string` | Yes | Partner-provided key, up to 128 chars |
| `onBehalfOf` | `number` | No | Server-wallet sub-account profile ID |
### Example
```bash theme={null}
curl -X POST https://api.limitless.exchange/amm/sell \
-H "lmts-api-key: $TOKEN_ID" \
-H "lmts-timestamp: $TIMESTAMP" \
-H "lmts-signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{
"market": "will-btc-hit-150k-2026",
"outcomeIndex": 0,
"collateralReturnAmount": "992015",
"slippageBps": 100,
"idempotencyKey": "sell-2026-03-06-001",
"onBehalfOf": 12345
}'
```
### Response — `201 Created`
```json theme={null}
{
"status": "SUBMITTED",
"market": "will-btc-hit-150k-2026",
"outcomeIndex": 0,
"collateralReturnAmount": "992015",
"expectedShares": "1959992",
"maxShares": "1979592",
"transactionId": "…",
"userOperationHash": "0x…",
"txHash": "0x…"
}
```
* `expectedShares` — shares required at the current quote to receive `collateralReturnAmount`
* `maxShares` — cap after applying `slippageBps`; the trade fails on-chain if it would require more
## Allowances
Every server wallet needs a one-time approval per market and side:
* **Buy:** the AMM must be approved to pull collateral (ERC20 `approve`).
* **Sell:** the AMM must be approved as an operator for outcome shares (ERC1155 `setApprovalForAll`).
Buy and sell approvals are independent. Approving one does not enable the other. Approvals persist on-chain and only need to be issued once per wallet, market, and side.
### Check allowance — `POST /amm/allowances/check`
Reads the current on-chain state.
```json theme={null}
{
"market": "will-btc-hit-150k-2026",
"side": "BUY",
"onBehalfOf": 12345
}
```
Response:
```json theme={null}
{
"status": "confirmed",
"confirmed": true,
"market": "will-btc-hit-150k-2026",
"marketAddress": "0x…",
"side": "BUY",
"walletAddress": "0x…",
"tokenAddress": "0x…",
"spenderOrOperator": "0x…",
"currentAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
}
```
* `status` is `confirmed` when the on-chain allowance is at or above the ready threshold, otherwise `missing`.
* `currentAllowance` is only returned for `BUY` (ERC20). `SELL` uses the boolean `isApprovedForAll` on ERC1155.
### Submit approval — `POST /amm/allowances/approve`
Same request body as `check`. Behavior:
* If the allowance is already `confirmed` on-chain, the endpoint returns `200 OK` with `status: "confirmed"` and no transaction.
* If the server submits a new approval, the endpoint returns `202 Accepted` with `status: "submitted"` and Privy transaction identifiers. The approval is asynchronous; poll `/amm/allowances/check` (or wait a few seconds) before submitting the trade.
```json theme={null}
{
"status": "submitted",
"confirmed": false,
"market": "will-btc-hit-150k-2026",
"marketAddress": "0x…",
"side": "BUY",
"walletAddress": "0x…",
"tokenAddress": "0x…",
"spenderOrOperator": "0x…",
"transactionId": "…",
"userOperationHash": "0x…"
}
```
## Idempotency and retries
`idempotencyKey` is **required** on `/amm/buy` and `/amm/sell` so that network hiccups and client retries never double-submit a trade.
* Keys are scoped to the target profile and retained for 24 hours.
* Reusing the same key with the same trade parameters returns the original submitted result, so it is safe to retry after a timeout.
* Reusing the same key with **different** parameters returns `409 Conflict`.
* Keys must contain at least one non-whitespace character and be at most 128 chars long.
Pick a key that a client can regenerate deterministically (for example `"{internal-trade-id}"`) so a lost response does not force a duplicate trade.
## Rate limits
AMM endpoints share a per-caller limit of **10 requests per 10 seconds**. Excess calls return `429 Too Many Requests` with a `retryAfterSeconds` field. The bucket key is the API token, actor profile, or wallet, whichever is available on the request.
## Error responses
| Status | Meaning |
| - | - |
| `400` | Invalid body — malformed amount, unknown market, invalid outcome, or missing required field |
| `403` | Missing scopes, legacy API key, or `onBehalfOf` is not a server-wallet sub-account you own |
| `409` | Market is closed, deadline passed, or the idempotency key was reused with different parameters |
| `422` | Insufficient balance for the trade or trade too small to satisfy slippage |
| `429` | Rate limit exceeded |
| `502` | On-chain read, quote, or Privy submission failed |
| `503` | Approval state or trading mode is temporarily unavailable |
**Redeeming resolved AMM positions.** Once a market resolves, claim payouts through the existing [`POST /portfolio/redeem`](/api-reference/portfolio/redeem) endpoint. It accepts the same `onBehalfOf` convention and requires the `trading` scope for API-token callers.
## Typical flow
Use your own accounting or [`GET /portfolio/positions`](/api-reference/portfolio/positions) with `x-on-behalf-of` set to the sub-account.
Call `POST /amm/allowances/check`. If `status` is `missing`, call `POST /amm/allowances/approve` and wait for confirmation.
Call `POST /amm/buy` or `POST /amm/sell` with an `idempotencyKey` your system can regenerate on retry.
Use the returned `transactionId` and `userOperationHash` to correlate with your accounting. Retry the exact same request if the response is lost. The idempotency key guarantees at-most-once execution.
# 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. If you have an existing API key, 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. You authenticate every request 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 |
If you use an SDK, 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
Most builders should generate a token from the UI. The programmatic flow below is for headless / CI environments where you can't open a browser.
#### From the UI (recommended)
Go to [limitless.exchange](https://limitless.exchange) and connect your wallet. No smart wallet required.
If your account has ever enabled 1-click (smart wallet) trading in the app, self-signed API orders will be rejected until you switch the profile to EOA trading mode. See [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
Open the API token modal → **API Tokens** tab → **Derive**. Copy the `tokenId` and `secret`. The browser handles the login session for you, so there's no identity token to copy by hand.
Pass them to the SDK as `hmacCredentials` (TypeScript), `hmac_credentials` (Python), or `WithHMACCredentials` (Go) — the SDK signs every request automatically. This is a one-time setup.
#### Programmatically (headless / CI)
When you can't use a browser, call the same endpoint the UI calls:
Authenticate with Privy and capture the `token` field from the authenticate response (not `privy_access_token`).
`POST /auth/api-tokens/derive` with the identity token in the `identity` header as `Bearer `. The response includes a `tokenId` and `secret`. Use these HMAC credentials for all subsequent requests. The JSON body is optional: `label` (string, up to 128 characters) and `scopes` (array). Without `scopes` the token gets `["trading"]` only; partner scopes must be listed explicitly and are rejected unless your account has them enabled.
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, you 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 you should migrate to scoped API tokens for improved security and access control.
If you hold a legacy static API key, it still works for now, but migrate to scoped API tokens (HMAC) above — they add request signing, scopes, and expiry. New keys are no longer issued, and all examples in these docs use HMAC.
# Build a Trading Agent
Source: https://docs.limitless.exchange/developers/build-a-trading-agent
Open-source TypeScript boilerplate for autonomous trading agents on Limitless: clone it, pass the preflight, run a strategy in dry-run, then build your own
[**agents-starter**](https://github.com/limitless-labs-group/agents-starter) is the canonical starter for an unattended trading agent on Limitless. It runs on the official [`@limitless-exchange/sdk`](/developers/sdk/typescript/getting-started), defaults to dry-run, and ships a preflight that tells you why an order would be rejected before you send one.
This is for **individual traders, builders, and AI agents** who want a working code template, not a partner integration. If you're building a platform where other users trade through your product, see [Programmatic API](/developers/programmatic-api) instead. If you want an assistant to trade with you approving each order in a chat, see the [MCP server](/developers/mcp-server).
## What's included
A small TypeScript project. Everything Limitless goes through the SDK; the repo adds the strategy loop, the safety rails, and a few helpers the SDK doesn't ship.
| Path | What it does |
| - | - |
| `src/strategies/template/` | The skeleton to copy: scan markets, compute a fair value, return decisions. |
| `src/strategies/` | Three worked strategies: **certainty-closer** (SDK-only, simplest), **oracle-arb** (Pyth oracle vs market), **cross-market-mm** (quote on Limitless, hedge on Polymarket; see [Cross-market market making](/developers/cross-market-market-making)). |
| `src/strategies/base-strategy.ts` | The tick → decide → execute loop. Gates on `DRY_RUN`, routes BUY and SELL, and waits out the taker delay when asked. |
| `src/core/limitless/` | Thin SDK wrappers: markets and orderbooks, orders (including cancel-replace, `POST /orders/status/batch`, and a taker-delay aware `awaitFill`), websocket streams, portfolio, on-chain redemption for standard and neg-risk markets. |
| `src/core/doctor.ts` | The preflight behind `npm run doctor`. |
| `src/examples/` | `place-order` (post-only GTC → order events → verify → cancel) and `stream-orderbook` (live books plus your order events). |
| `AGENTS.md`, `SKILL.md` | The operating contract and the full manual for a coding agent (Claude Code, OpenClaw, anything with shell and file access). |
| `mcp-skills/` | Claude skills for the chat path over the [MCP server](/developers/mcp-server): thesis building, ladders, portfolio review. |
Unattended agents are bound by the same conduct rules as any trader. Before going live, read [Building Responsible Trading Agents](/developers/responsible-agents): what is prohibited, which safeguards the exchange enforces, and which ones (spending limits, kill switches, self-trade handling) your bot has to enforce itself.
## Five-minute setup
Generate a **dedicated** wallet for this bot. Fund it with only what you're prepared to lose. Do not use your main wallet.
```bash theme={null}
git clone https://github.com/limitless-labs-group/agents-starter.git
cd agents-starter
npm install
npm run init # creates .env (mode 600) and lists the credentials to add
```
Node 20 or newer is required. Add two things to `.env`:
1. `PRIVATE_KEY`: the dedicated wallet's key.
2. `LMTS_TOKEN_ID` and `LMTS_TOKEN_SECRET`: a scoped HMAC token. On [limitless.exchange](https://limitless.exchange), connect the **same** wallet, open the API token modal, pick the **API Tokens** tab, and derive one. See [Authentication](/developers/authentication).
Fund the wallet with USDC on Base for collateral and a little ETH on Base for gas. Only approvals and redemptions are on-chain; placing and cancelling orders is free. There is no testnet; rehearse with small live orders.
While connected in the app, decline the one-time "choose your trading wallet" prompt if it offers 1-click (smart wallet) trading. Accepting it puts the profile in smart-wallet mode, and every order your bot signs with `PRIVATE_KEY` is rejected. Already accepted it? `npm start wallet-mode eoa` switches it back. See [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs) for what changes.
## Preflight
```bash theme={null}
npm run doctor # key, token, wallet mode, USDC and ETH balances
npm run doctor -- --market # plus USDC and CTF approvals for that market's exchange
```
`doctor` checks that the key is well-formed, the token authenticates, the token's account matches the signing wallet, the trading-wallet mode is `eoa`, and the wallet is funded. It exits non-zero when the key, token, profile, wallet mode or USDC allowance would reject orders; balances and CTF approval are reported as warnings. It never prints a secret. Run it after every credential change.
## Run a strategy
Every strategy starts with `DRY_RUN=true`: each order intent is logged, nothing is signed or sent.
```bash theme={null}
npm run certainty-closer # dry run by default
DRY_RUN=false npm run certainty-closer # live, once the dry-run log looks right
```
Before going live on a market, approve its exchange once (`npm start approve `). Neg-risk (grouped) markets use a separate exchange and adapter and need their own approval. Markets on [new trading venues](/developers/venue-system#new-trading-venues) also need a USDC approval to the venue's `feeModule` and orders signed with `feeRateBps` set to `0`.
Keep order sizes small for the first live runs ($1 to $2 per order). The strategies are conservative by default, but no bot is bug-free.
### Built-in strategies
* **`template`**: scans the newest CLOB markets, compares each price to a placeholder fair value, and buys when the gap clears a threshold. The placeholder returns the market's own price, so it trades nothing until you replace it with your signal. Copy this one.
* **`certainty-closer`**: buys near-resolution favourites sized by fractional Kelly. SDK-only, no external feeds. On its own it has no independent edge; the edge is the one you assert in its config. The cleanest example of the filter → decide → execute loop.
* **`oracle-arb`**: streams Pyth prices over Hermes SSE and fires FOK orders on short-dated crypto markets when the implied probability strays from the oracle by more than fees and slippage.
* **`cross-market-mm`**: quotes on Limitless and hedges fills on Polymarket to stay delta-neutral. It has its own guide: [Cross-market market making](/developers/cross-market-market-making).
## Bring your own strategy
Copy `src/strategies/template/`, rename it, and implement four methods on `BaseStrategy`:
```typescript theme={null}
import { BaseStrategy, type StrategyDeps, type StrategyConfig, type TradeDecision, type StrategyStats } from '../base-strategy.js';
import { toFraction } from '../../core/limitless/types.js';
export class MyStrategy extends BaseStrategy {
constructor(config: StrategyConfig, deps: StrategyDeps) {
super(config, deps);
this.tickIntervalMs = 30_000;
this.awaitFills = true; // block on taker orders until they settle (honors the taker delay)
}
async initialize() {} // connect feeds, load state
async tick(): Promise {
const markets = await this.limitless.getActiveMarkets({ tradeType: 'clob', limit: 25 });
return markets
.filter((m) => this.fairValue(m) - toFraction(m.prices?.[0]) > 0.1)
.map((m) => ({
action: 'BUY',
marketSlug: m.slug,
side: 'YES',
amountUsd: 1,
priceLimit: Math.round(toFraction(m.prices?.[0]) * 100) + 2,
orderType: 'FOK',
reason: 'fair value above market',
}));
}
async shutdown() {} // cancel quotes, close feeds
getStats(): StrategyStats { return { activePositions: 0, totalVolumeUsd: 0, pnlUsd: 0, lastTickDurationMs: this.lastTickDurationMs }; }
private fairValue(market: { prices?: number[] }): number {
return toFraction(market.prices?.[0]); // your signal goes here
}
}
```
`tick()` returns decisions; the base class places them through the SDK, logs the result, and keeps the loop alive if a tick throws. Add a `run.ts` next to it (copy the template's) and an entry in `package.json` scripts.
## Under the hood
Orders are signed with [EIP-712](/developers/eip712-signing) by the SDK, which picks the venue's `verifyingContract` (default CTF exchange or neg-risk exchange) from the market's `venue`. Requests are HMAC-signed from the token. The repo's own tests re-derive the documented order struct with an independent library and assert the SDK signs the same bytes, so an SDK bump that changes what is signed fails in CI rather than at a live order.
**Read `settlementStatus`, not `matched`.** The create-order response tells you what happened in `execution.settlementStatus`: `MINED` means settled; `UNMATCHED` means resting for a GTC but killed for a FOK or FAK; `DELAYED` means the market's taker delay is holding a marketable order until `eligibleAt`, which is not an error. The fill then arrives over [order events](/developers/websocket/order-events) or `POST /orders/status/batch`. `postOnly` quotes and GTC orders are never delayed. Most rejections on `POST /orders` come back as an HTTP status with a `message` and no machine-readable code; the exceptions are `FEE_RATE_MISMATCH` (with `expectedFeeRateBps`) and the maintenance-mode `425` bodies, which carry a `code`. `POST /v2/orders` wraps every other rejection as `code: "order_rejected"`. See [Maintenance mode](/developers/maintenance-mode) for the other reason a delayed fill can wait past `eligibleAt`.
For the protocol layers the agent talks to:
Scoped tokens, HMAC signing, and which auth mode fits which flow.
The exact typed data your wallet signs, and the trading-wallet mode rule.
Default CTF vs neg-risk exchange routing and approvals.
Orderbook snapshots and updates, your order events, positions.
Prohibited conduct, exchange-side safeguards, and the limits your bot must enforce.
## AI agent operation
`AGENTS.md` in the repo is the operating contract and `SKILL.md` is the manual. Point Claude Code, OpenClaw, or any coding agent with shell and file access at them. The agent handles setup, the preflight, strategy selection, deployment, and monitoring; the human keeps the private key and the token out of the agent's context and funds the wallet.
The manual covers the SDK surface, websocket semantics, fees and the taker delay, the partner flows, and the known footguns, so the agent does not have to guess which API call does what. It also points the agent at the docs MCP server for anything newer than the manual.
## Support
* Builders Chat: [t.me/LimitlessBuildersChat](https://t.me/LimitlessBuildersChat), the Telegram community for API and SDK builders
* Repo issues: [github.com/limitless-labs-group/agents-starter/issues](https://github.com/limitless-labs-group/agents-starter/issues)
* Docs MCP server for coding agents: `https://docs.limitless.exchange/mcp`
* Trading MCP server for AI assistants: [MCP server](/developers/mcp-server)
* SDK reference: [TypeScript SDK](/developers/sdk/typescript/getting-started)
# Cross-Market Market Making
Source: https://docs.limitless.exchange/developers/cross-market-market-making
Quote a market on Limitless and hedge your fills on Polymarket to stay delta-neutral — a runnable, open-source market-making skill
Quote the **same** prediction market on two venues at once: rest BUY quotes on
**Limitless** (the maker side, where you capture the spread and earn maker
rebates) and hedge any fill straight back on **Polymarket** so your net position
stays \~flat. You're earning the cross-venue spread, not taking a directional
bet.
This ships as the **`cross-market-mm`** strategy in
[**agents-starter**](https://github.com/limitless-labs-group/agents-starter) —
a runnable skill you can take from clone → dry-run → live → flat. Its
[`SKILL.md`](https://github.com/limitless-labs-group/agents-starter/blob/main/src/strategies/cross-market-mm/SKILL.md)
is the canonical guide; this page is the Limitless-framed quickstart.
This is for **traders, builders, and AI agents** running their own
market-making bot. Limitless is the venue you quote on; Polymarket is a
secondary hedge venue with a one-time setup. If you're building a platform
where other users trade through your product, see
[Programmatic API](/developers/programmatic-api) instead.
Moves real money on two chains (Base + Polygon). Use a **dedicated wallet**,
keep order sizes small, and always start in `DRY_RUN`. Profit depends on fill
rate and the Limitless maker reward programs, not on the bot alone — read the
economics below and in the repo before expecting profit.
## How it works
```
Polymarket book ──▶ quote one margin step inside it on Limitless (BUY YES + BUY NO)
get filled on Limitless ──▶ BUY the offsetting side on Polymarket (FAK) ──▶ delta-flat
```
Limitless is where the spread is captured. Your resting quotes are **maker
liquidity** — and [makers pay no Limitless fee](/user-guide/fees) — so they earn
from [maker rebates](/user-guide/maker-rebates) when filled and
[LP rewards](/user-guide/lp-rewards) for resting near the midpoint. Polymarket is
where directional risk is flattened; that hedge is a taker order, so the strategy
also doubles as a Polymarket volume engine.
## The wallet model — one key
Everything is controlled by **one dedicated EOA private key**. That key signs on
both chains; funds live in a few places:
| Identity | What it is | Holds |
| - | - | - |
| **Your EOA** (`PRIVATE_KEY`) | the key you control | Base USDC + a little ETH (Limitless collateral + gas) |
| **Limitless** | trades from the EOA directly ([EIP-712](/developers/eip712-signing)) | — (uses the EOA's USDC) |
| **Polymarket deposit wallet** | a **key-less** ERC-1271 proxy derived from your EOA | **pUSD** (the hedge collateral) |
When you connect this EOA to the Limitless app (e.g. to mint the API token), decline the one-time "choose your trading wallet" prompt if it offers 1-click (smart wallet) trading. Accepting it puts the profile in smart-wallet mode, and every order the bot signs is rejected. Already accepted it? See [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
You never manage a second key — the deposit wallet is validated by your EOA's
signature. The only other credentials are **two API tokens** (not wallet keys):
a Limitless [scoped HMAC token](/developers/authentication) and a Polymarket
relayer API key (for the one-time deposit-wallet setup).
Polymarket's CLOB rejects orders from a Gnosis Safe maker, so even existing
Safe users trade via the deposit-wallet flow (signature type 3). pUSD must
live **in the deposit wallet** — it's a separate address from your Polymarket
UI login, so it won't show in the UI. `npm run cross-market-mm:status` is how you
see it.
## Quick start — dry-run in under 10 minutes (no real money)
Watch the full cross-venue loop — quote on Limitless, compute and log the
Polymarket hedge, return delta-flat — signing nothing and funding nothing. You
only need a Limitless [scoped HMAC token](/developers/authentication) for this.
```bash theme={null}
git clone https://github.com/limitless-labs-group/agents-starter.git
cd agents-starter && npm install
cp .env.example .env && chmod 600 .env
# set PRIVATE_KEY (any dedicated EOA), LMTS_TOKEN_ID, LMTS_TOKEN_SECRET. Leave DRY_RUN=true.
```
```bash theme={null}
npm run cross-market-mm:find-pairs # liquidity-ranked cross-venue shortlist
cp src/strategies/cross-market-mm/config.example.yaml ./cross-market-mm.config.yaml
# paste one pair into market_pairs; order_size: 5; dry_run: true
```
Verify both sides resolve on identical criteria — same asset, threshold, UTC
moment, source. Title overlap is not enough.
```bash theme={null}
SIMULATE_FILL=YES:5 DRY_RUN=true npm run cross-market-mm # Ctrl-C, then:
npm run cross-market-mm:analyze
```
A synthetic Limitless fill flows through the real hedger, which fires the
offsetting Polymarket hedge (logged, not sent) and returns the book to
delta-flat — the whole strategy, for \$0.
## Go live (one-time, \~20–30 minutes — honest)
Going live means funding two chains and deploying the Polymarket deposit wallet.
Most of the time is moving funds, not running commands.
Add a Polymarket relayer key to `.env` (`RELAYER_API_KEY` +
`RELAYER_API_KEY_ADDRESS`), then:
```bash theme={null}
npm run cross-market-mm:setup-poly
```
Derives + deploys your deposit wallet and approves pUSD (buy) **and** CTF
(sell) on Polymarket's v2 exchanges. Prints the address — set it as
`poly_funder` with `poly_signature_type: 3`.
Base: **USDC** + \~\$1–2 **ETH** → your EOA. Polygon: **pUSD** → the
**deposit-wallet** address (not your EOA / Safe / UI login). Confirm with
`npm run cross-market-mm:status`.
```bash theme={null}
npm start approve
```
[NegRisk markets](/user-guide/negrisk-overview) use a separate exchange, so
they need their own approval. Preflight tells you if it's missing.
Markets on [new trading venues](/developers/venue-system#new-trading-venues)
also need a USDC approval to the venue's `feeModule`.
```bash theme={null}
npm run cross-market-mm:preflight # validates auth, funding, sig type, approvals, pairs
# set dry_run: false, then:
npm run cross-market-mm
```
A green preflight means the next live run places real orders with no
surprise rejections.
## Lifecycle commands
| Command | What it does |
| - | - |
| `npm run cross-market-mm:find-pairs` | Liquidity-ranked shortlist of equivalent cross-venue pairs |
| `npm run cross-market-mm:setup-poly` | One-time: deploy the Polymarket deposit wallet + approvals |
| `npm run cross-market-mm:preflight` | Validate auth, funding, sig type, approvals, pairs (gate before live) |
| `npm run cross-market-mm` | Run the bot (`DRY_RUN` by default) |
| `npm run cross-market-mm:status` | Cross-venue portfolio: Limitless + the deposit wallet's pUSD/positions + net delta |
| `npm run cross-market-mm:close` | Exit to flat on **both** venues |
| `npm run cross-market-mm:analyze` | Summarize a run (orders, fills, how flat it stayed, hedges) |
## Safety
* **Loss circuit-breaker** (`max_loss_usd`, default \$10): halts on drawdown.
* **Flatten on stop:** Ctrl+C or a tripped breaker cancels all orders **and**
sells inventory to flat on both venues — a stop never leaves unhedged
directional risk.
* **Settled reads:** fills and hedges re-read a settled position before acting,
so a lagged read can't double-fill or double-hedge.
* **Dedicated wallet, small `order_size`, `DRY_RUN` default.** If anything looks
wrong: halt, flatten, check `status`.
## Economics & rewards
Your Limitless quotes are **maker liquidity, and makers pay no Limitless fee**
([fees](/user-guide/fees) apply to takers only). Your only direct cost is the
Polymarket FAK hedge's taker fee. Revenue comes from three places:
* **Cross-venue spread** — the margin you quote inside the Polymarket book,
captured when both legs fill.
* **[Maker rebates](/user-guide/maker-rebates)** — when a taker fills your
resting order, a share of their taker fee is rebated to makers pro-rata, paid
daily in USDC. Only **executed fills** earn. Current config rebates **100% of
eligible taker fees on Daily, Hourly Crypto, and 15-minute Crypto** markets.
* **[LP rewards](/user-guide/lp-rewards)** — daily USDC for orders resting
within a spread of the midpoint, above a per-market minimum size — paid for
quote *presence*, not fills. Closer to mid + more size = larger share.
To actually earn the reward programs, **size up and target eligible markets**.
`order_size: 5` is a smoke-test size — below typical LP-reward minimums and
too small for meaningful rebate credit. Short-window crypto (Hourly /
15-minute) is both 100%-rebate-eligible and the cleanest cross-venue match.
Reward parameters are the program's current config and can change.
## Honest caveats
* **You need liquidity on *both* venues at once** — Limitless takers to fill
your maker quotes, and a Polymarket book to hedge into. The venues don't
always overlap; the cleanest matches are crypto price markets, recurring macro
prints, and [NegRisk](/user-guide/negrisk-overview) winner markets (sports
finals, elections). `find-pairs` ranks candidates, but you confirm the
resolution criteria match.
* **The binding constraint is fill rate, not fees.** Rebates and LP rewards only
pay where your maker liquidity is actually used — eligible markets with real
taker flow on both venues. Far-dated winner futures look balanced but trade
thinly; short-window crypto is where the flow and the eligibility line up.
## Learn more
The single-venue starter and the three built-in Limitless strategies.
Scoped HMAC tokens — the Limitless credential this skill uses.
Daily USDC for maker liquidity — the real profit engine when sized to qualify.
Concept + endpoint mapping if you're coming from Polymarket.
The full guide, troubleshooting, and reproducible demo live in the repo:
[`SKILL.md`](https://github.com/limitless-labs-group/agents-starter/blob/main/src/strategies/cross-market-mm/SKILL.md),
[`QUICKSTART.md`](https://github.com/limitless-labs-group/agents-starter/blob/main/src/strategies/cross-market-mm/QUICKSTART.md),
[`DEPLOY.md`](https://github.com/limitless-labs-group/agents-starter/blob/main/src/strategies/cross-market-mm/DEPLOY.md).
# 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**. Use the venue's exchange address 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.
Signing orders with your own EOA key does not replace request authentication. There is no keyless programmatic path: you must still HMAC-sign every API request, including `POST /orders`, with a scoped API token. See [Authentication](/developers/authentication).
## EIP-712 domain
```json theme={null}
{
"name": "Limitless CTF Exchange",
"version": "1",
"chainId": 8453,
"verifyingContract": ""
}
```
Fetch the `verifyingContract` 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 — must match your profile's trading wallet (see [Trading wallet mode](#trading-wallet-mode-whose-address-signs)) |
| `signer` | address | Same as maker in EOA trading mode (see [Trading wallet mode](#trading-wallet-mode-whose-address-signs)) |
| `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 | Must be `0`. Non-zero expiration is not currently supported and orders signed with a non-zero value are rejected. |
| `nonce` | uint256 | Must be `0`. Non-zero nonce is not currently supported and orders signed with a non-zero value are rejected. |
| `feeRateBps` | uint256 | On [new trading venues](/developers/venue-system#new-trading-venues) (`venue.feeModuleType: "COLLATERAL"`), always `0`. On other venues: on fee-enabled markets (`metadata.fee: true`), the order owner's `effectiveFeeRateBps`; on fee-disabled markets, `0`. |
| `side` | uint8 | `0` = BUY, `1` = SELL |
| `signatureType` | uint8 | `0` = EOA signature |
For your own account, read `effectiveFeeRateBps` from [`GET /profiles/me`](/api-reference/portfolio/get-current-profile) before signing. The effective rate can be lower than `rank.feeRateBps`. For a partner EOA sub-account, sign with the sub-account's rate, not the partner's. Partner credentials cannot read a sub-account's private profile with `GET /profiles/{account}`. If the signed fee does not match, the API returns `400 FEE_RATE_MISMATCH` with `expectedFeeRateBps`; build and sign a new order with that rate before retrying.
On [new trading venues](/developers/venue-system#new-trading-venues), sign `feeRateBps` as `0` regardless of `metadata.fee` or your profile's rate. Any other value is rejected with `FEE_RATE_MISMATCH`. The EIP-712 domain is the same on new and legacy venues.
## Trading wallet mode: whose address signs
The backend validates `signer` and `maker` against your **profile's trading wallet mode** (`tradeWalletOption` on your profile), not against how you authenticate:
| Profile trading mode | Expected `maker` | Expected `signer` |
| - | - | - |
| `eoa` | your wallet address (`account`) | the same address |
| `smartWallet` | your smart-wallet address | your embedded wallet address (a managed key you cannot export) |
An account connected with an external wallet can still be in `smartWallet` mode. Accepting the app's one-time "choose your trading wallet" prompt sets it, and the prompt does not reappear. In that state, every self-signed API order fails with:
```
400 — Signer does not match - you should use embedded address for smart wallet
```
Self-signed API orders need **`eoa` mode**. Switch it with an authenticated profile update (HMAC-signed like any other request):
```
PUT /profiles
{"tradeWalletOption": "eoa"}
```
The switch is immediate and reversible. Sending `{"tradeWalletOption": "smartWallet"}` restores the previous mode, since your profile keeps its stored smart-wallet association. One side effect: with `eoa` mode set, trading in the Limitless web app on this account runs through your raw wallet (a confirmation per action) instead of 1-click trading. For a dedicated bot or API account, that is usually the right end state.
The TypeScript SDK's `HttpClient` has no `put` helper. Send the request with any HMAC-signing HTTP client, or reach the SDK's underlying axios instance, which signs every verb: `(client.http as unknown as { client: AxiosInstance }).client.put('/profiles', { tradeWalletOption: 'eoa' })`. The property is private, so TypeScript needs the cast; plain JavaScript can call `client.http.client.put` directly.
Two related errors from the same validation: `Signer does not match authenticated profile account` and `Maker does not match expected profile wallet` mean the respective field doesn't match the table above for your current mode.
## Order types
| Type | Description |
| - | - |
| **GTC** (Good-til-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`
**API Reference:** [docs.limitless.exchange/api-reference](/api-reference/introduction)
**WebSocket URL:** `wss://ws.limitless.exchange`
**Regions:** Both URLs are single global endpoints. There are no regional base URLs, mirrors, or region selection. All clients connect to the same endpoints regardless of location.
**Network:** Base mainnet (chain ID `8453`). There is no sandbox, testnet, mock mode, or Base Sepolia deployment — all integrations run against production with real USDC. To rehearse a flow without significant capital, place small live orders (e.g. the minimum order size for a CLOB market) on a low-volume market.
Building on the API or SDKs? The Builders Chat on Telegram is the place for API/SDK updates, help, and to connect with other builders.
## 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 a scoped API token 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 a scoped API token (a token ID + secret) from the [Limitless Exchange UI](https://limitless.exchange) → profile menu → Api keys. Sign every request with HMAC-SHA256 and send the `lmts-api-key`, `lmts-timestamp`, and `lmts-signature` headers. See [Authentication](/developers/authentication) for the signing protocol.
If your account has ever enabled 1-click (smart wallet) trading in the app, switch the profile to EOA trading mode before placing API orders — see [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
`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.
## Docs MCP for coding agents
Point your coding agent or docs assistant at the live Model Context Protocol server to give it up-to-date access to this documentation:
```
https://docs.limitless.exchange/mcp
```
Useful for agent frameworks and IDE tooling that support MCP — the server exposes the full docs (endpoints, SDK references, guides) as searchable context. See [Build a Trading Agent](/developers/build-a-trading-agent) for an end-to-end agent example.
To let an assistant trade, use the [Limitless trading MCP](/developers/mcp-server) at `https://api.limitless.exchange/mcp`.
# Maintenance Mode
Source: https://docs.limitless.exchange/developers/maintenance-mode
How integrations should handle temporary trading restrictions and maintenance notices
Maintenance mode lets Limitless temporarily restrict trading actions during planned maintenance, incidents, or other operational events. Integrations should treat it as part of normal exchange availability and check it before placing or cancelling orders.
Maintenance mode is public status information. It does not require authentication.
## When to check status
Call [`GET /maintenance/status`](/api-reference/system/maintenance-status) when your app starts and refresh it before submitting trading actions. Bots and partner backends should also handle `425 Too Early` responses with a trading-mode `code` from trading endpoints, because active restrictions can change after your last status check.
## Trading modes
Maintenance status is reported through `effects`. For trading, the possible `mode` values are:
| Mode | What to expect |
| - | - |
| `normal` | Trading is open. This is represented by no active trading restriction in the status response. |
| `post_only` | Only post-only order creation and cancellations are expected to succeed. Orders that would take liquidity may be rejected. |
| `cancel_only` | New order creation is unavailable. Cancellations are expected to remain available. |
| `disabled` | Order creation and cancellation are unavailable. |
## Trading behavior by mode
Use the active trading effect to decide which actions your integration should attempt.
| Active trading mode | Create regular orders | Create post-only orders | Cancel orders |
| - | - | - | - |
| No active effect (`normal`) | Allowed | Allowed | Allowed |
| `post_only` | Blocked | Allowed | Allowed |
| `cancel_only` | Blocked | Blocked | Allowed |
| `disabled` | Blocked | Blocked | Blocked |
When maintenance mode blocks an action, trading endpoints can return `425 Too Early` with a trading-mode `code`. Do not retry the same blocked action in a tight loop. Refresh maintenance status and wait until the active mode allows the action again.
If a market uses taker delay, a taker order may already be accepted with `execution.settlementStatus: "DELAYED"` before maintenance starts. Active maintenance can postpone the delayed fill past its `eligibleAt` timestamp. Do not treat that gap as a failed order; keep listening for order events and reconcile on the terminal `MINED` or `FAILED` event.
## Active vs scheduled maintenance
The status response has two arrays:
| Field | Meaning |
| - | - |
| `active` | Restrictions currently in effect. Integrations should apply these immediately. |
| `scheduled` | Future maintenance notices. These help you warn users or adjust bot behavior ahead of time. |
Scheduled entries are notices. They tell you what is planned and when, but your integration should still rely on `active` and trading endpoint responses for the current enforced state.
## Recommended integration behavior
Call `GET /maintenance/status?target=trading` to get trading-specific maintenance information.
If `active` contains a trading effect, adjust your UI or bot behavior to the reported mode.
If `scheduled` contains entries, surface the `publicMessage`, `startsAt`, and `endsAt` values to users or operators.
If a trading request returns `425` with a trading-mode `code`, stop retrying the same action immediately and refresh maintenance status. Retry only when the mode allows that action again.
## Example
```json theme={null}
{
"active": [
{
"startsAt": "2026-06-22T15:00:00.000Z",
"endsAt": "2026-06-22T18:00:00.000Z",
"publicMessage": "Trading is temporarily limited to cancellations.",
"effects": [{ "target": "trading", "mode": "cancel_only" }]
}
],
"scheduled": [
{
"startsAt": "2026-06-22T18:00:00.000Z",
"endsAt": "2026-06-22T19:00:00.000Z",
"publicMessage": "Trading will be post-only during the next maintenance stage.",
"effects": [{ "target": "trading", "mode": "post_only" }]
}
]
}
```
## Error handling
When maintenance mode blocks a trading action, trading endpoints can return `425 Too Early`. Treat this as an exchange state signal, not as a malformed request.
For `POST /orders`, `425` is also used by receive-window checks. Use the response body to distinguish the cases: a trading-mode `code` means maintenance mode; a receive-window response does not include that `code`.
### `425` maintenance examples
`post_only` blocks non-post-only order creation. Cancellations and post-only orders are expected to remain available.
```json theme={null}
{
"code": "post_only_mode",
"message": "Trading is currently post-only. Only post-only orders and cancels are allowed.",
"mode": "post_only",
"resumeAt": "2026-06-22T18:00:00.000Z"
}
```
`cancel_only` blocks new order creation. Cancellations are expected to remain available.
```json theme={null}
{
"code": "cancel_only_mode",
"message": "Trading is currently cancel-only. New orders are not accepted, but cancels are allowed.",
"mode": "cancel_only",
"resumeAt": "2026-06-22T18:00:00.000Z"
}
```
`disabled` blocks both order creation and cancellations.
```json theme={null}
{
"code": "trading_disabled",
"message": "Trading is currently disabled.",
"mode": "disabled",
"resumeAt": "2026-06-22T18:00:00.000Z"
}
```
`resumeAt` may be omitted if no expected end time is available.
Recommended behavior:
* Refresh [`GET /maintenance/status?target=trading`](/api-reference/system/maintenance-status).
* Do not keep submitting blocked order actions in a tight loop.
* Continue actions that are allowed by the current mode, such as cancellations during `cancel_only`.
* For delayed taker orders, keep waiting for order events if maintenance is active when `eligibleAt` passes.
* Resume normal trading only after status and endpoint responses indicate trading is available again.
# MCP Server
Source: https://docs.limitless.exchange/developers/mcp-server
Connect Claude, Grok, or any MCP client to Limitless: research markets, read order books and price history, check positions, and propose orders that you approve in your browser
Limitless runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server. Add it to an AI assistant and the assistant can search markets, read order books and price history, check your balance and positions, and prepare orders, all from a chat window.
It is non-custodial. The server holds no keys and cannot move your funds. Every new order is a proposal that you review and submit yourself on limitless.exchange.
Questions about the connector, or building something on top of it? The Builders Chat is where integration questions get answered.
**Which integration do I want?** The MCP server is for **conversational** research and trading through an assistant, with a human confirming every order. If you want an unattended bot, see [Build a Trading Agent](/developers/build-a-trading-agent). If you are a platform placing orders on behalf of your own users, see [Programmatic API](/developers/programmatic-api).
An assistant trading through the connector is bound by the same conduct rules as any trader. [Building Responsible Trading Agents](/developers/responsible-agents) lists what is prohibited, what the exchange enforces, and where to report abuse.
## Connect
The endpoint is:
```
https://api.limitless.exchange/mcp
```
Add it as a remote MCP server in your client. Each client has its own way in, usually a custom connector or integration setting.
Settings, Connectors, add a custom connector, paste the endpoint, then sign in to Limitless in the browser window that opens and approve access.
```sh theme={null}
claude mcp add --transport http limitless https://api.limitless.exchange/mcp
```
Then run `/mcp` and complete the sign-in in your browser.
Add a custom connector from the connectors settings, paste the endpoint, and connect your Limitless account when prompted.
Any client that supports remote MCP servers over streamable HTTP with OAuth can connect using the same endpoint. The server accepts `POST` only, with JSON responses and no session state.
You do not need a Limitless account beforehand. If you have never used Limitless, sign in from the approval page and a wallet is created for you as part of the flow.
### Requirements
Three things must be true when you approve the connection:
1. **You are signed in to Limitless.** If you are signed out when your assistant sends you over, sign in and the flow continues.
2. **Your trading wallet is your Limitless Wallet.** Social login creates one automatically. If you connected an external wallet, the approval page offers to switch you. Switching also applies to trades you place yourself in the app.
3. **You have USDC on Base** in that wallet.
The wallet check runs on **every write**, not only at setup. If you later switch your trading mode back to an external wallet, `place_orders`, `cancel_order`, and `cancel_all_orders` return *Write actions require the user to select their Limitless Wallet* until you switch back. Read tools keep working.
## Wallets, funding, and custody
One address matters: your Limitless Wallet. If you signed in with email or a social account, it was created for you and there is nothing to choose. If you connected an external wallet, your Limitless Wallet is a separate address from that wallet, so do not send USDC to the one you connected with. See [Wallet Types](/user-guide/wallet-types) for the full picture.
Do not guess the address. Ask your assistant for your balance after connecting and `get_wallet_balance` returns the exact address to fund, plus how much of your balance is already committed to resting orders.
Send **USDC on Base**. Limitless holds no platform balance, so your funds stay in your own wallet throughout. Gas is sponsored for the Limitless Wallet, so you do not need ETH. Bridge to Base first: USDC sent on another chain is not credited.
### What the connector is allowed to do
Access is an OAuth grant on your account, not a key. The server never sees a private key.
* **No withdraw tool and no transfer tool exist.** The connector cannot move funds off the platform.
* **The grant carries a single scope, `trading`.** Withdrawal is a separate scope that this grant never requests and never receives.
* **Access tokens last one hour. The grant lasts 30 days.** Revoking it from Limitless invalidates every token immediately.
To withdraw, use the Withdraw action in the Limitless app as usual. Your assistant is not part of that path.
## Tools
Fifteen tools. Every one requires an authenticated connection, market data included. `place_orders`, `cancel_order`, and `cancel_all_orders` additionally require the `trading` scope, which every grant currently carries.
### Market data
| Tool | Returns |
| - | - |
| `search_markets` | Semantic search, `limit` 1 to 50, default 10. Each result carries `kind` (`market` or `group`), status, deadline, `prices` for YES and NO with a `source` of `orderbook_top`, `midpoint`, or `unavailable`, cumulative `volume`, and orderability. `orderableOnly` defaults to `true`, which drops markets that are not open, past their deadline, or AMM rather than CLOB, and groups with no orderable child; pass `orderableOnly: false` to include them. |
| `list_markets` | Paginated listing, up to 50 per page. Accepts `categoryPath` from `list_market_categories` and an optional `categoryFilter`. When a category is given, the response also returns the available `categoryFilters` for that category (for example Duration, Ticker) with option counts, plus `pagination`. Same entity shape and `orderableOnly` default as search. |
| `list_market_categories` | The navigation category tree with paths and market counts. |
| `get_market` | One market by slug: title, HTML description with the resolution criteria, status, deadline, YES and NO position ids, `volume`, orderability with a reason, trading rules including the fee model, and liquidity reward thresholds. |
| `get_market_group` | A group by slug with its child markets. Children carry id, slug, title, status, deadline, `volume`, and orderability, but no prices. Pass a **child** slug to any other tool. |
| `get_orderbook` | Point-in-time book for one market: `bids`, `asks`, `bestBid`, `bestAsk`, `spread`, `midpoint`, `lastTradePrice`, `bookState` (`empty`, `one_sided`, or `two_sided`), and a `liquidityRewards` block. Read reward thresholds from `get_market`; the orderbook's block can be partial. |
| `get_market_price_history` | Newest-first historical **YES midpoint** observations for one market. Intervals `5m`, `1h`, `6h`, `1d` (default), `1w`, `1m`, `all`. Longer lookbacks are downsampled. Takes one exact market slug; for a group, pass a child slug. |
### Your account
| Tool | Returns |
| - | - |
| `get_wallet_balance` | USDC balance of the Limitless Wallet, collateral locked by open BUY orders (`openBuyOrderCommitment`), and `availableForNewBuyOrders`. Includes the wallet address to fund. |
| `get_positions` | Cached position snapshot across the wallets linked to the account. `tokensBalance` is holdings. `marketValue` and `unrealizedPnl` use reference prices. `realisedPnl` is cumulative. |
| `get_open_orders` | LIVE orders across all markets, cursor-paginated, up to 100 per page. Includes `isEarning` from the latest reward epoch. |
| `get_trade_history` | Your fills, cursor-paginated, up to 100 per page, optional market filter. Each entry has an explicit `side` of `BUY`, `SELL`, or `null` for non-trade events. |
### Trading
| Tool | Behavior |
| - | - |
| `place_orders` | Builds a proposal for 1 to 10 orders and returns an `approvalUrl`. **Nothing is submitted** until you open the link and approve. |
| `check_order_status` | Outcome of a proposal by its `state`: `pending` (with the proposed `orders[]`), `approved` with `orderIds`, `rejected`, or `expired`. |
| `cancel_order` | Cancels one open order by id. **Executes immediately.** |
| `cancel_all_orders` | Cancels every open order in one market. **Executes immediately.** |
## How ordering works
The assistant cannot place an order. `place_orders` returns a link. You open it, see the exact terms on limitless.exchange, and submit. Nothing exists until you do.
* Up to **10 orders** share one approval.
* A proposal **expires after 10 minutes** if you do not act on it. After that `check_order_status` returns `expired` and the assistant needs a fresh `place_orders`.
* At most **50 proposals per account per 10 minutes**. Beyond that `place_orders` returns *Too many order approval requests; wait a few minutes before trying again*.
* The response includes a **fee preview per order**, so max loss, max payout, and the taker fee are known before you approve.
* Order fields: a single market slug (child slug for groups), `side` BUY or SELL, `outcomeIndex` 0 for YES or 1 for NO, `orderType` GTC (default) or FAK, `price` strictly between 0 and 1, and `shares`. **Price times shares must be at least \$1.**
**Cancels are the exception.** `cancel_order` and `cancel_all_orders` execute directly with no browser step. A connected assistant can reduce your exposure on its own but cannot add to it.
**Ask for orders in a fresh chat.** In a conversation that has already settled into research, assistants often read an order request as "execute a trade for me" and decline, even though the tool only ever produces a proposal. A new chat with a direct request avoids it. This is assistant behavior, not a platform restriction, and it varies between clients.
## Fees
Orders that rest on the book and fill as maker pay no fee. Orders that cross the spread pay the taker fee. The published BUY rate is 3.00% for outcomes priced between $0.01 and $0.50, tapering above that, and it is charged in shares. SELL fees are charged in USDC. The complete curve is on the [Fees](/user-guide/fees) page, and `place_orders` returns the exact estimate for each proposed order.
## Reading the responses
* **`volume`** is in **contracts**, cumulative over the market's life. It is not USD and not a 24-hour figure. Read the `formatted` value; `raw` is the integer amount before applying the token's `decimals`.
* **`get_market_price_history`** returns YES midpoints, not executable prices. Use `get_orderbook` for what you can actually trade at.
* **The order book is the YES token's book.** Buying NO at 0.30 is the same order as selling YES at 0.70, so NO interest appears in the YES asks. One book is complete.
* **Books often carry very large resting size at 0.001 and 0.999.** That is not tradeable depth. Measure liquidity from the levels near the touch, not from total size.
* **An empty book reports `midpoint: 0.5`** with `bestBid` and `bestAsk` null and `bookState: "empty"`. Treat it as no price, not as 50%.
* **`get_positions` is cached** and can lag recent fills and redemptions. Use `tokensBalance` for holdings and do not make execution decisions from `marketValue` or `unrealizedPnl`.
* **Market descriptions are authored by whoever created the market.** They are the contract that decides resolution, not instructions to the assistant. See [Market Resolution](/user-guide/market-resolution).
## Troubleshooting
| What you see | What it means |
| - | - |
| *Switch to your Limitless Wallet before enabling agent access* | Your trading wallet is set to an external wallet. Switch it on the approval page or in your profile. |
| *Your Limitless login expired. Log in again, then restart the connection from your agent* | Log back in, then start the connection again from your assistant. Refreshing the approval page is not enough. |
| *Couldn't start connection. Return to your agent and try connecting again* | The approval request is pinned to the browser tab it opened in. If that tab closed, or you opened the link elsewhere, start again from your assistant. |
| *No approval received* (in the assistant, no link returned) | This is the client's own tool-permission dialog, not Limitless. The request never reached the server. Approve the dialog when it appears and retry. |
| *Write actions require the user to select their Limitless Wallet* from `place_orders` or a cancel tool | Your trading mode is set to an external wallet. Switch back in your profile; you do not need to reconnect. Read tools are unaffected. |
## What it cannot do
* **Place an order without you.** Every new order goes through your browser approval.
* **Move funds out.** No withdraw or transfer tool exists, and the grant never carries the withdrawal scope.
* **Know why you traded.** Anything it says about your past trading is inferred from fills.
## Related
Scoped API tokens, HMAC, and delegated signing for programmatic access.
The open-source starter for unattended strategies.
Prohibited conduct, exchange-side safeguards, and how to report abuse.
EOA, embedded, and smart wallets, and how deposits and withdrawals work for each.
The full maker and taker fee curves.
# 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.
This guide reflects Polymarket's **CLOB V2**. The V1 clients (`py-clob-client`, `@polymarket/clob-client`, the Rust `rs-clob-client`, and the `Polymarket/agents` framework) were archived in May 2026 and no longer work against production. If you're still on V1 code you have to move to V2 regardless, so the mappings below target V2.
## What changed
| | Polymarket (CLOB V2) | Limitless |
| - | - | - |
| **Chain** | Polygon (137) | Base (8453) |
| **Collateral** | pUSD (6 decimals, wraps USDC.e) | USDC (6 decimals) |
| **REST hosts** | `gamma-api.polymarket.com` (markets, search) + `clob.polymarket.com` (orderbook, pricing, trading) + `data-api.polymarket.com` (positions, trades, activity) | `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 across 5 `POLY_*` headers | HMAC-signed scoped API tokens (`lmts-api-key`/`lmts-timestamp`/`lmts-signature`) for all integrations; granular scopes for [partner integrations](/developers/programmatic-api) |
| **SDKs** | V2 CLOB clients `@polymarket/clob-client-v2`, `py_clob_client_v2` (V1 clients archived) | [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, 1271 (`signatureType` 0-3) | EOA (`signatureType` 0) for self-signed API orders, or server-managed wallets via [delegated signing](/developers/programmatic-api#delegated-order-types). Other signature types exist for the web app's smart wallets and are verified on-chain, but they are not a Polymarket-style proxy/Gnosis mapping; use 0. Self-signing also requires the profile to be in EOA trading mode; see [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs) |
| **Market lookup** | `conditionId` or `clobTokenIds` | `slug` or address |
| **Token IDs** | `clobTokenIds` paired with `outcomes` by index | `tokens.yes` = Yes, `tokens.no` = No |
| **Order signing** | EIP-712 `verifyingContract` = CTF Exchange V2, domain version `"2"` | EIP-712 `verifyingContract` = `venue.exchange`, domain version `"1"` |
| **Order struct** | V2 dropped `taker`, `nonce`, `feeRateBps`, `expiration` from the signed order and added `timestamp`, `metadata`, `builder` | Classic CTF fields: `taker`, `expiration`, `nonce`, `feeRateBps` (no `timestamp`/`metadata`/`builder`) |
| **Order freshness** | V2 order carries a mandatory creation `timestamp` (ms); GTD orders have a \~1 minute expiration threshold | Optional `timestamp` + `recvWindow` on `POST /orders`; stale orders rejected with HTTP `425`. See [Receive Window](/api-reference/trading/create-order#receive-window). |
| **NegRisk** | Separate Neg Risk CTF Exchange V2 contract | 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 scoped API token (a token ID + secret) generated in the UI. You sign each request with HMAC-SHA256 and send three headers: `lmts-api-key`, `lmts-timestamp`, and `lmts-signature`.
```python theme={null}
from py_clob_client_v2 import ClobClient
client = ClobClient(host="https://clob.polymarket.com", chain_id=137, key=private_key)
creds = client.create_or_derive_api_key()
client = ClobClient(
host="https://clob.polymarket.com",
chain_id=137,
key=private_key,
creds=creds,
)
```
```python theme={null}
import hmac, hashlib, base64, requests
from datetime import datetime, timezone
API_BASE = "https://api.limitless.exchange"
TOKEN_ID = "your_token_id_here"
SECRET = "your_token_secret_here" # base64-encoded secret
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}
# Sign per request. The message is: {ISO timestamp}\n{METHOD}\n{path+query}\n{body}
# (GET body is ""). Add Content-Type for requests with a JSON body.
```
Generate your token at [limitless.exchange](https://limitless.exchange) → profile → **Api keys**.
No `POLY_*` headers, but you do sign each request: derive the three `lmts-*` headers with `sign_request(TOKEN_ID, SECRET, method, path, body)` for every authenticated call. The timestamp must be within 30 seconds of server time. See the full [Authentication guide](/developers/authentication) for details, and the [Programmatic API guide](/developers/programmatic-api) for partner scopes (sub-account management and delegated order signing).
## Endpoint mapping
### Market data
| Action | Polymarket (CLOB V2) | Limitless |
| - | - | - |
| List active markets | `GET gamma-api.polymarket.com/markets?closed=false` | `GET /markets/active` |
| Get market by slug | `GET gamma-api.polymarket.com/markets/slug/:slug` | `GET /markets/:slug` |
| Search markets | `GET gamma-api.polymarket.com/public-search?q=...` | `GET /markets/search?query=...` |
| Orderbook | `GET clob.polymarket.com/book?token_id=...` | `GET /markets/:slug/orderbook` |
| Price history | `GET clob.polymarket.com/prices-history?market=...` | `GET /markets/:slug/historical-price` |
| Midpoint price | `GET clob.polymarket.com/midpoint?token_id=...` | Compute from orderbook response |
### Trading
| Action | Polymarket (CLOB V2) | Limitless |
| - | - | - |
| Place order | `POST clob.polymarket.com/order` | `POST /orders` |
| Cancel order | `DELETE clob.polymarket.com/order` (`orderID` in body) | `DELETE /orders/:orderId` |
| Cancel multiple | `DELETE clob.polymarket.com/orders` (array of ids in body) | `POST /orders/batch-cancel` (`orderIds` or `clientOrderIds`; all in one market). `POST /orders/cancel-batch` is the older `orderIds`-only form and still works. |
| Cancel all | `DELETE clob.polymarket.com/cancel-all` | `DELETE /orders/all/:slug` |
| Get open orders | `GET clob.polymarket.com/data/orders` | `GET /markets/:slug/user-orders` |
| Get order by ID | `GET clob.polymarket.com/data/order/:id` | `POST /orders/status/batch` |
### Portfolio
| Action | Polymarket (CLOB V2) | Limitless |
| - | - | - |
| Get positions | `GET data-api.polymarket.com/positions?user=0x...` | `GET /portfolio/positions` |
| Get trades | `GET clob.polymarket.com/data/trades?maker_address=0x...` | `GET /portfolio/trades` |
| PnL chart | N/A | `GET /portfolio/pnl-chart` |
| Trade history | `GET data-api.polymarket.com/activity?user=0x...` | `GET /portfolio/history` |
## Fetch a market
```python theme={null}
# Gamma for metadata, CLOB for trading, data-api for positions/activity
market = requests.get(
"https://gamma-api.polymarket.com/markets/slug/will-trump-win"
).json()
# clobTokenIds and outcomes are parallel arrays — pair by index, don't assume [0] = Yes
token_ids = market["clobTokenIds"]
outcomes = market["outcomes"]
yes_idx = outcomes.index("Yes")
token_id = token_ids[yes_idx]
condition_id = market["conditionId"]
neg_risk = market["negRisk"]
tick_size = market["orderPriceMinTickSize"]
```
```python theme={null}
# Market reads are public, but signing also works on any endpoint.
path = "/markets/will-trump-win"
market = requests.get(
f"{API_BASE}{path}",
headers=sign_request(TOKEN_ID, SECRET, "GET", path),
).json()
token_id = market["tokens"]["yes"] # 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
Both sides use EIP-712 typed data, but the order field sets diverge in V2. Polymarket V2 dropped `taker`, `nonce`, `feeRateBps`, and `expiration` from the signed struct and added `timestamp`, `metadata`, and `builder`. Limitless keeps the classic CTF-style fields (`taker`, `expiration`, `nonce`, `feeRateBps`) and has no `timestamp`/`metadata`/`builder`. Key differences:
| Field | Polymarket (CLOB V2) | Limitless |
| - | - | - |
| `chainId` | `137` (Polygon) | `8453` (Base) |
| `verifyingContract` | CTF Exchange V2 `0xE111180000d2663C0091e4f400237545B87B996B` (Neg Risk: `0xe2222d279d744050d28e00520010520000310F59`) | `venue.exchange` from market data |
| `name` (EIP-712 domain) | `"Polymarket CTF Exchange"` (same for Neg Risk) | `"Limitless CTF Exchange"` |
| `version` (EIP-712 domain) | `"2"` | `"1"` |
| `tokenId` source | `clobTokenIds[i]` paired with `outcomes[i]` | `tokens.yes` / `tokens.no` |
| Fields dropped vs your V1 code | `taker`, `nonce`, `feeRateBps`, `expiration` | n/a |
| Fields added | `timestamp` (ms), `metadata`, `builder` | n/a |
| `feeRateBps` | Not in the signed order (removed in V2) | Must equal your `effectiveFeeRateBps` on fee-enabled markets; `0` on fee-disabled markets and on [new trading venues](/developers/venue-system#new-trading-venues) |
On fee-enabled markets (`metadata.fee: true`), sign with your profile's `effectiveFeeRateBps` from [`GET /profiles/me`](/api-reference/portfolio/get-current-profile). On fee-disabled markets, sign `0`. On [new trading venues](/developers/venue-system#new-trading-venues) (`venue.feeModuleType: "COLLATERAL"`), always sign `0`. If the API returns `FEE_RATE_MISMATCH`, build and sign a new order with `expectedFeeRateBps`. Polymarket V2 has no `feeRateBps` field.
```python theme={null}
from py_clob_client_v2 import OrderArgs, OrderType, PartialCreateOrderOptions, Side
resp = client.create_and_post_order(
order_args=OrderArgs(token_id=token_id, price=0.50, size=10, side=Side.BUY),
options=PartialCreateOrderOptions(tick_size="0.01"),
order_type=OrderType.GTC,
)
```
```python theme={null}
import json, time
import requests
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)
fee_rate_bps = 0 # always 0 on new trading venues (feeModuleType "COLLATERAL")
if market["venue"].get("feeModuleType") != "COLLATERAL" and (market.get("metadata") or {}).get("fee"):
path = "/profiles/me"
profile_resp = requests.get(
f"{API}{path}",
headers=sign_request("GET", path),
)
profile_resp.raise_for_status()
fee_rate_bps = profile_resp.json()["effectiveFeeRateBps"]
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": fee_rate_bps,
"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()
path = "/orders"
body = json.dumps({
# expiration is signed as uint256 0 but sent as the string "0"; price is wire-only and required for GTC
"order": {**order_data, "expiration": "0", "price": price, "signature": signature},
"ownerId": owner_id,
"orderType": "GTC",
"marketSlug": "will-trump-win",
})
auth_headers = sign_request(TOKEN_ID, SECRET, "POST", path, body)
auth_headers["Content-Type"] = "application/json"
resp = requests.post(f"{API_BASE}{path}", headers=auth_headers, data=body)
```
`ownerId` is your Limitless profile ID — a required field on every `POST /orders` request. Fetch it once from [`GET /profiles/me`](/api-reference/portfolio/get-current-profile) (the `id` field) and cache it. 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 Polymarket's V2 CLOB clients (`@polymarket/clob-client-v2`, `py_clob_client_v2`) work.
## Porting a Polymarket agent
If you're adapting a Polymarket trading bot (a V2 CLOB client integration, or a port of the now-archived `Polymarket/agents` framework), here's a checklist:
Replace `POLYGON_WALLET_PRIVATE_KEY` with your existing private key on Base. Add your scoped API token (`TOKEN_ID` + `SECRET`) and sign each request with HMAC (`lmts-api-key`/`lmts-timestamp`/`lmts-signature`) instead of Polymarket's `POLY_*` headers. You still drop Polymarket's L1/L2 credential derivation — Limitless tokens are issued in the UI.
Replace `gamma-api.polymarket.com`, `clob.polymarket.com`, and `data-api.polymarket.com` all with `api.limitless.exchange`. Positions and history that you read from `data-api` move to `/portfolio/*`.
Update `chainId` from `137` to `8453` in all EIP-712 domain definitions.
Change the domain `name` from `"Polymarket CTF Exchange"` to `"Limitless CTF Exchange"`, and the `version` from `"2"` to `"1"`.
Replace `clobTokenIds` lookups with the market's `tokens` object. On Limitless the convention is fixed: `tokens.yes` = Yes, `tokens.no` = No (unlike Polymarket, where you pair `clobTokenIds` against `outcomes`).
Fetch `venue.exchange` from `GET /markets/:slug` and use it as `verifyingContract`. On Polymarket V2 you targeted the fixed CTF Exchange V2 contract — on Limitless each market can have its own venue.
Remove the V2 fields `timestamp`, `metadata`, and `builder` — Limitless doesn't use them. Add back the classic CTF fields Limitless requires: `taker`, `expiration`, `nonce`, and `feeRateBps`. Also drop SDK-side params like `tick_size` and `signature_type` from your order construction.
`POST /orders` requires an `ownerId` field (your Limitless profile ID, from [`GET /profiles/me`](/api-reference/portfolio/get-current-profile)). 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 pUSD on Polygon) to `venue.exchange`. For NegRisk SELL orders, also approve Conditional Tokens to `venue.adapter`. On [new trading venues](/developers/venue-system#new-trading-venues), also approve USDC to `venue.feeModule`. See [venue system](/developers/venue-system).
## WebSocket
| | Polymarket (CLOB V2) | Limitless |
| - | - | - |
| **URL** | `wss://ws-subscriptions-clob.polymarket.com/ws/market` and `/ws/user` | `wss://ws.limitless.exchange` |
| **Namespace** | Channel-based (`market`, `user`) | Socket.IO namespace `/markets` |
| **Auth** | `auth` object (`apiKey`, `secret`, `passphrase`) in the user-channel subscribe message | HMAC auth headers (`lmts-api-key`/`lmts-timestamp`/`lmts-signature`) during handshake |
See [WebSocket events](/developers/websocket/overview) for the full event reference.
## Quick reference for agents
If you're building an LLM-powered agent or bot, here's the minimal flow:
```python theme={null}
import os, json, time, hmac, hashlib, base64, requests
from datetime import datetime, timezone
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import Web3
API = "https://api.limitless.exchange"
TOKEN_ID = os.environ["LMTS_TOKEN_ID"] # scoped API token id
SECRET = os.environ["LMTS_TOKEN_SECRET"] # base64-encoded secret
PK = os.environ["PRIVATE_KEY"] # 0x...
OWNER_ID = int(os.environ["OWNER_ID"]) # Profile ID from GET /profiles/me
def sign_request(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}
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=sign_request("GET", "/markets/active"),
).json()
slug = markets["data"][0]["slug"] # rows with a nested "markets" list are groups; pick a child slug for trading
# 2. Get venue + token IDs (cache per market — these are static)
market_path = f"/markets/{slug}"
market = requests.get(
f"{API}{market_path}",
headers=sign_request("GET", market_path),
).json()
venue_exchange = market["venue"]["exchange"]
yes_token = market["tokens"]["yes"]
fee_rate_bps = 0 # always 0 on new trading venues (feeModuleType "COLLATERAL")
if market["venue"].get("feeModuleType") != "COLLATERAL" and (market.get("metadata") or {}).get("fee"):
profile = requests.get(
f"{API}/profiles/me",
headers=sign_request("GET", "/profiles/me"),
)
profile.raise_for_status()
fee_rate_bps = profile.json()["effectiveFeeRateBps"]
# 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": fee_rate_bps,
"side": 0,
"signatureType": 0,
}
# 4. Sign (EIP-712) — same encode_typed_data mechanism as Polymarket, different fields
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 (HMAC-sign the request over method + path + body)
order_body = json.dumps({
# expiration is signed as uint256 0 but sent as the string "0"; price is wire-only and required for GTC
"order": {**order, "expiration": "0", "price": 0.50, "signature": sig},
"ownerId": OWNER_ID,
"orderType": "GTC",
"marketSlug": slug,
})
order_headers = sign_request("POST", "/orders", order_body)
order_headers["Content-Type"] = "application/json"
resp = requests.post(f"{API}/orders", headers=order_headers, data=order_body)
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
GTC (limit), FAK (fill-and-kill) and FOK (fill-or-kill) 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.
# Migrate to the new trading venues
Source: https://docs.limitless.exchange/developers/migrate-to-new-trading-venues
What changes for API traders and integrators when markets launch on the new trading venues
New markets on Limitless launch on new trading venues. On these venues the trading fee is paid in USDC from your wallet, instead of being part of the signed order. Markets created before the switch stay on their original venue and work exactly as before.
## Who needs to do what
| You trade through | What to do |
| - | - |
| The Limitless web or mobile app | Nothing. The app handles it. |
| An official SDK | Upgrade to a version listed under [SDK versions](#sdk-versions). |
| Your own code that signs orders | Follow the [checklist](#checklist) below. |
| A server wallet (created and managed by Limitless) | Nothing. Its approvals are set up for you. |
## Checklist
`GET /markets/{slug}` returns a `venue` object. A market is on a new venue when `venue.feeModuleType` is `"COLLATERAL"`; the venue then also has a `feeModule` address. Legacy venues don't have either field.
```json theme={null}
{
"venue": {
"exchange": "0x05c748E2f4DcDe0ec9Fa8DDc40DE6b867f923fa5",
"adapter": null,
"feeModule": "0x7B6ffe5bA8B5ec7afB0AaBaE629D12dA5bC62565",
"feeModuleType": "COLLATERAL"
}
}
```
From the wallet that signs your orders, approve USDC to `venue.feeModule`. A max approval is easiest, and you need it only once per fee module. Every order except `postOnly` needs it, both BUY and SELL.
Simple and NegRisk (grouped) markets use different fee modules, so read the address from the market rather than hardcoding it. See [addresses](#addresses).
On new-venue markets, sign every order with `feeRateBps` set to `0`, whatever the market's `metadata.fee` flag or your profile's rate. Orders on legacy venues keep the [existing fee rate rules](/developers/eip712-signing#field-reference).
On new venues a buy needs the order amount plus the fee, so leave some USDC in the wallet beyond what you plan to spend.
**Nothing else changes.** The exchange and adapter contracts are the same, your existing USDC and Conditional Token approvals stay valid, and the [EIP-712 domain](/developers/eip712-signing#eip-712-domain) is unchanged.
```typescript theme={null}
import { erc20Abi, maxUint256 } from 'viem';
const API_URL = 'https://api.limitless.exchange';
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const { venue } = await (await fetch(`${API_URL}/markets/${slug}`)).json();
const isNewVenue = venue?.feeModuleType === 'COLLATERAL';
if (isNewVenue) {
// Once per fee module, from the wallet that signs orders
await walletClient.writeContract({
address: USDC,
abi: erc20Abi,
functionName: 'approve',
args: [venue.feeModule, maxUint256],
});
}
// legacyFeeRateBps follows the legacy rules: your effectiveFeeRateBps
// on fee-enabled markets, 0 on fee-disabled markets.
const feeRateBps = isNewVenue ? 0 : legacyFeeRateBps;
```
## Addresses
On Base. Always prefer the address returned in `venue.feeModule`.
| Contract | Address |
| - | - |
| Fee module, simple markets | `0x7B6ffe5bA8B5ec7afB0AaBaE629D12dA5bC62565` |
| Fee module, NegRisk markets | `0x01b7dF643247B377D5c39FcC40b9c28B4A827524` |
| USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
The exchange and adapter addresses are unchanged. See [Smart Contracts](/user-guide/smart-contracts) for the full list.
## SDK versions
Version 1.2.0 of every official SDK detects the venue and signs `feeRateBps` as `0` on new-venue markets, including cancel-replace and delegated orders.
| SDK | Minimum version |
| - | - |
| [TypeScript](/developers/sdk/typescript/getting-started) (`@limitless-exchange/sdk`) | 1.2.0 |
| [Python](/developers/sdk/python/getting-started) (`limitless-sdk`) | 1.2.0 |
| [Go](/developers/sdk/go/getting-started) (`limitless-exchange-go-sdk`) | v1.2.0 |
| [Rust](/developers/sdk/rust/getting-started) (`limitless-exchange-rust-sdk`) | 1.2.0 |
```bash theme={null}
npm install @limitless-exchange/sdk@^1.2.0
pip install -U "limitless-sdk>=1.2.0"
go get github.com/limitless-labs-group/limitless-exchange-go-sdk@v1.2.0
cargo add limitless-exchange-rust-sdk@1.2.0
```
The SDKs don't send the fee module approval for you. Approve it once per signing wallet, as in the [checklist](#checklist).
If you build orders with a low-level builder, such as `OrderBuilder`, instead of the order client, set `feeRateBps` to `0` yourself on new venues. Builders don't look up the market's venue.
## Errors
| Error | Cause | Fix |
| - | - | - |
| `Insufficient collateral allowance for the order fee.` | The signing wallet hasn't approved USDC to `venue.feeModule`. | Approve USDC to the fee module, then resubmit. |
| `FEE_RATE_MISMATCH` | The order was signed with a non-zero `feeRateBps` on a new venue. | Sign a new order with `feeRateBps` set to `0`. |
For how venues work in general, see [Venue system](/developers/venue-system#new-trading-venues).
# 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. Everything runs through HMAC-authenticated API tokens with scoped permissions.
Building a partner integration? Join the Builders Chat on Telegram for API and SDK updates, integration help, and to connect with other builders.
**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; applications from individual traders are not approved, since they already have full self-serve access.
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.
## Maintenance awareness
Production integrations should check [Maintenance Mode](/developers/maintenance-mode) before submitting trading actions and handle `425 Too Early` responses with a trading-mode `code` from trading endpoints. During maintenance, trading can temporarily become post-only, cancel-only, or disabled.
## 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 signing 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.
**Trading only with your own wallet? Do not apply.** Applications from individual traders and personal bot runners are not approved, because there is nothing to approve: everything you need is already self-serve with a `trading`-scope token from the [Authentication](/developers/authentication) page. Apply only if **other users** will trade through your platform (sub-accounts, delegated signing, managed wallets).
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 and list 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` |
| List or recover sub-accounts (`GET /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. You submit 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.
`winningOutcomeIndex` is an index into the fixed `outcomeTokens: ['Yes', 'No']` array: `0` means **YES** resolved, `1` means **NO** resolved, `null` means either not yet resolved or resolved to a payout **split**. For NegRisk groups, the field is set on the individual sub-market that resolved YES.
**Split resolutions.** A binary market can resolve to a payout split (for example 50/50 when a market is unwound at pre-cancellation odds) rather than a single winner. Split-resolved markets have `status: RESOLVED`, `winningOutcomeIndex: null`, and a `payoutNumerators` array of two non-negative integers whose ratio defines the payout across YES and NO. Both sides are redeemable at that ratio through [`POST /portfolio/redeem`](/api-reference/portfolio/redeem). Winner-take-all resolutions leave `payoutNumerators` as `null` and populate `winningOutcomeIndex`.
`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`. You prove 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.
If it returns `Signer does not match - you should use embedded address for smart wallet` instead, the sub-account's profile is in smart-wallet trading mode — common when the user has previously traded in the Limitless app. Self-signed orders need EOA mode; see [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs) for the one-call switch.
#### 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 [Node.js quick start](/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: the sub-account's `effectiveFeeRateBps` | Needed to sign on fee-enabled markets. Partner credentials cannot read the sub-account's private profile; if this rate is stale, `POST /orders` returns `FEE_RATE_MISMATCH` with `expectedFeeRateBps`. Sign a new order with that rate. |
| 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. |
You can recover partner-owned sub-account metadata with `GET /profiles/partner-accounts?account=`, or list all child accounts with `GET /profiles/partner-accounts`.
Recovery covers sub-accounts you created. A profile that already exists for a wallet, created by the user through the app, cannot be attached to your partner account afterwards: `POST /profiles/partner-accounts` returns `409 Conflict` for an address that already has a profile, and there is no endpoint that links or adopts an existing one. Such users keep trading their own profile directly and are not reachable through `onBehalfOf`. Treat `walletAddress -> profileId` persistence as recommended integration state for latency and auditability, but the API can recover the mapping when needed.
**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
You can read portfolio, order, and position data for your 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 |
| GET | `/v2/orders/status/:id` | Sub-account's async order status |
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 |
| [`GET /profiles/me`](/api-reference/portfolio/get-current-profile) | Any (`apiToken`, API key, Privy, session) | Read the authenticated caller's profile |
| [`POST /profiles/partner-accounts`](/api-reference/partner-accounts/create-partner-account) | HMAC | Create a sub-account |
| [`GET /profiles/partner-accounts`](/api-reference/partner-accounts/list-partner-accounts) | HMAC | List or recover partner-owned sub-accounts |
| [`POST /orders`](/api-reference/trading/create-order) | HMAC | Place orders (with optional delegated signing) |
| [`POST /heartbeats`](/api-reference/trading/heartbeats) | HMAC (`trading`) | Set or remove an order-cancellation deadline for the authenticated profile, not a sub-account |
| [`POST /amm/buy`](/developers/amm-trading#buying-outcome-shares) | HMAC | Buy AMM outcome shares from a server-wallet sub-account |
| [`POST /amm/sell`](/developers/amm-trading#selling-outcome-shares) | HMAC | Sell AMM outcome shares from a server-wallet sub-account |
| [`POST /amm/allowances/check`](/developers/amm-trading#allowances) | HMAC | Check the AMM approval state for a server wallet |
| [`POST /amm/allowances/approve`](/developers/amm-trading#allowances) | HMAC | Submit the one-time AMM approval from a server wallet |
| [`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`
# 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 you 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}
LMTS_TOKEN_ID=your_token_id
LMTS_TOKEN_SECRET=your_base64_secret
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.
**Preflight: check your trading wallet mode before placing orders.** If you ever accepted the app's one-time "choose your trading wallet" prompt, your profile is in smart-wallet mode. Every self-signed order will then fail with `Signer does not match - you should use embedded address for smart wallet`. Check `GET /profiles/me` first. If `tradeWalletOption` is `"smartWallet"`, switch with `PUT /profiles` and body `{"tradeWalletOption": "eoa"}` (HMAC-signed like any other request). See [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
## 1. Authentication
You sign authenticated REST requests (e.g. submitting orders) with HMAC-SHA256 using your scoped API token. Each request carries three headers — `lmts-api-key`, `lmts-timestamp`, and `lmts-signature` — computed over a canonical message of `timestamp`, HTTP method, request path (with query string), and body. Public market data (browsing markets, orderbooks) needs no authentication. See [Authentication](/developers/authentication) for the full reference.
```typescript api.ts theme={null}
import { createHmac } from 'crypto';
const API_URL = process.env.API_URL ?? 'https://api.limitless.exchange';
const TOKEN_ID = process.env.LMTS_TOKEN_ID;
const TOKEN_SECRET = process.env.LMTS_TOKEN_SECRET; // base64-encoded
// Build HMAC auth headers for a single request (method + path + body).
function signRequest(
tokenId: string,
secret: string,
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 };
}
// Public endpoints (market data, orderbooks) require no authentication.
export async function apiGet(path: string): Promise {
const res = await fetch(`${API_URL}${path}`);
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}
export async function apiGetPrivate(path: string): Promise {
if (!TOKEN_ID || !TOKEN_SECRET) throw new Error('Missing LMTS_TOKEN_ID / LMTS_TOKEN_SECRET');
const headers = signRequest(TOKEN_ID, TOKEN_SECRET, 'GET', path);
const res = await fetch(`${API_URL}${path}`, { headers });
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}
// Authenticated POST — signs the exact body bytes that are sent.
export async function apiPost(path: string, body: unknown): Promise {
if (!TOKEN_ID || !TOKEN_SECRET) {
throw new Error('Missing LMTS_TOKEN_ID / LMTS_TOKEN_SECRET. Derive a token at limitless.exchange → API Tokens');
}
const serialized = JSON.stringify(body);
const auth = signRequest(TOKEN_ID, TOKEN_SECRET, 'POST', path, serialized);
const res = await fetch(`${API_URL}${path}`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: serialized,
});
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json();
}
```
```bash curl theme={null}
# GET request (public — no auth)
curl https://api.limitless.exchange/markets/btc-100k-weekly
# POST request: HMAC-signed. Compute lmts-signature over
# "{timestamp}\nPOST\n/orders\n{body}" with HMAC-SHA256 (base64-decoded secret),
# then base64-encode it. The timestamp must be within 30s of server time.
curl -X POST \
-H "lmts-api-key: $LMTS_TOKEN_ID" \
-H "lmts-timestamp: $LMTS_TIMESTAMP" \
-H "lmts-signature: $LMTS_SIGNATURE" \
-H "Content-Type: application/json" \
-d '{"order":{...},"orderType":"GTC","marketSlug":"btc-100k-weekly","ownerId":12345}' \
https://api.limitless.exchange/orders
```
Generating `lmts-signature` by hand on the command line is awkward — for shell usage, run the `signRequest` helper above (or your SDK) to produce the three headers. See [Authentication](/developers/authentication) for ready-to-use signing snippets.
## 2. Fetching market data and caching venue info
Fetch market details via `GET /markets/:slug`. The response includes `venue` (exchange, adapter) and `tokens` (the YES and NO token IDs) for CLOB markets.
Call `GET /markets/{slug}` to retrieve market data including venue addresses.
Use `venue.exchange` as the EIP-712 `verifyingContract`. Use `tokens.yes` for YES and `tokens.no` 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 | null;
feeModule?: string; // new trading venues only
feeModuleType?: 'COLLATERAL'; // new trading venues only
}
interface ClobMarket {
slug: string;
venue: Venue;
tokens: { yes: string; no: string }; // YES / NO token IDs
metadata?: { fee?: boolean };
}
export async function getMarket(slug: string): Promise {
const data = await apiGet(`/markets/${slug}`);
if (!data.venue?.exchange || !data.tokens?.yes || !data.tokens?.no) {
throw new Error(`Market ${slug} is not a CLOB market or missing venue data`);
}
return data;
}
```
## 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'; // tokens.yes = YES, tokens.no = NO
priceCents: number; // e.g. 65 = $0.65
shares: number; // number of shares
orderType: 'GTC' | 'FAK' | 'FOK';
nonce: number;
feeRateBps: number;
}): Promise<{ order: Record; signature: string }> {
const { market, side, outcome, priceCents, shares, nonce, feeRateBps } = 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.tokens.yes : market.tokens.no;
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: BigInt(feeRateBps),
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 {
// Send salt as a decimal string (it can exceed JS's safe-integer range);
// makerAmount, takerAmount and nonce are JSON numbers.
order: {
salt: order.salt.toString(),
maker: account.address,
signer: account.address,
taker: ZERO_ADDRESS,
tokenId: tokenId,
makerAmount: Number(order.makerAmount),
takerAmount: Number(order.takerAmount),
expiration: '0',
nonce,
price: params.priceCents / 100, // dollars, at most 3 decimals; required for GTC/FAK, omit for FOK
feeRateBps,
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.
On fee-enabled markets (`metadata.fee: true`), sign with your profile's `effectiveFeeRateBps` from [`GET /profiles/me`](/api-reference/portfolio/get-current-profile). On fee-disabled markets, sign `0`. On [new trading venues](/developers/venue-system#new-trading-venues) (`venue.feeModuleType: "COLLATERAL"`), always sign `0` and approve USDC to `venue.feeModule` before trading. If the API returns `FEE_RATE_MISMATCH`, create and sign a new order using `expectedFeeRateBps`.
## 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' | 'FAK' | '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 feeRateBps = market.venue.feeModuleType !== 'COLLATERAL' && market.metadata?.fee
? (await apiGetPrivate<{ effectiveFeeRateBps: number }>('/profiles/me')).effectiveFeeRateBps
: 0;
const { order } = await createSignedOrder({
market,
feeRateBps,
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';
// Market price/orderbook streams are public — no authentication required.
const socket = io(`${WS_URL}${NAMESPACE}`, {
transports: ['websocket'],
});
socket.on('connect', () => {
socket.emit('subscribe_market_prices', {
marketAddresses: ['0x1234...'], // AMM markets
marketSlugs: ['btc-100k-weekly'], // CLOB markets
});
});
// Live updates carry an updatedPrices array and no top-level marketAddress; the initial
// snapshot after subscribing carries a top-level marketAddress and an updatedPrices object.
socket.on('newPriceData', (data: {
marketAddress?: string;
updatedPrices:
| Array<{ marketAddress: string; marketId: number; yesPrice: number; noPrice: number }>
| { marketAddress: string; marketId: number; yesPrice: number; noPrice: number; collateralDecimals: number };
blockNumber?: number;
timestamp?: 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 { apiGetPrivate } from './api';
import { getMarket } from './market';
import { createSignedOrder } from './orders';
import { submitOrder } from './submit';
import { io } from 'socket.io-client';
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 feeRateBps = market.venue.feeModuleType !== 'COLLATERAL' && market.metadata?.fee
? (await apiGetPrivate<{ effectiveFeeRateBps: number }>('/profiles/me')).effectiveFeeRateBps
: 0;
const { order } = await createSignedOrder({
market,
feeRateBps,
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);
// Public market stream — no authentication required.
const socket = io(`${WS_URL}/markets`, {
transports: ['websocket'],
});
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 feeRateBps = market.venue.feeModuleType !== 'COLLATERAL' && market.metadata?.fee
? (await apiGetPrivate<{ effectiveFeeRateBps: number }>('/profiles/me')).effectiveFeeRateBps
: 0;
const { order } = await createSignedOrder({
market, feeRateBps, 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, FAK, 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
* **Scoped API token**: Derive a token at [limitless.exchange](https://limitless.exchange) → profile menu → API Tokens → **Derive**. You get a **token ID** and a **secret** (base64-encoded). The secret is shown once — store it securely. See [Authentication](/developers/authentication) for the full flow.
* **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
You sign authenticated API requests (e.g. submitting orders) with HMAC-SHA256 using your scoped API token. Each request carries three headers — `lmts-api-key`, `lmts-timestamp`, and `lmts-signature` — computed over a canonical message. See [Authentication](/developers/authentication) for the full reference.
Your **private key** is used only for **EIP-712 order signing**. The **scoped API token** (token ID + secret) handles request authentication. Both are required for trading. Public market data (browsing markets, orderbooks) needs no authentication.
```python theme={null}
import os
import hmac
import hashlib
import base64
from datetime import datetime, timezone
API_BASE = "https://api.limitless.exchange"
TOKEN_ID = os.environ["LMTS_TOKEN_ID"] # token ID from token derivation
TOKEN_SECRET = os.environ["LMTS_TOKEN_SECRET"] # base64-encoded secret
def sign_request(token_id: str, secret: str, method: str, path: str, body: str = "") -> dict:
"""Build HMAC auth headers for a single request (method + path + body)."""
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}
```
The signed `path` must include the **query string** if present. The timestamp must be within **30 seconds** of server time. For GET requests, the body is an empty string.
**Preflight: check your trading wallet mode before placing orders.** If you ever accepted the app's one-time "choose your trading wallet" prompt, your profile is in smart-wallet mode. Every self-signed order will then fail with `Signer does not match - you should use embedded address for smart wallet`. Check `GET /profiles/me` first. If `tradeWalletOption` is `"smartWallet"`, switch with `PUT /profiles` and body `{"tradeWalletOption": "eoa"}` (HMAC-signed like any other request). See [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
## 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. Public endpoint — no auth."""
resp = requests.get(f"{API_BASE}/markets/{slug}")
resp.raise_for_status()
return resp.json()
```
```python theme={null}
# Example response (CLOB market)
{
"slug": "btc-above-100k-march-2025",
"venue": {
"exchange": "0xA1b2C3d4E5f6...",
"adapter": null
},
"tokens": {"yes": "19633...", "no": "19633..."}
}
```
**tokens.yes** is the YES token ID, **tokens.no** is the NO token ID. 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-til-cancelled |
| `orderType` | `FAK` | Fill-and-kill (limit price, partial fills allowed, remainder 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 `tokens.yes` for YES, `tokens.no` 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 (HMAC-authenticated)."""
body = json.dumps(order_payload)
auth = sign_request(TOKEN_ID, TOKEN_SECRET, "POST", "/orders", body)
resp = requests.post(
f"{API_BASE}/orders",
headers={**auth, "Content-Type": "application/json"},
data=body,
)
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 json
import hmac
import hashlib
import base64
from datetime import datetime, timezone
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
TOKEN_ID = os.environ["LMTS_TOKEN_ID"]
TOKEN_SECRET = os.environ["LMTS_TOKEN_SECRET"] # base64-encoded
PRIVATE_KEY = os.environ["PRIVATE_KEY"]
OWNER_ID = int(os.environ.get("OWNER_ID", "0")) # Profile ID from API
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}
def fetch_market(slug: str) -> dict:
# Public endpoint — no auth required.
resp = requests.get(f"{API_BASE}/markets/{slug}")
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"]
tokens = market["tokens"]
account = Account.from_key(PRIVATE_KEY)
maker = account.address
fee_rate_bps = 0 # always 0 on new trading venues (feeModuleType "COLLATERAL")
if venue.get("feeModuleType") != "COLLATERAL" and (market.get("metadata") or {}).get("fee"):
path = "/profiles/me"
profile_resp = requests.get(
f"{API_BASE}{path}",
headers=sign_request(TOKEN_ID, TOKEN_SECRET, "GET", path),
)
profile_resp.raise_for_status()
fee_rate_bps = profile_resp.json()["effectiveFeeRateBps"]
# YES token is tokens["yes"], NO token is tokens["no"]
token_id = tokens["yes"]
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": fee_rate_bps,
"side": 0, # BUY
"signatureType": 0, # EOA
}
signature = sign_order(order_data, venue["exchange"], PRIVATE_KEY)
order_payload = {
# expiration is signed as a uint256 (int) above, but the API body expects it as a string
# price is a wire-only field (not part of the EIP-712 message); required for GTC/FAK, at most 3 decimals
"order": {**order_data, "expiration": str(order_data["expiration"]), "price": price_per_share, "signature": signature},
"ownerId": OWNER_ID,
"orderType": order_type,
"marketSlug": market_slug,
}
body = json.dumps(order_payload)
auth = sign_request(TOKEN_ID, TOKEN_SECRET, "POST", "/orders", body)
resp = requests.post(
f"{API_BASE}/orders",
headers={**auth, "Content-Type": "application/json"},
data=body,
)
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 LMTS_TOKEN_ID="your_token_id"
export LMTS_TOKEN_SECRET="your_base64_secret"
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.
On fee-enabled markets (`metadata.fee: true`), sign with your profile's `effectiveFeeRateBps` from [`GET /profiles/me`](/api-reference/portfolio/get-current-profile). On fee-disabled markets, sign `0`. On [new trading venues](/developers/venue-system#new-trading-venues) (`venue.feeModuleType: "COLLATERAL"`), always sign `0` and approve USDC to `venue.feeModule` before trading. If the API returns `FEE_RATE_MISMATCH`, create and sign a new order using `expectedFeeRateBps`.
## Troubleshooting
* Verify `LMTS_TOKEN_ID` and `LMTS_TOKEN_SECRET` are set and the token is active in the Limitless UI.
* Ensure `lmts-timestamp` is within **30 seconds** of server time (check your clock).
* Confirm the signed `path` matches the request path **including any query string**, and the signed body is the exact bytes you send.
* 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.
* On [new trading venues](/developers/venue-system#new-trading-venues), also approve USDC to `venue.feeModule`. Without it, orders that are not `postOnly` fail with `Insufficient collateral allowance for the order fee.`
Use `tokens.yes` for YES and `tokens.no` 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.
Scoped API token setup and HMAC request signing.
# 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.
Sign the connection handshake with your scoped API token for **authenticated features**:
* All public events
* `positions` (position balance updates)
Required for `subscribe_positions`. Pass three HMAC headers (`lmts-api-key`, `lmts-timestamp`, `lmts-signature`) during the handshake — see [Authentication](/developers/authentication). Generate a scoped API token at [limitless.exchange](https://limitless.exchange) → profile → Api keys.
**Never** commit your API token ID or secret to version control or expose them in client-side code. Use environment variables (e.g. `LMTS_TOKEN_ID` / `LMTS_TOKEN_SECRET`) and load them at runtime.
Cookie-based session authentication is **deprecated**. Authenticate the handshake with HMAC-signed headers (`lmts-api-key`, `lmts-timestamp`, `lmts-signature`) — see [Authentication](/developers/authentication).
## Connection details
| Setting | Value |
| - | - |
| **URL** | `wss://ws.limitless.exchange` |
| **Namespace** | `/markets` |
| **Transport** | WebSocket only (no polling) |
Authenticated connections sign the Socket.IO handshake with HMAC. The signed canonical message uses a fixed path and empty body, and the HMAC key is the base64-decoded `secret`:
```python ws_auth.py theme={null}
import base64
import hashlib
import hmac
from datetime import datetime, timezone
def ws_auth_headers(token_id: str, secret: str) -> dict:
timestamp = datetime.now(timezone.utc).isoformat()
message = f"{timestamp}\nGET\n/socket.io/?EIO=4&transport=websocket\n"
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,
}
```
See [Authentication](/developers/authentication) for how to generate the token ID and secret.
```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 (HMAC) theme={null}
import asyncio
import os
import socketio
from ws_auth import ws_auth_headers
token_id = os.environ.get("LMTS_TOKEN_ID")
secret = os.environ.get("LMTS_TOKEN_SECRET")
if not token_id or not secret:
raise ValueError("LMTS_TOKEN_ID and LMTS_TOKEN_SECRET environment variables 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"],
# A function, not a dict, so every reconnect is signed with a fresh timestamp
headers=lambda: ws_auth_headers(token_id, secret),
)
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.
The server answers every `subscribe_market_prices` call, first or repeat, with one `orderbookUpdate` per CLOB slug carrying the full current book, including books with empty `bids` and `asks`. You do not need a REST call to seed local state. See [Initial snapshot](/developers/websocket/market-data#initial-snapshot).
## Subscribing to positions
Position updates require authentication. Emit `subscribe_positions` after connecting. Pass the markets you want position updates for (`marketSlugs` and/or `marketAddresses`); an empty payload is ignored server-side.
```python theme={null}
# Requires HMAC auth headers during the handshake (see ws_auth_headers above)
await sio.emit("subscribe_positions", {"marketSlugs": ["your-market-slug"]}, 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 |
| `exception` | No | Unhandled gateway exception, e.g. unauthorized `subscribe_positions`: `{ "status": "error", "message": "..." }` |
| `error` | No | Subscription rejected: `{ "message": "..." }` (missing marketSlugs/marketAddresses, all requested markets resolved, subscribe failure) |
### Event payloads
**`newPriceData`** — AMM market price update. Live updates carry `updatedPrices` as an array and no top-level `marketAddress`:
```json theme={null}
{
"updatedPrices": [
{ "marketAddress": "0x1234...", "marketId": 123, "yesPrice": 0.65, "noPrice": 0.35 }
],
"blockNumber": 12345678,
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
The initial snapshot sent after subscribing carries a top-level `marketAddress` and an `updatedPrices` object:
```json theme={null}
{
"marketAddress": "0x1234...",
"updatedPrices": {
"collateralDecimals": 6,
"marketAddress": "0x1234...",
"marketId": 123,
"yesPrice": 0.65,
"noPrice": 0.35
},
"blockNumber": 0,
"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 base64
import hashlib
import hmac
import logging
import os
from datetime import datetime, timezone
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"
def ws_auth_headers(token_id: str, secret: str) -> dict:
timestamp = datetime.now(timezone.utc).isoformat()
message = f"{timestamp}\nGET\n/socket.io/?EIO=4&transport=websocket\n"
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,
}
class LimitlessWebSocketClient:
def __init__(
self,
token_id: Optional[str] = None,
secret: Optional[str] = None,
):
self.token_id = token_id or os.environ.get("LMTS_TOKEN_ID")
self.secret = secret or os.environ.get("LMTS_TOKEN_SECRET")
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 exception(data: dict):
logger.error("Exception: %s", data)
@self.sio.event(namespace=NAMESPACE)
async def error(data: dict):
logger.error("Subscription rejected: %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",
{"marketSlugs": self._market_slugs, "marketAddresses": self._market_addresses},
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.token_id or not self.secret:
raise ValueError("API token required for position subscription")
self._subscribe_positions = True
await self._resubscribe()
async def connect(self) -> None:
kwargs = {
"transports": ["websocket"],
"namespaces": [NAMESPACE],
}
if self.token_id and self.secret:
# A function, not a dict, so every reconnect is signed with a fresh timestamp
kwargs["headers"] = lambda: ws_auth_headers(self.token_id, self.secret)
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():
token_id = os.environ["LMTS_TOKEN_ID"]
secret = os.environ["LMTS_TOKEN_SECRET"]
client = LimitlessWebSocketClient(token_id=token_id, secret=secret)
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 |
| - | - |
| `LMTS_TOKEN_ID` | Scoped API token ID for authenticated features (positions). Omit for public-only. |
| `LMTS_TOKEN_SECRET` | Token secret used to sign the HMAC handshake. 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.
Authenticated connections pass `headers` as a function. python-socketio calls it before every connection attempt, so each reconnect carries a new timestamp and signature. A plain dict is sent again unchanged on reconnect. The socket still connects, but the server checks the handshake headers on every `subscribe_positions` and `subscribe_order_events`, and a timestamp outside the 30-second window fails that check with `Invalid HMAC authentication`, so the re-subscription is rejected.
Our load balancer closes every connection after 24 hours (see [WebSocket overview](/developers/websocket/overview#connection)), so a long-running client always goes through this reconnect path at least once a day.
Store your API token ID and secret securely. Rotate the token if you suspect it has been exposed. Do not log or print the secret.
# Building Responsible Trading Agents
Source: https://docs.limitless.exchange/developers/responsible-agents
What an automated trader may and may not do on Limitless, which safeguards the exchange enforces, which ones your agent must enforce itself, and how to report suspected abuse
An agent that trades on Limitless is bound by the same rules as a person trading in the app. This page is the developer-facing summary of those rules: the conduct the [Terms of Service](/user-guide/terms-of-service#6-prohibited-activities) prohibit, the safeguards the exchange enforces on every order, the safeguards your agent has to enforce on its own, and where to report something that looks wrong.
It applies to every automated path: the [MCP server](/developers/mcp-server), bots built on the SDKs or [agents-starter](/developers/build-a-trading-agent), and partner integrations placing orders for their own users through the [Programmatic API](/developers/programmatic-api). The Terms govern; this page does not replace them.
## Prohibited conduct
The Terms prohibit any activity intended to artificially influence market prices or outcomes. The list below is what that means for an agent. It is not exhaustive, and Limitless may monitor all platform activity and suspend accounts, restrict addresses, or share information with authorities without prior notice.
| Prohibited | What it looks like in an agent |
| - | - |
| **Wash trading** | Trading between accounts you control, or routing volume through your own accounts to inflate volume, earn rewards, or print a last-trade price. |
| **Spoofing and layering** | Resting size you intend to cancel before it fills, so the book shows supply or demand that is not real. |
| **Coordinated trading** | Arranging fills, quotes, or timing with other accounts to move a price or split a reward. |
| **Multiple accounts to evade restrictions** | Creating another account or wallet to get around a block, a rewards exclusion, or a limit. |
| **Exploiting vulnerabilities** | Using a bug in the exchange, a contract, or a reward program for gain instead of reporting it. |
| **Fraud or illegal use** | False information, money laundering, or any use that is illegal where you operate. |
Market making is not on this list. Quoting both sides, cancelling and replacing quotes as fair value moves, and pulling quotes when inventory or risk limits trip are normal. The line is intent: every order your agent rests must be one it is willing to have filled.
## What the exchange enforces
These controls run on every order, whether it comes from the app, an SDK, or an assistant. They are what your agent can rely on.
| Safeguard | What it does | Conduct it addresses |
| - | - | - |
| **Self-trade prevention** | `POST /orders` never matches an incoming order against the same profile's resting order on the same token. The default `cancel_maker` policy cancels your resting order and continues; `cancel_taker` rejects the incoming order with `execution.reason: "STP_TAKER_REJECTED"`; `cancel_both` does both. See [Create Order](/api-reference/trading/create-order#self-trade-prevention). | Accidental self-trades from one profile. It does not span profiles. |
| **Public book only** | The signed order's `taker` must be the zero address. Orders cannot be directed at a chosen counterparty. | Pre-arranged fills. |
| **Scoped API tokens** | Self-service tokens carry `trading` by default. `withdrawal` is a separate, partner-only scope. Revoking a token takes effect immediately. See [Authentication](/developers/authentication). | Limits what a leaked or runaway credential can do. |
| **Human approval on the MCP server** | Every new order is a proposal that the account owner approves in the browser. Cancels execute directly. No withdraw or transfer tool exists. See [MCP Server](/developers/mcp-server). | An assistant cannot add exposure on its own. |
| **Address blocklist at order submission** | A blocked trade-wallet address is rejected on every exchange with `TRADER_BLOCKED: address is currently blocked from order submission. Contact support.` | Enforcement outcome for Terms violations and for wallets that break settlement. |
| **Action attribution** | Every order create and cancel is recorded with the acting profile, the authentication method, and the request id. When a partner acts for a sub-account, the partner's own profile is recorded as the actor. | Every order is traceable to who submitted it and how. |
| **Maintenance mode** | Trading can be restricted to `post_only`, `cancel_only`, or `disabled`. Blocked actions return `425 Too Early`. See [Maintenance Mode](/developers/maintenance-mode). | Orderly halts during incidents. |
| **Rate limits** | Enforced at the API edge with `429 Too Many Requests`. See [Rate limits](/api-reference/introduction#rate-limits). | Runaway loops. |
**Cancel through the API, never on-chain.** Legitimate cancels go through `POST /orders/cancel` and the related endpoints. An on-chain `OrderCancelled` for a CLOB order is treated as a settlement-integrity signal, and the maker address can be blocked from order submission as a result.
### What the exchange does not do for you
Design your agent around these gaps rather than assuming they are covered.
* **No per-account exposure or position caps.** The only ceiling on an order is your collateral.
* **No account-wide cancel-all.** [`DELETE /orders/all/{slug}`](/api-reference/trading/cancel-all) cancels one market or one group. A full flatten loops over markets.
* **Self-trade prevention is per profile.** Two profiles you control can match each other. That is wash trading, not an STP gap.
* **Attribution is not your audit log.** The exchange records who acted, not why. Your decision log is yours to keep.
## What your agent must enforce
Each production safeguard below maps to a prohibited behavior or to a failure mode that turns into one. [Agents Academy](https://academy.limitless.exchange/academies/agents_academy/) Module 14 walks through the runtime code for all of them; the cross-market strategy in agents-starter ships them configured.
### Provenance logging
Send a `clientOrderId` on every order. It is your idempotency key, up to 128 characters, unique per profile, and it is the identifier you will use in a report or a support thread. Send an `x-request-id` header too; it is stored with the action record. Keep a local log that links each `clientOrderId` to the strategy, the signal, and the outcome, so that "why did my agent do this" has an answer.
### Spending limits
Enforce hard limits in code, before the SDK call, not in the prompt: a per-order cap, a daily notional budget, a maximum open-position count, and per-market exposure. The cross-market strategy in agents-starter adds a `max_loss_usd` circuit breaker that cancels all orders and halts, and a `max_net_shares` inventory guard that stops quoting while hedges catch up. Run with `DRY_RUN=true` until the limits have tripped in a log and you have read what they said.
### Anti-self-trading
If several strategies share one profile, set `stpPolicy` explicitly. `cancel_taker` protects resting quotes from your own taker orders; the default `cancel_maker` protects the taker at the cost of the quote. If you run several profiles, self-trade prevention does not apply between them, and your agent must never route an order that could fill against another account you control. Self-trades also earn no maker rebate, so there is nothing to gain.
### Manipulation checks
Before resting an order, the agent should be able to answer yes to "would I accept this fill". Cancel-replace to track fair value is fine; resting size to change what the book shows is not. Do not treat reward programs as a reason to trade against yourself or to coordinate with another account. Do not create a second account or wallet to get around a block, a rewards exclusion, or a limit.
### Kill switches
Wire a stop that works in seconds without touching the LLM: a file flag or panel button the loop checks at the top of every iteration. When it trips, cancel resting orders first, per market, with [`DELETE /orders/all/{slug}`](/api-reference/trading/cancel-all). Halting the loop does not cancel orders already on the book. If the credential itself is suspect, revoke the token with [`DELETE /auth/api-tokens/{tokenId}`](/api-reference/api-tokens/revoke-token), or revoke the MCP grant from Limitless; both invalidate access immediately.
## Reporting suspected abuse
Report market manipulation, a suspected exploit, or an agent that is misbehaving to **[help@limitless.network](mailto:help@limitless.network)** with the subject line **Market integrity report**. Include:
* the market slug or group slug
* order ids, `clientOrderId` values, or transaction hashes
* UTC timestamps
* the wallet addresses involved
* what you observed and why it looks wrong
Reports are handled under the Terms of Service. Detection methods, thresholds, and the outcome of individual reports are not published. For integration questions rather than reports, use the [Builders Chat](https://t.me/LimitlessBuildersChat).
## Related
The prohibited-activities section this page summarizes.
`stpPolicy` values and the response fields they produce.
Hard limits, budgets, and the file-flag kill switch, in TypeScript and Python.
The agents-starter template with dry run and circuit breakers built in.
# 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 }
```
### Create 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 {
label := "(no label)"
if token.Label != nil {
label = *token.Label
}
fmt.Println(token.TokenID, label, token.Scopes)
}
```
## 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"` |
| `ScopeWithdrawal` | `"withdrawal"` (required for `POST /portfolio/withdraw`, `ServerWallets.Withdraw`) |
# 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. GTC and FAK orders use `Price` and `Size`; FOK orders use `MakerAmount` (with `TakerAmount` always set to 1)
3. The SDK posts the order 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 server submits the signed order 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 seven 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 |
| `*ConflictError` | `409` | Conflict, for example a reused `clientOrderId` or a duplicate order hash |
| `*UnprocessableEntityError` | `422` | Semantic validation failure such as insufficient balance or an invalid AMM quote |
| `*TooEarlyError` | `425` | Receive-window failure or maintenance block |
| `*UpstreamUnavailableError` | `502`, `503` | Upstream dependency (RPC, quote, Redis, market state) unavailable |
```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 |
| `425` | Too Early | Receive-window failure or maintenance block (`code`). Re-stamp and retry only for receive-window failures; refresh maintenance status for maintenance blocks (matched by `*TooEarlyError`). |
| `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")
}
```
## Raw HTTP responses
Every API-backed service method has a `...WithRawResponse` sibling that returns `*RawResult[T]`. Use it when you need the HTTP status, response headers (rate-limit metadata, request IDs, `Content-Type`), or the exact response bytes alongside the decoded value. The base method (without the suffix) is unchanged and still returns just the decoded value.
The raw variant still returns the same typed errors (`*APIError` and its subclasses) on any response with status `>= 400`. It only exposes the raw response on success.
```go theme={null}
import "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
// Default: decoded value only.
markets, err := marketFetcher.GetActiveMarkets(ctx, nil)
// Opt in to the raw response.
result, err := marketFetcher.GetActiveMarketsWithRawResponse(ctx, nil)
if err != nil {
return err
}
fmt.Println("HTTP", result.Raw.Status) // 200
fmt.Println("rate limit", result.Raw.Headers.Get("X-RateLimit-Remaining"))
fmt.Println("markets returned:", len(result.Data.Data)) // decoded value
// result.Raw.Body holds the original JSON body as json.RawMessage.
```
### RawResult\[T]
| Field | Type | Description |
| - | - | - |
| `Data` | `T` | The decoded response value, identical to what the base method returns. |
| `Raw` | `*RawResponse` | The underlying HTTP response. |
### RawResponse
| Field | Type | Description |
| - | - | - |
| `Status` | `int` | HTTP status code. |
| `Headers` | `http.Header` | Response headers. |
| `Body` | `json.RawMessage` | The original response body. |
The `...WithRawResponse` variant is available on every API-backed method (markets, portfolio, orders, AMM, API tokens, delegated orders, partner accounts, and market pages). WebSocket subscriptions are not affected.
## 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` retries `*APIError` values whose `Status` is in `StatusCodes`, and also transport failures (EOF, connection resets, `net.OpError`, `url.Error` timeouts, temporary network errors). Context cancellation is never retried, and cancellation or expiry of the context passed to `WithRetry` stops further attempts; a timeout from a request that used its own shorter deadline counts as a transport failure and is retried. Any other error propagates immediately. Because a transport error on `CreateOrder` can mean the order was accepted, check open orders before retrying placements.
### 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(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
retryable := limitless.NewRetryableClient(client, limitless.RetryConfig{
StatusCodes: []int{429, 500, 502, 503, 504},
MaxRetries: 3,
ExponentialBase: 2.0,
})
// Call through retryable directly; transient failures are retried transparently
var market limitless.Market
err := retryable.Get(ctx, "/markets/btc-100k-weekly", &market)
```
The `RetryableClient` exposes the same request methods as `HttpClient` (`Get`, `GetRaw`, `Post`, `Patch`, `Delete`) and transparently retries on the configured status codes. Use these methods for raw requests that need retry coverage.
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: {lmts-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.24+**. 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"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
}
```
```go theme={null}
func main() {
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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
Authentication uses scoped API tokens with HMAC-SHA256 request signing. A scoped API token is a **token ID** plus a one-time **secret**, generated at [limitless.exchange](https://limitless.exchange) → profile menu → **Api keys**.
* HMAC headers `lmts-api-key`, `lmts-timestamp`, `lmts-signature` — the SDK builds and signs them for you when you pass HMAC credentials
Store the token ID and secret in your environment, then pass them as HMAC credentials:
```bash theme={null}
export LMTS_TOKEN_ID="your-token-id"
export LMTS_TOKEN_SECRET="your-base64-secret"
```
```go theme={null}
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
```
Set the `LIMITLESS_API_KEY` environment variable and the `HttpClient` loads it automatically, or pass it explicitly with `WithAPIKey`:
```bash theme={null}
export LIMITLESS_API_KEY="lmts_your_key_here"
```
```go theme={null}
client := limitless.NewHttpClient() // auto-loads LIMITLESS_API_KEY
// Or pass it explicitly:
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.
**Legacy:** an older API key (`LIMITLESS_API_KEY`, sent as the `X-API-Key` header) still works but is not recommended for new integrations. Prefer a scoped API token.
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). Authenticate it with scoped-token HMAC credentials:
```go theme={null}
client := limitless.NewClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
// client.Markets, client.Portfolio, client.Pages,
// client.ApiTokens, client.PartnerAccounts, client.DelegatedOrders,
// client.ServerWallets, client.AMM
```
The legacy `WithAPIKey` option still works but is not recommended for new integrations:
```go theme={null}
client := limitless.NewClient(
limitless.WithAPIKey("lmts_your_key_here"),
)
```
## 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 `X-API-Key` auth (still works; prefer a scoped API token) |
| `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.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
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 (
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
pageFetcher := limitless.NewMarketPageFetcher(client)
```
## Navigation tree
Fetch the full navigation hierarchy. Each node represents a browsable 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 (nil when absent) |
| `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 (nil when absent) |
| `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.MarketPageSort("-createdAt"),
})
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.MarketPageSort("-createdAt"),
})
if err != nil {
log.Fatal(err)
}
for _, market := range firstPage.Data {
fmt.Println(market.Slug)
}
if c := firstPage.Cursor; c != nil && c.NextCursor != nil {
nextPage, err := pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Cursor: *c.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": []any{"btc", "eth"}},
})
// Combined filters
result, err = pageFetcher.GetMarkets(ctx, page.ID, &limitless.MarketPageMarketsParams{
Limit: intPtr(10),
Sort: limitless.MarketPageSort("-createdAt"),
Filters: map[string]any{
"ticker": []any{"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`, `bool`, a number, or `[]any` for multi-select (OR) filters; multi-select only applies when the filter group's `AllowMultiple` is true. `[]string` is also expanded on Go SDK newer than v1.1.0. |
## 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"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func intPtr(i int) *int { return &i }
func main() {
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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.MarketPageSort("-createdAt"),
Filters: map[string]any{"ticker": []any{"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 (
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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.SortByNewest,
})
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` | `25` (server default) | Markets per page, 1 to 25; larger values are rejected with 400 |
| `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 |
| `SortByTrending` | Sort by trending markets (Go SDK newer than v1.1.0) |
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 shares\n", bid.Price, bid.Size/1e6)
}
fmt.Println("Asks:")
for _, ask := range orderbook.Asks {
fmt.Printf(" Price: %.3f Size: %.2f shares\n", ask.Price, ask.Size/1e6)
}
fmt.Printf("Midpoint: %.4f\n", orderbook.AdjustedMidpoint)
```
Level `Size` and `MinSize` are in raw 6-decimal units (1 share = 1,000,000); `MinSize` is a decimal string. `Price` is not scaled.
## 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 %s remaining of %s @ %s\n", order.ID, order.Side, order.RemainingSize, order.OriginalSize, order.Price)
}
```
Sizes and amounts are decimal strings in raw 6-decimal units; divide by 1e6 for shares or USDC. Go SDK v1.1.0 cannot decode a non-empty user-orders response; these fields exist on the SDK after v1.1.0.
## 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,
},
})
```
Call `GetMarket(slug)` before placing orders on a market so the venue is cached. Otherwise the `OrderClient` fetches the market on the first order (logging a warning) and pays one extra round-trip.
## 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"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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-til-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"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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).
**Wallet-mode preflight.** Accepting the one-time "choose your trading wallet" prompt in the app enables 1-click (smart wallet) trading on your Limitless profile. Once that mode is set, self-signed orders are rejected with `Signer does not match - you should use embedded address for smart wallet`. Switch the profile to EOA trading mode first: see [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
## 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.
**New trading venues.** If the market's venue has `feeModuleType` set to `"COLLATERAL"`, the signing wallet also needs a USDC approval to the venue's `feeModule` address (once per fee module), and orders must be signed with a fee rate of `0`. Updated SDK releases handle venue detection and the fee rate. See [New trading venues](/developers/venue-system#new-trading-venues).
## GTC orders (Good-til-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` | Must be `"0"` (the default). Non-zero expiration is not currently supported and the API rejects it. |
| `Nonce` | `*int` | Must be `0` (the default when `nil`). Non-zero nonce is not currently supported and the API rejects it. |
| `Taker` | `string` | Optional taker address (defaults to zero address) |
## Self-trade prevention
Set `StpPolicy` on `CreateOrderParams` to control what happens when your incoming order would match your own resting order on the same token. It is a top-level request field — not part of the EIP-712 signed order args — and applies to any order type. Leave it empty to keep the server default, `cancel_maker`.
`StpPolicy` on `CreateOrderParams` requires a Go SDK newer than v1.1.0. On v1.1.0 the only ways to set the policy are `CancelReplaceOrderParams.STPPolicy` (`STPCancelMaker`, `STPCancelTaker`, `STPCancelBoth`) or a raw `client.PostRaw(ctx, "/orders", payload)` with an `stpPolicy` key.
```go theme={null}
result, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeGTC,
MarketSlug: "btc-above-100k-march-2025",
StpPolicy: limitless.StpPolicyCancelMaker, // or StpPolicyCancelTaker, StpPolicyCancelBoth
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.65,
Size: 10.0,
},
})
```
| Value | Result |
| - | - |
| `cancel_maker` | Default. Cancels your conflicting resting order and continues with the incoming order. |
| `cancel_taker` | Rejects the incoming order before it self-trades. |
| `cancel_both` | Cancels your conflicting resting order and rejects the incoming order. |
The create-order response carries an `Execution` pointer (nil when the API omits `execution`) with the outcome:
```go theme={null}
if ex := result.Execution; ex != nil && ex.SettlementStatus == "CANCELED" && ex.Reason != nil && *ex.Reason == "STP_TAKER_REJECTED" {
fmt.Println("Order rejected by self-trade prevention")
} else if ex != nil && len(ex.StpMakerCancels) > 0 {
fmt.Printf("Cancelled own resting orders: %v\n", ex.StpMakerCancels)
}
```
| `Execution` field | Type | Description |
| - | - | - |
| `SettlementStatus` | `string` | `CANCELED` when a `cancel_taker` / `cancel_both` order is rejected. |
| `Reason` | `*string` | `STP_TAKER_REJECTED` when the incoming order was rejected; nil otherwise. |
| `StpMakerCancels` | `[]string` | Resting order ids cancelled by `cancel_maker` / `cancel_both`. |
Self-trade prevention blocks same-profile matches on the **same token** only. Orders on a different token of the same profile are unaffected. The wire field is always `stpPolicy`, regardless of the SDK.
## 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` | Must be `"0"` (the default). Non-zero expiration is not currently supported and the API rejects it. |
| `Nonce` | `*int` | Must be `0` (the default when `nil`). Non-zero nonce is not currently supported and the API rejects it. |
| `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` | Must be `"0"` (the default). Non-zero expiration is not currently supported and the API rejects it. |
| `Nonce` | `*int` | Must be `0` (the default when `nil`). Non-zero nonce is not currently supported and the API rejects it. |
| `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 with the market's venue exchange as the EIP-712 verifyingContract
signature, err := orderClient.SignOrderForMarket(ctx, "btc-above-100k-march-2025", unsigned)
if err != nil {
log.Fatal(err)
}
fmt.Println("Signature:", signature)
```
`SignOrder(unsigned)` only works when the client was created with `WithSigningConfig(limitless.OrderSigningConfig{ChainID: 8453, ContractAddress: market.Venue.Exchange})`. `NewOrderClient` sets only the chain ID, so without that option `SignOrder` returns `order signing config is incomplete: verifying contract address is required`.
## AMM trading
CLOB orders trade against the orderbook. AMM (FPMM) markets trade against a pool, and the SDK exposes them through `sdk.AMM`. The service calls `POST /amm/allowances/check`, `POST /amm/allowances/approve`, `POST /amm/buy`, and `POST /amm/sell` on behalf of a partner server wallet.
Use it when the market is an AMM market and you want the server to hold custody, sign the trade, and pay gas. See [AMM Trading (Server Wallets)](/developers/amm-trading) for the underlying endpoints and market model.
### Requirements
* Authenticate with an HMAC API token that holds **both** the `trading` and `delegated_signing` scopes, or call the `*WithIdentity` variants with a Privy identity token. Legacy `x-api-key` credentials are rejected.
* The trade runs against a server-wallet sub-account. Set `OnBehalfOf` to the sub-account profile ID (`1..=2147483647`), or omit it (zero value) to trade from the authenticated profile.
* Amounts are **positive integer strings** in the collateral token's base units (for USDC: `"1000000"` = 1 USDC). Never use `float64`.
* `SlippageBps` is optional (`*int`). `nil` uses the server default of `100` (1%); values range from `0` to `1000`.
* `OutcomeIndex` is `AMMOutcomeYes` (0) or `AMMOutcomeNo` (1).
### One-time approval per wallet and market
`BUY` and `SELL` approvals are independent and set up once per wallet and market. `Buy` and `Sell` do **not** preflight allowances themselves. Confirm the allowance first.
`EnsureAllowance` runs `CheckAllowance`, submits `ApproveAllowance` at most once when missing, then polls the check until `Confirmed` is true. Polling defaults to every two seconds; use the context timeout to bound how long it waits.
```go theme={null}
marketSlug := "btc-100k-weekly"
childProfileID := 12345
for _, side := range []limitless.AMMAllowanceSide{
limitless.AMMAllowanceSideBuy,
limitless.AMMAllowanceSideSell,
} {
allowanceCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
_, err := sdk.AMM.EnsureAllowance(allowanceCtx, limitless.AMMAllowanceParams{
Market: marketSlug,
Side: side,
OnBehalfOf: childProfileID,
})
cancel()
if err != nil {
return err
}
}
```
A `submitted` response from `ApproveAllowance` (HTTP 202) is not confirmation. Either use `EnsureAllowance`, or poll `CheckAllowance` until `Confirmed` is true.
### Buy shares
`Buy` spends an exact collateral amount on the chosen outcome. Pass a unique `IdempotencyKey` per trade. On a timeout retry, reuse the same immutable params value so the serialized body and idempotency key stay byte-identical.
```go theme={null}
slippageBps := 100
buyParams := limitless.AMMBuyParams{
Market: marketSlug,
OutcomeIndex: limitless.AMMOutcomeYes,
CollateralAmount: "1000000", // 1 USDC in base units
SlippageBps: &slippageBps,
IdempotencyKey: "buy-unique-key-001",
OnBehalfOf: childProfileID,
}
buy, err := sdk.AMM.Buy(ctx, buyParams)
if err != nil {
return err
}
fmt.Println(buy.Status, buy.ExpectedShares, buy.MinShares)
```
### Sell shares
`Sell` requests an exact collateral return by selling outcome shares.
```go theme={null}
sellParams := limitless.AMMSellParams{
Market: marketSlug,
OutcomeIndex: limitless.AMMOutcomeYes,
CollateralReturnAmount: "992015",
IdempotencyKey: "sell-unique-key-001",
OnBehalfOf: childProfileID,
}
sell, err := sdk.AMM.Sell(ctx, sellParams)
if err != nil {
return err
}
fmt.Println(sell.Status, sell.ExpectedShares, sell.MaxShares)
```
Reusing an `IdempotencyKey` with different params raises `ConflictError` (HTTP 409). The four AMM routes share a rate limit of 10 requests / 10 seconds per actor. Use the `*WithRawResponse` variants (e.g. `sdk.AMM.BuyWithRawResponse`) when you need the underlying HTTP status and headers.
## 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)
```
The SDK has no wrapper for `POST /orders/cancel` (cancel by `clientOrderId`), `POST /orders/cancel-batch`, `POST /orders/status/batch`, the async `POST /v2/orders` and `GET /v2/orders/status/{id}` endpoints, or `POST /heartbeats`. `CreateOrderParams` also has no `ClientOrderID`; only a cancel-replace replacement (`CancelReplaceOrderParams.ClientOrderID`) can tag an order with one. Call the uncovered endpoints through the raw client:
```go theme={null}
raw, err := client.PostRaw(ctx, "/orders/cancel", map[string]string{"clientOrderId": "my-tag-42"})
if err != nil {
log.Fatal(err)
}
fmt.Println(string(raw.Body))
```
## Cancel and replace
`CancelReplace` cancels one open order and submits a replacement in the same request. Use it to reprice or resize a resting order in one round-trip instead of separate cancel and create calls. `CancelReplaceBatch` runs several of these operations in a single call.
The cancel and the replacement are **not atomic**: they report independent outcomes, and a successful cancel does not guarantee a successful replacement. Choose the failure mode with `CancelReplaceMode`:
| Mode | Cancellation fails | Replacement result |
| - | - | - |
| `CancelReplaceStopOnFailure` | Stops the operation | `NOT_ATTEMPTED` |
| `CancelReplaceAllowFailure` | Continues the operation | `SUCCESS`, `FAILURE`, or `UNKNOWN` independently |
Identify the order to cancel with `CancelByOrderID` or `CancelByClientOrderID`. The replacement is a normal signed order and uses the same argument structs as `CreateOrder` (`GTCOrderArgs`, `FAKOrderArgs`, `FOKOrderArgs`).
### Single cancel-replace
```go theme={null}
result, err := orderClient.CancelReplace(ctx, limitless.CancelReplaceParams{
Cancel: limitless.CancelByOrderID("order_abc123"),
Mode: limitless.CancelReplaceStopOnFailure,
Replacement: limitless.CancelReplaceOrderParams{
MarketSlug: market.Slug,
OrderType: limitless.OrderTypeGTC,
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: limitless.SideBuy,
Price: 0.62,
Size: 100,
},
},
})
if err != nil {
log.Fatal(err)
}
if result.Cancel.Status() == limitless.CancelReplaceCancelSuccess {
if data, ok := result.Cancel.Success(); ok {
fmt.Println("Cancelled:", data.OrderID)
}
}
if result.Replacement.Status() == limitless.CancelReplaceReplacementSuccess {
if data, ok := result.Replacement.Success(); ok {
fmt.Println("Replacement placed:", data.Order.ID)
}
}
```
### Batch cancel-replace
Each operation runs independently and its result is returned with the caller's `Index`:
```go theme={null}
batch, err := orderClient.CancelReplaceBatch(ctx, []limitless.CancelReplaceParams{
{
Cancel: limitless.CancelByOrderID("order_abc123"),
Mode: limitless.CancelReplaceAllowFailure,
Replacement: limitless.CancelReplaceOrderParams{
MarketSlug: market.Slug,
OrderType: limitless.OrderTypeGTC,
Args: limitless.GTCOrderArgs{TokenID: market.Tokens.Yes, Side: limitless.SideBuy, Price: 0.62, Size: 100},
},
},
{
Cancel: limitless.CancelByClientOrderID("my-tag-42"),
Mode: limitless.CancelReplaceStopOnFailure,
Replacement: limitless.CancelReplaceOrderParams{
MarketSlug: market.Slug,
OrderType: limitless.OrderTypeGTC,
Args: limitless.GTCOrderArgs{TokenID: market.Tokens.No, Side: limitless.SideSell, Price: 0.41, Size: 50},
},
},
})
if err != nil {
log.Fatal(err)
}
for _, item := range batch.Results {
fmt.Println(item.Index, item.Cancel.Status(), item.Replacement.Status())
}
```
### Delegated cancel-replace
Partners with the `delegated_signing` scope call `delegatedOrders.CancelReplace` and `delegatedOrders.CancelReplaceBatch`. The server signs the replacement using the sub-account's managed wallet, so no signing key is required. Set `OnBehalfOf` (the sub-account profile ID) on every operation:
```go theme={null}
_, err := delegatedOrders.CancelReplace(ctx, limitless.DelegatedCancelReplaceParams{
Cancel: limitless.CancelByOrderID("order_abc123"),
Mode: limitless.CancelReplaceStopOnFailure,
OnBehalfOf: partnerAccount.ProfileID,
Replacement: limitless.CancelReplaceOrderParams{
MarketSlug: market.Slug,
OrderType: limitless.OrderTypeGTC,
Args: limitless.GTCOrderArgs{TokenID: market.Tokens.Yes, Side: limitless.SideBuy, Price: 0.62, Size: 100},
},
})
```
See [`POST /orders/cancel-replace`](/api-reference/trading/cancel-replace) and [`POST /orders/cancel-replace/batch`](/api-reference/trading/cancel-replace-batch) for the full request and response shapes, per-status fields, and failure semantics.
## 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-til-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"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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)
```
Check new server wallets with `CheckAllowances` before the first delegated trade. If retryable targets are missing or failed, call `RetryAllowances` and poll again.
## List and recover sub-accounts
Use `ListAccounts` to list partner-owned sub-accounts or recover a specific child profile by wallet address. This calls `GET /profiles/partner-accounts` and requires HMAC credentials with the `account_creation` scope.
```go theme={null}
accounts, err := client.PartnerAccounts.ListAccounts(ctx, limitless.ListPartnerAccountsParams{
Limit: 25,
Page: 1,
})
if err != nil {
log.Fatal(err)
}
for _, account := range accounts.Data {
fmt.Println(account.ProfileID, account.Account, account.DisplayName)
}
recovered, err := client.PartnerAccounts.ListAccounts(ctx, limitless.ListPartnerAccountsParams{
Account: "0x1676716Ef7F19B5C5d690631CB57cf0bFD900A3d",
Limit: 25,
Page: 1,
})
if err != nil {
log.Fatal(err)
}
if len(recovered.Data) > 0 {
fmt.Println("profile ID:", recovered.Data[0].ProfileID)
}
```
| Parameter | Description |
| - | - |
| `Account` | Optional exact EVM address filter for recovering a known child account |
| `Limit` | Optional page size. The API caps values above `25` to `25` |
| `Page` | Optional 1-indexed page number |
Do not send `x-on-behalf-of` to this endpoint. Results are always scoped to the authenticated partner behind the HMAC token.
## 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
You must allowlist explicit treasury destinations for server-wallet withdrawals 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 profile, open positions, and trading history from the Limitless Exchange API. It requires an authenticated `HttpClient`.
## Setup
```go theme={null}
import (
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
portfolio := limitless.NewPortfolioFetcher(client)
```
`PortfolioFetcher` requires an authenticated client. Use `WithHMACCredentials` with a scoped API token (recommended). The legacy `LIMITLESS_API_KEY` / `WithAPIKey` still works but is not recommended for new integrations.
## Current profile
Use `GetCurrentProfile()` to fetch the authenticated caller's private profile via `GET /profiles/me`. This is the preferred helper when the client is already authenticated and you do not want to pass a wallet address.
```go theme={null}
ctx := context.Background()
profile, err := portfolio.GetCurrentProfile(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Profile ID: %d\n", profile.ID)
fmt.Printf("Account: %s\n", profile.Account)
if profile.Rank != nil {
fmt.Printf("Fee rate: %d bps\n", profile.Rank.FeeRateBps)
}
```
## Fetching a profile by address
Use `GetProfile()` to retrieve your own profile by wallet address (the API returns 403 for any other address). Prefer `GetCurrentProfile()`, which needs no address. Other users' public profiles are served by `GET /profiles/public/{address}`, which has no SDK method yet; call it with `client.GetRaw`.
```go theme={null}
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 (nil if unset) |
| `DisplayName` | `*string` | Display name (nil if unset) |
| `Rank` | `*UserRank` | User rank with fee rate |
| `Points` | `*float64` | Accumulated points (nil if unset) |
## 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` | `*string` | Accumulated points (nil if unset) |
| `Rewards` | `*PortfolioRewards` | Accumulated rewards (nil if unset) |
`GetPositions`, `GetUserHistory` and `GetUserOrders` always read the authenticated profile: the Go client has no per-request header hook for GET requests, so it cannot send `x-on-behalf-of` on a single call. To read a server-wallet sub-account, build a second `HttpClient` with `WithAdditionalHeaders(map[string]string{"x-on-behalf-of": ""})` and use it only for these reads. The header is rejected with 400 on `POST /orders` and the cancel endpoints, so never set it on the client that places or cancels orders.
## 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 — yes: %s, no: %s\n", pos.Market.Title, pos.TokensBalance.Yes, pos.TokensBalance.No)
}
```
### CLOBPosition fields
| Field | Type | Description |
| - | - | - |
| `Market` | `PositionMarket` | Market title, slug, and metadata |
| `MakerAddress` | `string` | Your wallet address |
| `Positions` | `CLOBPositionSides` | Position details per side |
| `TokensBalance` | `TokenBalance` | Current token balance (per-outcome) |
| `LatestTrade` | `LatestTrade` | Most recent trade on this position |
| `Orders` | `*CLOBPositionOrders` | Open orders (nil if none) |
## 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 cursor-paginated trading history. The signature is `GetUserHistory(ctx, cursor string, limit int)` — pass an empty cursor for the first page, then `*history.NextCursor` for subsequent pages:
```go theme={null}
history, err := portfolio.GetUserHistory(ctx, "", 20) // first page, 20 items
if err != nil {
log.Fatal(err)
}
fmt.Printf("Fetched %d trades\n", len(history.Data))
for _, entry := range history.Data {
fmt.Printf("%+v\n", entry)
}
// Next page, if any
if history.NextCursor != nil {
next, err := portfolio.GetUserHistory(ctx, *history.NextCursor, 20)
if err != nil {
log.Fatal(err)
}
_ = next
}
```
## Complete example
```go theme={null}
package main
import (
"context"
"fmt"
"log"
"os"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
func main() {
client := limitless.NewHttpClient(
limitless.WithHMACCredentials(limitless.HMACCredentials{
TokenID: os.Getenv("LMTS_TOKEN_ID"),
Secret: os.Getenv("LMTS_TOKEN_SECRET"),
}),
)
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 — yes: %s, no: %s\n", pos.Market.Title, pos.TokensBalance.Yes, pos.TokensBalance.No)
}
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 (cursor-based; empty cursor = first page)
history, err := portfolio.GetUserHistory(ctx, "", 10)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nRecent trades: %d\n", len(history.Data))
}
```
# 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, prices, order events, transactions, and market lifecycle 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(
// Required for authenticated channels (positions, order events, transactions).
// The SDK signs the handshake automatically.
limitless.WithWebSocketHMACCredentials(limitless.HMACCredentials{
TokenID: "your_token_id",
Secret: "your_token_secret", // base64 secret from token creation
}),
)
```
| Option | Type | Default | Description |
| - | - | - | - |
| `WithWebSocketURL(url)` | `string` | `wss://ws.limitless.exchange` | WebSocket server URL |
| `WithWebSocketHMACCredentials(creds)` | `HMACCredentials` | -- | HMAC credentials (`TokenID`, `Secret`) for authenticated channels; the SDK signs the handshake. See [Authentication](/developers/authentication). |
| `WithAutoReconnect(b)` | `bool` | `true` | Automatically reconnect on disconnection |
| `WithReconnectDelay(ms)` | `int` | `1000` | Initial delay between reconnection attempts, in milliseconds (doubles per attempt with jitter, capped at 60 s) |
| `WithMaxReconnectAttempts(n)` | `int` | `0` (unlimited) | Maximum reconnection attempts |
| `WithWebSocketTimeout(ms)` | `int` | `10000` | Connection timeout in milliseconds |
| `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 you can use with `Off()` to unregister. The raw handler receives the message as `json.RawMessage`.
```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, midpoint %.3f\n",
update.MarketSlug, len(update.Orderbook.Bids), len(update.Orderbook.Asks),
update.Orderbook.AdjustedMidpoint)
})
ws.OnNewPriceData(func(price limitless.NewPriceData) {
fmt.Printf("AMM price update for %s (block %d)\n", price.MarketAddress, price.BlockNumber)
})
// Order lifecycle (authenticated; subscribe to ChannelSubscribeOrderEvents)
ws.OnMatchedOrderEvent(func(ev limitless.MatchedOrderEvent) {
fmt.Printf("Order matched (pre-settlement): %+v\n", ev)
})
ws.OnExecutionOrderEvent(func(ev limitless.ExecutionOrderEvent) {
fmt.Printf("FAK/FOK terminal: %+v\n", ev)
})
ws.OnTransaction(func(tx limitless.TransactionEvent) {
txHash := ""
if tx.TxHash != nil {
txHash = *tx.TxHash
}
fmt.Printf("Transaction %s: %s\n", txHash, tx.Status)
})
// Market lifecycle (subscribe to ChannelSubscribeMarketLifecycle)
ws.OnMarketCreated(func(ev limitless.MarketCreatedEvent) {
fmt.Printf("Market created: %+v\n", ev)
})
ws.OnMarketResolved(func(ev limitless.MarketResolvedEvent) {
fmt.Printf("Market resolved: %+v\n", ev)
})
```
### Available events
| Event | Typed Handler | Payload | Delivered by channel |
| - | - | - | - |
| `orderbookUpdate` | `OnOrderbookUpdate` | `OrderbookUpdate` | `ChannelSubscribeMarketPrices` |
| `newPriceData` | `OnNewPriceData` | `NewPriceData` | `ChannelSubscribeMarketPrices` |
| `oraclePriceData` | `OnOraclePriceData` | `OraclePriceData` | `ChannelSubscribeMarketPrices` |
| order matched | `OnMatchedOrderEvent` | `MatchedOrderEvent` | `ChannelSubscribeOrderEvents` |
| order terminal | `OnExecutionOrderEvent` | `ExecutionOrderEvent` | `ChannelSubscribeOrderEvents` |
| `tx` | `OnTransaction` | `TransactionEvent` | `ChannelSubscribeTransactions` |
| market created | `OnMarketCreated` | `MarketCreatedEvent` | `ChannelSubscribeMarketLifecycle` |
| market resolved | `OnMarketResolved` | `MarketResolvedEvent` | `ChannelSubscribeMarketLifecycle` |
Position updates are delivered as a raw `positions` event — register with `ws.On("positions", func(data json.RawMessage) { ... })`.
## Subscribing to channels
After connecting, subscribe to a channel to receive its events. CLOB orderbook updates are delivered through the market-price subscription:
```go theme={null}
err := ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, limitless.SubscriptionOptions{
MarketSlugs: []string{"btc-above-100k-march-2025"},
})
if err != nil {
log.Fatal(err)
}
```
The server answers every `subscribe_market_prices` call, first or repeat, with one `orderbookUpdate` per CLOB slug carrying the full current book, including books with empty `bids` and `asks`. You do not need a REST call to seed local state. See [Initial snapshot](/developers/websocket/market-data#initial-snapshot).
### Public channels (no authentication required)
| Constant | Description |
| - | - |
| `ChannelSubscribeMarketPrices` | Orderbook + price updates for specific markets |
| `ChannelSubscribeMarketLifecycle` | Market created / resolved events |
| `ChannelSubscribeLiveSports` | Live sports updates |
| `ChannelSubscribeLiveEsports` | Live esports updates |
### Authenticated channels (require HMAC credentials or a legacy API key)
| Constant | Description |
| - | - |
| `ChannelSubscribePositions` | Your position updates |
| `ChannelSubscribeOrderEvents` | Your order lifecycle events |
| `ChannelSubscribeTransactions` | Your transaction updates |
Each authenticated `Subscribe` re-validates the handshake HMAC headers against a 30-second window measured from `Connect`. Subscribe to positions, order events and transactions immediately after `Connect`; a later `Subscribe` fails with `Invalid HMAC authentication` even though REST calls succeed. Auto-reconnect re-signs, so restored subscriptions are unaffected. If you must subscribe late, call `Disconnect()` then `Connect()` first.
### 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
The server has no generic unsubscribe event, so `ws.Unsubscribe` cannot succeed (it returns `unsubscribe failed: ack timeout after 5s`). Use the channel-specific mechanisms instead:
```go theme={null}
// Market prices: re-subscribe with the remaining set. The server replaces your
// market rooms on every subscribe_market_prices.
err := ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, limitless.SubscriptionOptions{
MarketSlugs: []string{"eth-above-5k-march-2025"}, // btc-above-100k-march-2025 dropped
})
if err != nil {
log.Fatal(err)
}
// Market lifecycle: the server has a dedicated unsubscribe event.
if err := ws.Subscribe(ctx, limitless.ChannelUnsubscribeMarketLifecycle, limitless.SubscriptionOptions{}); err != nil {
log.Fatal(err)
}
```
To stop everything, call `ws.Disconnect()`.
## 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. The client establishes a new connection
4. The client re-signs the handshake and re-emits every subscription it recorded
Subscriptions are restored automatically after a reconnect. Subscribe to all market slugs in one `Subscribe` call: the server replaces your market rooms on every `subscribe_market_prices`, so separate calls only keep the last set. You can observe reconnects through `ws.State()`.
## 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 before connecting
ws.OnOrderbookUpdate(func(update limitless.OrderbookUpdate) {
fmt.Printf("[%s] Orderbook: %d bids, %d asks, midpoint %.3f\n",
update.MarketSlug, len(update.Orderbook.Bids), len(update.Orderbook.Asks),
update.Orderbook.AdjustedMidpoint)
})
// Connect
ctx := context.Background()
if err := ws.Connect(ctx); err != nil {
log.Fatal(err)
}
defer ws.Disconnect()
// Orderbook updates are delivered through the market-price subscription
if err := ws.Subscribe(ctx, limitless.ChannelSubscribeMarketPrices, 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 }
```
### Create 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. GTC and FAK orders use `price` and `size`; FOK orders use `maker_amount` (with `taker_amount` always set to 1)
3. The SDK posts the order 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 server submits the signed order 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 |
| `425` | Too Early | Receive-window failure or maintenance block (`code`). Re-stamp and retry only for receive-window failures; refresh maintenance status for maintenance blocks. |
| `429` | Too Many Requests | Rate limit exceeded |
| `500` | Internal Server Error | Transient server-side failure |
## Raw HTTP responses
Every API-backed service method accepts an opt-in `with_raw_response: bool` argument that returns an `HttpRawResponse` instead of the decoded model. Use it when you need the HTTP status, response headers (rate-limit metadata, request IDs, `content-type`), or the untouched response body alongside your normal call. The default return shape is unchanged when the argument is omitted or `False`.
The raw variant still raises `APIError` (and its subclasses) on any response with status `>= 400`. It only exposes the raw response on success.
```python theme={null}
from limitless_sdk.api import HttpRawResponse
# Default: decoded model.
markets = await market_fetcher.get_active_markets()
# Opt in to the raw response.
raw: HttpRawResponse = await market_fetcher.get_active_markets(
with_raw_response=True,
)
print("HTTP", raw.status) # 200
print("rate limit", raw.headers.get("x-ratelimit-remaining"))
print("markets returned:", len(raw.data["data"])) # untouched JSON body
```
### HttpRawResponse
| Field | Type | Description |
| - | - | - |
| `status` | `int` | HTTP status code. |
| `headers` | `dict[str, str]` | Response headers, lower-cased keys. |
| `data` | `Any` | The original response body, parsed as JSON when the response is JSON. |
The argument is available on every API-backed method (markets, portfolio, orders, AMM, API tokens, delegated orders, partner accounts, and market pages). WebSocket subscriptions are not affected.
## 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, delay: print(f"Retry {attempt} in {delay}s: {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, delay)` |
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, RetryConfig
http_client = HttpClient()
retryable_client = RetryableClient(
http_client,
RetryConfig(
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: {'lmts-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 raise 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)
```
The API blocks Python's built-in `urllib.request` (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
import os
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
# Authenticate with a scoped API token (token ID + secret)
http_client = HttpClient(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
```
```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
Authentication uses **scoped API tokens** with HMAC-SHA256 request signing. Generate one at [limitless.exchange](https://limitless.exchange) → profile menu → **Api keys**; you receive a **token ID** and a one-time **secret** (base64-encoded). Store the secret securely — it is shown only once.
* HMAC headers `lmts-api-key`, `lmts-timestamp`, `lmts-signature` — the SDK builds and signs them for you when you pass HMAC credentials
Store the token ID and secret in environment variables and pass them as HMAC credentials:
```bash theme={null}
export LMTS_TOKEN_ID="your-token-id"
export LMTS_TOKEN_SECRET="your-base64-secret"
```
```python theme={null}
import os
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
http_client = HttpClient(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
```
The root `Client` composes all domain services and takes the same scoped-token credentials:
```python theme={null}
import os
from limitless_sdk import Client, HMACCredentials
client = Client(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
```
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.
**Legacy:** an older API key (`LIMITLESS_API_KEY`, sent as the `X-API-Key` header) still works but is not recommended for new integrations. Prefer a scoped API token. If you must use it, pass `HttpClient(api_key="lmts_your_key_here")` or set the `LIMITLESS_API_KEY` environment variable.
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 `X-API-Key` auth (still works; prefer a scoped API token) |
| `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}
import os
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
http_client = HttpClient(
base_url="https://api.limitless.exchange",
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
```
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 browsable 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}
import os
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
from limitless_sdk.markets import MarketFetcher
# Authenticate with a scoped API token (token ID + secret)
http_client = HttpClient(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
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": "newest",
}
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` | — | Sort order: `"lp_rewards"`, `"ending_soon"`, `"newest"`, `"high_value"`, or `"liquidity"` |
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 / 1e6} shares")
print("Asks:")
for ask in orderbook.asks:
print(f" Price: {ask.price} Size: {ask.size / 1e6} shares")
print(f"Min rewardable size: {int(orderbook.min_size) / 1e6} shares")
```
`get_orderbook()` returns an `OrderBook` model, so read fields as attributes, not dictionary keys. Level `size` and `min_size` are in raw 6-decimal units (1 share = 1,000,000); `min_size` is a decimal string. `price` is not scaled.
## 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
import os
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
from limitless_sdk.markets import MarketFetcher
async def main():
http_client = HttpClient(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
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-til-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
import os
from eth_account import Account
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
from limitless_sdk.markets import MarketFetcher
from limitless_sdk.orders import OrderClient
from limitless_sdk.types import Side, OrderType
# Authenticate with a scoped API token (token ID + secret)
http_client = HttpClient(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
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.
**Wallet-mode preflight.** Accepting the one-time "choose your trading wallet" prompt in the app enables 1-click (smart wallet) trading on your Limitless profile. Once that mode is set, self-signed orders are rejected with `Signer does not match - you should use embedded address for smart wallet`. Switch the profile to EOA trading mode first: see [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
## 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.
**New trading venues.** If the market's venue has `feeModuleType` set to `"COLLATERAL"`, the signing wallet also needs a USDC approval to the venue's `feeModule` address (once per fee module), and orders must be signed with a fee rate of `0`. Updated SDK releases handle venue detection and the fee rate. See [New trading venues](/developers/venue-system#new-trading-venues).
## GTC orders (Good-til-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`. |
## Self-trade prevention
Pass `stp_policy` to control what happens when your incoming order would match your own resting order on the same token. It is a top-level request field — not part of the EIP-712 signed order — and applies to any order type. Omit it to keep the server default, `cancel_maker`.
```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",
stp_policy="cancel_maker", # "cancel_maker" | "cancel_taker" | "cancel_both"
)
```
| Value | Result |
| - | - |
| `cancel_maker` | Default. Cancels your conflicting resting order and continues with the incoming order. |
| `cancel_taker` | Rejects the incoming order before it self-trades. |
| `cancel_both` | Cancels your conflicting resting order and rejects the incoming order. |
The create-order response carries an `execution` object with the outcome:
```python theme={null}
execution = result.execution
if execution.settlement_status == "CANCELED" and execution.reason == "STP_TAKER_REJECTED":
print("Order rejected by self-trade prevention")
elif execution.stp_maker_cancels:
print("Cancelled own resting orders:", execution.stp_maker_cancels)
```
| `execution` field | Type | Description |
| - | - | - |
| `settlement_status` | `str` | `CANCELED` when a `cancel_taker` / `cancel_both` order is rejected. |
| `reason` | `str \| None` | `STP_TAKER_REJECTED` when the incoming order was rejected. |
| `stp_maker_cancels` | `list[str] \| None` | Resting order ids cancelled by `cancel_maker` / `cancel_both`. |
Self-trade prevention blocks same-profile matches on the **same token** only. Orders on a different token of the same profile are unaffected. The wire field is always `stpPolicy`, regardless of the SDK.
## 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",
)
```
## AMM trading
CLOB orders trade against the orderbook. AMM (FPMM) markets trade against a pool, and the SDK exposes them through `client.partner_amm`. The service calls `POST /amm/allowances/check`, `POST /amm/allowances/approve`, `POST /amm/buy`, and `POST /amm/sell` on behalf of a partner server wallet.
Use it when the market is an AMM market and you want the server to hold custody, sign the trade, and pay gas. See [AMM Trading (Server Wallets)](/developers/amm-trading) for the underlying endpoints and market model.
### Requirements
* Authenticate with an HMAC API token that holds **both** the `trading` and `delegated_signing` scopes, or pass a per-call Privy `identity_token`. Legacy `x-api-key` credentials are rejected.
* The trade runs against a server-wallet sub-account. Set `on_behalf_of` to the sub-account profile ID, or omit it to trade from the authenticated profile.
* Amounts are **positive integer strings** in the collateral token's base units (for USDC: `"1000000"` = 1 USDC). Never pass floats.
* `slippage_bps` is optional and ranges from `0` to `1000`. The server default is `100` (1%).
* `outcome_index` is `0` for YES and `1` for NO.
### One-time approval per wallet and market
`BUY` and `SELL` approvals are independent and set up once per wallet and market. `buy` and `sell` do **not** preflight allowances themselves. Confirm the allowance first.
`ensure_allowance` runs `check_allowance`, submits `approve_allowance` at most once when missing, then polls the check until `confirmed` is true. Tune the poll with `interval` (default `2` seconds) and `max_attempts` (default `30`).
```python theme={null}
from limitless_sdk import Client, HMACCredentials, AmmAllowanceParams
client = Client(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
market_slug = "btc-100k-weekly"
child_profile_id = 12345
await client.partner_amm.ensure_allowance(
AmmAllowanceParams(market=market_slug, side="BUY", on_behalf_of=child_profile_id)
)
await client.partner_amm.ensure_allowance(
AmmAllowanceParams(market=market_slug, side="SELL", on_behalf_of=child_profile_id)
)
```
A `submitted` response from `approve_allowance` (HTTP 202) is not confirmation. Either use `ensure_allowance`, or poll `check_allowance` until `confirmed` is true.
### Buy shares
`buy` spends an exact collateral amount on the chosen outcome. Pass a unique `idempotency_key` per trade. On a timeout retry, reuse the exact same params so the server replays the original submission rather than opening a second trade.
```python theme={null}
from limitless_sdk import AmmBuyParams
buy = await client.partner_amm.buy(
AmmBuyParams(
market=market_slug,
outcome_index=0, # 0 = YES, 1 = NO
collateral_amount="1000000", # 1 USDC in base units
slippage_bps=100, # optional, 0..1000
idempotency_key="buy-unique-key-001",
on_behalf_of=child_profile_id, # omit for a direct profile
)
)
print(buy.status, buy.expected_shares, buy.min_shares)
```
### Sell shares
`sell` requests an exact collateral return by selling outcome shares.
```python theme={null}
from limitless_sdk import AmmSellParams
sell = await client.partner_amm.sell(
AmmSellParams(
market=market_slug,
outcome_index=0,
collateral_return_amount="992015",
idempotency_key="sell-unique-key-001",
on_behalf_of=child_profile_id,
)
)
print(sell.status, sell.expected_shares, sell.max_shares)
```
Reusing an `idempotency_key` with different params raises `ConflictError` (HTTP 409). The four AMM routes share a rate limit of 10 requests / 10 seconds per actor. Pass `with_raw_response=True` to any AMM method to receive an `HttpRawResponse` exposing `status`, `headers`, and `data`.
## 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")
```
## Cancel and replace
`cancel_replace` cancels one open order and submits a replacement in the same request. Use it to reprice or resize a resting order in one round-trip instead of separate cancel and create calls. `cancel_replace_batch` runs several of these operations in a single call.
The cancel and the replacement are **not atomic**: they report independent outcomes, and a successful cancel does not guarantee a successful replacement. Choose the failure mode with `CancelReplaceMode`:
| Mode | Cancellation fails | Replacement result |
| - | - | - |
| `STOP_ON_FAILURE` | Stops the operation | `NOT_ATTEMPTED` |
| `ALLOW_FAILURE` | Continues the operation | `SUCCESS`, `FAILURE`, or `UNKNOWN` independently |
Identify the order to cancel by exactly one of `order_id` or `client_order_id`. The replacement is a normal signed order and uses the same fields as `create_order` (`order_type`, `market_slug`, `token_id`, `side`, `price`, `size` or `maker_amount`, `post_only`, `stp_policy`, etc.).
### Single cancel-replace
```python theme={null}
from limitless_sdk.types import CancelReplaceMode, OrderType, Side
result = await order_client.cancel_replace(
order_id="order_abc123",
mode=CancelReplaceMode.STOP_ON_FAILURE,
market_slug=market.slug,
order_type=OrderType.GTC,
token_id=market.tokens.yes,
side=Side.BUY,
price=0.62,
size=100,
)
if result.cancel.status == "SUCCESS":
print("Cancelled:", result.cancel.order_id)
if result.replacement.status == "SUCCESS":
print("Replacement placed:", result.replacement.data.id)
```
### Batch cancel-replace
Each operation is a dict of the same keyword arguments accepted by `cancel_replace`. Results come back with the caller's `index`:
```python theme={null}
batch = await order_client.cancel_replace_batch([
{
"order_id": "order_abc123",
"mode": CancelReplaceMode.ALLOW_FAILURE,
"market_slug": market.slug,
"order_type": OrderType.GTC,
"token_id": market.tokens.yes,
"side": Side.BUY,
"price": 0.62,
"size": 100,
},
{
"client_order_id": "my-tag-42",
"mode": CancelReplaceMode.STOP_ON_FAILURE,
"market_slug": market.slug,
"order_type": OrderType.GTC,
"token_id": market.tokens.no,
"side": Side.SELL,
"price": 0.41,
"size": 50,
},
])
for item in batch.results:
print(item.index, item.cancel.status, item.replacement.status)
```
### Delegated cancel-replace
Partners with the `delegated_signing` scope call `delegated_order_service.cancel_replace` and `.cancel_replace_batch`. The server signs the replacement using the sub-account's managed wallet, so no private key is required. Pass `on_behalf_of` (the sub-account profile ID) on every operation:
```python theme={null}
await delegated_order_service.cancel_replace(
order_id="order_abc123",
mode=CancelReplaceMode.STOP_ON_FAILURE,
on_behalf_of=partner_account.profile_id,
market_slug=market.slug,
order_type=OrderType.GTC,
token_id=market.tokens.yes,
side=Side.BUY,
price=0.62,
size=100,
)
```
See [`POST /orders/cancel-replace`](/api-reference/trading/cancel-replace) and [`POST /orders/cancel-replace/batch`](/api-reference/trading/cancel-replace-batch) for the full request and response shapes, per-status fields, and failure semantics.
## Enums reference
### Side
| Value | Description |
| - | - |
| `Side.BUY` | Buy shares with USDC |
| `Side.SELL` | Sell shares for USDC |
### OrderType
| Value | Description |
| - | - |
| `OrderType.GTC` | Good-til-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
import os
from eth_account import Account
from limitless_sdk.api import HttpClient, APIError
from limitless_sdk import HMACCredentials
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(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
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
```
Check new server wallets with `check_allowances()` before the first delegated trade. If retryable targets are missing or failed, call `retry_allowances()` and poll again.
## List and recover sub-accounts
Use `list_accounts()` to list partner-owned sub-accounts or recover a specific child profile by wallet address. This calls `GET /profiles/partner-accounts` and requires HMAC credentials with the `account_creation` scope.
```python theme={null}
from limitless_sdk import ListPartnerAccountsParams
accounts = await client.partner_accounts.list_accounts(
ListPartnerAccountsParams(limit=25, page=1)
)
for account in accounts.data:
print(account.profile_id, account.account, account.display_name)
recovered = await client.partner_accounts.list_accounts(
ListPartnerAccountsParams(
account="0x1676716Ef7F19B5C5d690631CB57cf0bFD900A3d",
limit=25,
page=1,
)
)
profile_id = recovered.data[0].profile_id if recovered.data else None
```
| Parameter | Description |
| - | - |
| `account` | Optional exact EVM address filter for recovering a known child account |
| `limit` | Optional page size. The API caps values above `25` to `25` |
| `page` | Optional 1-indexed page number |
Do not send `x-on-behalf-of` to this endpoint. Results are always scoped to the authenticated partner behind the HMAC token.
## 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
You must allowlist explicit treasury destinations for server-wallet withdrawals 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 profile, open positions, and portfolio data from the Limitless Exchange API. It requires an authenticated `HttpClient`.
## Setup
```python theme={null}
import os
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
from limitless_sdk.portfolio import PortfolioFetcher
# Authenticate with a scoped API token (token ID + secret)
http_client = HttpClient(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
portfolio = PortfolioFetcher(http_client)
```
`PortfolioFetcher` requires an authenticated client. Pass scoped API-token credentials via `hmac_credentials` (as above), or use the root `Client` with the same credentials. A legacy `LIMITLESS_API_KEY` (`X-API-Key`) still works but is not recommended for new integrations.
## Current profile
Use `get_current_profile()` to fetch the authenticated caller's private profile via `GET /profiles/me`. This is the preferred helper when the client is already authenticated and you do not want to pass a wallet address.
```python theme={null}
profile = await portfolio.get_current_profile()
print(profile["id"]) # numeric profile ID
print(profile["account"]) # authenticated wallet address
print(profile.get("rank", {}).get("feeRateBps"))
```
Use `get_profile(address)` only when you need the address-based route:
```python theme={null}
profile_by_address = await portfolio.get_profile("0xYOUR_WALLET_ADDRESS")
```
`get_current_profile()` calls `GET /profiles/me`; `get_profile(address)` calls `GET /profiles/:account`. Both return raw API responses matching the private profile schema.
## 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
import os
from limitless_sdk.api import HttpClient
from limitless_sdk import HMACCredentials
from limitless_sdk.portfolio import PortfolioFetcher
async def main():
http_client = HttpClient(
hmac_credentials=HMACCredentials(
token_id=os.environ["LMTS_TOKEN_ID"],
secret=os.environ["LMTS_TOKEN_SECRET"],
),
)
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` | `float` | `1.0` | 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)
@ws_client.on("oraclePriceData")
async def on_oracle(data):
print("Oracle price:", data)
```
### Available events
| Event | Payload | Auth | Description |
| - | - | - | - |
| `connect` | None | — | Fired when the WebSocket connection is established |
| `orderbookUpdate` | `dict` | Public | CLOB orderbook changes (new bids/asks, removals) |
| `newPriceData` | `dict` | Public | AMM market price updates for subscribed markets |
| `oraclePriceData` | `OraclePriceData` | Public | Oracle price updates for markets that resolve on an oracle (e.g. Chainlink, Pyth). Delivered on the `subscribe_market_prices` channel alongside `orderbookUpdate` / `newPriceData`. |
| `positions` | `dict` | Authenticated | Your position changes (see [Authenticated Subscriptions](#authenticated-subscriptions)) |
| `orderEvent` | `dict` | Authenticated | Your CLOB order lifecycle and settlement events |
`OraclePriceData` fields: `marketAddress` (optional), `marketSlug`, `timestamp` (unix ms), `value` (float).
## 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`.
The server answers every `subscribe_market_prices` call, first or repeat, with one `orderbookUpdate` per CLOB slug carrying the full current book, including books with empty `bids` and `asks`. You do not need a REST call to seed local state. See [Initial snapshot](/developers/websocket/market-data#initial-snapshot).
## Authenticated subscriptions
The `subscribe_positions` and `subscribe_order_events` channels are per-account and require authentication. Pass `hmac_credentials` to the config — the SDK signs the WebSocket handshake automatically (no manual headers needed). This is the same flow as the [TypeScript SDK](/developers/sdk/typescript/websocket); see [Authentication](/developers/authentication) to generate a token.
```python theme={null}
from limitless_sdk import HMACCredentials
from limitless_sdk.websocket import WebSocketClient, WebSocketConfig
config = WebSocketConfig(
url="wss://ws.limitless.exchange",
hmac_credentials=HMACCredentials(
token_id="your_token_id",
secret="your_token_secret", # base64-encoded secret from token creation
),
auto_reconnect=True,
)
ws_client = WebSocketClient(config)
```
### Positions
Subscribe to real-time updates when your positions change:
```python theme={null}
@ws_client.on("connect")
async def on_connect():
await ws_client.subscribe(
"subscribe_positions",
{"marketSlugs": ["btc-above-100k-march-2025"]},
)
@ws_client.on("positions")
async def on_positions(data):
print("Position update:", data)
```
### Order events
Subscribe to your CLOB order lifecycle and settlement events. This channel is per-account and takes no payload:
```python theme={null}
@ws_client.on("connect")
async def on_connect():
await ws_client.subscribe("subscribe_order_events")
@ws_client.on("orderEvent")
async def on_order_event(event):
# Several event shapes arrive on the same `orderEvent` name — distinguish by source, then type
if event.get("source") == "OME":
print("Order state:", event.get("type"), event.get("orderId"), event.get("clientOrderId"))
else:
print("Settlement:", event.get("type"), event.get("orderId"), event.get("txHash"))
```
Authenticated `subscribe()` calls raise `ValueError` if no `hmac_credentials` is set. Auth failures surface on the `exception` event. See the [WebSocket Events reference](/developers/websocket/order-events) for the full `orderEvent` payload shapes (`source` `OME` vs `SETTLEMENT`, and `type` values).
## 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. The client establishes a new connection
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}")
# Connect, then keep the client alive to receive events
await ws_client.connect()
await asyncio.Event().wait() # runs until interrupted (Ctrl+C)
asyncio.run(main())
```
`await ws_client.connect()` returns once the connection is established — it does **not** block. Keep the event loop alive afterwards (e.g. `await asyncio.Event().wait()`), otherwise the program exits before any events arrive. Register your `@ws_client.on(...)` handlers and `subscribe()` inside the `connect` handler before calling `connect()`.
# 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)?;
```
The Rust `DeriveApiTokenInput` always sends `scopes`; pass at least one scope explicitly, e.g. `scopes: vec![SCOPE_TRADING.to_string()]`. An empty vector is rejected with HTTP 400 ("At least one scope is required"). The backend's `["trading"]` default only applies when the key is absent from the request body.
## 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 backend submits the signed order to the exchange engine.
# Error Handling & Retry
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);
}
_ => {}
}
```
## Raw HTTP responses
Every API-backed service method has a `*_with_raw` sibling that returns `Result>`. Use it when you need the HTTP status, response headers (rate-limit metadata, request IDs, `content-type`), or the exact response bytes alongside the decoded value. The base method (without the suffix) is unchanged and still returns just the decoded value.
The raw variant still returns the same `LimitlessError::Api(ApiError)` on any response with status `>= 400`. It only exposes the raw response on success. Exception: `cancel_replace` / `cancel_replace_batch` (signed and delegated) treat HTTP 409 as a decoded `CancelReplaceResult`, so the raw variant returns `Ok` with `raw.status == 409`.
```rust theme={null}
use limitless_exchange_rust_sdk::SdkResponse;
// Default: decoded value only.
let markets = client.markets.get_active_markets(None).await?;
// Opt in to the raw response.
let response: SdkResponse<_> = client
.markets
.get_active_markets_with_raw(None)
.await?;
println!("HTTP {}", response.raw.status);
if let Some(remaining) = response
.raw
.headers
.get("x-ratelimit-remaining")
.and_then(|v| v.to_str().ok())
{
println!("rate limit remaining: {remaining}");
}
println!("markets returned: {}", response.data.data.len());
// response.raw.body holds the original response bytes.
```
### `SdkResponse`
| Field / method | Type | Description |
| - | - | - |
| `data` | `T` | The decoded response value, identical to what the base method returns. |
| `raw` | `RawResponse` | The underlying HTTP response. |
| `into_data()` | `T` | Consume the wrapper and return just the decoded value. |
| `map(f)` | `SdkResponse` | Transform `data` while preserving `raw`. |
### `RawResponse`
| Field | Type | Description |
| - | - | - |
| `status` | `u16` | HTTP status code. |
| `headers` | `HeaderMap` | Response headers. |
| `body` | `Vec` | The original response body bytes. |
The `*_with_raw` variant is available on every API-backed method (markets, portfolio, orders, AMM, API tokens, delegated orders, partner accounts, server wallets, and market pages). WebSocket subscriptions are not affected.
## `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
`425 Too Early` is not retried by default. If the API body has a trading-mode `code`, refresh maintenance status and do not retry the same blocked action. Otherwise, treat `POST /orders` failures as receive-window failures and rebuild a fresh signed order.
## `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, HmacCredentials, LogLevel, RetryConfig, RetryableClient,
};
let http = Client::builder()
.hmac_credentials(HmacCredentials {
token_id: std::env::var("LMTS_TOKEN_ID")?,
secret: std::env::var("LMTS_TOKEN_SECRET")?,
})
.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.2.0"
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, HmacCredentials};
#[tokio::main]
async fn main() -> Result<(), Box> {
// Authenticate with a scoped API token (HMAC credentials).
let http = Client::builder()
.hmac_credentials(HmacCredentials {
token_id: std::env::var("LMTS_TOKEN_ID")?,
secret: std::env::var("LMTS_TOKEN_SECRET")?,
})
.build()?;
let client = Client::from_http_client(http)?;
let positions = client.portfolio.get_positions().await?;
println!("CLOB positions: {}", positions.clob.len());
Ok(())
}
```
## Authentication
Authentication uses scoped API tokens with HMAC-SHA256 request signing:
* A scoped API token is a **token ID** plus a one-time **secret**, generated at limitless.exchange → profile menu → **Api keys**
* HMAC headers `lmts-api-key`, `lmts-timestamp`, `lmts-signature` — the SDK builds and signs them for you when you pass HMAC credentials
Pass your token ID and secret as HMAC credentials via `Client::builder()`. Read them from the environment so they never live in source:
```bash theme={null}
export LMTS_TOKEN_ID="your-token-id"
export LMTS_TOKEN_SECRET="your-base64-secret"
```
```rust theme={null}
use limitless_exchange_rust_sdk::{Client, HmacCredentials};
let http = Client::builder()
.hmac_credentials(HmacCredentials {
token_id: std::env::var("LMTS_TOKEN_ID")?,
secret: std::env::var("LMTS_TOKEN_SECRET")?,
})
.build()?;
let client = Client::from_http_client(http)?;
```
**Legacy:** an older API key (`LIMITLESS_API_KEY`, sent as the `X-API-Key` header) still works but is not recommended for new integrations. Prefer a scoped API token.
Set `LIMITLESS_API_KEY` and let `Client::new()` 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()?;
```
Or pass it explicitly with the `api_key(...)` builder:
```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
// client.amm
```
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 `X-API-Key` auth (still works; prefer a scoped API token) |
| `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
Pass filters 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::Newest),
}))
.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` (server default 25) | Markets per page, 1 to 25. Larger values return HTTP 400. |
| `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 |
The API also accepts `trending`, which crate 1.1.0 does not expose. There is no `liquidity` sort; the server rejects it with HTTP 400.
## 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 {} {} shares", bid.price, bid.size / 1e6);
}
for ask in &orderbook.asks {
println!("ask {} {} shares", ask.price, ask.size / 1e6);
}
println!("midpoint {}", orderbook.adjusted_midpoint);
```
Level `size` and `min_size` are in raw 6-decimal units (1 share = 1,000,000); `min_size` is a decimal string. `price` is not scaled.
## 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.status, order.price);
}
```
Crate 1.1.0 types `UserOrder` as the signed-order shape, but the endpoint returns matching-engine rows (`ownerId`, `marketId` as text, decimal-string `price` and `remainingSize`), so any non-empty response fails with `LimitlessError::Decode`. On 1.1.0, read the endpoint raw with `client.http.get_raw(&format!("/markets/{slug}/user-orders"), RequestOptions::default()).await?` and parse `raw.body` yourself.
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.
**Wallet-mode preflight.** Accepting the one-time "choose your trading wallet" prompt in the app enables 1-click (smart wallet) trading on your Limitless profile. Once that mode is set, self-signed orders are rejected with `Signer does not match - you should use embedded address for smart wallet`. Switch the profile to EOA trading mode first: see [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
## 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.
**New trading venues.** If the market's venue has `feeModuleType` set to `"COLLATERAL"`, the signing wallet also needs a USDC approval to the venue's `feeModule` address (once per fee module), and orders must be signed with a fee rate of `0`. Updated SDK releases handle venue detection and the fee rate. See [New trading venues](/developers/venue-system#new-trading-venues).
## 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` | Must be `"0"` / `None` (the default). Non-zero expiration is not currently supported and the API rejects it. |
| `nonce` | `Option` | Must be `0` / `None` (the default). Non-zero nonce is not currently supported and the API rejects it. |
| `taker` | `Option` | Optional taker address |
## Self-trade prevention
The published crate (1.1.0) does not expose `stp_policy` on `CreateOrderParams`; new orders use the server default, `cancel_maker`. `stp_policy` is available on `ReplacementOrderParams` for cancel-replace (see below).
```rust theme={null}
use limitless_exchange_rust_sdk::{CreateOrderParams, GtcOrderArgs, OrderType, Side};
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?;
```
Where it works today, on a cancel-replace replacement:
```rust theme={null}
replacement: ReplacementOrderParams {
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(),
client_order_id: None,
timestamp: None,
recv_window: None,
stp_policy: Some(StpPolicy::CancelMaker), // CancelMaker | CancelTaker | CancelBoth
},
```
| Value | Result |
| - | - |
| `StpPolicy::CancelMaker` | Default. Cancels your conflicting resting order and continues with the incoming order. |
| `StpPolicy::CancelTaker` | Rejects the incoming order before it self-trades. |
| `StpPolicy::CancelBoth` | Cancels your conflicting resting order and rejects the incoming order. |
The create-order response carries an `execution: Option` field with the outcome:
```rust theme={null}
if let Some(execution) = result.execution.as_ref() {
if execution.settlement_status == "CANCELED" && execution.reason.as_deref() == Some("STP_TAKER_REJECTED") {
println!("order rejected by self-trade prevention");
} else if let Some(cancels) = execution.stp_maker_cancels.as_deref().filter(|c| !c.is_empty()) {
println!("cancelled own resting orders: {:?}", cancels);
}
}
```
| `execution` field | Type | Description |
| - | - | - |
| `settlement_status` | `String` | `CANCELED` when a `cancel_taker` / `cancel_both` order is rejected. |
| `reason` | `Option` | `STP_TAKER_REJECTED` when the incoming order was rejected. |
| `stp_maker_cancels` | `Option>` | Resting order ids cancelled by `cancel_maker` / `cancel_both`. |
Self-trade prevention blocks same-profile matches on the **same token** only. Orders on a different token of the same profile are unaffected. The wire field is always `stpPolicy`, regardless of the SDK.
## 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` | Must be `"0"` / `None` (the default). Non-zero expiration is not currently supported and the API rejects it. |
| `nonce` | `Option` | Must be `0` / `None` (the default). Non-zero nonce is not currently supported and the API rejects it. |
| `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(),
},
)?;
```
## AMM trading
CLOB orders trade against the orderbook. AMM (FPMM) markets trade against a pool, and the SDK exposes them through `sdk.amm`. The service calls `POST /amm/allowances/check`, `POST /amm/allowances/approve`, `POST /amm/buy`, and `POST /amm/sell` on behalf of a partner server wallet.
Use it when the market is an AMM market and you want the server to hold custody, sign the trade, and pay gas. See [AMM Trading (Server Wallets)](/developers/amm-trading) for the underlying endpoints and market model.
### Requirements
* Authenticate with an HMAC API token that holds **both** the `trading` and `delegated_signing` scopes, or call the `*_with_identity` variants with a Privy identity token. Legacy `x-api-key` credentials are rejected.
* The trade runs against a server-wallet sub-account. Set `on_behalf_of` to the sub-account profile ID (`1..=2147483647`), or leave it `None` to trade from the authenticated profile.
* Amounts are **positive integer strings** in the collateral token's base units (for USDC: `"1000000"` = 1 USDC). Never use floats.
* `slippage_bps` is `Option`. `None` uses the server default of `100` (1%); values range from `0` to `1000`.
* `outcome_index` is `0` for YES and `1` for NO.
### One-time approval per wallet and market
`BUY` and `SELL` approvals are independent and set up once per wallet and market. `buy` and `sell` do **not** preflight allowances themselves. Confirm the allowance first.
`ensure_allowance` runs `check_allowance`, submits `approve_allowance` at most once when missing, then polls the check until `confirmed` is true. Configure the poll via `AmmAllowancePollOptions` (default 2s interval, 30 attempts).
```rust theme={null}
use limitless_exchange_rust_sdk::{
AmmAllowanceParams, AmmAllowanceSide, Client, HmacCredentials,
};
let sdk = Client::from_http_client(
Client::builder()
.hmac_credentials(HmacCredentials {
token_id: std::env::var("LIMITLESS_API_TOKEN_ID")?,
secret: std::env::var("LIMITLESS_API_TOKEN_SECRET")?,
})
.build()?,
)?;
let market = "btc-100k-weekly".to_string();
for side in [AmmAllowanceSide::Buy, AmmAllowanceSide::Sell] {
sdk.amm
.ensure_allowance(
&AmmAllowanceParams {
market: market.clone(),
side,
on_behalf_of: Some(12345),
},
None,
)
.await?;
}
```
A `submitted` response from `approve_allowance` (HTTP 202) is not confirmation. Either use `ensure_allowance`, or poll `check_allowance` until `confirmed` is true.
### Buy shares
`buy` spends an exact collateral amount on the chosen outcome. Pass a unique `idempotency_key` per trade. On a timeout retry, reuse the exact same params so the server replays the original submission rather than opening a second trade.
```rust theme={null}
use limitless_exchange_rust_sdk::AmmBuyParams;
let buy = sdk
.amm
.buy(&AmmBuyParams {
market: market.clone(),
outcome_index: 0, // 0 = YES, 1 = NO
collateral_amount: "1000000".to_string(), // 1 USDC in base units
slippage_bps: Some(100), // optional, 0..1000
idempotency_key: "buy-unique-key-001".to_string(),
on_behalf_of: Some(12345), // None for a direct profile
})
.await?;
println!("status={:?} expected={} min={}", buy.status, buy.expected_shares, buy.min_shares);
```
### Sell shares
`sell` requests an exact collateral return by selling outcome shares.
```rust theme={null}
use limitless_exchange_rust_sdk::AmmSellParams;
let sell = sdk
.amm
.sell(&AmmSellParams {
market: market.clone(),
outcome_index: 0,
collateral_return_amount: "992015".to_string(),
slippage_bps: None,
idempotency_key: "sell-unique-key-001".to_string(),
on_behalf_of: Some(12345),
})
.await?;
println!("status={:?} expected={} max={}", sell.status, sell.expected_shares, sell.max_shares);
```
Reusing an `idempotency_key` with different params returns `LimitlessError::Api` with `status == 409`. The four AMM routes share a rate limit of 10 requests / 10 seconds per actor. Every AMM method has a `*_with_raw` sibling (e.g. `sdk.amm.buy_with_raw`) that returns `SdkResponse` with the HTTP status, headers, and body.
## 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);
```
## Cancel and replace
`cancel_replace` cancels one open order and submits a replacement in the same request. Use it to reprice or resize a resting order in one round-trip instead of separate cancel and create calls. `cancel_replace_batch` runs several of these operations in a single call.
The cancel and the replacement are **not atomic**: they report independent outcomes, and a successful cancel does not guarantee a successful replacement. Choose the failure mode with `CancelReplaceMode`:
| Mode | Cancellation fails | Replacement result |
| - | - | - |
| `CancelReplaceMode::StopOnFailure` | Stops the operation | `NOT_ATTEMPTED` |
| `CancelReplaceMode::AllowFailure` | Continues the operation | `SUCCESS`, `FAILURE`, or `UNKNOWN` independently |
Identify the order to cancel with `CancelTarget::OrderId` or `CancelTarget::ClientOrderId`. The replacement uses the same `OrderArgs` fields as `create_order`.
### Single cancel-replace
```rust theme={null}
use limitless_exchange_rust_sdk::{
CancelReplaceCancelResult, CancelReplaceMode, CancelReplaceParams,
CancelReplaceReplacementResult, CancelTarget, GtcOrderArgs, OrderType,
ReplacementOrderParams, Side,
};
let tokens = market.tokens.as_ref().expect("CLOB market exposes tokens");
let result = order_client
.cancel_replace(CancelReplaceParams {
cancel: CancelTarget::OrderId { order_id: "order_abc123".to_string() },
mode: CancelReplaceMode::StopOnFailure,
on_behalf_of: None,
replacement: ReplacementOrderParams {
order_type: OrderType::Gtc,
market_slug: market.slug.clone(),
args: GtcOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Buy,
price: 0.62,
size: 100.0,
expiration: None,
nonce: None,
taker: None,
post_only: false,
}
.into(),
client_order_id: None,
timestamp: None,
recv_window: None,
stp_policy: None,
},
})
.await?;
if let CancelReplaceCancelResult::Success { order_id, .. } = &result.cancel {
println!("Cancelled: {order_id}");
}
if let CancelReplaceReplacementResult::Success { data } = &result.replacement {
println!("Replacement placed: {}", data.order.id);
}
```
### Batch cancel-replace
Each operation runs independently and its result is returned with the caller's `index`:
```rust theme={null}
let batch = order_client
.cancel_replace_batch(vec![
CancelReplaceParams {
cancel: CancelTarget::OrderId { order_id: "order_abc123".to_string() },
mode: CancelReplaceMode::AllowFailure,
on_behalf_of: None,
replacement: ReplacementOrderParams { /* ... */ },
},
CancelReplaceParams {
cancel: CancelTarget::ClientOrderId { client_order_id: "my-tag-42".to_string() },
mode: CancelReplaceMode::StopOnFailure,
on_behalf_of: None,
replacement: ReplacementOrderParams { /* ... */ },
},
])
.await?;
for item in &batch.results {
println!("{} {:?} {:?}", item.index, item.cancel, item.replacement);
}
```
### Delegated cancel-replace
Partners with the `delegated_signing` scope call `delegated_orders.cancel_replace` and `delegated_orders.cancel_replace_batch`. The server signs the replacement using the sub-account's managed wallet, so no signing key is required. Set `on_behalf_of` (the sub-account profile ID) and `fee_rate_bps` on every operation:
```rust theme={null}
use limitless_exchange_rust_sdk::DelegatedCancelReplaceParams;
let tokens = market.tokens.as_ref().expect("CLOB market exposes tokens");
delegated_orders
.cancel_replace(DelegatedCancelReplaceParams {
cancel: CancelTarget::OrderId { order_id: "order_abc123".to_string() },
mode: CancelReplaceMode::StopOnFailure,
on_behalf_of: partner_account.profile_id,
fee_rate_bps: 100,
replacement: ReplacementOrderParams {
order_type: OrderType::Gtc,
market_slug: market.slug.clone(),
args: GtcOrderArgs {
token_id: tokens.yes.clone(),
side: Side::Buy,
price: 0.62,
size: 100.0,
expiration: None,
nonce: None,
taker: None,
post_only: false,
}
.into(),
client_order_id: None,
timestamp: None,
recv_window: None,
stp_policy: None,
},
})
.await?;
```
See [`POST /orders/cancel-replace`](/api-reference/trading/cancel-replace) and [`POST /orders/cancel-replace/batch`](/api-reference/trading/cancel-replace-batch) for the full request and response shapes, per-status fields, and failure semantics.
# 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. Creating a server wallet additionally requires the `delegated_signing` scope on the token; without it the API returns 403 "Server wallet creation requires delegated\_signing scope":
```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);
```
Check new server wallets with `check_allowances` before the first delegated trade. If retryable targets are missing or failed, call `retry_allowances` and poll again.
## List and recover sub-accounts
Use `list_accounts` to list partner-owned sub-accounts or recover a specific child profile by wallet address. This calls `GET /profiles/partner-accounts` and requires HMAC credentials with the `account_creation` scope.
```rust theme={null}
use limitless_exchange_rust_sdk::ListPartnerAccountsParams;
let accounts = client
.partner_accounts
.list_accounts(&ListPartnerAccountsParams {
limit: Some(25),
page: Some(1),
..Default::default()
})
.await?;
for account in accounts.data {
println!("{} {} {:?}", account.profile_id, account.account, account.display_name);
}
let recovered = client
.partner_accounts
.list_accounts(&ListPartnerAccountsParams {
account: Some("0x1676716Ef7F19B5C5d690631CB57cf0bFD900A3d".to_string()),
limit: Some(25),
page: Some(1),
})
.await?;
let profile_id = recovered.data.first().map(|account| account.profile_id);
```
| Parameter | Description |
| - | - |
| `account` | Optional exact EVM address filter for recovering a known child account |
| `limit` | Optional page size. The API caps values above `25` to `25` |
| `page` | Optional 1-indexed page number |
Do not send `x-on-behalf-of` to this endpoint. Results are always scoped to the authenticated partner behind the HMAC token.
## 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
You must allowlist explicit treasury destinations for server-wallet withdrawals 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
* the API returns `409 Conflict` 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 scoped API-token HMAC credentials (or a legacy `LIMITLESS_API_KEY`) before calling these endpoints.
## Current profile
Use `get_current_profile()` to fetch the authenticated caller's private profile via `GET /profiles/me`. This is the preferred helper when the client is already authenticated and you do not want to pass a wallet address.
```rust theme={null}
let profile = client.portfolio.get_current_profile().await?;
println!("id {}", profile.id);
println!("account {}", profile.account);
if let Some(rank) = profile.rank.as_ref() {
println!("fee rate {} bps", rank.fee_rate_bps);
}
```
## Fetching your own profile by address
`get_profile(address)` calls `GET /profiles/{address}`, which only resolves the authenticated account: pass your own wallet address exactly as stored (the comparison is case-sensitive). Any other address returns `LimitlessError::Api` with `status == 403`. Prefer `get_current_profile()`. Public profiles (`GET /profiles/public/{address}`) are not exposed by the Rust SDK; use `client.http.get_raw("/profiles/public/0x...", RequestOptions::default())`.
```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 Streaming
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, HmacCredentials, LogLevel, WebSocketClient, WebSocketConfig,
};
let ws = WebSocketClient::new(Some(WebSocketConfig {
// Required for authenticated channels (positions, order events, transactions);
// the SDK signs the handshake automatically.
hmac_credentials: Some(HmacCredentials {
token_id: "your_token_id".to_string(),
secret: "your_token_secret".to_string(), // base64 secret from token creation
}),
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 |
| `hmac_credentials` | `Option` | `None` | HMAC credentials (`token_id`, `secret`) for authenticated channels; the SDK signs the handshake. See [Authentication](/developers/authentication). |
| `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 |
| `api_key` | `Option` | `LIMITLESS_API_KEY` env | Legacy `X-API-Key` credential; ignored when `hmac_credentials` is set |
## 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("orderEvent", |data| {
println!("first order event {}", data);
});
ws.off("orderEvent", &[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={} midpoint={:.3}",
update.market_slug,
update.orderbook.bids.len(),
update.orderbook.asks.len(),
update.orderbook.adjusted_midpoint
);
});
ws.on_new_price_data(|price| {
println!("AMM price update: {:?}", price);
});
// Order lifecycle (authenticated; subscribe to SubscribeOrderEvents).
// on_order_event receives raw serde_json::Value; on_order_event_typed deserializes to OrderEvent.
ws.on_order_event_typed(|event| {
println!("order event: {:?}", event);
});
ws.on_transaction(|tx| {
println!("{:?} {}", tx.tx_hash, tx.status);
});
// Market lifecycle (subscribe to SubscribeMarketLifecycle)
ws.on_market_created(|event| {
println!("created: {:?}", event);
});
ws.on_market_resolved(|event| {
println!("resolved: {:?}", event);
});
```
## Subscribing to channels
After connecting, subscribe to specific channels:
```rust theme={null}
use limitless_exchange_rust_sdk::{SubscriptionChannel, SubscriptionOptions};
ws.connect().await?;
// CLOB orderbook updates are delivered through the market-price subscription
ws.subscribe(
SubscriptionChannel::SubscribeMarketPrices,
SubscriptionOptions {
market_slugs: vec!["btc-above-100k-march-2025".to_string()],
..Default::default()
},
)
.await?;
```
Authenticated channels are re-authenticated on every `subscribe_*` message against the handshake timestamp, which the server accepts for 30 seconds. Issue `SubscribePositions`, `SubscribeOrderEvents` and `SubscribeTransactions` immediately after `connect()`. To add one later, call `ws.disconnect().await?` then `ws.connect().await?` (the SDK signs a fresh timestamp on connect) before subscribing.
The server answers every `subscribe_market_prices` call, first or repeat, with one `orderbookUpdate` per CLOB slug carrying the full current book, including books with empty `bids` and `asks`. You do not need a REST call to seed local state. See [Initial snapshot](/developers/websocket/market-data#initial-snapshot).
### Public channels
| Variant | Description |
| - | - |
| `SubscriptionChannel::SubscribeMarketPrices` | Orderbook + price updates for specific markets |
| `SubscriptionChannel::SubscribeMarketLifecycle` | Market created / resolved events |
| `SubscriptionChannel::SubscribeLiveSports` | Live sports updates |
| `SubscriptionChannel::SubscribeLiveEsports` | Live esports updates |
### Authenticated channels
| Variant | Description |
| - | - |
| `SubscriptionChannel::SubscribePositions` | Your position updates |
| `SubscriptionChannel::SubscribeOrderEvents` | Your order lifecycle events |
| `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
The server has no generic unsubscribe. For `subscribe_market_prices`, re-emit the subscription with the remaining slug set; the server replaces the previous set:
```rust theme={null}
ws.subscribe(
SubscriptionChannel::SubscribeMarketPrices,
SubscriptionOptions {
market_slugs: vec!["eth-above-5k-march-2025".to_string()],
..Default::default()
},
)
.await?;
```
For market lifecycle, use the dedicated channel:
```rust theme={null}
ws.subscribe(
SubscriptionChannel::UnsubscribeMarketLifecycle,
SubscriptionOptions::default(),
)
.await?;
```
`ws.unsubscribe()` in 1.1.0 emits an event the server does not handle and returns `Err(LimitlessError::WebSocket("ack timeout after 5s"))` while the server keeps streaming; avoid it.
## Updating credentials
You can rotate credentials at runtime:
```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, so 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 }
```
### Create 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'` |
| `ScopeWithdrawal` | `'withdrawal'` |
`withdrawal` is required by `serverWallets.withdraw()` (`POST /portfolio/withdraw`).
# 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. GTC and FAK orders use `price` and `size`; FOK orders use `makerAmount` (with `takerAmount` always set to 1)
3. The SDK posts the order 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 server submits the signed order 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
The SDK throws all HTTP errors 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 |
| `url` | `string?` | Request URL |
| `method` | `string?` | HTTP method |
### Error subclasses
All extend `APIError`, so `instanceof APIError` still matches.
| Class | Status |
| - | - |
| `ValidationError` | 400 |
| `AuthenticationError` | 401, 403 |
| `ConflictError` | 409 |
| `UnprocessableEntityError` | 422 |
| `TooEarlyError` | 425 |
| `RateLimitError` | 429 |
| `UpstreamUnavailableError` | 502, 503 |
| `APIError` | any other status (404, 500, 504, ...) |
## Common status codes
| Code | Meaning | Action |
| - | - | - |
| `400` | Bad request (invalid parameters) | Fix request parameters |
| `401` | Unauthorized (invalid or missing credentials) | Check your scoped API token (`LMTS_TOKEN_ID` / `LMTS_TOKEN_SECRET`) |
| `403` | Forbidden (insufficient permissions) | Verify account permissions |
| `404` | Not found (invalid slug or resource) | Check market slug or resource ID |
| `425` | Too Early — receive-window or maintenance block | If `error.data.code` is a trading-mode code, refresh maintenance status and do not retry the same blocked action. Otherwise, treat `POST /orders` failures as receive-window failures and re-stamp `timestamp`. |
| `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 |
## Raw HTTP responses
Every API-backed service method accepts an opt-in `{ withRawResponse: true }` option that returns the decoded value alongside the underlying HTTP status, headers, and response body. Use it when you need to inspect status codes, response headers (rate limit metadata, request IDs, `content-type`), or the exact bytes the API returned. The default return shape is unchanged when the option is omitted or set to `false`.
The raw variant still throws the same typed errors (`APIError` and its subclasses) on any response with status `>= 400`. It only exposes the raw response on success.
```typescript theme={null}
import { SdkResponse } from '@limitless-exchange/sdk';
// Default: decoded value only.
const markets = await marketFetcher.getActiveMarkets();
// Opt in to the raw response.
const response: SdkResponse =
await marketFetcher.getActiveMarkets(undefined, { withRawResponse: true });
const { status, headers, data } = response.getRaw();
console.log('HTTP', status); // 200
console.log('rate limit', headers['x-ratelimit-remaining']);
console.log(response.data.data.length, 'markets'); // decoded value, same as the default return
```
### SdkResponse\
| Property / method | Type | Description |
| - | - | - |
| `data` | `T` | The decoded, typed value. Identical to what the base method returns. |
| `getRaw()` | `HttpRawResponse` | The underlying HTTP response (`status`, `headers`, `data`). |
The option is available on every API-backed method (markets, portfolio, orders, AMM, API tokens, delegated orders, partner accounts, and market pages). WebSocket subscriptions are not affected. The exceptions are `cancelReplace` and `cancelReplaceBatch` (on both `OrderClient` and `delegatedOrders`), which take no options argument.
## withRetry wrapper
The SDK provides a `withRetry` utility function that wraps any async operation with configurable retry logic.
```typescript theme={null}
import { withRetry, APIError, OrderType, Side } from '@limitless-exchange/sdk';
const result = await withRetry(
() => orderClient.createOrder({
marketSlug: 'btc-100k-weekly',
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.65,
size: 100,
orderType: OrderType.GTC,
}),
{
statusCodes: [429, 500, 502, 503, 504],
maxRetries: 3,
delays: [1, 2, 4], // Seconds between retries
onRetry: (attempt, error, delay) => {
console.warn(`Retry ${attempt} in ${delay}s: ${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[]` | — | Explicit delays in **seconds** for each attempt. If omitted, exponential backoff is used. If fewer delays than retries, the last is reused. |
| `exponentialBase` | `number` | `2` | Base for exponential backoff when `delays` is omitted (`base^attempt` seconds) |
| `maxDelay` | `number` | `60` | Maximum backoff delay in **seconds** |
| `onRetry` | `(attempt: number, error: Error, delay: 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 { OrderClient, 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: 60,
})
async placeOrder(params: Parameters[0]) {
return this.orderClient.createOrder(params);
}
}
```
### Decorator options
`@retryOnErrors` takes the same `RetryConfigOptions` as `withRetry`.
| 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[]` | — | Explicit per-attempt delays in **seconds** (overrides exponential backoff) |
| `exponentialBase` | `number` | `2` | Base for exponential backoff when `delays` is omitted (`base^attempt` seconds) |
| `maxDelay` | `number` | `60` | Maximum backoff delay in **seconds** |
| `onRetry` | `(attempt: number, error: Error, delay: number) => void` | — | Callback invoked before each retry |
When `delays` is omitted, the delay for each retry is calculated (in **seconds**) as:
```
delay = min(exponentialBase ^ attempt, maxDelay)
```
For example, with `exponentialBase: 2` and `maxDelay: 60`:
| Attempt | Delay |
| - | - |
| 0 | 1 s |
| 1 | 2 s |
| 2 | 4 s |
| 3 | 8 s |
| 4 | 16 s |
| 5 | 32 s |
| 6 | 60 s (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, OrderType, Side } 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.tokens.yes,
side: Side.BUY,
price: 0.65,
size: 100,
orderType: 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',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
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',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
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}
# Scoped API token (recommended) — generate at limitless.exchange → profile → Api keys
LMTS_TOKEN_ID=your_token_id
LMTS_TOKEN_SECRET=your_base64_secret
PRIVATE_KEY=0xYourPrivateKeyHere
# Legacy API key (still works; not recommended for new integrations)
LIMITLESS_API_KEY=lmts_your_api_key_here
```
Pass `LMTS_TOKEN_ID` / `LMTS_TOKEN_SECRET` to the client as `hmacCredentials`. The legacy `LIMITLESS_API_KEY` is auto-loaded if no credentials are passed, but a scoped API token is preferred.
```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 { data: 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 scoped API token credentials and wallet:
```typescript theme={null}
import { ethers } from 'ethers';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
});
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!);
```
## Environment variables
| Variable | Required | Description |
| - | - | - |
| `LMTS_TOKEN_ID` | Recommended | Scoped token ID used for HMAC request signing. Generate at limitless.exchange → profile → Api keys. |
| `LMTS_TOKEN_SECRET` | Recommended | Base64 token secret used for HMAC request signing. Shown once at creation. |
| `PRIVATE_KEY` | For order signing | Ethereum private key used for EIP-712 order signatures. Never commit to version control. |
| `LIMITLESS_API_KEY` | Legacy (optional) | Legacy `X-API-Key` value, auto-loaded by the SDK. Still works; prefer a scoped API token. |
## 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',
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.
**Legacy:** an older API key (`LIMITLESS_API_KEY`, sent as the `X-API-Key` header) still works but is not recommended for new integrations. To use it, pass `apiKey: 'lmts_...'` or set the `LIMITLESS_API_KEY` env var instead of `hmacCredentials`. Prefer a scoped API token.
| Option | Type | Default | Description |
| - | - | - | - |
| `baseURL` | `string` | -- | API base URL |
| `apiKey` | `string` | `process.env.LIMITLESS_API_KEY` | Legacy `X-API-Key` auth (still works; prefer a scoped API token) |
| `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',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
});
```
## 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 browsable 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 (default 20, silently clamped to 100) |
| `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 { data: markets, totalMarketsCount } = await marketFetcher.getActiveMarkets({
limit: 20,
page: 1,
sortBy: 'newest',
});
console.log(`${totalMarketsCount} active markets`);
for (const market of markets) {
console.log(`${market.slug} — ${market.title}`);
}
```
### Parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `limit` | `number` | `25` | Markets per page. Maximum 25; larger values return 400. |
| `page` | `number` | `1` | Page number (1-indexed) |
| `sortBy` | `string` | (none) | Sort order. When omitted the server orders by priority, then by deadline. |
### 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.tokens.yes);
console.log('NO token ID:', market.tokens.no);
```
`getMarket()` caches the venue (signing contract addresses) of the returned market for `OrderClient`; the market itself is fetched from the API on every call. Always fetch the market before placing orders so 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)
};
tokens?: { yes: string; no: string }; // YES / NO token IDs (MarketTokens)
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[]; // YES bids, price descending
asks: OrderbookLevel[]; // YES asks, price ascending
tokenId: string; // YES position token id
adjustedMidpoint: number; // midpoint after excluding orders below minSize
midpoint: number; // midpoint of the best displayed bid and ask
maxSpread: string; // e.g. "0.035"
minSize: string; // raw 6-decimal integer string, "50000000" = 50 shares
lastTradePrice: number | null;
}
interface OrderbookLevel {
price: number; // 0 to 1
size: number; // raw 6-decimal integer, 56000000 = 56 shares
side: 'BUY' | 'SELL';
}
```
`bids` / `asks` include levels below `minSize`; `adjustedMidpoint` excludes them, so the top level and `adjustedMidpoint` can disagree. Use `midpoint` for the displayed touch.
### 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',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
});
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.markets.length);
```
```typescript theme={null}
const submarket = group.markets[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.tokens.yes);
console.log('NO token:', submarketDetail.tokens.no);
```
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',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
});
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` | `EthersLikeWallet` (e.g. `ethers.Wallet`) | Yes | Signer for EIP-712 orders |
| `marketFetcher` | `MarketFetcher` | No | Enables automatic venue caching. If omitted, venue data is fetched on each order. |
| `signingConfig` | `{ chainId, contractAddress }` | No | Override the auto-configured EIP-712 domain (custom deployments) |
| `logger` | `ILogger` | No | Optional logger |
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.
**Wallet-mode preflight.** Accepting the one-time "choose your trading wallet" prompt in the app enables 1-click (smart wallet) trading on your Limitless profile. Once that mode is set, self-signed orders are rejected with `Signer does not match - you should use embedded address for smart wallet`. Switch the profile to EOA trading mode first: see [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs).
## 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.
**New trading venues.** If the market's venue has `feeModuleType` set to `"COLLATERAL"`, the signing wallet also needs a USDC approval to the venue's `feeModule` address (once per fee module), and orders must be signed with a fee rate of `0`. Updated SDK releases handle venue detection and the fee rate. See [New trading venues](/developers/venue-system#new-trading-venues).
## 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.tokens.yes, // 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.tokens.yes, // 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.tokens.no, // 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.tokens` (yes / no) |
| `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`. |
## Self-trade prevention
Pass `stpPolicy` to control what happens when your incoming order would match your own resting order on the same token. It is a top-level request field, not part of the EIP-712 signed order, and applies to any order type. Omit it to keep the server default, `cancel_maker`.
SDK 1.1.0: `orderClient.createOrder()` and `client.delegatedOrders.createOrder()` do not accept `stpPolicy`. Passing it is a TypeScript excess-property error, and at runtime the field is dropped from the request, so the server applies `cancel_maker`. The SDK sends `stpPolicy` only on the `replacement` of `cancelReplace()` / `cancelReplaceBatch()` (and the delegated variants). To set it on a fresh order, call [`POST /orders`](/api-reference/trading/create-order) directly.
```typescript theme={null}
import { CancelReplaceMode, OrderType, Side } from '@limitless-exchange/sdk';
const result = await orderClient.cancelReplace({
cancel: { orderId: restingOrderId },
mode: CancelReplaceMode.ALLOW_FAILURE,
replacement: {
marketSlug: market.slug,
orderType: OrderType.GTC,
tokenId: market.tokens.yes, // YES token
side: Side.BUY,
price: 0.65,
size: 100,
stpPolicy: 'cancel_maker', // 'cancel_maker' | 'cancel_taker' | 'cancel_both'
},
});
```
| Value | Result |
| - | - |
| `cancel_maker` | Default. Cancels your conflicting resting order and continues with the incoming order. |
| `cancel_taker` | Rejects the incoming order before it self-trades. |
| `cancel_both` | Cancels your conflicting resting order and rejects the incoming order. |
The create-order response (`result.execution`) and the cancel-replace `replacement.data.execution` carry an `execution` object with the outcome:
```typescript theme={null}
const execution = result.replacement.data?.execution; // or `result.execution` on a POST /orders response
if (execution?.settlementStatus === 'CANCELED' && execution.reason === 'STP_TAKER_REJECTED') {
console.log('Order rejected by self-trade prevention');
} else if (execution?.stpMakerCancels?.length) {
console.log('Cancelled own resting orders:', execution.stpMakerCancels);
}
```
| `execution` field | Type | Description |
| - | - | - |
| `settlementStatus` | `string` | `CANCELED` when a `cancel_taker` / `cancel_both` order is rejected. |
| `reason` | `string?` | `STP_TAKER_REJECTED` when the incoming order was rejected. |
| `stpMakerCancels` | `string[]?` | Resting order ids cancelled by `cancel_maker` / `cancel_both`. |
Self-trade prevention blocks same-profile matches on the **same token** only. Orders on a different token of the same profile are unaffected. The wire field is always `stpPolicy`, regardless of the SDK.
## 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.tokens.yes, // YES token
side: Side.BUY,
price: 0.45,
size: 100,
orderType: OrderType.FAK,
});
if (result.makerMatches?.length) {
console.log(`FAK order matched immediately with ${result.makerMatches.length} fill(s)`);
} else {
console.log('FAK order found no resting liquidity and was killed.');
}
```
### Sell with FAK
```typescript theme={null}
const result = await orderClient.createOrder({
marketSlug: market.slug,
tokenId: market.tokens.no, // 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.tokens` (yes / no) |
| `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.tokens.yes, // 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.tokens.yes, // 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.tokens` (yes / no) |
| `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**. The group slug is not tradeable. 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.tokens.yes, // 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.
## AMM trading
CLOB orders trade against the orderbook. AMM (FPMM) markets trade against a pool, and the SDK exposes them through `client.amm`. The service calls `POST /amm/allowances/check`, `POST /amm/allowances/approve`, `POST /amm/buy`, and `POST /amm/sell` on behalf of a partner server wallet.
Use it when the market is an AMM market and you want the server to hold custody, sign the trade, and pay gas. See [AMM Trading (Server Wallets)](/developers/amm-trading) for the underlying endpoints and market model.
### Requirements
* Authenticate with an HMAC API token that holds **both** the `trading` and `delegated_signing` scopes, or pass a per-call Privy `identityToken`. Legacy `x-api-key` credentials are rejected.
* The trade runs against a server-wallet sub-account. Set `onBehalfOf` to the sub-account profile ID, or omit it to trade from the authenticated profile.
* Amounts are **positive integer strings** in the collateral token's base units (for USDC: `"1000000"` = 1 USDC). Never pass floats.
* `slippageBps` is optional and ranges from `0` to `1000`. The server default is `100` (1%).
* `outcomeIndex` is `0` for YES and `1` for NO.
### One-time approval per wallet and market
`BUY` and `SELL` approvals are independent and set up once per wallet and market. `buy` and `sell` do **not** preflight allowances themselves. Confirm the allowance first.
`ensureAllowance` runs `checkAllowance`, submits `approveAllowance` at most once when missing, then polls the check until `confirmed` is true. Tune the poll with `intervalMs` (default `2000`) and `maxAttempts` (default `30`).
```typescript theme={null}
import { Client } from '@limitless-exchange/sdk';
const client = new Client({
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID!,
secret: process.env.LMTS_TOKEN_SECRET!,
},
});
const marketSlug = 'btc-100k-weekly';
const childProfileId = 12345;
await client.amm.ensureAllowance({ market: marketSlug, side: 'BUY', onBehalfOf: childProfileId });
await client.amm.ensureAllowance({ market: marketSlug, side: 'SELL', onBehalfOf: childProfileId });
```
A `submitted` response from `approveAllowance` (HTTP 202) is not confirmation. Either use `ensureAllowance`, or poll `checkAllowance` until `confirmed` is true.
### Buy shares
`buy` spends an exact collateral amount on the chosen outcome. Pass a unique `idempotencyKey` per trade. On a timeout retry, reuse the exact same params so the server replays the original submission rather than opening a second trade.
```typescript theme={null}
const buy = await client.amm.buy({
market: marketSlug,
outcomeIndex: 0, // 0 = YES, 1 = NO
collateralAmount: '1000000', // 1 USDC in base units
slippageBps: 100, // optional, 0..1000
idempotencyKey: 'buy-unique-key-001',
onBehalfOf: childProfileId, // omit for a direct profile
});
console.log(buy.status, buy.expectedShares, buy.minShares);
```
### Sell shares
`sell` requests an exact collateral return by selling outcome shares.
```typescript theme={null}
const sell = await client.amm.sell({
market: marketSlug,
outcomeIndex: 0,
collateralReturnAmount: '992015',
idempotencyKey: 'sell-unique-key-001',
onBehalfOf: childProfileId,
});
console.log(sell.status, sell.expectedShares, sell.maxShares);
```
Reusing an `idempotencyKey` with different params raises `ConflictError` (HTTP 409). The four AMM routes share a rate limit of 10 requests / 10 seconds per actor. Pass `{ withRawResponse: true }` to any AMM method to receive an `SdkResponse` with HTTP status and headers.
## Cancelling 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);
```
## Cancel and replace
`cancelReplace` cancels one open order and submits a replacement in the same request. Use it to reprice or resize a resting order in one round-trip rather than issuing separate cancel + create calls. `cancelReplaceBatch` runs several of these operations in a single call.
The two actions are **not atomic**: the cancel and the replacement have independent outcomes, and a successful cancel does not guarantee a successful replacement. Choose the failure mode with `CancelReplaceMode`:
| Mode | Cancellation fails | Replacement result |
| - | - | - |
| `STOP_ON_FAILURE` | Stops the operation | `NOT_ATTEMPTED` |
| `ALLOW_FAILURE` | Continues the operation | `SUCCESS`, `FAILURE`, or `UNKNOWN` independently |
Identify the order to cancel by exactly one of `orderId` or `clientOrderId`. The replacement is a normal signed order and accepts the same fields as `createOrder` (`orderType`, `marketSlug`, `tokenId`, `side`, `price`, `size` or `makerAmount`, `postOnly`, `stpPolicy`, etc.).
### Single cancel-replace
```typescript theme={null}
import { CancelReplaceMode, OrderType, Side } from '@limitless-exchange/sdk';
const result = await orderClient.cancelReplace({
cancel: { orderId: 'order_abc123' },
mode: CancelReplaceMode.STOP_ON_FAILURE,
replacement: {
marketSlug: market.slug,
orderType: OrderType.GTC,
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.62,
size: 100,
},
});
if (result.cancel.status === 'SUCCESS') {
console.log('Cancelled:', result.cancel.orderId);
}
if (result.replacement.status === 'SUCCESS') {
console.log('Replacement placed:', result.replacement.data.order.id);
}
```
### Batch cancel-replace
Each entry runs independently and its result is returned with the caller's `index`:
```typescript theme={null}
const batch = await orderClient.cancelReplaceBatch({
operations: [
{
cancel: { orderId: 'order_abc123' },
mode: CancelReplaceMode.ALLOW_FAILURE,
replacement: { marketSlug: market.slug, orderType: OrderType.GTC, tokenId: market.tokens.yes, side: Side.BUY, price: 0.62, size: 100 },
},
{
cancel: { clientOrderId: 'my-tag-42' },
mode: CancelReplaceMode.STOP_ON_FAILURE,
replacement: { marketSlug: market.slug, orderType: OrderType.GTC, tokenId: market.tokens.no, side: Side.SELL, price: 0.41, size: 50 },
},
],
});
for (const item of batch.results) {
console.log(item.index, item.cancel.status, item.replacement.status);
}
```
### Delegated cancel-replace
Partners with the `delegated_signing` scope call `client.delegatedOrders.cancelReplace` and `client.delegatedOrders.cancelReplaceBatch`. The server signs the replacement using the sub-account's managed wallet, so no private key is required. Set `onBehalfOf` to the sub-account profile ID on every operation:
```typescript theme={null}
await client.delegatedOrders.cancelReplace({
cancel: { orderId: 'order_abc123' },
mode: CancelReplaceMode.STOP_ON_FAILURE,
onBehalfOf: partnerAccount.profileId,
replacement: {
marketSlug: market.slug,
orderType: OrderType.GTC,
tokenId: market.tokens.yes,
side: Side.BUY,
price: 0.62,
size: 100,
},
});
```
See [`POST /orders/cancel-replace`](/api-reference/trading/cancel-replace) and [`POST /orders/cancel-replace/batch`](/api-reference/trading/cancel-replace-batch) for the full request and response shapes, per-status fields, and failure semantics.
## Order states
Three status vocabularies appear on the wire, depending on the surface:
| Surface | Field | Values |
| - | - | - |
| Resting order (`market.getUserOrders()`, `GET /markets/{slug}/user-orders`) | `status` | `LIVE`, `MATCHED`, `CANCELED`, `UNMATCHED` (single L) |
| Create-order response (`result.execution`) | `settlementStatus` | `UNMATCHED`, `MATCHED`, `MINED`, `CONFIRMED`, `RETRYING`, `FAILED`, `DELAYED`, `CANCELED` |
| WebSocket `orderEvent` with `source: 'OME'` and `type: 'EXECUTION'` (FAK/FOK terminal frame) | `status` | `FILLED`, `PARTIALLY_FILLED`, `KILLED` |
The SDK does not export an order-status enum; `getUserOrders()` rows are untyped (`any[]`), and `execution.settlementStatus` is typed as `string`.
## 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.tokens.yes,
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 & 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%. The API rejects prices outside this range.
```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. Server-wallet mode (`createServerWallet: true`) additionally requires the `delegated_signing` scope; without it the call returns 403.
## 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.
Requires both `account_creation` and `delegated_signing` scopes on the token.
```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
```
Check new server wallets with `checkAllowances()` before the first delegated trade. If retryable targets are missing or failed, call `retryAllowances()` and poll again.
## List and recover sub-accounts
Use `listAccounts()` to list partner-owned sub-accounts or recover a specific child profile by wallet address. This calls `GET /profiles/partner-accounts` and requires HMAC credentials with the `account_creation` scope.
```typescript theme={null}
const accounts = await client.partnerAccounts.listAccounts({
limit: 25,
page: 1,
});
for (const account of accounts.data) {
console.log(account.profileId, account.account, account.displayName);
}
const recovered = await client.partnerAccounts.listAccounts({
account: '0x1676716Ef7F19B5C5d690631CB57cf0bFD900A3d',
limit: 25,
page: 1,
});
const profileId = recovered.data[0]?.profileId;
```
| Parameter | Description |
| - | - |
| `account` | Optional exact EVM address filter for recovering a known child account |
| `limit` | Optional page size. The API caps values above `25` to `25` |
| `page` | Optional 1-indexed page number |
Do not send `x-on-behalf-of` to this endpoint. Results are always scoped to the authenticated partner behind the HMAC token.
## 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
You must allowlist explicit treasury destinations for server-wallet withdrawals 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 profile, positions, trade history, and accumulated points. Authentication is required.
```typescript theme={null}
import { HttpClient, PortfolioFetcher } from '@limitless-exchange/sdk';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
});
const portfolio = new PortfolioFetcher(httpClient);
```
## Current profile
Call `getProfile()` without an address to fetch the authenticated caller's private profile via `GET /profiles/me`. This avoids passing or persisting the wallet address when the client is already authenticated.
```typescript theme={null}
const currentProfile = await portfolio.getProfile();
console.log(currentProfile.id); // numeric profile ID
console.log(currentProfile.account); // authenticated wallet address
console.log(currentProfile.rank?.feeRateBps); // fee rate used for signing
```
To keep the existing address-based lookup, pass an address:
```typescript theme={null}
const profileByAddress = await portfolio.getProfile('0xYOUR_WALLET_ADDRESS');
```
`getProfile()` without an address calls `GET /profiles/me`; `getProfile(address)` calls `GET /profiles/:account`. Both return the private authenticated profile shape and require authentication.
## 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: shares ${position.tokensBalance.yes}, PnL ${position.positions.yes.unrealizedPnl}`);
console.log(` NO: shares ${position.tokensBalance.no}, PnL ${position.positions.no.unrealizedPnl}`);
}
```
### CLOB position structure
```typescript theme={null}
interface ClobPosition {
market: { id: number | string; slug: string; title: string; closed: boolean; deadline: string; /* ... */ };
makerAddress: string;
positions: { yes: PositionSide; no: PositionSide };
tokensBalance: { yes: string; no: string }; // shares held per side, 6-decimal integer strings
latestTrade: { latestYesPrice: number; latestNoPrice: number; outcomeTokenPrice: number };
orders?: { liveOrders: any[]; totalCollateralLocked: string };
rewards?: { epochs: any[]; isEarning: boolean };
}
interface PositionSide {
cost: string; // cost basis in USDC, 6-decimal integer string
fillPrice: string; // average fill price, 6-decimal integer string
marketValue: string; // current value, 6-decimal integer string
realisedPnl: string; // 6-decimal integer string
unrealizedPnl: string; // 6-decimal integer string
}
```
| Field | Description |
| - | - |
| `tokensBalance.yes` / `.no` | Shares held per side (6-decimal integer string) |
| `positions..cost` | Cost basis in USDC (6-decimal integer string) |
| `positions..marketValue` | Current value in USDC (6-decimal integer string) |
| `positions..unrealizedPnl` | Unrealized PnL in USDC (6-decimal integer string) |
| `positions..realisedPnl` | Realized PnL in USDC (6-decimal integer string) |
## AMM positions
Retrieve only your AMM market positions:
```typescript theme={null}
const ammPositions = await portfolio.getAMMPositions();
for (const position of ammPositions) {
const side = position.outcomeIndex === 0 ? 'YES' : 'NO';
console.log(`Market: ${position.market.slug}`);
console.log(` ${side}: ${position.outcomeTokenAmount} shares, PnL ${position.unrealizedPnl}`);
}
```
AMM positions are one entry per outcome (`outcomeIndex` 0 = YES, 1 = NO); amounts are decimal strings.
## Trade history
Fetch paginated trade history:
```typescript theme={null}
const history = await portfolio.getUserHistory();
for (const entry of history.data) {
console.log(
entry.strategy, // e.g. "Market Buy" / "Market Sell" / "Split" / "Convert"
entry.market?.slug,
entry.outcomeTokenAmount, // filled size (shares)
entry.outcomeTokenPrice, // size-weighted average price
entry.transactionHash, // settlement tx hash — your join key to a placed order
);
}
// Next page: pass the returned cursor
if (history.nextCursor) {
const page2 = await portfolio.getUserHistory(history.nextCursor, 20);
}
```
### Parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `cursor` | `string` | — | Opaque cursor from the previous response's `nextCursor`. Omit for the first page. |
| `limit` | `number` | `20` | Number of entries per page (max 100) |
### Response
`getUserHistory()` resolves to `{ data, nextCursor }`. The feed is a merged, newest-first activity stream (CLOB trades, AMM trades, splits/merges, NegRisk conversions, claims); tell entry kinds apart by `strategy`. Each entry in `data`:
| Field | Type | Description |
| - | - | - |
| `blockTimestamp` | `number` | Mined time, Unix seconds |
| `market` | `object` | Market snapshot (`id`, `slug`, `title`, `deadline`) |
| `outcomeIndex` | `number` | `0` = YES, `1` = NO |
| `outcomeTokenAmount` | `string` | Filled size (shares) |
| `outcomeTokenAmounts` | `string[]` | Per-outcome split `[yes, no]` |
| `outcomeTokenPrice` | `number` | Size-weighted average fill price |
| `collateralAmount` | `string` | USDC spent / received |
| `strategy` | `string` | `Buy` · `Sell` · `Limit Buy` · `Limit Sell` · `Market Buy` · `Market Sell` · `Split` · `Merge` · `Convert` · `Claim` |
| `transactionHash` | `string` | Settlement tx hash — the key to match an entry to an order you placed |
| `orderId` | `string?` | CLOB rows: the order the fill belongs to |
| `tradeEventId` | `string?` | CLOB rows: settlement trade event id |
| `makerMatchId` | `string?` | CLOB maker rows only |
A filled order maps to a single trade entry, keyed by `transactionHash`. This is the same value returned as `execution.txHash` on the mined `POST /orders` response and on the `MINED` settlement WebSocket event. CLOB rows also carry `orderId` (your order), `tradeEventId`, and for maker fills `makerMatchId`; `transactionHash` remains the key shared with `execution.txHash` and the `MINED` settlement event. The SDK `HistoryEntry` type does not declare these ids yet, so widen the type or read them as `(entry as { orderId?: string }).orderId`.
## 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.tokensBalance.yes} shares, PnL: ${position.positions.yes.unrealizedPnl}`);
console.log(` NO: ${position.tokensBalance.no} 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 API token credentials.');
} else {
console.error(`API error ${error.status}: ${error.message}`);
}
} else {
throw error;
}
}
```
See the [Error Handling & 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, order lifecycle events, and transaction notifications.
```typescript theme={null}
import { WebSocketClient } from '@limitless-exchange/sdk';
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
autoReconnect: true,
});
await ws.connect();
```
Samples use top-level `await` (ESM). In CommonJS wrap them in an `async` function.
### Constructor options
| Option | Type | Default | Description |
| - | - | - | - |
| `url` | `string` | `wss://ws.limitless.exchange` | WebSocket server URL |
| `autoReconnect` | `boolean` | `true` | Automatically reconnect on disconnection |
| `hmacCredentials` | `{ tokenId, secret }` | -- | Required for authenticated subscriptions (positions, order events, transactions). The SDK signs the handshake automatically; on 1.1.0 it does not re-sign on reconnect (see Connection management). |
| `apiKey` | `string` | -- | Legacy `X-API-Key` auth; ignored when `hmacCredentials` is set |
| `reconnectDelay` | `number` | `1000` | Initial reconnect delay, ms (backs off to 60 s) |
| `maxReconnectAttempts` | `number` | `Infinity` | Give up after this many attempts |
| `timeout` | `number` | `10000` | Connect timeout, ms |
Public subscriptions (market prices) do not require authentication. Authenticated subscriptions (positions, order events, transactions) require `hmacCredentials` — see [Authentication](/developers/authentication) to generate a token.
## 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',
});
await ws.connect();
await 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. To stop receiving a market, re-send `subscribe_market_prices` with the remaining set. The only server-side unsubscribe is `unsubscribe_market_lifecycle`, sent through `subscribe()`; `ws.unsubscribe()` emits a generic `unsubscribe` event the server does not handle, so it rejects after its 5 second ack timeout.
The server answers every `subscribe_market_prices` call, first or repeat, with one `orderbookUpdate` per CLOB slug carrying the full current book, including books with empty `bids` and `asks`. You do not need a REST call to seed local state. See [Initial snapshot](/developers/websocket/market-data#initial-snapshot).
## Authenticated subscriptions
These subscriptions require `hmacCredentials` passed to the constructor. The SDK signs the WebSocket handshake automatically.
The server re-validates the handshake signature on every authenticated `subscribe` call and accepts `lmts-timestamp` values at most 30 seconds old. Issue `subscribe_positions`, `subscribe_order_events` and `subscribe_transactions` immediately after `connect()`; to subscribe later, call `await ws.disconnect(); await ws.connect();` first so the handshake is re-signed.
```typescript theme={null}
const ws = new WebSocketClient({
url: 'wss://ws.limitless.exchange',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
});
await ws.connect();
```
### Positions
Subscribe to real-time updates when your positions change:
```typescript theme={null}
await ws.subscribe('subscribe_positions', {
marketSlugs: ['btc-100k-weekly'],
marketAddresses: ['0x1234...'],
});
ws.on('positions', (data) => {
console.log('Position update:', data);
});
```
`subscribe_positions` requires `marketSlugs` and/or `marketAddresses`; a call without them is ignored by the server (no ack, no error). It has replace semantics like `subscribe_market_prices`.
### Order events
Subscribe to CLOB order lifecycle and settlement events for your account:
```typescript theme={null}
await ws.subscribe('subscribe_order_events');
ws.on('orderEvent', (event: OrderEvent) => {
if (event.source === 'OME') {
console.log('Order state:', event.type, event.orderId, event.clientOrderId);
} else {
console.log('Settlement:', event.type, event.orderId, event.clientOrderId, event.txHash);
}
});
```
`orderEvent` uses `source` as a discriminator:
* `source: 'OME'` — off-chain order state changes: placement, update, cancellation, and `EXECUTION`, the terminal frame for FAK/FOK orders (`status` FILLED, PARTIALLY\_FILLED or KILLED); on this frame `price` and `remainingSize` are strings in raw 6-decimal units.
* `source: 'SETTLEMENT'` — on-chain settlement results for the taker order and each matched maker order.
For settlement events, `clientOrderId` is the id of the recipient's own `orderId`. If only one side of a trade supplied a `clientOrderId`, only that side's event includes it. Counterparty order ids inside `takerOrderId` or `makerMatches[]` are not resolved to client ids.
### Transactions
Subscribe to transaction confirmations:
```typescript theme={null}
await ws.subscribe('subscribe_transactions');
ws.on('tx', (data) => {
console.log('Transaction:', data.txHash, data.status, data.source);
});
```
## Events
### orderbookUpdate (CLOB markets)
Fired once per slug right after you subscribe, then whenever a CLOB market orderbook changes. Contains the full current 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) => {
const prices = Array.isArray(data.updatedPrices) ? data.updatedPrices : [data.updatedPrices];
for (const p of prices) {
console.log(data.marketAddress, 'YES:', p.yesPrice, 'NO:', p.noPrice);
}
});
```
The first `newPriceData` after `subscribe_market_prices` is a snapshot whose `updatedPrices` is a single object (with `collateralDecimals`) rather than an array; the sample above normalises it with `Array.isArray`.
### oraclePriceData
Fired when the oracle backing a subscribed market publishes a new price (e.g. Chainlink, Pyth). Delivered on the same `subscribe_market_prices` channel as `orderbookUpdate` and `newPriceData`, so no separate subscription is required.
```typescript theme={null}
ws.on('oraclePriceData', (data: OraclePriceData) => {
console.log('Market:', data.marketSlug);
console.log('Oracle value:', data.value);
console.log('At (ms):', data.timestamp);
});
```
`OraclePriceData` fields: `marketAddress` (nullable), `marketSlug`, `timestamp` (unix ms), `value` (number).
### 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.txHash);
console.log('Status:', data.status);
});
```
## Type definitions
```typescript theme={null}
interface OrderbookUpdate {
marketSlug: string;
version: number; // monotonic per market; drop frames with version <= last seen
timestamp: string;
orderbook: {
bids: { price: number; size: number; side: 'BUY' }[]; // size: raw 6-decimal units, 1 share = 1,000,000
asks: { price: number; size: number; side: 'SELL' }[]; // size: raw 6-decimal units, 1 share = 1,000,000
tokenId: string;
adjustedMidpoint: number;
midpoint: number;
maxSpread: number;
minSize: number; // same raw 6-decimal units as size
};
}
interface NewPriceData {
marketAddress: string;
updatedPrices: { marketId: number; marketAddress: string; yesPrice: number; noPrice: number }[];
blockNumber: number;
timestamp: string;
}
interface OraclePriceData {
marketAddress: string | null;
marketSlug: string;
timestamp: number;
value: number;
}
// One frame per market.
type PositionEvent =
| {
type: 'CLOB';
account: string;
marketSlug: string;
tokenIds: string[];
positions: Array<{
tokenId: string;
marketId: number | string;
ctfBalance: string; // shares, 6-decimal integer string
marketValue: string;
averageFillPrice: string;
costBasis: string;
}>;
timestamp?: string; // live frames only
}
| { type: 'AMM'; account: string; marketAddress: string; positions: unknown[] };
type OrderEvent = OmeOrderEvent | SettlementOrderEvent;
interface OmeLifecycleOrderEvent {
source: 'OME';
type: 'PLACEMENT' | 'UPDATE' | 'CANCELLATION';
eventId: number;
orderId: string;
clientOrderId?: string;
userId: number;
marketId: string;
token: string;
side: 'BUY' | 'SELL';
price: number;
remainingSize: number;
reason?: string;
occurredAt?: string | null;
publishedAt?: string;
timestamp: string;
}
interface OmeExecutionOrderEvent {
source: 'OME';
type: 'EXECUTION';
status: 'FILLED' | 'PARTIALLY_FILLED' | 'KILLED';
eventId: string;
orderId: string;
userId: number;
marketId: string;
token: string;
side: 'BUY' | 'SELL';
price: string;
remainingSize: string;
occurredAt?: string | null;
publishedAt?: string;
timestamp: string;
}
type OmeOrderEvent = OmeLifecycleOrderEvent | OmeExecutionOrderEvent;
interface SettlementOrderEvent {
source: 'SETTLEMENT';
type: 'MATCHED' | 'MINED' | 'FAILED';
eventId: string;
tradeEventId: string;
orderId?: string;
clientOrderId?: string;
takerOrderId?: string;
takerAccount?: string;
makerMatches?: Array<{
account: string;
orderId: string;
matchedSize: string;
price: string;
}>;
marketSlug?: string;
tokenId?: string;
side?: 'BUY' | 'SELL';
price?: string;
amountContracts?: string;
amountCollateral?: string;
configuredFeeRateBps?: number;
effectiveFeeBps?: number;
feeAmountContracts?: string;
feeAmountCollateral?: string;
isEstimate?: boolean; // true on MATCHED frames: fee fields are estimates
matchedAt?: string | null;
occurredAt?: string | null;
publishedAt?: string;
txHash?: string;
timestamp: string;
}
interface TransactionEvent {
txHash?: string;
status: 'CONFIRMED' | 'FAILED';
source: string;
timestamp: string;
marketAddress?: string;
marketSlug?: string;
tokenId?: string;
conditionId?: string;
amountContracts?: string;
amountCollateral?: string;
price?: string;
side?: 'BUY' | 'SELL';
orderId?: string;
clientOrderId?: string;
tradeEventId?: string;
}
```
## Connection management
### Disconnect handling
Listen for disconnections and clean up resources:
```typescript theme={null}
ws.on('disconnect', (reason: string) => {
console.log('Disconnected:', reason);
});
```
When `autoReconnect` is enabled the client reconnects on its own. `WebSocketClient` exposes no `reconnect` event; its connection events are `connect`, `disconnect` and `error`. On SDK 1.1.0 an automatic reconnect does not restore any subscription, public or authenticated, and authenticated channels are rejected with `Invalid HMAC authentication` because the original handshake signature is replayed. To survive a drop, listen for `disconnect`, call `await ws.disconnect(); await ws.connect();` and re-issue your `subscribe()` calls. The SDK `main` branch re-signs the handshake and re-subscribes automatically; that fix is not yet published to npm.
### 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
`WebSocketClient` has no catch-all listener in 1.1.0 (there is no `onAny`). Log the events you care about one by one; `system` carries server-side messages:
```typescript theme={null}
for (const event of ['orderbookUpdate', 'newPriceData', 'orderEvent', 'positions', 'tx', 'system'] as const) {
ws.on(event, (data: unknown) => console.log(`[WS] ${event}:`, JSON.stringify(data, 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',
hmacCredentials: {
tokenId: process.env.LMTS_TOKEN_ID,
secret: process.env.LMTS_TOKEN_SECRET,
},
autoReconnect: true,
});
await ws.connect();
await ws.subscribe('subscribe_market_prices', {
marketSlugs: ['btc-100k-weekly'],
});
await ws.subscribe('subscribe_positions', { marketSlugs: ['btc-100k-weekly'] });
await ws.subscribe('subscribe_order_events');
await 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('orderEvent', (event) => {
console.log('[OrderEvent]', event.source, event.type, event.orderId, event.clientOrderId ?? '(no client id)');
});
ws.on('tx', (data) => {
console.log(`[Tx] ${data.txHash} ${data.status}`);
});
ws.on('disconnect', (reason) => {
console.log('[Disconnected]', reason);
});
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..."
}
}
```
| Field | Description |
| - | - |
| `exchange` | Exchange contract. Use it as the EIP-712 `verifyingContract`. |
| `adapter` | NegRisk adapter contract. `null` on simple CLOB markets. |
| `feeModule` | Fee module contract. Present only on [new trading venues](#new-trading-venues). |
| `feeModuleType` | `"COLLATERAL"` on [new trading venues](#new-trading-venues). Legacy venues omit both `feeModule` and `feeModuleType`. |
## 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` |
| **BUY** and **SELL** (not `postOnly`) | [New trading venues](#new-trading-venues) | USDC → `venue.feeModule` (in addition to the approvals above) |
For NegRisk SELL orders, you must approve to **both** the exchange and the adapter addresses.
## New trading venues
A market is on a new trading venue when `venue.feeModuleType` is `"COLLATERAL"`. The venue then also includes a `feeModule` address. Legacy venues omit both fields and work as before.
Moving existing code over? Follow the [migration guide](/developers/migrate-to-new-trading-venues), which has the checklist, fee module addresses, minimum SDK versions and common errors.
```json theme={null}
{
"venue": {
"exchange": "0x…",
"adapter": null,
"feeModule": "0x…",
"feeModuleType": "COLLATERAL"
}
}
```
To trade on a new venue:
1. **Approve USDC to `venue.feeModule`** from the wallet that signs orders. A max approval is recommended. Approve once per fee module address. Simple CLOB and NegRisk markets use different fee modules, so always read the address from the market and never hardcode it. The approval is required for every order that is not `postOnly`, both BUY and SELL. Without it, the API rejects the order with `Insufficient collateral allowance for the order fee.`
2. **Keep your existing approvals.** The USDC and Conditional Token approvals to `venue.exchange` and `venue.adapter` remain valid, and the [EIP-712 domain](/developers/eip712-signing#eip-712-domain) (`verifyingContract` = `venue.exchange`) is unchanged.
3. **Sign `feeRateBps` as `0`.** Any other value is rejected with `FEE_RATE_MISMATCH`. Orders on legacy venues keep the current [fee rate rules](/developers/eip712-signing#field-reference).
4. **Keep a little extra USDC.** Buy orders need USDC for the order plus the trading fee.
```typescript theme={null}
import { erc20Abi, maxUint256 } from 'viem';
const API_URL = 'https://api.limitless.exchange';
const USDC = '0x…'; // USDC token address on Base
const { venue } = await (await fetch(`${API_URL}/markets/${slug}`)).json();
const isNewVenue = venue?.feeModuleType === 'COLLATERAL';
if (isNewVenue) {
// One-time per fee module address, from the wallet that signs orders
await walletClient.writeContract({
address: USDC,
abi: erc20Abi,
functionName: 'approve',
args: [venue.feeModule, maxUint256],
});
}
// Sign this value. legacyFeeRateBps follows the legacy rules: your
// effectiveFeeRateBps on fee-enabled markets, 0 on fee-disabled markets.
const feeRateBps = isNewVenue ? 0 : legacyFeeRateBps;
```
Wallets that Limitless provisions and manages (server wallets and API-key managed wallets) get these approvals automatically. The updated TypeScript, Python, Go, and Rust SDKs handle venue detection and the fee rate for you.
# Market Data
Source: https://docs.limitless.exchange/developers/websocket/market-data
Subscribe to price and orderbook updates and read the newPriceData and orderbookUpdate payloads
This page covers subscribing to price and orderbook updates with `subscribe_market_prices`, and the `newPriceData` and `orderbookUpdate` payloads that subscription delivers.
## 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.
### Initial snapshot
Right after the `system` acknowledgement, the server emits one `orderbookUpdate` per CLOB slug in your subscription with the full current book. This happens on every `subscribe_market_prices` call, including a re-subscribe over a live connection, and it covers every slug in the new set, not only the ones you added. A market with no resting orders still gets its snapshot: `bids` and `asks` are empty arrays and `midpoint` is `0.5`.
The snapshot carries the same `orderbook` object as [`GET /markets/{slug}/orderbook`](/api-reference/trading/orderbook) minus `lastTradePrice`, so you can seed local state from the socket alone.
Two cases do not produce a snapshot:
* A slug that does not resolve to a market. It is listed in the acknowledgement's `markets` array and joined, but no book is sent.
* A resolved market. It is dropped from `markets` and nothing is sent. If every requested slug is resolved you get an `error` event instead.
If a slug you expect is missing after the acknowledgement, fetch it over REST.
A re-subscribe replaces the previous subscription, so there is a brief gap in live frames between the two calls. The snapshots that follow close it: replace your local copy of each book with its snapshot, and use `version` to discard any live frame that arrives with a lower value (see [`orderbookUpdate`](/developers/websocket/market-data#orderbookupdate)).
AMM addresses receive an initial `newPriceData` the same way, with `blockNumber` set to `0`, when a price is available.
## Event payloads
### `newPriceData`
Live update (`updatedPrices` is an array; there is no top-level `marketAddress`):
```json theme={null}
{
"updatedPrices": [
{ "marketAddress": "0x1234...", "marketId": 512, "yesPrice": 0.65, "noPrice": 0.35 }
],
"blockNumber": 12345678,
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
Initial snapshot (one per subscribed address after the `system` acknowledgement; `blockNumber` is `0` and `updatedPrices` is a single object):
```json theme={null}
{
"marketAddress": "0x1234...",
"updatedPrices": { "collateralDecimals": 6, "marketAddress": "0x1234...", "marketId": 512, "yesPrice": 0.65, "noPrice": 0.35 },
"blockNumber": 0,
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
`yesPrice` and `noPrice` are JSON numbers between 0 and 1. On live frames read the market from `updatedPrices[i].marketAddress`.
**Looking for individual events?** `orderbookUpdate` is market-wide state, not an event feed. For discrete events:
* **Your own orders, in real time** — [`subscribe_order_events`](/developers/websocket/order-events). One event per lifecycle change, authenticated, and it only ever carries your own orders.
* **Every mined trade in a market** — [Get Market Events](/api-reference/trading/market-events). Public, newest first, paginated. Cached, so not real time, and mined trades only: no placements or cancellations.
There is no public WebSocket stream of every trade in a market. Market-wide over WebSocket is the coalesced book only.
### `orderbookUpdate`
Emitted for CLOB markets when the book changes (new bids/asks, removals, or fills). `orderbook` carries the full updated book. Each side is a sorted array of price levels.
Updates are **coalesced**: several changes in quick succession arrive as a single message, so on a busy market one frame can represent several fills. Each message is the resulting book, not a list of what changed, so replace your local copy with what arrives rather than diffing consecutive messages or counting frames as events.
```json theme={null}
{
"marketSlug": "btc-100k-weekly",
"orderbook": {
"bids": [
{ "price": 0.53, "size": 100000000 },
{ "price": 0.52, "size": 250000000 }
],
"asks": [
{ "price": 0.55, "size": 80000000 },
{ "price": 0.56, "size": 300000000 }
]
},
"version": 48213,
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
| Field | Type | Description |
| - | - | - |
| `marketSlug` | `string` | CLOB market slug |
| `orderbook.bids` | `{ price: number, size: number }[]` | Bid levels, highest price first. `size` is in raw 6-decimal units (1 share = 1,000,000) |
| `orderbook.asks` | `{ price: number, size: number }[]` | Ask levels, lowest price first. `size` is in raw 6-decimal units (1 share = 1,000,000) |
| `orderbook.midpoint` | `number` | Mid price between best bid and best ask (0.5 on an empty book) |
| `orderbook.adjustedMidpoint` | `number` | Midpoint computed from levels at or above `minSize` |
| `orderbook.maxSpread` | `number` | Maximum distance from the midpoint at which a resting order earns LP rewards |
| `orderbook.minSize` | `number` | Minimum rewardable order size, in the same raw 6-decimal units as `size` (`100000000` = 100 shares) |
| `orderbook.tokenId` | `string` | Token the book is quoted in |
| `version` | `number` | Publisher sequence for the book. It increases with every frame for a market while the same publisher is active and can restart after a backend failover, so do not treat it as contiguous. Use it to drop a frame that arrives out of order right after subscribing. A snapshot served from the database fallback carries `0`; accept it only if you have not yet received a live frame for that slug. |
| `timestamp` | `string` | ISO-8601 event timestamp |
Live frames may also carry a `side` field on each level.
`price` and `size` are JSON numbers; coerce defensively to preserve decimal precision. `size` is in raw 6-decimal units on the initial snapshot and on every later frame, so divide by `1e6` to get shares; `price` is not scaled. For a one-shot snapshot, use [`GET /markets/{slug}/orderbook`](/api-reference/trading/orderbook), which returns the same shape and units, except that REST sends `minSize` and `maxSpread` as decimal strings. Accept either form. You also receive this message once per CLOB slug immediately after `subscribe_market_prices`, carrying the current book (see [Initial snapshot](/developers/websocket/market-data#initial-snapshot)).
## Related
* [WebSocket overview](/developers/websocket/overview): connection details, handshake authentication, and the full event reference
# Market Lifecycle
Source: https://docs.limitless.exchange/developers/websocket/market-lifecycle
Subscribe to market creation and resolution events and read the marketCreated and marketResolved payloads
This page covers subscribing to market creation and resolution events with `subscribe_market_lifecycle`, and the `marketCreated` and `marketResolved` payloads that subscription delivers.
## 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 payloads
### `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",
"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?` | Present only when the event is for a market group; then `groupSlug` equals `slug`. Submarkets do not emit their own `marketCreated`. |
| `categoryIds` | `number[]` | Category IDs (always present, may be empty) |
| `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 |
## Related
* [WebSocket overview](/developers/websocket/overview): connection details, handshake authentication, and the full event reference
# Order Events
Source: https://docs.limitless.exchange/developers/websocket/order-events
Subscribe to OME and settlement events for your CLOB orders and read the orderEvent payloads
This page covers subscribing to the lifecycle of your CLOB orders with `subscribe_order_events`, and the `orderEvent` payload shapes that subscription delivers.
## Subscribing to order events
Subscribe to the full lifecycle of your CLOB orders by emitting `subscribe_order_events`. Requires an HMAC-signed handshake. See [Authentication](/developers/authentication). The subscription takes no payload. It is a per-user channel scoped to the profile that signed the handshake: it delivers events for orders **owned by that profile**, whether you were the maker or the taker.
**Sub-account orders are not included.** An order you place for a sub-account is owned by that sub-account, so its events go to the sub-account's channel, not yours. That applies however you delegated it: the `onBehalfOf` field on [Create Order](/api-reference/trading/create-order), the `onBehalfOf` query parameter on cancel, or the `x-on-behalf-of` header on read endpoints.
Delegation is REST-only. There is no WebSocket equivalent, and the handshake cannot be widened to cover sub-accounts.
To follow a sub-account's order state, use a REST read that accepts delegation with the `x-on-behalf-of` header: [`POST /orders/status/batch`](/api-reference/trading/order-status-batch) for specific order ids, or [`GET /markets/{slug}/user-orders`](/api-reference/trading/user-orders) for a market's orders. Both require `delegated_signing` and a sub-account the partner owns. Or authenticate as the sub-account itself.
Several event shapes arrive on the same Socket.IO event name `orderEvent`. Distinguish first by `source`, then by `type`:
* **`OME`** — off-chain matching engine updates: resting-order state changes (`PLACEMENT`, `UPDATE`, `CANCELLATION`) and the terminal result of an immediate-or-cancel order (`EXECUTION`). Each change is its own frame; this stream is never coalesced, unlike `orderbookUpdate`. Persisting these frames gives you a complete record of everything your orders did, including cancellations, which do not appear in trade history.
* **`SETTLEMENT`** — settlement lifecycle for CLOB trades: a provisional `MATCHED` the moment the engine fills your order (before the on-chain transaction), then a terminal `MINED` or `FAILED`.
```typescript theme={null}
import { io } from 'socket.io-client';
const socket = io('wss://ws.limitless.exchange/markets', {
transports: ['websocket'],
extraHeaders: wsAuthHeaders(TOKEN_ID, SECRET),
});
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' && data.type === 'EXECUTION') {
console.log(`[EXECUTION ${data.status}] order=${data.orderId} remaining=${data.remainingSize}`);
} else if (data.source === 'OME') {
console.log(`[OME ${data.type}] order=${data.orderId} clientOrder=${data.clientOrderId ?? '(none)'}`);
} else {
console.log(
`[SETTLEMENT ${data.type}] order=${data.orderId} clientOrder=${data.clientOrderId ?? '(none)'} tx=${data.txHash ?? '(none)'}`,
);
}
});
```
**One subscription per connection.** Sending `subscribe_order_events` a second time re-binds; the previous subscription is cancelled. There is no payload. A repeat `subscribe_order_events` is accepted only within 30 seconds of the handshake timestamp; after that, reconnect with freshly signed headers (see [Overview](/developers/websocket/overview)).
**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 HMAC auth headers are 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.
**Taker delay: order submission can be asynchronous.** Some markets apply a short hold to marketable (taker) orders before the matching engine fills them. On such a market, `POST /orders` returns right away with `execution.settlementStatus: "DELAYED"` and an `eligibleAt` timestamp instead of blocking until settlement. This stream is how you observe the fill. Wait for the provisional `MATCHED` frame, then the terminal `MINED` / `FAILED`, correlating by `orderId` (the `order.id` from the `POST /orders` response) and `tradeEventId`; `MATCHED` does not carry `clientOrderId`. A released order produces the same frames as one that was never held: a FAK that fills partially gets one `MATCHED` and one terminal frame for the whole fill, plus an `OME` `EXECUTION` frame with `status: "PARTIALLY_FILLED"`. A FAK that matches nothing when it is released emits a single `OME` `EXECUTION` frame with `status: "KILLED"` and no settlement frames, so treat that frame as terminal too; `POST /orders/status/batch` reports the same outcome as `settlementStatus: "UNMATCHED"`. Maintenance mode can postpone delayed fills beyond `eligibleAt`; keep the order open in your integration until a terminal event arrives. `postOnly` (maker) orders are never delayed. A market's current delay is readable as `settings.takerDelayMs` (milliseconds; `0` = none) on the market response.
## Event payload
### `orderEvent`
Emitted for both OME state changes and on-chain settlement results. Distinguish the two shapes by the `source` field.
#### Timestamp fields
Every `orderEvent` frame carries a `timestamp` plus additive semantic timestamps. Read `occurredAt`, `matchedAt`, and `publishedAt` directly. Do not substitute one for another. All timestamps are ISO-8601 UTC strings, and any missing semantic timestamp arrives as `null` rather than a value copied from another field.
| Field | Availability | Meaning |
| - | - | - |
| `timestamp` | Always present. Deprecated — kept for backward compatibility. | Source-dependent legacy time. On OME frames, the source transition time. On settlement frames (`MATCHED`, `MINED`, `FAILED`), always the original match time. |
| `occurredAt` | All frames; `string \| null`. | Lifecycle fact time. On OME frames, the source transition time. On `MATCHED`, the match time. On `MINED` / `FAILED`, the terminal decision time. |
| `matchedAt` | Settlement frames only; `string \| null`. | Persisted match time from the trade record. Present on `MATCHED`, `MINED`, and `FAILED`. |
| `publishedAt` | All frames; optional. | Per-client gateway queue time, captured immediately before the Socket.IO frame is written to your connection. Useful for isolating delivery latency from upstream lifecycle latency. |
**Prefer semantic timestamps.** Use `occurredAt` for lifecycle time, `matchedAt` for the persisted match time on settlement frames, and `publishedAt` for gateway queue time. Treat `timestamp` as a legacy field only.
#### OME event (`source: "OME"`)
The `OME` source carries two shapes: ongoing **lifecycle** state changes (`PLACEMENT` / `UPDATE` / `CANCELLATION`) and a one-shot **terminal** result for immediate-or-cancel orders (`EXECUTION`). Tell them apart by `type`.
##### Lifecycle (`type: "PLACEMENT" | "UPDATE" | "CANCELLATION"`)
Emitted for every resting-order state change recorded by the matching engine. `price` and `remainingSize` are sent unquoted as JSON numbers. Coerce defensively to preserve decimal precision.
```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",
"occurredAt": "2026-04-20T10:15:30.000Z",
"publishedAt": "2026-04-20T10:15:30.042Z"
}
```
| 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` | `number` | Limit price (unquoted JSON number) |
| `remainingSize` | `number` | Size remaining on the book (unquoted JSON number) |
| `reason` | `string?` | Engine cancellation reason. `STP_MAKER_CANCELLED` on a `CANCELLATION` event when self-trade prevention cancelled your resting maker order. Omitted otherwise. |
| `timestamp` | `string` | Deprecated legacy timestamp. See [Timestamp fields](/developers/websocket/order-events#timestamp-fields). |
| `occurredAt` | `string \| null` | Source transition time for this OME event. |
| `publishedAt` | `string?` | Per-client gateway queue time, immediately before the frame is written to your connection. |
`type` transitions:
* `PLACEMENT` — order accepted by the OME.
* `UPDATE` — remaining size changed (partial fill or amend).
* `CANCELLATION` — removed from the book. Carries `reason: "STP_MAKER_CANCELLED"` when self-trade prevention cancelled your resting maker order against your own incoming order.
##### FAK/FOK terminal (`type: "EXECUTION"`)
Emitted once when an immediate-or-cancel order reaches a recorded terminal state — a FAK (fill-and-kill), which always terminates, or a FOK (fill-or-kill) **that filled**. Delivered only to the order owner. Unlike the lifecycle frames, `eventId` is a string (`terminal:`), the frame carries a `status` label, and `price` and `remainingSize` are strings, with `remainingSize` in raw 6-decimal units.
```json theme={null}
{
"source": "OME",
"type": "EXECUTION",
"status": "FILLED",
"eventId": "terminal:550e8400-e29b-41d4-a716-446655440000",
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"userId": 42,
"marketId": "17",
"token": "87893014956437093847...",
"side": "BUY",
"price": "0.53",
"remainingSize": "0",
"timestamp": "2026-04-20T10:15:40.000Z",
"occurredAt": "2026-04-20T10:15:40.000Z",
"publishedAt": "2026-04-20T10:15:40.031Z"
}
```
A zero-fill FAK, as observed on production:
```json theme={null}
{
"source": "OME",
"type": "EXECUTION",
"status": "KILLED",
"eventId": "terminal:821277de-1b92-4b37-bba3-34ff3d26a79e",
"orderId": "821277de-1b92-4b37-bba3-34ff3d26a79e",
"userId": 42,
"marketId": "409786",
"token": "73663029485913087603...",
"side": "BUY",
"price": "0.01",
"remainingSize": "50000000",
"timestamp": "2026-09-25T13:55:55.098321+00:00",
"occurredAt": "2026-09-25T13:55:55.100Z",
"publishedAt": "2026-09-25T13:55:55.106Z"
}
```
| Field | Type | Description |
| - | - | - |
| `source` | `'OME'` | Discriminator for the OME shape |
| `type` | `'EXECUTION'` | Terminal result of a FAK/FOK order |
| `status` | `'FILLED' \| 'PARTIALLY_FILLED' \| 'KILLED'` | Final outcome (see below) |
| `eventId` | `string` | `terminal:` — string form, unlike the numeric lifecycle `eventId` |
| `orderId` | `string` | UUID of the order |
| `userId` | `number` | Internal user id of the order owner |
| `marketId` | `string` | Numeric market id (CLOB) |
| `token` | `string` | CTF token id (decimal string) — the raw token, not `YES` / `NO` |
| `side` | `'BUY' \| 'SELL'` | Order side |
| `price` | `string` | Order price as a decimal string |
| `remainingSize` | `string` | Unfilled size at termination in raw 6-decimal units (`"0"` on `FILLED`, the original size on `KILLED`; 50 shares is `"50000000"`) |
| `timestamp` | `string` | Deprecated legacy timestamp. See [Timestamp fields](/developers/websocket/order-events#timestamp-fields). |
| `occurredAt` | `string \| null` | Terminal decision time recorded by the matching engine. |
| `publishedAt` | `string?` | Per-client gateway queue time, immediately before the frame is written to your connection. |
`status` outcomes:
* `FILLED` — the order matched in full (a FAK that matched completely, or a FOK).
* `PARTIALLY_FILLED` — a FAK matched part of its size; the unfilled remainder was cancelled. `remainingSize` is that cancelled remainder.
* `KILLED` — a FAK matched nothing and was cancelled in full. `remainingSize` equals the original size. A taker rejected by self-trade prevention (`stpPolicy: "cancel_taker"` or `"cancel_both"`) also surfaces here as `status: "KILLED"`. This frame carries no STP reason. The `STP_TAKER_REJECTED` reason is returned only on the synchronous `POST /orders` response.
**`price` and `remainingSize` are strings on the terminal frame**, unlike the lifecycle frames, where they are JSON numbers. `remainingSize` is in raw 6-decimal units. Parse both explicitly instead of reusing the lifecycle parser.
**No fee or `clientOrderId` on the terminal frame.** It reports only the lifecycle outcome. For the realized fee, read the `POST /orders` response or the `MINED` settlement frame. `token` is the raw CTF token id, not the `YES` / `NO` outcome.
**A FOK is all-or-nothing.** A fill-or-kill order fills completely (`status: "FILLED"`) or is rejected. A rejected FOK returns HTTP 400 and may additionally surface as `status: "KILLED"` with `remainingSize` equal to the original size; it never emits `PARTIALLY_FILLED`. Only a FAK reports a partial fill (`PARTIALLY_FILLED`).
#### Settlement event (`source: "SETTLEMENT"`)
The `SETTLEMENT` source carries the settlement lifecycle of a CLOB trade. Each participant receives its own events, keyed by `userId`: one for the taker order and one for each matched maker order. Two `type`s arrive in sequence:
* **`MATCHED`** — *provisional*. Emitted the instant the matching engine fills your order, **before** the on-chain settlement transaction. This is the early "your order will be matched for N" signal. Fee fields are estimates (`isEstimate: true`), there is no `txHash` yet, and the fill can still be rolled back by a later `FAILED`.
* **`MINED` / `FAILED`** — *terminal*. Emitted after the settlement transaction resolves on-chain. `MINED` carries `txHash` and the taker's realized fee; `FAILED` means the trade did not execute and no funds moved.
A `MATCHED` and its terminal `MINED` / `FAILED` share the same `tradeEventId` and `orderId` but use different `eventId` namespaces (`matched:…` vs `settlement:…`), so they never dedup-collide. Correlate provisional → terminal by `tradeEventId` + `orderId` (`MATCHED` does not carry `clientOrderId`).
`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.
The part of an order that matches on arrival is always a single trade, however many makers it fills against. This holds for full and partial fills, for orders released after a [taker delay](#subscribing-to-order-events), and for EOA and smart-wallet accounts alike. The taker gets one `tradeEventId`, one `MATCHED` and one terminal `MINED` / `FAILED`. The taker `MATCHED` aggregates size, cost and average price across every maker, and the terminal frame lists each maker in `makerMatches`. A GTC remainder that rests and fills later appears in other takers' trades, with its own frames for each fill.
##### Provisional match (`type: "MATCHED"`)
Emitted pre-chain, the moment the engine fills the order. Per-profile: the taker and each maker receive their own frame carrying their **own** side, token, size, price, and fee estimate (in a cross-outcome match the taker and maker hold opposite tokens). The taker frame aggregates the whole fill; each maker frame describes only that maker's leg.
```json theme={null}
{
"source": "SETTLEMENT",
"type": "MATCHED",
"eventId": "matched:77985c10…:d45b884d…",
"tradeEventId": "77985c10…",
"orderId": "d45b884d…",
"takerOrderId": "d45b884d…",
"marketSlug": "will-abc-happen-by-2026",
"tokenId": "27102822276156300166...",
"token": "NO",
"side": "BUY",
"price": "0.53",
"amountContracts": "25",
"amountCollateral": "13.25",
"configuredFeeRateBps": 30,
"effectiveFeeBps": 27,
"feeAmountContracts": "0.0675",
"isEstimate": true,
"timestamp": "2026-04-20T10:15:40.000Z",
"matchedAt": "2026-04-20T10:15:40.000Z",
"occurredAt": "2026-04-20T10:15:40.000Z",
"publishedAt": "2026-04-20T10:15:40.050Z"
}
```
| Field | Type | Description |
| - | - | - |
| `source` | `'SETTLEMENT'` | Discriminator for the settlement shape |
| `type` | `'MATCHED'` | Provisional, pre-chain match |
| `eventId` | `string` | `matched::` — distinct namespace from the terminal `settlement:…` id |
| `tradeEventId` | `string` | Trade id shared with the terminal `MINED` / `FAILED` for this fill |
| `orderId` | `string` | UUID of the recipient's own order in this fill |
| `takerOrderId` | `string` | UUID of the taker order in the trade |
| `marketSlug` | `string` | CLOB market slug |
| `tokenId` | `string` | CTF token id of the recipient's own side (decimal string) |
| `token` | `'YES' \| 'NO'` | Outcome the recipient filled — its own side, not necessarily the taker's |
| `side` | `'BUY' \| 'SELL'` | Recipient order side |
| `price` | `string` | Recipient's fill price (decimal string) — the weighted-average across the fill on the taker frame, the maker's own resting price on a maker frame |
| `amountContracts` | `string` | Filled contract amount for the recipient order |
| `amountCollateral` | `string` | Filled collateral amount for the recipient order |
| `configuredFeeRateBps` | `number` | Fee rate configured for the order |
| `effectiveFeeBps` | `number` | Effective fee rate used for the estimate |
| `feeAmountContracts` | `string?` | Fee **estimate** in contracts — present on BUY fills |
| `feeAmountCollateral` | `string?` | Fee **estimate** in collateral — present on SELL fills |
| `isEstimate` | `true` | Always `true` on `MATCHED`: fee fields are estimates, not realized on-chain charges |
| `timestamp` | `string` | Deprecated legacy timestamp — always the match time on settlement frames. See [Timestamp fields](/developers/websocket/order-events#timestamp-fields). |
| `matchedAt` | `string \| null` | Persisted match time from the trade record. |
| `occurredAt` | `string \| null` | Match time for `MATCHED`. |
| `publishedAt` | `string?` | Per-client gateway queue time, immediately before the frame is written to your connection. |
**`MATCHED` is provisional. Do not settle books on it.** Use it as an early acknowledgement / UI signal only. Its fee fields are estimates (`isEstimate: true`), not realized charges, and the fill can still end in `FAILED`. Reconcile on the terminal `MINED` frame, which carries `txHash` and the taker's realized fee.
**Fee currency follows side.** A BUY fill reports the fee estimate in `feeAmountContracts` (`feeAmountCollateral` absent); a SELL fill reports it in `feeAmountCollateral`. The same convention applies on `MINED`.
**Makers are not charged a fee. Only the taker pays.** On a maker's frame, the fee estimate fields (`configuredFeeRateBps`, `effectiveFeeBps`, `feeAmountContracts` / `feeAmountCollateral`) mirror the maker order's configured rate and can be non-zero. A maker is not charged a fee on a matched CLOB trade; the realized maker fee is `0`. Treat the taker-side estimate as a real pre-settlement charge to reconcile on `MINED`, and treat the maker-side estimate as informational only.
##### Terminal settlement (`type: "MINED" | "FAILED"`)
Emitted when a CLOB trade settles on-chain. Each participant receives a settlement event for their own order: one for the taker order and one for each matched maker order.
```json theme={null}
{
"source": "SETTLEMENT",
"type": "MINED",
"eventId": "settlement:1b3a…:550e8400-e29b-41d4-a716-446655440000",
"tradeEventId": "1b3a…",
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"clientOrderId": "client-order-001",
"takerOrderId": "e4c3…",
"takerAccount": "0xAbC…123",
"makerMatches": [
{
"account": "0xDeF…456",
"orderId": "cb12…",
"matchedSize": "25",
"price": "0.53"
}
],
"marketSlug": "will-abc-happen-by-2026",
"tokenId": "87893014956437093847...",
"side": "BUY",
"price": "0.53",
"amountContracts": "25",
"amountCollateral": "13.25",
"configuredFeeRateBps": 0,
"effectiveFeeBps": 0,
"feeAmountContracts": "0",
"txHash": "0xabc…",
"timestamp": "2026-04-20T10:15:40.000Z",
"matchedAt": "2026-04-20T10:15:40.000Z",
"occurredAt": "2026-04-20T10:15:41.180Z",
"publishedAt": "2026-04-20T10:15:41.205Z"
}
```
| Field | Type | Description |
| - | - | - |
| `source` | `'SETTLEMENT'` | Discriminator for the settlement shape |
| `type` | `'MINED' \| 'FAILED'` | Settlement outcome |
| `eventId` | `string` | Stable id in the form `settlement::` |
| `tradeEventId` | `string` | Settlement trade id shared by all participant events for the same match |
| `orderId` | `string?` | UUID of the recipient's own order for this event |
| `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. |
| `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). A taker event can include multiple maker matches; each maker also receives a separate event for its own order. |
| `marketSlug` | `string?` | CLOB market slug |
| `tokenId` | `string?` | CTF token id for the recipient side |
| `side` | `'BUY' \| 'SELL'?` | Recipient order side |
| `price` | `string?` | Execution price as a decimal string |
| `amountContracts` | `string?` | Filled contract amount for the recipient order |
| `amountCollateral` | `string?` | Collateral amount for the recipient order |
| `configuredFeeRateBps` | `number?` | Fee rate configured for the order |
| `effectiveFeeBps` | `number?` | Effective fee rate applied to this trade |
| `feeAmountContracts` | `string?` | Fee amount in contracts |
| `feeAmountCollateral` | `string?` | Fee amount in USDC (SELL fills; `feeAmountContracts` is then absent) |
| `txHash` | `string?` | Settlement transaction hash, when known |
| `timestamp` | `string` | Deprecated legacy timestamp — always the match time on settlement frames, including `MINED` and `FAILED`. See [Timestamp fields](/developers/websocket/order-events#timestamp-fields). |
| `matchedAt` | `string \| null` | Persisted match time from the trade record. |
| `occurredAt` | `string \| null` | Terminal decision time for `MINED` / `FAILED`. |
| `publishedAt` | `string?` | Per-client gateway queue time, immediately before the frame is written to your connection. |
**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`. If only one side supplied a `clientOrderId`, only that side's event includes it.
**Maker fee on `MINED` is informational too.** Like the `MATCHED` estimate, a maker's `MINED` fee fields are computed from the maker order's configured rate, not a realized charge. Makers are not charged, so the realized maker fee is `0`. Only the taker's `MINED` fee is a realized charge.
* `type: "MINED"` — on-chain settlement confirmed.
* `type: "FAILED"` — settlement failed on-chain; the taker order did not execute and funds were not moved.
## Related
* [WebSocket overview](/developers/websocket/overview): connection details, handshake authentication, and the full event reference
# WebSocket Overview
Source: https://docs.limitless.exchange/developers/websocket/overview
Connection details, handshake authentication, and the event reference for the Limitless WebSocket API
This page covers how to connect to the Limitless WebSocket API, how to sign the handshake for authenticated channels, and the full list of client and server events. Each subscription has its own page pairing the subscribe call with the payloads it delivers.
## Connection
**URL:** `wss://ws.limitless.exchange`
**Namespace:** `/markets`
**Transport:** WebSocket only (no polling fallback)
**No client PING required.** The server runs the Socket.IO heartbeat (server-initiated `ping` / client `pong`) automatically. Every official Socket.IO client (JavaScript, Python `python-socketio`, Go, Rust) responds to it out of the box. Do not send your own PING frames. Keep the connection open and rely on the built-in heartbeat and the auto-reconnect flow in [WebSocket quick start](/developers/quickstart/websocket#auto-reconnection).
**Connections close after 24 hours.** Our load balancer closes every WebSocket connection 24 hours after it opens, whether or not it is active. The close arrives without a close frame, so your client sees code `1006`. Reconnect with freshly signed HMAC headers (the original handshake timestamp is past the 30-second window) and re-emit your subscriptions, since the server does not keep them across connections. The example below and the [quick start client](/developers/quickstart/websocket#auto-reconnection) both re-sign on reconnect. To avoid a gap, open a second connection shortly before the 24-hour mark and switch over.
Authenticated channels (positions, order events) require an HMAC-signed handshake. Sign a fixed canonical message — `{ISO-8601 timestamp}\nGET\n/socket.io/?EIO=4&transport=websocket\n` — with the base64-decoded `secret` and pass three headers as `extraHeaders`:
```typescript theme={null}
import { createHmac } from 'crypto';
import { io } from 'socket.io-client';
function wsAuthHeaders(tokenId: string, secret: string) {
const timestamp = new Date().toISOString();
const message = `${timestamp}\nGET\n/socket.io/?EIO=4&transport=websocket\n`;
const signature = createHmac('sha256', Buffer.from(secret, 'base64'))
.update(message)
.digest('base64');
return { 'lmts-api-key': tokenId, 'lmts-timestamp': timestamp, 'lmts-signature': signature };
}
const socket = io('wss://ws.limitless.exchange/markets', {
transports: ['websocket'],
extraHeaders: wsAuthHeaders(TOKEN_ID, SECRET),
});
// Socket.IO replays the original headers on reconnect, and their timestamp is
// stale by then. Re-sign before every attempt.
socket.io.on('reconnect_attempt', () => {
socket.io.opts.extraHeaders = wsAuthHeaders(TOKEN_ID, SECRET);
});
```
See [Authentication](/developers/authentication) for how to generate `TOKEN_ID` and `SECRET`. If you use an official SDK, prefer its WebSocket client, which signs the handshake automatically from `hmacCredentials` (TypeScript) / `hmac_credentials` (Python) / `WithHMACCredentials` (Go).
**Authenticated subscribes must be sent within 30 seconds of the handshake timestamp.** The server re-validates the handshake HMAC headers on every `subscribe_positions`, `subscribe_order_events`, and `subscribe_transactions` message and rejects any whose `lmts-timestamp` is more than 30 seconds from the server clock with an `exception` carrying `Invalid HMAC authentication`. Emit these subscriptions immediately after `connect`. To change an authenticated subscription later (for example to narrow `subscribe_positions`), open a new connection with freshly signed headers; a re-emit on a connection older than 30 seconds is rejected. Public channels are not affected.
## 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_transactions` | Client → Server | Yes | Subscribe to settlement results for your own transactions (`tx`) |
| `subscribe_market_lifecycle` | Client → Server | No | Subscribe to market creation/resolution events |
| `unsubscribe_market_lifecycle` | Client → Server | No | Unsubscribe from market lifecycle events |
| `subscribe_oracle_price_data` | Client → Server | No | Subscribe to oracle price feeds for the given markets (same `marketAddresses` / `marketSlugs` payload; legacy alias `subscribe_chainlink_oracle_data`) |
| `unsubscribe_oracle_price_data` | Client → Server | No | Unsubscribe from oracle price feeds (legacy alias `unsubscribe_chainlink_oracle_data`) |
| `subscribe_unrealized_pnl` | Client → Server | No | Subscribe to Unrealized PnL leaderboard invalidation hints |
| `unsubscribe_unrealized_pnl` | Client → Server | No | Unsubscribe from Unrealized PnL invalidation hints |
| `unrealizedPnlProjectionChanged` | Server → Client | No | Hint that a subscribed Unrealized PnL leaderboard changed; refetch the REST route |
| `newPriceData` | Server → Client | No | AMM market price update |
| `orderbookUpdate` | Server → Client | No | CLOB orderbook update |
| `oraclePriceData` | Server → Client | No | Oracle price update for a subscribed market |
| `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 lifecycle, FAK/FOK execution, and settlement updates for your CLOB orders |
| `tx` | Server → Client | Yes | Settlement result for one of your transactions (`status` is `CONFIRMED` or `FAILED`) |
| `system` | Server → Client | No | System notifications |
| `exception` | Server → Client | No | Error notifications |
| `error` | Server → Client | No | Error frame: `{ message }` or `{ code, message }` |
There is no authentication confirmation event. A successful authenticated subscribe is acknowledged only by that channel's `system` message; a failed one surfaces on `exception`.
The server also handles `subscribe_live_sports`, `subscribe_live_esports`, `subscribe_ugm_live_offers` and `unsubscribe_ugm_live_offers`. These channels are not documented and are subject to change.
## Unsubscribing
There is no generic `unsubscribe` event. Only two channels have an explicit unsubscribe: `unsubscribe_market_lifecycle` and `unsubscribe_unrealized_pnl`.
`subscribe_market_prices`, `subscribe_positions`, and `subscribe_order_events` have none. Each of those **replaces** its previous subscription on the same connection when you emit it again, so to drop a market you re-emit the subscribe with the smaller set (for the authenticated channels, only within 30 seconds of the handshake; otherwise reconnect). To stop everything, disconnect. An unsubscribe event name that the server does not register never gets an acknowledgement, so a client waiting on one times out.
## Related
Each subscription has its own page pairing the subscribe call with the payloads it delivers:
* [Market data](/developers/websocket/market-data): `subscribe_market_prices`, `newPriceData`, `orderbookUpdate`
* [Positions](/developers/websocket/positions): `subscribe_positions`, `positions`
* [Order events](/developers/websocket/order-events): `subscribe_order_events`, `orderEvent`
* [Market lifecycle](/developers/websocket/market-lifecycle): `subscribe_market_lifecycle`, `unsubscribe_market_lifecycle`, `marketCreated`, `marketResolved`
* [Unrealized PnL](/developers/websocket/unrealized-pnl): `subscribe_unrealized_pnl`, `unsubscribe_unrealized_pnl`, `unrealizedPnlProjectionChanged`
# Positions
Source: https://docs.limitless.exchange/developers/websocket/positions
Subscribe to position updates and read the positions payload
This page covers subscribing to position updates with `subscribe_positions`, and the `positions` payload that subscription delivers.
## Subscribing to position updates
Subscribe to real-time position changes by emitting `subscribe_positions`. This requires an HMAC-signed handshake. See [Authentication](/developers/authentication).
`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: wsAuthHeaders(TOKEN_ID, SECRET),
});
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. A repeat `subscribe_positions` is accepted only within 30 seconds of the handshake timestamp; after that, reconnect with freshly signed headers (see [Overview](/developers/websocket/overview)).
If neither `marketSlugs` nor `marketAddresses` is given the server ignores the message: no `system` acknowledgement, no snapshot, no frames.
Position updates are pushed automatically when your balances change (e.g. after a trade is mined). You do not need to poll.
The initial snapshot is rate-limited per account (10 subscribes per 10 seconds by default). Beyond that the subscription is still active and live updates arrive, but the snapshot is skipped; fetch positions over REST if you need it.
## Event payload
### `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..."],
"timestamp": 1783728000000,
"type": "CLOB"
}
```
| Field | Type | Description |
| - | - | - |
| `timestamp` | `number` | Optional. Epoch milliseconds of the on-chain balance change that triggered this update. Only present on balance-change updates; the initial snapshot after `subscribe_positions` omits it. Use it to order snapshots against trade confirmations. |
## Related
* [WebSocket overview](/developers/websocket/overview): connection details, handshake authentication, and the full event reference
# Unrealized PnL
Source: https://docs.limitless.exchange/developers/websocket/unrealized-pnl
Subscribe to Unrealized PnL leaderboard invalidation hints and read the unrealizedPnlProjectionChanged payload
This page covers subscribing to Unrealized PnL leaderboard invalidation hints with `subscribe_unrealized_pnl`, and the `unrealizedPnlProjectionChanged` payload that subscription delivers.
## Subscribing to unrealized PnL updates
Subscribe to invalidation hints for the [live Unrealized PnL leaderboards](/api-reference/leaderboard/unrealized-pnl-market) by emitting `subscribe_unrealized_pnl`. No authentication required. Two scopes are available: one leaderboard per open market, and a global biggest-open-positions list.
This event carries only a hint, not the leaderboard payload. When it arrives, refetch the corresponding REST route — [`GET /leaderboard/pnl/unrealized/markets/{marketId}`](/api-reference/leaderboard/unrealized-pnl-market) or [`GET /leaderboard/pnl/unrealized/biggest-positions`](/api-reference/leaderboard/biggest-positions) — which remains the serving contract.
```typescript theme={null}
// Follow the leaderboard for one market
socket.emit('subscribe_unrealized_pnl', { schemaVersion: 1, scope: 'MARKET', marketId: 7348 });
// Follow the global biggest open positions
socket.emit('subscribe_unrealized_pnl', { schemaVersion: 1, scope: 'BIGGEST_POSITIONS' });
socket.on('unrealizedPnlProjectionChanged', (msg) => {
// Refetch the matching REST route for `msg.scope` (and `msg.marketId` when scope === 'MARKET').
});
```
To unsubscribe, emit `unsubscribe_unrealized_pnl` with the same payload shape.
**At most 50 `MARKET` scopes per connection.** Additional `subscribe_unrealized_pnl` calls with new `marketId`s beyond the limit are rejected with an `error` frame carrying `code: "UNREALIZED_PNL_SCOPE_LIMIT"`. The `BIGGEST_POSITIONS` scope does not count against the market limit.
Errors arrive as `error` frames with `code` set to `UNREALIZED_PNL_SCOPE_LIMIT` (more than 50 market scopes), `INVALID_UNREALIZED_PNL_SUBSCRIPTION` (bad payload; `message` lists the problems) or `UNREALIZED_PNL_DISABLED` (feature off). A successful subscribe or unsubscribe is acknowledged with a `system` frame `{ message, subscription }` echoing your payload.
**`state` on the hint tracks the projection.** `state: "READY"` means the leaderboard you refetch will be current; `BUILDING`, `STALE`, or `DEGRADED` mean the REST route may return an empty or stale snapshot. Retry shortly.
## Event payload
### `unrealizedPnlProjectionChanged`
Invalidation hint delivered to `subscribe_unrealized_pnl` subscribers when the underlying projection for their scope changes. It does **not** carry the leaderboard rows. Refetch the matching REST route to get the updated snapshot.
**Market scope:**
```json theme={null}
{
"schemaVersion": 1,
"scope": "MARKET",
"marketId": 7348,
"state": "READY",
"projectionVersion": "42",
"scopeVersion": "17",
"presentationVersion": "3",
"asOf": "2026-04-20T10:15:40.000Z"
}
```
**Biggest positions scope:**
```json theme={null}
{
"schemaVersion": 1,
"scope": "BIGGEST_POSITIONS",
"state": "READY",
"projectionVersion": "42",
"scopeVersion": "17",
"presentationVersion": "3",
"asOf": "2026-04-20T10:15:40.000Z"
}
```
| Field | Type | Description |
| - | - | - |
| `schemaVersion` | `1` | Message schema version |
| `scope` | `'MARKET' \| 'BIGGEST_POSITIONS'` | Leaderboard scope this hint applies to |
| `marketId` | `number` | Market id — present on `MARKET` scope only |
| `state` | `'READY' \| 'STALE' \| 'BUILDING' \| 'DEGRADED'` | Readiness of the projection you'll read on refetch |
| `projectionVersion` | `string` | Monotonic projection version |
| `scopeVersion` | `string` | Monotonic version for this scope's ranking |
| `presentationVersion` | `string` | Monotonic version of the presentation layer |
| `asOf` | `string` | ISO-8601 timestamp of the new snapshot |
## Related
* [WebSocket overview](/developers/websocket/overview): connection details, handshake authentication, and the full event reference
# 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 Limitless 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 order book
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.
# Custom Markets
Source: https://docs.limitless.exchange/user-guide/custom-markets
Write your own yes/no market, fund it, share the link, and settle it yourself
## Overview
A **custom market** is a prediction market written by a Limitless user. You write the question and the rules, pick the opening odds, fund the liquidity pool from your own wallet, and settle the market yourself when the answer is known. Limitless screens the text for prohibited content and hosts the market. It does not judge the outcome and does not handle disputes.
Custom markets are **unlisted**. They do not appear in the market feed, in search, or on category pages. People reach a custom market through the link you share.
Custom markets are one of two kinds of user-generated market. The other kind is the
price market (for example "BTC above \$X by …"), which trades on the order book and
pays its creator a share of taker fees. That flow is described in
[User-Generated Markets](/user-guide/user-generated-markets). Both kinds carry the green
**Community** badge.
## At a glance
| | |
| - | - |
| **Question format** | Two outcomes. Default labels are Yes and No; you can rename them. |
| **Collateral** | USDC on Base |
| **Liquidity** | You fund an on-chain liquidity pool yourself. You are the only liquidity provider. |
| **Minimum pool** | Set by Limitless and shown in the wizard. Currently 1 USDC. |
| **Trading fee** | 2% of each trade, paid into the pool. You withdraw it, together with your remaining liquidity, when you settle. Limitless takes no share. |
| **Opening odds** | 1% to 99% for the first outcome |
| **End time** | Any time in the future, in 5-minute steps |
| **Who settles** | You, from the wallet you created the market with. There is no deadline for settling. |
| **Visibility** | Unlisted. Link only. |
| **Points** | Trades on user-generated markets do not earn points. |
| **Daily limit** | A per-account limit on new custom markets applies. The wizard tells you when you have reached it. |
## Before you start
* **Sign in and hold USDC on Base** in your trading wallet. The liquidity step shows your available balance and will not let you fund more than you hold.
* **The wallet you create with becomes the resolving wallet.** It is written into the market on chain and cannot be changed. Only that wallet can settle the market. If you later switch between an embedded wallet and an external wallet (see [Wallet Types](/user-guide/wallet-types)), you have to switch back to settle.
* **If you never settle, funds stay locked.** Limitless cannot settle a custom market for you. You confirm this in the wizard before creating:
> I control this wallet and will resolve this market. If I do not resolve it, funds can remain locked indefinitely. Limitless cannot resolve it for me.
## Creating a market
Open **Create Market** in the top navigation, or go to [limitless.exchange/create-custom-market](https://limitless.exchange/create-custom-market). The wizard has six steps. Each step is saved as a draft in your browser, so you can leave and come back.
Write the title (10 to 140 characters) and set the two outcome labels. Each label is 1 to 24
characters and the two must differ. The **first** label is the side that pays out when you
settle "Yes", the **second** pays out on "No". A first label that reads as a refusal (such as
"No") or a second label that reads as agreement is rejected, because the card would contradict
the payout. You can add an optional image (JPEG, PNG, WebP, or GIF under 1 MB).
The title is checked for prohibited content before you can continue (see
[Content screening](#content-screening)).
Choose the opening probability of the first outcome, between 1% and 99%. Quick picks are 50%,
60%, and 75%. This only sets where trading starts. Trades move the price from there.
Enter the USDC amount to deposit into the pool, at or above the current minimum. This is what
people trade against. As the only liquidity provider you earn all trading fees, and your
deposit is at risk as people trade. You get back what is left in the pool, plus the fees, when
you settle.
Describe how you will settle the market, in 20 to 2,000 characters. The criteria are shown on
the market page and are what traders rely on. A good shape is: *Resolves YES if…*, *Source:
where you will check*, *Edge cases: what does and does not count*. The criteria are checked
for prohibited content before you can continue.
Pick any future date and time, in 5-minute steps. Quick picks are 24 hours, 7 days, and 30
days. Trading opens as soon as the market is created and funded, and closes at the end time.
You can settle before the end time if the answer is already known.
Check everything. Nothing can be edited after creation. Tick the resolving-wallet confirmation,
then click **Create market**. Limitless deploys the market on chain, then asks your wallet to
sign the funding transaction (a USDC approval first if your wallet has not approved the pool
before). When the funding lands the market opens and you are taken to its page.
A market whose funding never arrives is abandoned after a short grace period and cannot be
traded. If your funding transaction was sent but the page lost track of it, reopen the market
page: a funding transaction that lands late is still picked up for 24 hours.
## Content screening
The title, the outcome labels, and the resolution criteria are screened automatically for sexual content, hate, harassment, violence, self-harm, and illicit activity. Flagged text has to be changed before you can continue. Early checks in the wizard are limited to a few per hour per account; the final check at creation does not count against that limit.
Limitless can also take a market page down for a violation of the [Terms of Service](/user-guide/terms-of-service). Taking a page down does not touch positions or funds, and the creator can still settle it.
## Sharing your market
After creation you land on the market page with a share panel. The page tells you that your market is not listed anywhere and is only reachable by link. Copy the link (`limitless.exchange/markets/`) and share it wherever your audience is.
Traders who open the link see a **This Market Is User-Made** notice and accept it before their first trade:
> I understand that this Prediction Market was created by another Platform User, has not necessarily been independently verified or endorsed by the Company, and that participating may result in financial loss.
The market page also shows who created it, the resolution criteria, and a **How it works** summary: a user made this market, their rules are the contract, they resolve it, and it is private.
Your own markets are listed under **Profile → My markets**, with volume, liquidity provided, and the markets that are waiting for you to settle.
## Trading on a custom market
For traders a custom market works like other AMM markets on Limitless: you buy and sell outcome shares against the pool, the price moves with each trade, and the market's 2% fee is charged per trade. Read the resolution criteria before trading. The creator picks the winning outcome and that decision is final. There is no appeal to Limitless.
Trades on user-generated markets do not earn points and do not count toward points-program activity thresholds.
## Settling your market
When you know the answer, open the market page or **Profile → My markets** and click **Resolve market** (shown as **Resolve early** while trading is still open). Pick the winning outcome and sign from the resolving wallet.
Settlement is one action that does three things:
1. Withdraws your remaining liquidity and the fees the pool earned.
2. Reports the outcome on chain. Every position on the winning outcome becomes redeemable.
3. Redeems any winning shares you hold yourself.
A smart wallet signs this once. An external wallet signs the calls one at a time and sees a progress counter. The liquidity is pulled before the outcome is public, so nobody can buy the known winner out of your pool between the two steps.
Settlement cannot be undone. Check the outcome against your own criteria before you sign.
After settlement, Limitless re-checks the pool within about 30 minutes. If any of your liquidity, fees, or winnings are still on chain, a **Finish settlement** button appears on the market page so you can collect them.
There is no deadline for settling. Until you settle, the traders' funds stay in the market. Markets past their end time appear in your profile as waiting for an outcome.
## Limits
| Field | Limit |
| - | - |
| Title | 10 to 140 characters |
| Outcome labels | 1 to 24 characters each, must differ |
| Resolution criteria | 20 to 2,000 characters |
| Opening odds | 1% to 99% |
| Minimum pool | Shown in the wizard, currently 1 USDC |
| Image | JPEG, PNG, WebP, or GIF under 1 MB |
| New markets per account | Daily limit, shown by the wizard when reached |
Custom markets are in a pilot. The total number of custom markets on the platform is capped
while the pilot runs. If the cap is reached, the wizard says so and no new market can be
created until it is raised.
## For developers
A custom market is a normal market record. [Get market](/api-reference/markets/get-market) by slug returns it even though it is not listed in market feeds, with `source` set to `ugm:custom` and lifecycle fields: `customState` (`UNFUNDED`, `OPEN`, `AWAITING_OUTCOME`, `RESOLVED`, or `ABANDONED`), `customOracleAddress`, `fundedAmount`, `resolvedAt`, and `resolvedOutcome`.
Market creation happens in the app. The creation endpoints are not part of the documented public API.
## Summary
* You write the question and the rules, set the opening odds, and fund the pool yourself.
* The market is unlisted. Share the link.
* Traders pay a 2% fee per trade into your pool. You collect it, with your remaining liquidity, when you settle.
* Only the wallet you created with can settle. There is no deadline, and Limitless cannot do it for you.
* Trades on user-generated markets do not earn points.
# Embed Limitless Markets
Source: https://docs.limitless.exchange/user-guide/embed-markets
Add live Limitless odds to your own site and earn from the traders you bring
Add live Limitless odds to your own site and earn from the traders you bring. Paste a link, configure the look, copy the code. The widget stays live on its own. To collect referral rewards, put the referral code from your Limitless account into the snippet (one field in the builder).
## What you get
A live prediction-market widget in an iframe on your page: the market question, real-time odds, a price chart, and buy buttons that take readers straight to the market on Limitless. Odds refresh every few seconds. You set it up once and never touch it again. Data stays live, resolved markets are replaced automatically when you embed a category, and rendering improvements ship without any change to your page.
Embedding is free. No account is needed to embed; a Limitless account is needed only to collect referral rewards.
## Quick start
Open the builder at [embed.limitless.exchange](https://embed.limitless.exchange) and paste a Limitless market or category link, for example `https://limitless.exchange/markets/…` or `https://limitless.exchange/finance`.
Configure layout, theme, and size. The preview is exactly what your readers will see.
To earn referral rewards, paste your referral code into the **Affiliate code** field (see [Referral attribution](#referral-attribution) below).
Copy the snippet and paste it into your page's HTML.
The builder protects you from the common mistakes. It won't let you copy a snippet until a real market has loaded, and it warns you if the category you chose currently has no live markets.
Always copy the snippet from the builder rather than writing it by hand. The script URL and its integrity hash are unique to each release, and editing them breaks the widget.
## Choose a format
The builder offers three snippet formats. All three show the same widget; they differ in how they fit into your platform.
| Format | Best for | How it works |
| - | - | - |
| **Default** | Your own site, WordPress, Ghost | Styled iframe with SEO markup, so search engines see the market question and odds too. Fixed size. |
| **Script** | Sites where you control the HTML | Loads via a small script that auto-sizes the widget's height and keeps it matched to your page. If scripts are blocked, a plain link to the market shows instead. |
| **Minimal** | Platforms that accept plain iframes (e.g. Notion) | A bare iframe, nothing extra. Fixed size. Some platforms (Medium, Substack) don't accept raw iframes at all; link to the market there instead. |
Not sure which to pick? Use **Default**. Choose **Script** only if you want auto-height and your platform allows script tags.
A Default snippet looks like this:
```html theme={null}
```
And a Script snippet:
```html theme={null}
```
The builder fills in the real script filename and integrity value for you. Both examples are illustrations: always copy your real snippet from the builder.
## Make it yours
Everything below is a toggle in the builder. You never need to edit the code.
* **Layout** — Card (with chart) or Banner (compact strip, no chart).
* **Theme** — Light, Dark, or Auto (follows each reader's system preference).
* **Size** — Fit container, or a fixed width between 260 and 800 px. The Script format also adjusts its own height as content changes.
* **Chart** — show or hide the odds chart, grid lines, and Y axis. The chart opens on a 1-day view; readers switch the range (5M to ALL) right in the chart.
* **Buttons** — show or hide buy buttons, share prices, and (for crypto Up/Down markets) the target price.
* **Details** — show or hide volume and select the attribution style: text, mark, or logo.
Crypto price markets also show a live asset-price line from Pyth, with an Odds ↔ Price toggle right in the widget. Sports fixtures render as a scoreboard with the live score.
## Embed a category
Paste a category link (for example `limitless.exchange/finance`) instead of a single market and the widget rotates through that category's top live markets. You control:
* **Rotation** — on or off, and seconds per market (4–60).
* **Pool size** — how many top markets to rotate through (1–10).
* **Sort** — Volume, Trending, or Newest.
When a market resolves, the widget walks to a live replacement on its own. If your category temporarily has no live markets, it shows top markets from all categories until yours come back.
## Referral attribution
Every snippet from the builder carries a referral code (the `via` parameter in the code). Clicks land on Limitless tagged with it, and your embed's stats are tracked under it.
To earn referral rewards, use the referral code from your Limitless account. Open [limitless.exchange/referrals](https://limitless.exchange/referrals), copy your code, and paste it into the **Affiliate code** field in the builder before copying the snippet. That field is where your referral code goes. You earn a share of the trading fees your referred users generate: 10% to 40% depending on your tier, in USDC, paid out daily. Your earnings and referred users are shown on the same Referrals page. See the [Referral Program](/user-guide/referral-program) for tiers and details.
The builder fills the field with a random code automatically. That code keeps your embed's stats working, but it is **not tied to your account, so it earns nothing**. Replace it with yours. If you clear the field, the snippet ships without a referral code. The widget works as usual, but clicks are not linked to you, so you get no stats and no rewards.
## What to expect on your page
* Odds refresh every few seconds; the live price line on crypto markets moves every few seconds too.
* About five crypto widgets open at once stay fully live for a given reader. Beyond that, Limitless's price provider may briefly throttle some of them; they recover on their own.
* With the Script format the widget sizes itself to its container and adjusts its own height. Default and Minimal snippets keep the size you chose in the builder (or full width, with Fit container).
* If the Limitless data source is briefly unreachable, the widget keeps showing the last good data rather than going blank, and recovers on its own.
* Polling pauses in hidden tabs, so the widget never drains a reader's battery in the background.
* The embed is monitored around the clock; we are typically alerted to production problems within the hour.
If you don't know what a Content-Security-Policy is, skip this note; most sites need no changes. If your site enforces one, allow `https://embed.limitless.exchange` in `frame-src` (and in `script-src` if you use the Script format). Every request the widget makes happens inside its own frame, so your page's policy never has to allow anything else. A strict policy may log the Default snippet's inline SEO-metadata block as blocked; that is harmless and needs no change.
Single-page apps swap pages without a full reload (most blogs and CMSs do not). On a single-page app, have your developer call `window.limitless.load()` after client-side navigation to scan for newly added widgets. Script format only.
## Privacy
We count widgets, not your readers. A widget load is one impression. Each impression is tagged with: your site's hostname, the market or category shown, the referral code, the layout, the widget version, a timestamp, and a bot/prefetch flag derived from the request. That is the full list. The widget sets no cookies and uses no browser storage or fingerprinting. Reader IP addresses and user agents are never stored; the user agent is only inspected in-flight to flag bots, then discarded. What we retain is per-site counts, kept as raw rows for 30 days and as daily totals thereafter. Per-site numbers are internal; we never publish or share your traffic figures.
Two details worth knowing. Crypto price markets fetch live prices from Pyth inside the widget frame, so Pyth receives what any web request carries (including the reader's IP address) and nothing more. Your site's hostname comes from the standard `Referer` header. If your referrer policy hides it, impressions show up as unattributed, while clicks tagged with your referral code still count.
## Troubleshooting
| Symptom | Likely cause | What to do |
| - | - | - |
| Widget area is completely blank | The snippet was modified (Script format: the script's `integrity` attribute must stay exactly as copied), a content blocker removed the iframe, or your site's Content-Security-Policy blocks the widget | Re-copy the snippet from the builder, unmodified. Check the page without ad blockers, and see the policy note above. |
| Widget shows markets outside my chosen category | Your category currently has no live markets, or the category page was removed on Limitless | Nothing to fix on your side; it switches back automatically when the category has live markets. If it persists, contact us. |
| Price chart stops moving but odds still update | Temporary price-feed throttling. The widget backs off and recovers automatically. | Wait a minute; if it persists across reloads, contact us. |
| Widget looks broken after I edited the snippet | The snippet is a sealed unit | Paste a fresh copy from the builder instead of editing. |
| A solid dark (or white) rectangle shows below or around the card | The iframe is taller than the card and the snippet was copied before 2026-07-27, so the browser paints the gap opaque | Paste a fresh copy from the builder; current snippets pin `color-scheme` on the iframe. |
## Support
Questions or something looks wrong? Join the [Limitless Discord](https://discord.com/invite/yb4SscD8RZ) and message the team. That's the fastest way to reach us.
# Fees
Source: https://docs.limitless.exchange/user-guide/fees
How trading fees work on Limitless Exchange
## Overview
Fees on Limitless adapt to the **market price**: the fee model is designed to reward early conviction and discourage manipulation. The base taker rate is the same for every account; a lower rate applies only where Limitless has set a per-profile fee cap.
## 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.
[Custom markets](/user-guide/custom-markets) written by users run on their own liquidity pool with the pool fee set at creation, currently **2%** per trade. That fee goes to the market creator as the pool's only liquidity provider, not to Limitless.
### 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 order book.
| 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.45% |
| \$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.
### Reading the exact fee programmatically
The curve is not published as a closed-form formula, and the tables above list representative prices rather than every point. You do not need either: every order execution reports the fee that was actually applied.
* `effectiveFeeBps` — the rate applied to that execution
* `usdFee` and `contractsFee` — the amount charged, in the asset you received
* `feeRateBps` — the fee rate your profile is charged on that market (`0` on fee-free markets); the signed order must carry the same value or it is rejected with `FEE_RATE_MISMATCH`
See [Create Order](/api-reference/trading/create-order) for where these appear in the response.
## 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 fee curve 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 — Limitless 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
* You earn rewards 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 meet or exceed the **minimum size threshold** set on a per-market basis
* Limitless calculates rewards **every minute** across all markets and pays them out daily at **22:30 UTC**
* The daily payout pays your **full unpaid balance**. There is no minimum payout amount, and sub-cent balances are included.
\*To maintain a fair market-making experience, only limit orders at or above the minimum shares threshold count toward the midpoint calculation.
## Rewards parameters
Each market has 3 parameters that define how rewards are paid out:
| Parameter | Illustrative example | Description |
| - | - | - |
| **Rewards** | \$10 | 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.
**All three values are set per market and change over time.** The numbers
above only illustrate the shape of the parameters, not what any market pays.
For the live, authoritative values, see the
[Limitless Rewards page](https://limitless.exchange/rewards) or the market
itself on the platform.
## 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% and 95%**. This provides LPs (liquidity providers) with greater flexibility in managing their positions while still fostering a robust and efficient market structure. Inside that band a one-sided quote earns a reduced score (its size divided by a per-market factor), so two-sided quoting always earns more.
## Time-based reward schedules
A market's daily rewards budget can scale up or down at specific hours or days of the week. Schedules run in **Eastern Time**, and the active window multiplies the per-minute reward rate (for example, a 2× boost during peak hours or a 0.5× dim overnight).
* By default, no schedule applies and rewards pay at the flat rate 24/7.
* A platform-wide schedule can dim rewards during low-activity windows, and it never increases a market's daily cap.
* Individual markets can override the platform schedule to boost specific windows, up to the platform ceiling of \$5,000 per market per day.
* Category or tag schedules can apply to a family of markets; when several match, the lowest rate wins. A schedule rule may also set a fixed daily amount instead of a multiplier.
Limitless records the applied multiplier per market per minute, so your payouts always reflect the schedule that was active when your orders earned.
## 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, Limitless credits a portion of that fee 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 (every few minutes):** as trades execute, Limitless calculates rebate credit for eligible maker fills.
2. **USDC payout (daily):** once per day (UTC), Limitless 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.
The rebate rate is not applied to 100% of a market's trading fees. Two deductions can
come off the top before the rebate pool is funded:
* **Referral share.** When the taker who paid the fee was referred by another user,
that referrer's share is taken first. See
[Referral share comes first](#referral-share-comes-first) below.
* **Creator fee.** On **user-generated markets** (markets with a **Community** badge),
the market creator's fee is taken before the rebate pool is funded. See
[User-generated markets](/user-guide/user-generated-markets).
## 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, Limitless determines rebate credit from three inputs:
* The fill's execution details (size and price)
* The applicable taker fee
* 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 every few minutes 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, Limitless 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, Limitless distributes the full scheduled amount for that day. If the rebate wallet balance is lower than the scheduled payout amount, Limitless distributes the available USDC pro-rata and records the shortfall.
Fees paid in outcome tokens (BUY fills) are valued conservatively until the market resolves and revalued at resolution; a later payout run tops up the difference.
### 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. Limitless allocated the full **\$27.90 USDC** to the rebate pool (100%) and split it between makers based on each maker's share of the day's eligible fee-generating fills.
This example assumes none of the takers were referred users and that the market has
**no creator fee**, so 100% of the eligible fees fund the rebate pool. When a referred
user pays a taker fee, the referrer's share is deducted first, and on user-generated
markets a creator fee is deducted first — see below.
## Referral share comes first
Limitless pays [referral rewards](/user-guide/referral-program) in USDC. When a user who
signed up through someone's referral link pays a taker fee, part of that fee goes to the
user who referred them. That **referral share is deducted first**, and the maker rebate
pool for the market is funded from what remains:
```
Referral share = Taker fee × Referrer's referral rate (only when the taker was referred)
Maker rebate pool = (Trading fees − Referral share) × Rebate rate
```
Two things determine how much, if anything, is deducted on a given day:
* **Whether the taker was referred.** Fees paid by takers who have no referrer are not
reduced at all. On a day when no referred user takes liquidity in a market, the full
eligible fee amount funds that market's rebate pool.
* **The referrer's tier.** The referral rate is set by the referrer's own trading volume
and ranges from 10% (Starter) to 40% (Diamond). See
[Earning rates](/user-guide/referral-program#earning-rates) for the current ladder.
The deduction depends on the **taker** on each fill, not on the maker. It is only taken
from fees paid by referred takers, whoever the maker on those fills was. Because a
market's rebate pool is shared between all of its makers pro-rata, the deduction reduces
the pool for every maker in that market, not only for the maker who filled the referred
taker's order.
### Example
On one UTC day, an eligible market with a **90% rebate rate** collects **\$100** in taker
fees. \$40 of that came from a referred user whose referrer is on the Starter tier (10%);
the other \$60 came from takers with no referrer.
* **Referral share:** 10% × \$40 = **\$4.00**, paid to the referrer.
* **Fees left for the rebate pool:** \$100 − \$4.00 = **\$96.00**
* **Maker rebate pool:** 90% × \$96.00 = **\$86.40 USDC**, split between makers pro-rata by
rebate credit.
Had none of the takers been referred, the pool would have been 90% × \$100 = \$90.00.
## User-generated markets: the creator fee comes first
On [user-generated markets](/user-guide/user-generated-markets) — user-created markets
with a **Community** badge — the market creator earns a share of trading fees. That
**creator fee is deducted first**, and the maker rebate pool is funded from what
remains. On these markets the rebate rate therefore applies to the fees left
**after** the creator's cut, not to the full trading-fee amount:
```
Maker rebate pool = (Trading fees − Creator fee) × Rebate rate
```
The creator's cut is taken off the top, and makers share the rest at the market's
rebate rate — so the larger a market's creator fee, the smaller the share left for the
maker rebate pool. Markets created by Limitless have no creator fee, so on those markets
only the [referral share](#referral-share-comes-first) (when it applies) is deducted
before the rebate pool is funded. See
[User-generated markets](/user-guide/user-generated-markets) for full details.
Fees on user-generated markets do not currently accrue referral earnings, so today a
market never has both deductions at once: Limitless-created markets can carry a
referral share, user-generated markets carry the creator fee.
## 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
Rebate rates are configured **per market** and change over time. The live, authoritative rates — along with LP Rewards and minimum-contract thresholds — are shown on the [Limitless Rewards page](https://limitless.exchange/rewards?view=rebates). Check there for the current rate on a specific market rather than relying on a value hardcoded here.
The rebate rate applies to trading fees **after** any deductions, not to the full fee
amount: the [referral share](#referral-share-comes-first) on fees paid by referred
takers, and the creator fee on
[user-generated markets](/user-guide/user-generated-markets).
## Funding and program economics
Maker Rebates is funded by taker fees and paid from a dedicated rebate wallet in USDC.
Referral rewards and creator fees are funded from the same taker fees and are deducted
before the rebate pool is sized, so a market's fee pool is split in this order:
referral share, creator fee, share-link author fee (on orders placed through a shared
link, when the share-fee program is enabled), maker rebates.
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 every few minutes as trades are finalized.
* Limitless computes the daily payout after the UTC day closes.
* Limitless distributes USDC 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.
### Where the payout shows up
Once the daily payout run completes, Limitless sends the rebate as a direct **USDC transfer to the same wallet you traded from**. The rebate is not a redeemable position and does not appear as a fill or trade in your order history. Instead, your wallet's USDC balance simply increases by the rebate amount. On Base, this is a standard USDC transfer, so any block explorer (for example, [BaseScan](https://basescan.org)) shows it as an inbound transfer from the rebate wallet.
## 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.
Select "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, click the **cancel button** next to it. Your locked funds return 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
Place a market order for quick trades. Enter the number of shares you want to buy or sell, and Limitless matches your order to the best available price.
## How it works
1. Select **Buy** or **Sell**
2. Select the outcome — **Yes** or **No**
3. Enter the number of shares
4. Click **Trade** — your order matches instantly
Market orders are simple and efficient, especially when you need immediate execution.
Market orders execute at the **best available price**. This may differ from the displayed price if the order book moves between when you view it and when Limitless matches your order.
## 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"). Limitless accepts no new trades after this time.
The designated oracle source provides the verified result. **Pyth Network** now resolves the majority of markets automatically.
For manually resolved markets (admin-created event markets with no data feed), 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. If the market resolves [50-50](#50-50-resolution), every share is redeemable for \$0.50 instead.
## Claiming vs. selling before resolution
Redeeming winning shares after a market resolves is **not the same** as selling them on the order book before resolution:
| Action | When | Payout | Fees |
| - | - | - | - |
| **Claim / redeem** | After the market resolves and the on-chain payout is posted | Exactly \$1.00 USDC per winning share (\$0.50 per share on a 50-50 resolution) | No trading fee on the redemption itself |
| **Sell on the order book** | Anytime before resolution (or before you claim) | Current market price (less than \$1.00) | Taker fee applies if the order matches immediately (see [Fees](/user-guide/fees)) |
If you sold or accepted "claim now" liquidity on the order book before resolution, the USDC you received is the market price minus the taker sell fee. That amount is not the \$1.00 redemption value. The sell fee on CLOB markets is dynamic and peaks at **1.50%** near \$0.50. See the [sell fee curve](/user-guide/fees#sell-fee-curve) for the full table. Maker (resting limit) sells are fee-free.
## Oracle sources
Market resolution and on-chain redemption readiness are related but not identical. A market can appear resolved in the API before the underlying payout is fully settled on-chain. 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.
**Pyth Network** now resolves the majority of markets. 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 |
| **Chainlink TWAP** | Short-cadence crypto up/down markets (5-minute and 15-minute) | Automatic at deadline |
| **Sports data providers** | Football, tennis and cricket (API-Sports) and esports (PandaScore) markets | Automatic; checked every 5 minutes (esports props every 15 minutes, esports match groups hourly) once the match reaches full time |
| **External venue parity** | Markets mirrored from another venue | Automatic, within minutes of the source venue settling |
| **Creator (user-generated custom markets)** | [Custom markets](/user-guide/custom-markets) created by users | The creator's wallet reports the outcome, at any time; Limitless syncs it every 15 minutes |
| **Manual** | Admin-created event markets with no data feed (politics and other custom events) | Typically 24–72 hours after deadline |
For Pyth-resolved markets, both the captured baseline price (`metadata.openPrice`) and the strike price quoted in the auto-generated market description use the full precision of the underlying Pyth feed. That is typically 8 decimal places for crypto feeds. Chainlink-resolved markets use the 18-decimal padded value matching the on-chain feed.
### Chainlink TWAP resolution
Short-cadence crypto up/down markets are moving to **time-weighted average price (TWAP)** resolution. Instead of comparing two instantaneous prices, both sides of the comparison come from the same Chainlink TWAP stream: an average over a fixed window. The market's description names the exact feed and window length.
* The **Price to Beat** is the TWAP captured at the market's open time.
* At the deadline, the closing TWAP is read from the same stream. The market resolves **Up** when the closing value is greater than or equal to the Price to Beat, and **Down** otherwise.
* Because the value is an average over the window rather than a single tick, a momentary price spike at the boundary does not decide the market on its own.
* The report at the exact resolution time is used first. If it is unavailable, the first Chainlink observation within the market's stated tolerance window is used. If no report exists in that window, the market is not resolved automatically and is handled per its description.
Each TWAP market's description states the exact feed, window, tolerance, and Price to Beat — always the authoritative source for that market's rule.
## Resolution for different market types
### Standard (CLOB) markets
Binary Yes/No markets normally resolve with one winning outcome and one losing outcome. When explicitly permitted by the market's resolution criteria, they may instead resolve [50-50](#50-50-resolution).
### 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.
## Market-specific resolution rules
The mechanics above describe how a Yes/No outcome pays out once chosen. The resolution criteria on each market page decide **which** outcome wins. Those criteria can cover event-specific edge cases — for example, a tied match, a cancelled game, or a "None of the above / Other" clause on a bracket-style market.
Always read the market page before you trade. It defines exactly which real-world results resolve **Yes**, which resolve **No**, and how ties, cancellations, or unlisted outcomes are handled. Unless the market's criteria specify a 50-50 resolution or another applicable outcome, failure to meet the stated criteria for **Yes** results in the market resolving **No**. This includes cases where the market page lists a fallback such as "Other" or "None of the above".
## 50-50 resolution
Some markets may resolve 50-50 when the market's resolution criteria do not allow either outcome to be selected as the sole winner. A 50-50 resolution is only available for CLOB, AMM, and Ladder markets. A Ladder consists of multiple CLOB markets grouped under a shared title.
The circumstances that trigger a 50-50 resolution are defined in the resolution criteria on each market page. Always check the specific market's rules before trading.
When a market resolves 50-50, every share pays out \$0.50 USDC, regardless of which outcome it represents.
For example, if a trader holds:
* 100 shares of Yes
* 50 shares of No
the total payout is:
(100 × \$0.50) + (50 × \$0.50) = \$75.00 USDC
A 50-50 resolution is not a refund. The payout is based on the number of shares held at resolution, not the amount originally paid for those shares.
### Tennis match markets
Tennis match markets (Player A vs Player B) resolve based on the official outcome of the match. The scenarios below describe how each situation is handled. The market page states the exact rules and deadline for each match.
| Scenario | Resolution |
| - | - |
| **Walkover**: a player withdraws before the match begins and the opponent advances automatically | 50-50 |
| **Retirement, default, or disqualification**: the match begins but is not completed, and one player advances | The player who advances |
| **Cancelled match**: the match is called off before it begins and neither player withdraws | 50-50 |
| **Tie** | 50-50 |
| **Delayed or postponed match**, played by the deadline on the market page | The winner of the match |
| **Delayed or postponed match**, no winner by the deadline on the market page | 50-50 |
**Walkover.** Player A withdraws before the match starts and Player B advances automatically. The match was never played, so the market resolves 50-50.
**Retirement, default, or disqualification.** Player A and Player B begin their match and play one set. Player B retires during the match, so Player A advances and the market resolves to Player A.
**Cancelled match.** Player A and Player B are scheduled to play, but the match is called off before it begins and neither player withdraws. The market resolves 50-50. This differs from a walkover: in a walkover a player withdraws and the other advances, whereas in a cancellation the match itself is called off.
**Delayed or postponed match.** The market page sets a deadline for a winner to be determined, currently 23:59 UTC on the 14th day after the scheduled start. If the match between Player A and Player B is postponed and played before that deadline, the market stays open and resolves to whichever player wins. If no winner has been determined by the deadline, the market resolves 50-50.
Tennis total sets and total games markets follow different rules. If the match is not completed, they resolve to any outcome already guaranteed by the play so far, and 50-50 otherwise. Check the market page for the exact criteria.
If the published criteria are ambiguous or you cannot apply them to the real-world result, Limitless treats the market as misresolved and refunds users per the [Refund Policy](/user-guide/refund-policy). You get back the bet amount, not the \$1.00 payout.
## Disputes and misresolutions
If a market is misresolved, Limitless refunds the bet amount. 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 order book. 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 order book.
## Why use merge / split?
* **Capital efficiency** — Open and close positions without relying on order book liquidity
* **Better execution** — Avoid paying the spread
* **Liquidity defragmentation** — Under the hood, this enables better order book 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.
## Merge and split via the API
Server-wallet accounts can split and merge programmatically through the REST API:
* [`POST /portfolio/split`](/api-reference/portfolio/split) — convert collateral into a full outcome set
* [`POST /portfolio/merge`](/api-reference/portfolio/merge) — convert a full outcome set back into collateral
These endpoints sign the on-chain transaction with the managed server wallet, so they only work for server-wallet (Web2 mode) accounts. If you trade with an EOA or your own smart wallet, use the **Merge/Split** option in the UI or call `splitPosition` / `mergePositions` on the Conditional Tokens Framework contract yourself. See [Smart Contracts](/user-guide/smart-contracts) for the deployed addresses.
# 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 where only one answer is correct.
Each outcome has a Yes and No order book, but here's the twist: **all the "No" contracts across the different outcomes are linked**.
This setup lets you 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**.
# Packs
Source: https://docs.limitless.exchange/user-guide/packs
Bundle multiple market predictions into one offer with a higher multiplier
## What is a pack?
A **pack** is a curated bundle of 2 to 10 predictions from different markets. You win only if every leg lands. Because every leg has to win together, packs pay out at a much higher multiplier than trading one market on its own. If any one leg misses, the pack loses.
Your risk is fixed: you can only ever lose your stake, no matter how large the payout. A pack is also a real, self-custodied position. When you buy one, you receive an on-chain token in your own wallet, not just an entry in a database. The payout is backed on-chain and you claim it yourself ([more below](#under-the-hood)).
## Stake, payout, and fees
| | |
| - | - |
| **Collateral** | USDC on Base |
| **Stake range** | \$1 to \$70 (\$1 to \$100 on custom, maker-funded packs) |
| **Maximum multiplier** | 20x (after vig) |
| **Maximum payout** | \$1,400 per pack |
| **Vig (platform fee)** | 10% on crypto and sports packs, already included in the displayed multiplier |
The multiplier you see on a pack is the **net** payout multiplier, with the vig already baked in. A \$10 stake on a 10x pack pays out \$100 if every leg wins. Limitless rejects quotes with a stake above the ceiling with the message `Stake exceeds $70 maximum`.
Packs bought before a vig change keep the vig they were sold at. If you bought a pack when the vig was 4%, your locked-in multiplier and payout are unchanged.
Multipliers move with live market prices and can drift between sessions. The multiplier you lock in is the one displayed at the moment you buy a pack; later visitors may see a slightly different number on the same pack.
## Buying a pack
Browse the available packs on Limitless. Each card shows the pack name, multiplier, market start time, and the legs included.
Select a stake between \$1 and \$70. The interface shows your potential payout (stake x multiplier).
Every pack purchase requires a confirmation. With an external wallet, the first purchase also includes a one-time USDC approval for \$100.
Your pack appears as an active position with leg-by-leg status.
## Pack states on the listing
The state of a pack on the public listing tells you whether you can still buy in. Packs you already bought continue to resolve regardless of the listing state.
| State | What it means for you |
| - | - |
| **Active** | The pack is live. You can buy at the displayed multiplier. |
| **Hidden** | The pack has been temporarily removed from the public list. New buys are paused; packs you already bought continue to resolve normally. |
| **Closed** | The earliest leg deadline has passed. No new buys. Packs you already bought continue to resolve. |
## Tracking your packs
Open your portfolio to see every pack you hold. Each shows:
* The locked-in multiplier and potential payout.
* Leg-by-leg progress (for example, "1 of 3 legs won").
* Overall status: **Active** (legs still playing out), **Won**, or **Lost**.
A pack flips to **Won** only after every leg has resolved in your favor. If any leg misses, the pack flips to **Lost** as soon as that miss is confirmed. A Won pack shows a **Claim** button until you collect your winnings; claiming does not change the status.
A pack shows as **Pending** until its purchase is confirmed on-chain. A leg whose market resolves to a split payout neither wins nor loses the pack; the other legs decide it.
## Claiming winnings
When your pack reaches **Won**, a **Claim** button appears on the position. Click it to pull your winnings to your wallet.
Claiming runs on Base. If you use an external EOA wallet (such as MetaMask), you need a small ETH balance on Base for gas. Limitless Safe wallets do not need ETH. If the claim signature fails on an external wallet, top up gas and try again.
## Under the hood
Packs are fully on-chain, self-custodied positions, not a database entry.
* **You hold a real on-chain token.** Buying a pack mints a single Conditional Token (CTF) to your own wallet that represents the whole bundle. It becomes redeemable only if every leg resolves your way; if any leg misses, it expires worthless.
* **You sign your exact purchase.** Confirming a pack signs an EIP-712 intent in your wallet covering the pack you picked, your stake, and a deadline. That signature locks the bet in, so it can't be changed before it goes on-chain.
* **You claim your own winnings.** Redemption is permissionless and fully on-chain: once every leg lands, you claim directly from the contract on Base. You never depend on Limitless to release your payout.
## Frequently asked questions
No. Once you've bought a pack, every leg has to play out. There is no early cancel or partial cashout.
The vig (10% on crypto and sports packs) is already baked into the displayed multiplier, which is capped at 20x after vig. Multipliers also move with live market prices.
Either its earliest leg deadline has passed (the pack is now **Closed** on the listing), or an admin temporarily hid it (for example, a leg price drifted out of range). If you already bought it, your pack continues to resolve as normal.
Limitless catches up on leg resolution every 30 minutes. If your pack has not flipped within that window, contact support so the team can sync it.
No. Packs are a buy-side product only. There is no maker side, no order book, and no fee rebate to earn on packs. The price you see is the price you buy at. If you want to quote and rebate on Limitless, use CLOB markets ([Cross-market market making](/developers/cross-market-market-making), [Maker rebates](/user-guide/maker-rebates)).
USDC in a wallet connected to Limitless on Base. If you use an external EOA wallet, you also need a small ETH balance on Base for gas when claiming winnings.
# 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.
# Referral Program
Source: https://docs.limitless.exchange/user-guide/referral-program
Earn USDC from the trading fees your referred users generate
## Overview
The Referral Program pays you a share of the taker fees your referred users generate, in USDC. When someone signs up through your referral link and trades, Limitless credits you a percentage of the fees they pay.
The program works in two stages:
1. **Earnings accrual (continuous):** as your referred users trade, Limitless credits referral earnings to you at your current tier rate.
2. **USDC payout (daily):** once per day (UTC), Limitless pays out settled earnings in USDC to your trade wallet. No claiming is needed.
Your referral link, earnings, referred users, and the leaderboard all live on the [Referrals page](https://limitless.exchange/referrals).
## How referrals are attributed
* A user who signs up through your referral link is credited to you **permanently**. The first referrer wins and the attribution is never reassigned.
* You earn on the **taker fees** your referred users pay on CLOB markets (see [Trading fees](/user-guide/fees)). If a referred user's order rests on the book and is filled as maker liquidity, that fill pays no taker fee and generates no referral earnings.
* Only finalized (on-chain settled) fills count.
* Your share is deducted from the market's taker fees **before** the
[maker rebate](/user-guide/maker-rebates#referral-share-comes-first) pool for that
market is funded. Makers on your referred users' fills earn their rebate on the fees
that remain.
Fees generated on **user-generated markets** (markets with a **Community**
badge) do not currently accrue referral earnings.
## Earning rates
Your referral rate is determined by your **own trading volume**: the total of your own maker and taker fills on CLOB markets, counted from the program launch. Trade more yourself, earn a higher rate on the fills you refer.
| Tier | Your trading volume | Referral rate |
| - | - | - |
| Starter | \$0 | 10% |
| Bronze | \$25,000 | 18% |
| Silver | \$250,000 | 25% |
| Gold | \$3,000,000 | 32% |
| Diamond | \$20,000,000 | 40% |
Tiers are refreshed once a day at 00:15 UTC. A tier you reach during a UTC day applies from the next refresh: fills from that day onward earn the new rate, while earlier fills keep the rate they were earned at.
Partner arrangements can carry a pinned minimum tier. A pin acts as a floor:
if your volume clears a higher tier, the higher tier wins.
## Pending vs. settled earnings
Referral earnings appear on your dashboard as soon as your referred users trade, but they become **payable only once the underlying market resolves**:
* Fills on **unresolved markets** are valued conservatively and count as pending earnings.
* When the market resolves, the fill is revalued at its final fee value and the earnings become payable.
This means your displayed total can run ahead of what has been paid out. The difference is earnings on markets that have not resolved yet.
## Payout mechanics
* Payouts run **once per day (UTC)** as a single batched USDC transfer to your trade wallet.
* The minimum payout is **\$1**. Smaller balances are never lost. They roll forward and are included in a later payout once you clear the minimum.
* Each payout covers everything you are owed that has settled, across all days since your last payout.
## Example
A user signs up through your referral link and pays \$100 in taker fees trading a market. You are on the Starter tier (10%), so Limitless credits you \$10. While the market is open the \$10 shows as pending; once the market resolves, it becomes payable and is included in the next daily payout.
If your own trading volume later passes \$25,000, you move to Bronze and new fills from your referred users earn 18% instead.
## Programmatic access
Your referral stats, referred users, and the leaderboard are also available over the API — see the [Referral API reference](/api-reference/referral/my-stats).
# Refund Policy
Source: https://docs.limitless.exchange/user-guide/refund-policy
Limitless Exchange refund policy for misresolved markets
## Refund policy
If a market is misresolved, Limitless refunds **only the bet amount**.
### How misresolution refunds work
If a market is resolved to the wrong outcome:
* If you held shares on the **incorrectly winning side**, you keep any payout you received. Payouts settle on-chain and cannot be voided or clawed back; Limitless absorbs this cost
* If you held shares on the **actual winning side** (the side that should have won), you receive a refund equal to the amount you originally paid for those shares
* The refund covers the **cost basis** of the shares, not the potential payout
In other words, if the market resolved to side A but should have resolved to side B, and you held side B shares, you receive a refund of the amount you paid for those shares. You are not financially harmed by the misresolution beyond your 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 on-chain 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
On-chain contract addresses for Limitless Exchange on Base
## Overview
Limitless Exchange runs on **Base** (Ethereum L2). All trading is settled on-chain 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` |
| Fee Module ([new trading venues](/developers/migrate-to-new-trading-venues)) | `0x7B6ffe5bA8B5ec7afB0AaBaE629D12dA5bC62565` |
## 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 Fee Module ([new trading venues](/developers/migrate-to-new-trading-venues)) | `0x01b7dF643247B377D5c39FcC40b9c28B4A827524` |
| 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: September 15, 2026*
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: Russia, Belarus, Cuba, Iran, North Korea, Syria, and the
Crimea, Donetsk and Luhansk regions.
The Platform is not available for trading to users from the following jurisdictions: United States of America, Republic of China, and the Canadian
provinces of Ontario (ON) and Alberta (AB).
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. User-created prediction markets
### 4.1. Creation of prediction markets
The Platform may allow Users to create and submit Prediction Markets for publication and participation by other Users. A User who creates or submits a Prediction Market is referred to in this Section as a "**Market Creator**."
By creating or submitting a Prediction Market, the Market Creator represents and warrants that:
* all information, descriptions, questions, rules, and other content submitted in connection with the Prediction Market are accurate, complete, and not misleading to the best of the Market Creator's knowledge;
* the Prediction Market is based on an objectively verifiable event or outcome capable of being determined through the designated Oracle Source(s);
* the Prediction Market and all related content comply with these Terms, applicable laws and regulations, and any additional rules or requirements communicated by the Company;
* the Market Creator will not create or submit a Prediction Market for the purpose of manipulating the Platform, influencing market prices or outcomes, misleading other Users, or facilitating any prohibited or unlawful activity; and
* the Market Creator has the right to submit all information and content provided to the Platform and such submission does not knowingly infringe any third-party rights.
The Market Creator acknowledges that the submission of a Prediction Market does not guarantee that the Prediction Market will be published, remain available, or be made available for trading. The Company may, in its sole discretion, review, reject, modify, suspend, restrict, or remove any Prediction Market or related content at any time, including where the Company determines that such action is necessary to comply with applicable law, regulatory requirements, these Terms, or to protect the integrity or security of the Platform.
The creation, publication, or availability of a Prediction Market by a Market Creator does not constitute an endorsement, recommendation, verification, or guarantee by the Company of the Market Creator, the Prediction Market, the information contained in it, or any potential outcome.
The Market Creator acknowledges that other Users may rely on or participate in a Prediction Market created by the Market Creator and agrees not to make any false, misleading, or deceptive statements concerning the Prediction Market, its potential outcomes, or the likelihood of any outcome.
### 4.2. Participation in user-created prediction markets
Prediction Markets available on the Platform may be created by the Company or by other Users. A User who participates in a Prediction Market created by another User is referred to in this Section as a "**Participant**."
Before participating in a Prediction Market created by another User, the Participant acknowledges and understands that:
* the Prediction Market may have been created and submitted by another Platform User and not by the Company;
* the information, descriptions, statements, and other content provided by the Market Creator constitute User-Generated Content and may not have been independently verified by the Company;
* the publication or availability of a User-Created Prediction Market does not constitute an endorsement, recommendation, or guarantee by the Company of the Market Creator or the accuracy, completeness, or reliability of the Market Creator's content;
* the Participant is responsible for reviewing and evaluating the information available in connection with the Prediction Market before deciding whether to participate;
* participation in a Prediction Market involves financial risk, including the potential loss of all funds wagered; and
* the outcome of the Prediction Market will be determined in accordance with the applicable market rules and the designated Oracle Source(s), as described in these Terms.
A Market Creator may, at their sole discretion and without requiring the Company's prior approval, unilaterally close, suspend or modify a Prediction Market they have created, subject to the applicable market rules and any restrictions imposed by the Company.
The Participant acknowledges that the Company does not guarantee the accuracy, completeness, reliability, or timeliness of User-Generated Content and that the Participant's decision to participate in a User-Created Prediction Market is made at the Participant's own discretion and risk.
The Participant further acknowledges that the Company may suspend, cancel, modify, or remove a User-Created Prediction Market where the Company determines that it is necessary or appropriate, including where the market cannot be resolved definitively, violates these Terms or applicable law, or presents legal, regulatory, security, or integrity concerns.
### 4.3. User acknowledgement
Where a Prediction Market has been created by another User, the Company may require the Participant to provide an express acknowledgement before participating in such Prediction Market.
By providing such acknowledgement, the Participant confirms that the Participant:
* understands that this Prediction Market was created by another Platform User, that the content of the Prediction Market has not necessarily been independently verified or endorsed by the Company, and that the Participant has reviewed the available information and accepts the risks associated with participating in the Prediction Market.
The Company may maintain a record of such acknowledgement for compliance, risk management, dispute resolution, and other legitimate business purposes.
### 4.4. No reliance on user-created content
Users acknowledge that User-Created Prediction Markets and related User-Generated Content may contain opinions, interpretations, assumptions, or information provided by individual Users. Users should not rely solely on such content when deciding whether to participate in a Prediction Market.
Nothing in a User-Created Prediction Market or related User-Generated Content constitutes investment, financial, legal, or other professional advice provided by the Company.
## 5. 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.
## 6. 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.
## 7. 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.
## 8. 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.
## 9. 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.
## 10. 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.
## 11. 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.
## 12. 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.
## 13. 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.
## 14. 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.
## 15. 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.
## 16. 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).
# User-Generated Markets
Source: https://docs.limitless.exchange/user-guide/user-generated-markets
Markets created by users and how the creator fee works
## Overview
**User-generated markets** are prediction markets created by users rather than by the
Limitless team. They carry a green **Community** badge across the app. Any signed-in
user can create one (subject to a daily limit and per-duration capacity caps), and the
creator earns a share of the trading fees their market generates — an incentive to
launch markets people actually want to trade.
A user-generated market is identified by its **Community** badge. Markets created by
the Limitless team do not have a creator fee.
This page covers **price markets** created by users, which trade on the order book and pay
the creator a share of taker fees. Users can also write their own yes/no question on a
liquidity pool they fund and settle themselves. Those are **custom markets**, covered in
[Custom Markets](/user-guide/custom-markets).
## The creator fee
When a user-generated market is created, a **creator fee** is attached to it. This fee
is a share of the **trading (taker) fees** the market generates, and it is paid to the
market's creator.
| Property | Detail |
| - | - |
| **What it applies to** | Taker fees generated by trades in the market |
| **Rate** | Set when the market is created and shown on the market's **Community** badge. It can differ between markets and change for new markets over time |
| **Who receives it** | The market creator's wallet |
| **Token** | USDC |
| **When** | Paid daily, on the same schedule as [maker rebates](/user-guide/maker-rebates) |
The creator fee is **stamped onto each market when it is created and frozen for that
market's lifetime**. The exact rate can differ between markets and may be adjusted for
new markets over time, so check a market's **Community** badge for its current rate.
Changing the platform-wide rate never changes the fee on a market that already exists.
## How the creator fee interacts with maker rebates
This is the important part for market makers. On a user-generated market, **the creator
fee is taken first**, off the top of the market's trading fees. The
[maker rebate](/user-guide/maker-rebates) pool is then funded from **what remains**.
In other words, on user-generated markets a maker rebate is **not** computed on the full
trading-fee amount — it's computed on the fees left after the creator's cut.
### Order of operations
The market collects taker fees from trades during the day (UTC).
The creator is paid their fee out of the market's total trading fees.
The maker rebate pool for that market is funded from the **remaining** fees, at the
market's rebate rate.
Both the creator fee and the maker rebates are paid out daily in USDC.
### Conceptual formula
```
Creator payout = Trading fees × Creator fee rate
Maker rebate pool = (Trading fees − Creator payout) × Rebate rate
```
The maker rebate is measured against the fees that remain after the creator's cut, not
the full trading-fee amount. The larger a market's creator fee, the smaller the share
left to fund its maker rebate pool.
## Markets created by Limitless
Markets created by the Limitless team have **no creator fee**. On those markets, the full
eligible taker-fee amount funds the maker rebate pool (subject to the market's rebate
rate), exactly as described in [Maker rebates](/user-guide/maker-rebates).
## Can I trade in a market I created?
Yes. A user-generated market is a standard CLOB market, and you can place orders in it like any other trader. Two rules still apply:
* **Self-trade prevention (STP).** If your own resting order would match your own incoming order, the matching engine cancels one side per the market's STP policy. You can't fill your own quotes. See [self-trade prevention](/api-reference/trading/create-order#self-trade-prevention) for the exact outcomes.
* **No market manipulation.** The [Terms of Service](/user-guide/terms-of-service) prohibit wash trading, spoofing, layering, and coordinated trading to influence prices or outcomes. This applies whether or not you created the market.
## Creating a market
Market creation happens in the app at [limitless.exchange](https://limitless.exchange), not through these docs. For a custom yes/no market that you fund and settle yourself, follow [Custom Markets](/user-guide/custom-markets). If you get stuck or don't see the option to create a market:
* Ask on [Discord](https://discord.com/invite/yb4SscD8RZ). This is the fastest way to reach the team.
* Email [help@limitless.network](mailto:help@limitless.network).
* Use the support channel in the footer at [limitless.exchange](https://limitless.exchange).
For how your market gets resolved after its deadline, see [Market Resolution](/user-guide/market-resolution).
## Summary
* User-generated markets are created by users and carry a **Community** badge.
* The creator earns a share of the market's trading fees.
* The creator fee is **taken first**; maker rebates are funded from the remainder.
* The exact rate is set per market, can vary, and is **frozen per market** at creation —
check the badge for a market's current rate.
* Creator fees and maker rebates are both paid **daily in USDC**.
# 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. Keep it secure and never share it.
**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:** You connect an EOA from the main login menu. For every on-chain transaction, you must approve it in your 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 you sign in via social login (e.g., Google, Twitter), an embedded wallet is created automatically after authentication. You can execute transactions 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 to hold native tokens (e.g., ETH) to pay transaction fees. You sign in via social login, and a smart wallet instance is created based on your embedded wallet. Limitless sponsors transaction fees, so you only need to hold **USDC** for trading activity.
## Trading wallet mode
Accounts connected with an external wallet may see a one-time **"choose your trading wallet"** prompt offering 1-click (smart wallet) trading. Accepting it sets your profile's trading mode to smart wallet: app trades then run gas-free through a smart wallet instead of requiring a confirmation in your external wallet for every action. The prompt does not reappear after you choose.
This choice matters if you later trade **through the API**: in smart-wallet mode, the API rejects orders signed with your external wallet's own key. Developers can switch the profile back to EOA trading mode with a single authenticated call. See [Trading wallet mode](/developers/eip712-signing#trading-wallet-mode-whose-address-signs) in the developer docs.
## Summary
| Wallet type | Created when | Fee responsibility | User experience |
| - | - | - | - |
| **EOA** | External wallet connection | You pay gas in ETH | Requires approval per transaction in external wallet |
| **Embedded** | Social login | You pay gas in ETH | In-app transactions; no external approval step |
| **Smart** | Social login (from embedded wallet) | Platform sponsors gas | You hold only USDC; no native token needed for fees |
## Exporting private keys
How (and whether) you can export a private key depends on the wallet type:
* **EOA wallet**: your wallet interface (MetaMask, Rabby, Coinbase Wallet, etc.) manages the private key. Follow that wallet's export instructions. Limitless never has access to it.
* **Embedded wallet**: the wallet is created and secured by the embedded-wallet provider (Privy). By design, the raw private key is **not exposed** to the app or to the user. This is a provider-level security guarantee, not a Limitless restriction, so the key cannot be exported for external signing.
* **Smart wallet**: a smart wallet is a contract account and does not have a single private key. It is controlled by its owner key (the embedded wallet), which is also non-exportable.
For programmatic trading from a social-login account, you cannot lift the
embedded wallet's key into an external script. Instead, either:
* Trade from a **separate EOA** whose private key you control (sign orders yourself), or
* Use **delegated signing** via the [Programmatic API](/developers/programmatic-api), where the server signs on your behalf using the managed embedded wallet — no private key handling required.
See [Authentication](/developers/authentication) and
[EIP-712 Order Signing](/developers/eip712-signing) for the full flows.
A Limitless **API token secret** is not a wallet private key. It is shown
**once at creation** and cannot be retrieved afterward — if you lose it, derive
a new token from the Limitless UI.
## Depositing and withdrawing funds
Limitless does not hold a platform balance. Your USDC always lives in your wallet on **Base**. Depositing and withdrawing therefore look different depending on your wallet type.
### Depositing
* **EOA wallet**: bridge or send USDC on Base to your EOA address. Once the balance is on Base, it is immediately usable for trading.
* **Embedded or smart wallet** (social login): the Limitless UI shows your embedded and smart wallet addresses. Send USDC on Base to that address to fund the account. You do not need to hold ETH for gas when trading from a smart wallet.
### Withdrawing
* **EOA wallet**: your USDC already sits in your own wallet. To move it elsewhere, send it from your wallet interface (MetaMask, Rabby, etc.) like any other ERC-20 transfer on Base. Limitless does not need to be involved.
* **Embedded or smart wallet** (social login): use the **Withdraw** action inside the Limitless UI to transfer USDC from your smart wallet to another address on Base. The platform sponsors gas.
* **Programmatic partners**: server-managed wallets withdraw via the [`POST /portfolio/withdraw`](/api-reference/portfolio/withdraw) endpoint. The destination address must either be the authenticated account, its smart wallet, or an allowlisted withdrawal address on the profile. Allowlisted addresses are managed with `POST` and `DELETE /portfolio/withdrawal-addresses`, which only a Privy (browser) session can call; an HMAC token cannot edit the list.
All deposits and withdrawals settle on **Base**. Sending USDC from another chain (Ethereum mainnet, Arbitrum, etc.) without bridging first will not be credited. Bridge to Base before sending.
# 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 lets you freely trade natural language expressions of arbitrary market conditions, creating a forecasting and risk engine for the global economy.
You 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 on-chain 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.