# Errors Source: https://docs.totalis.trade/api-reference/errors Every error the Totalis API returns: the envelope, HTTP statuses, top-level codes, per-surface rejection reasons, and which ones to retry. ## Envelope Every error, on every endpoint, has the same shape: ```json theme={null} { "error": { "code": "VALIDATION_ERROR", "message": "Human-readable description.", "details": {} } } ``` | Field | Use | | - | - | | `error.code` | Stable. Safe to branch on. | | `error.message` | For humans. Can change without notice, so never match on it. | | `error.details` | Surface-specific context, most often `details.reason`. | ## HTTP statuses | Status | Meaning | Your move | | - | - | - | | `400` | The request is malformed or a value is out of range. | Fix the request. Retrying will not help. | | `401` | The credential is missing, invalid, revoked, or expired. | Check the key. Do not retry with the same one. | | `403` | The credential is valid but not permitted to do this. | Check scopes, or the key format. | | `404` | The object does not exist, is not yours, or has already been evicted. | Treat as normal on short-lived objects. | | `409` | A state conflict. Something changed underneath you. | Re-read state and decide. Often not retryable. | | `410` | The object expired and is gone, such as a cashout auction past its window. | Start a new one. | | `413` | Payload too large. | Reduce the request body. | | `429` | Rate limited. | Wait the seconds in the `Retry-After` header. Main API routes also put them in `error.retry_after`. | | `500` | Something failed on our side. | Retry with backoff, unless the endpoint documents an indeterminate outcome. Then check state before you retry. | | `502` | Partial success, such as `PARTIAL_VAULT_ONLY` on a withdrawal. | Read `error.details` before retrying. See [Funding](/guides/funding#when-it-fails). | | `503` | Temporarily unavailable. | Retry with backoff. | A parlay id you do not own returns `404`, not `403`, so the API never confirms that another account's parlay exists. A quote request id you do not own returns `403`. ## Top level codes | `error.code` | Status | | - | - | | `VALIDATION_ERROR` | 400 | | `UNAUTHORIZED` | 401 | | `FORBIDDEN` | 403 | | `NOT_FOUND` | 404 | | `CONFLICT` | 409 | | `GONE` | 410 | | `PAYLOAD_TOO_LARGE` | 413 | | `RATE_LIMITED` | 429 | | `INTERNAL_ERROR` | 500 | | `PARTIAL_VAULT_ONLY` | 502 | | `SERVICE_UNAVAILABLE` | 503 | ## Authentication | Condition | REST | SSE | `details.reason` | | - | - | - | - | | Credential does not start with `api_` | `403` | `403` | `wrong_api_key_type` | | Well-formed key, but invalid or revoked | `401` | `401` | | | Valid key, missing the required scope | `403` | `403` | `insufficient_scope` | The `403` for a malformed key carries a hint about the expected format. Keys match `api_(live|test)_[A-Za-z0-9_-]{28,36}`. The scope catalog is on [Authentication](/guides/authentication#scopes). ## Committing a quote Returned by [`POST /v1/quote-requests/{id}/commit`](/api-reference/quote-service/commit) in `error.details.reason`. No commit failure moves funds: if the commit did not return success, the trade did not happen. | Reason | What happened | Your move | | - | - | - | | `QUOTE_CHANGED` | The request changed since the version you committed against. | Re-read the request and commit against the new version. | | `QUOTE_EXPIRED` | Every quote on the request has expired, or the best quote expired between selection and commit. | Wait for a fresh `best_quote` event, then retry. | | `MARKET_NOT_LIVE` | A leg's market expired, halted, or was delisted. | Remove the affected leg. Retrying will not help. | | `COMMIT_FAILED` | The commit could not complete, for example because no eligible maker had enough collateral. | Not retryable. | | `COMMIT_FAILED_RETRYABLE` | A temporary failure. | Retry after a short delay. | | `mm_confirmation_timeout` | The chosen maker did not confirm in time. Returned as `409`, with the maker in `failed_mm_id`. | The request stays open. Commit the next best quote, or leave that maker out with `exclude_mm_ids`. | ## Submitting a quote Returned by [`PUT /v1/mm/quote-requests/{id}/quote`](/api-reference/quote-service-mm/submit-quote). | Status | `details.reason` | Meaning | | - | - | - | | 400 | `insufficient_collateral` | The quote needs more collateral than you have free. Carries `required` and `available`. | | 503 | `collateral_check_unavailable` | Your balance could not be verified. Retry. | | 503 | `fee_unavailable` | The taker fee rate could not be read, so the quote was rejected rather than priced at 0 bps. Retry. | | 409 | `version_mismatch` | Your `request_version` or `request_hash` is stale. Re-read the latest event and resubmit both. | Both `503`s fail closed on purpose, so a quote is never admitted against a balance or fee rate that could not be read. Priced at an unknown fee, your odds would apply to the gross bet and you would underwrite more than you modeled. ## Bidding on a cashout auction Returned by [`PUT /v1/mm/cashout-requests/{id}/quote`](/api-reference/quote-service-mm/cashout-quote). | Status | `details.reason` | Meaning | | - | - | - | | 400 | validation | Non-integer or non-positive `price_micro`, `expires_in_ms` out of range, unknown field, or malformed JSON. | | 400 | `price_above_max_payout` | Your bid exceeds `total_payout`. The response carries `max_price_micro`. | | 400 | `price_above_buyback_bound` | You back this position and the bid exceeds what your collateral covers after the fee. | | 403 | `insufficient_scope` | Your key lacks `mm:quote`. | | 404 | | The auction is unknown, or already evicted at its TTL (time to live). The normal "too slow" outcome. | | 409 | `self_quote` | You opened this auction, so you are the seller. Bidding on a position you back is allowed: that win closes the position. | | 409 | `not_active` | The auction was committed or cancelled before your bid landed. | | 409 | `expired` | The auction expired as your bid landed. | | 409 | `MARKET_NEAR_CLOSE` | The underlying market is too near its close to exit. | | 409 | `buyback_wallet_missing` | You back this position but have no wallet on record, so a closing exit cannot be built. See below. | `buyback_wallet_missing` fails only your bids. Others can still win the auction as an acquisition, so do not blacklist the position. ### Rejections you never see These cashout rejections happen when the user commits, not when you bid. They never reach you as an HTTP response. You learn of them by not being filled. | Reason | Meaning | | - | - | | `amount_over_bound` | The fee-aware ceiling, re-checked at commit when the fee rate was unavailable at bid time. | | `insufficient_buyer_balance` | You would acquire the position but your free balance does not cover it. | | `buyer_no_vault` | You would acquire the position but have no vault. | | `already_transferred` | The position had already moved. | | `buyer_is_counterparty` | A safety check: your wallet already backs the position, so the win has to close it rather than acquire it. | ## Triage | Class | Treatment | | - | - | | `400`, `401`, `403` | **Alert.** These are your bug. If every request fails this way, you are silently out of the market. | | `404` and race-class `409` (`not_active`, `expired`) | **Log quietly.** On short-lived objects these are ordinary lost races, not failures. | | `429` | **Back off** for the seconds in `Retry-After`. | | `500`, `503` | **Retry** with exponential backoff, unless the outcome is documented as indeterminate. Then check state first. | Separate `400` / `401` / `403` from `404` / `409` in your alerting from day one. Logged together, a losing auction and a broken integration look identical. A market maker whose key silently lost the `mm:quote` scope sees nothing but quiet `403`s. # Wallet delegation status Source: https://docs.totalis.trade/api-reference/funds/delegation-status GET /v1/wallet/delegation-status Check whether the embedded (TEE) wallet has trading delegation enabled. # Enable trading Source: https://docs.totalis.trade/api-reference/funds/enable-trading POST /v1/wallet/enable-trading Check that wallet delegation is in place and return its status. Delegation itself is granted in the app. On older accounts, also credits any pending welcome airdrop. # Get vault Source: https://docs.totalis.trade/api-reference/funds/get-vault GET /v1/vault The caller's vault state and active position summaries. # P&L timeseries Source: https://docs.totalis.trade/api-reference/funds/pnl GET /v1/pnl Realized P&L per day over the requested window (1D / 1W / 1M / ALL). # Portfolio Source: https://docs.totalis.trade/api-reference/funds/portfolio GET /v1/portfolio Consolidated portfolio: balance, stats, status counts, and summary. # Deposit into the vault Source: https://docs.totalis.trade/api-reference/funds/vault-deposit POST /v1/vault/deposit Move USDC from your wallet into your on-chain vault. Optional: trading and quoting both draw on wallet USDC too. You do not need this call to trade or to quote. Placing a parlay moves the stake from your wallet into the vault in the same step. Maker quotes are backed by wallet USDC plus vault free balance. Your vault is created with your first filled position, as bettor or maker. Until then this call returns `404`. Needs the `funds:deposit` scope and trading enabled. Limited to 10 calls per minute per account. # Withdraw from the vault Source: https://docs.totalis.trade/api-reference/funds/vault-withdraw POST /v1/vault/withdraw Move free balance from your on-chain vault back to your wallet. To send USDC out of Totalis, use the wallet withdraw instead. This moves USDC from the vault to your wallet only. Only the free balance moves: the on-chain program keeps locked collateral in place. To send USDC to an external address, use [`POST /v1/wallet/withdraw`](/api-reference/funds/wallet-withdraw), which taps the vault for you. Needs the `funds:withdraw` scope. Limited to 10 calls per minute. # Withdraw to external address Source: https://docs.totalis.trade/api-reference/funds/wallet-withdraw POST /v1/wallet/withdraw Withdraw USDC from the embedded wallet to an external Solana address. # API overview Source: https://docs.totalis.trade/api-reference/introduction The Totalis REST, SSE, and WebSocket API: base URL, authentication, response envelope, pagination, units, errors, and rate limits. The Totalis API is REST over HTTPS with JSON bodies, in `snake_case`. It adds Server-Sent Events (SSE) for live quote pricing and a [WebSocket](/guides/websocket) for post-trade events. ## Base URL ``` https://api.totalis.trade ``` ## Try it without a key Market data is public: ```bash theme={null} curl "https://api.totalis.trade/markets?limit=3" ``` ## Authentication Send your API key in the `X-API-Key` header on every authenticated request: ``` X-API-Key: ``` The web dashboard authenticates with Privy instead, sending `Authorization: Bearer `. Either scheme works on any authenticated endpoint. Minting a new API key (`POST /v1/api-keys`) requires a Privy JWT or a key with the `keys:manage` scope. Every key is scoped. The scope catalog is on [Authentication](/guides/authentication#scopes). ## Response envelope Successful responses wrap the resource in `data`: ```json theme={null} { "data": { "...": "resource" } } ``` List endpoints add cursor-based pagination in `meta`: ```json theme={null} { "data": { "rfqs": [] }, "meta": { "cursor": "opaque-string-or-null", "has_more": false } } ``` To page, pass the previous response's `meta.cursor` as `?cursor=`. Cursors are opaque, so do not parse or build them. ## Units Any field whose name ends in `_micro` is an **integer of microUSDC**, where `1000000` equals 1.00 USDC. Every other money field is a **decimal USDC number**. Both conventions appear in the same payload on the cashout surface. Market prices are decimal probabilities from 0 to 1 on every venue. The full unit table is in the [Glossary](/guides/glossary#units). ## Errors Every error, on every endpoint, has the same shape: ```json theme={null} { "error": { "code": "VALIDATION_ERROR", "message": "Human-readable description.", "details": {} } } ``` Branch on `error.code`, never on `error.message`. Codes, rejection reasons, and retry guidance are on [Errors](/api-reference/errors). ## Rate limits Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. The per-minute limits are on [Limits and fees](/guides/limits-and-fees#rate-limits). ## Endpoint groups Browse prediction markets across Kalshi and Polymarket. Public. Open a quote request, stream live pricing, and commit the best quote. List your parlays and read settlement detail. Balances, portfolio, P\&L, and withdrawals. Configure signed event delivery. Stream quote requests, submit quotes, and confirm trades. Every code and rejection reason, and which to retry. Endpoints added, retired, and renamed. # Get market Source: https://docs.totalis.trade/api-reference/markets/get GET /markets/{ticker} Fetch a single market by Kalshi ticker or Polymarket condition ID. Use the optional venue query parameter when you already know the source venue. # List markets Source: https://docs.totalis.trade/api-reference/markets/list GET /markets List active Kalshi and Polymarket markets, grouped by event, with filters for category, subcategory, frequency, date window, and full-text search. **Building parlays.** Every market carries an `exclusion_keys` array. Two markets cannot share a parlay if and only if their `exclusion_keys` intersect. A plain set-intersection check is all you need, with no correlation rule tables to maintain client-side. The keys encode every rule: cross-series groups, threshold ladders, same-game sports correlation, and Polymarket sub-events. # List markets (flat) Source: https://docs.totalis.trade/api-reference/markets/list-flat GET /v1/markets/list A flat, paginated feed of individual markets, ungrouped. Built for market makers and partners syncing the tradeable universe. **Building parlays.** Every market carries an `exclusion_keys` array. Two markets can share a parlay only if their arrays do not intersect. A plain set-intersection check is all you need, with no correlation rules to maintain client-side. All prices (`yes_bid`, `yes_ask`, `no_bid`, `no_ask`, `last_price`) are decimal probabilities in `[0, 1]` on every venue. Kalshi's cent prices are normalized. The feed returns tradeable markets only (`status` of `active` or `open`) by default. Pass `status=all` for every status. Paging stops once `offset + limit` passes 10,000. For a full sync, split the universe with filters such as `venue` or `category` instead of paging one long list. `GET /markets/events/{event_ticker}` returns every market in one event, such as a single game, in one request. # Confirm quote Source: https://docs.totalis.trade/api-reference/mm/confirm-quote POST /v1/mm/quotes/{quoteId}/confirm Confirm an accepted quote to lock in the trade and begin vault settlement. Confirm an accepted quote to lock in the trade. Confirming triggers an atomic Solana transaction that locks the user's stake and your collateral in their respective vaults. When a user commits your quote, you receive a `quote:accepted` WebSocket event. Confirm before its `confirmation_deadline`. The window is 5 seconds from acceptance by default, but it is configurable server-side, so honor the timestamp rather than a fixed duration. You may also receive `mm_quote:accepted` on the same `mm:quotes:{mm_id}` channel. It adds richer quote economics and `exposure_legs`, for local risk reservation or analytics. Both events carry the same `quote_id`, and either is valid for confirmation. Their delivery order isn't guaranteed. If you miss the deadline, the trade unwinds: the RFQ and your accepted quote both become `expired`, with reason `mm_confirmation_timeout`. **Base URL:** `https://api.totalis.trade` ## Authentication API key required. Pass your API key in the `X-API-Key` header. ``` X-API-Key: ``` ## Path parameters The accepted quote ID (UUID): the `quote_id` from the `quote:accepted` WebSocket event. It also matches `quote_id` on the optional `mm_quote:accepted` event. ## Request body No request body required. ## Response `"confirmed"` The confirmed quote ID. Execution ID for tracking the vault settlement. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this quote. | | 404 | `NOT_FOUND` | Quote not found. | | 409 | `CONFLICT` | Quote is not in `accepted` status (already confirmed, expired, or cancelled). | | 409 | `CONFLICT` | Insufficient balance. Your aggregate risk across pending positions exceeds your available USDC (wallet + vault free balance). | | 503 | `SERVICE_UNAVAILABLE` | Unable to verify your balance. Retry after a short delay. | ```bash theme={null} curl -X POST https://api.totalis.trade/v1/mm/quotes/d4e5f6a7-8901-bcde-f234-567890abcdef/confirm \ -H "X-API-Key: $API_KEY" ``` ```javascript JavaScript theme={null} // After receiving a quote:accepted WebSocket event ws.on('message', async (msg) => { const event = JSON.parse(msg); if (event.type === 'quote:accepted') { const res = await fetch( `https://api.totalis.trade/v1/mm/quotes/${event.data.quote_id}/confirm`, { method: 'POST', headers: { 'X-API-Key': API_KEY }, } ); const { data } = await res.json(); console.log('Confirmed:', data.execution_id); } }); ``` ```python Python theme={null} import requests # After receiving a quote:accepted WebSocket event quote_id = "d4e5f6a7-8901-bcde-f234-567890abcdef" resp = requests.post( f"https://api.totalis.trade/v1/mm/quotes/{quote_id}/confirm", headers={"X-API-Key": API_KEY}, ) data = resp.json()["data"] print(f"Confirmed: {data['execution_id']}") ``` ```json 200 theme={null} { "data": { "status": "confirmed", "quote_id": "d4e5f6a7-8901-bcde-f234-567890abcdef", "execution_id": "exec-a1b2c3d4e5f6" } } ``` # Get position Source: https://docs.totalis.trade/api-reference/mm/get-position GET /v1/mm/positions/{id} Fetch one of your active market-maker positions by its RFQ id, with full parlay legs. Resolve one RFQ id to your position and its full parlay legs. Pass an `rfq_id` you already hold, for example from [Get vault](/api-reference/funds/get-vault). This is the by-id counterpart of [List positions](/api-reference/mm/list-positions). Scoped to you as the market maker. An id that belongs to another maker, does not exist, or has already settled returns `404`, so the endpoint never reveals another maker's book. **Base URL:** `https://api.totalis.trade` ## Authentication API key required. Pass your API key in the `X-API-Key` header. ``` X-API-Key: ``` ## Path parameters The RFQ (parlay ticket) id: the `rfq_id` from [List positions](/api-reference/mm/list-positions) or [Get vault](/api-reference/funds/get-vault). Must be a UUID. ## Response The position. Same shape as each entry in [List positions](/api-reference/mm/list-positions). On-chain position PDA. On-chain position id, hex encoded (32 lowercase chars, no `0x`). The early cashout join key: match it against the `position_id` on a broadcast [`cashout_request`](/api-reference/quote-service-mm/stream#cashout_request) to see whether you back that position. The RFQ (parlay ticket) id. Matches the `id` path parameter. The counterparty bettor's Privy DID (`did:privy:...`). Your market maker Privy DID (`did:privy:...`). The bettor's net stake in USDC (`bet_amount - taker_fee`). Your maximum risk (max loss) in USDC. Collateral this position added to your lock when it opened, in USDC: the rise in your book's worst-case loss, from 0 up to `mm_risk`. `null` before it lands. See [Collateral and the ILP](/guides/market-maker#collateral-and-the-ilp). Total pot paid to the winner if the parlay resolves (`user_stake + mm_risk`). `active`, or an in-flight release state listed on [List positions](/api-reference/mm/list-positions). ISO 8601 creation timestamp. The parlay legs, in leg order. Each leg has the fields documented in [List positions](/api-reference/mm/list-positions). ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 404 | `NOT_FOUND` | The id is malformed, or is not one of your active positions. | ```bash theme={null} curl "https://api.totalis.trade/v1/mm/positions/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40" \ -H "X-API-Key: $API_KEY" ``` ```python Python theme={null} import requests rfq_id = "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40" resp = requests.get( f"https://api.totalis.trade/v1/mm/positions/{rfq_id}", headers={"X-API-Key": API_KEY}, ) if resp.status_code == 404: print("Not one of your active positions") else: position = resp.json()["data"]["position"] for leg in position["legs"]: print(leg["market_title"], leg["side"]) ``` ```json 200 theme={null} { "data": { "position": { "position_pda": "7sKq9fGh2mNp4rTv6wXy8zAb1cDe3fGh5jKl7mNp9qRs", "position_id": "9f2c7a4e1b6d8305c4a9e7f21d8b306e", "rfq_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "user_id": "did:privy:cmabc123def456ghi789jkl", "market_maker_id": "did:privy:cmxyz987uvw654rst321opq", "user_stake": 24.75, "mm_risk": 74.25, "mm_collateral_locked": 74.25, "total_payout": 99, "status": "active", "created_at": "2026-06-01T18:45:45.000Z", "legs": [ { "market_title": "Lakers", "event_title": "NBA Finals Winner", "side": "yes", "outcome": "yes", "yes_sub_title": "Los Angeles Lakers", "no_sub_title": null, "selection_label": "Lakers", "bet_group": null, "venue": "kalshi", "event_ticker": "KXNBAFINALS-26", "market_ticker": "KXNBAFINALS-26-LAL", "exclusivity_group": "KXNBAFINALS-26", "exclusivity_group_kind": "partition", "venue_url": "https://kalshi.com/markets/kxnbafinals", "market_resolved": false, "market_result": null, "market_resolved_at": null } ] } } } ``` # List positions Source: https://docs.totalis.trade/api-reference/mm/list-positions GET /v1/mm/positions List your active market-maker positions, each with its full parlay legs. Offset paginated, with an optional exposure filter. List your active positions as the market maker, newest first, each with its full parlay legs. A position here is a trade where a user committed one of your quotes and you confirmed it. Use it to render your live book, reconcile exposure, or resolve an `rfq_id` you already hold (for example from [Get vault](/api-reference/funds/get-vault)) to its legs. The bettor-scoped RFQ reads are keyed to the user who placed the parlay and return `404` to you; this is the maker-side counterpart. Only active positions are returned, including in-flight release states (see `status`). Settled trades are not included. **Base URL:** `https://api.totalis.trade` ## Authentication API key required. Pass your API key in the `X-API-Key` header. ``` X-API-Key: ``` ## Query parameters Page size. Default 20, maximum 100. Number of positions to skip, for offset-based pagination. Default 0. Return only positions whose `mm_risk` falls in a size band: `lt_10`, `10_to_100`, `100_to_1000`, or `gt_1000`. Lower bounds are inclusive, upper bounds exclusive (`10_to_100` matches `10 <= mm_risk < 100`). Omit for no filter. An unrecognized value is ignored. ## Response Active positions, newest first. On-chain position PDA. On-chain position id, hex encoded (32 lowercase chars, no `0x`). The early cashout join key: match it against the `position_id` on a broadcast [`cashout_request`](/api-reference/quote-service-mm/stream#cashout_request) to see whether you back that position. The RFQ (parlay ticket) id. Pass it to [Get position](/api-reference/mm/get-position). Matches the `rfq_id` from [Get vault](/api-reference/funds/get-vault). The counterparty bettor's Privy DID (`did:privy:...`). Your market maker Privy DID (`did:privy:...`). The bettor's net stake in USDC (`bet_amount - taker_fee`). Your maximum risk (max loss) in USDC. Collateral this position added to your lock when it opened, in USDC: the rise in your book's worst-case loss, from 0 up to `mm_risk`. `null` before it lands. See [Collateral and the ILP](/guides/market-maker#collateral-and-the-ilp). Total pot paid to the winner if the parlay resolves (`user_stake + mm_risk`). `active` for a live position. `expired_pending_mm_release`, `reconciling_mm_release`, `bought_back_pending_db`, and `reconciling_bought_back_db` are in-flight release states; your collateral is still held in them. ISO 8601 creation timestamp. The parlay legs, in leg order. Per-market label (e.g. `Lakers`). May be null on legacy rows. Parent event label (e.g. `NBA Finals Winner`). May be null on legacy rows. The side taken on the leg: `yes` or `no`. The bettor's selected outcome for the leg. Yes-side subtitle snapshot (team/option label). May be null. No-side subtitle snapshot (team/option label). May be null. Canonical pick label snapshotted at bet time. May be null. Polymarket-only bucket/strike label (e.g. `"$100k-$110k"`). Null for Kalshi and single-market events. Source venue: `kalshi` or `polymarket`. Event-level ticker on the source venue. Market-level ticker on the source venue. Mutual-exclusivity key: legs sharing a value bet into a one-of-N outcome set. `null` when the leg is not in a group. This is a per-leg snapshot. To pre-check whether two markets can combine in a parlay, use the `exclusion_keys` arrays on [`GET /v1/markets/list`](/api-reference/markets/list-flat). How members of `exclusivity_group` relate: `binary` (strict two-outcome complements) or `partition` (one-of-N). `null` exactly when `exclusivity_group` is null. Canonical URL of the leg's market on its source venue. May be null. `true` once the venue has a recognized result for this leg's market. Normalized market result: `yes`, `no`, or `invalid`. `null` while unresolved. ISO 8601 time the venue resolved the market. `null` while unresolved. `true` when this page came back full, so more pages may exist. Offset pagination is approximate: the next page can still be empty, for example if positions settled between pages. Advance `offset` to fetch it. The effective page size applied. The offset applied. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | ```bash theme={null} curl "https://api.totalis.trade/v1/mm/positions?limit=20" \ -H "X-API-Key: $API_KEY" ``` ```python Python theme={null} import requests # Page through your live book and index positions by RFQ id. positions, offset = {}, 0 while True: resp = requests.get( "https://api.totalis.trade/v1/mm/positions", headers={"X-API-Key": API_KEY}, params={"limit": 100, "offset": offset}, ).json() for p in resp["data"]["positions"]: positions[p["rfq_id"]] = p if not resp["meta"]["has_more"]: break offset += 100 ``` ```json 200 theme={null} { "data": { "positions": [ { "position_pda": "7sKq9fGh2mNp4rTv6wXy8zAb1cDe3fGh5jKl7mNp9qRs", "position_id": "9f2c7a4e1b6d8305c4a9e7f21d8b306e", "rfq_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "user_id": "did:privy:cmabc123def456ghi789jkl", "market_maker_id": "did:privy:cmxyz987uvw654rst321opq", "user_stake": 24.75, "mm_risk": 74.25, "mm_collateral_locked": 74.25, "total_payout": 99, "status": "active", "created_at": "2026-06-01T18:45:45.000Z", "legs": [ { "market_title": "Lakers", "event_title": "NBA Finals Winner", "side": "yes", "outcome": "yes", "yes_sub_title": "Los Angeles Lakers", "no_sub_title": null, "selection_label": "Lakers", "bet_group": null, "venue": "kalshi", "event_ticker": "KXNBAFINALS-26", "market_ticker": "KXNBAFINALS-26-LAL", "exclusivity_group": "KXNBAFINALS-26", "exclusivity_group_kind": "partition", "venue_url": "https://kalshi.com/markets/kxnbafinals", "market_resolved": false, "market_result": null, "market_resolved_at": null } ] } ] }, "meta": { "has_more": false, "limit": 20, "offset": 0 } } ``` # List quotes Source: https://docs.totalis.trade/api-reference/mm/list-quotes GET /v1/mm/quotes List your quotes and their lifecycle status. Filter by status or RFQ, with cursor pagination. List your quotes that reached the committed trade lifecycle, newest first. A quote appears here when a user commits a quote you submitted: | Status | When | | - | - | | `accepted` | The user committed it. Awaiting your confirmation. | | `confirmed` | You [confirmed it](/api-reference/mm/confirm-quote). | | `expired` | The confirmation window lapsed. | Use it to reconcile. If your WebSocket connection drops and you miss a `quote:accepted` event, poll with `status=accepted` and confirm anything still open. A confirm on a quote whose window already lapsed returns `409`, so a late attempt is safe. **Base URL:** `https://api.totalis.trade` ## Authentication API key required. Pass your API key in the `X-API-Key` header. ``` X-API-Key: ``` ## Query parameters Return only quotes in this status. One of `accepted`, `confirmed`, `expired`, `pending`, `executed`, `rejected`, `withdrawn`. The current flow produces only `accepted`, `confirmed`, and `expired`. An unrecognized value is ignored and the response is unfiltered. Return only quotes on this RFQ (UUID). Page size. Default 50, maximum 100. Opaque pagination cursor. Pass `meta.cursor` from the previous page to fetch the next one. ## Response Quotes, newest first. Quote UUID. Matches `quote_id` on the `quote:accepted` and `mm_quote:accepted` WebSocket events. The RFQ this quote belongs to. Matches the `rfq_id` filter. Your market maker ID. The payout multiplier you quoted. Applies to the net stake (`user_cost`), not the gross bet amount; see [Fees](/guides/limits-and-fees#fees). Cost to the user in USDC: the net stake (`bet_amount` minus `taker_fee`). Total payout in USDC if the parlay wins. Your risk in USDC (`total_payout - user_cost`). Taker fee locked on the quote in USDC (`bet_amount - user_cost`). `0` only if the computed fee was zero when the quote was created. ISO 8601 timestamp when the quote's pricing validity ended. Lifecycle status: `accepted` (committed by the user, awaiting your confirmation), `confirmed` (locked in), or `expired` (confirmation window lapsed and the trade unwound). ISO 8601 creation timestamp. ISO 8601 timestamp of the last status change. Your wallet address backing the quote's collateral. May be omitted. Cursor for the next page. `null` on the last page. `true` if more pages exist. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | `rfq_id` is not a valid UUID. | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | ```bash theme={null} curl "https://api.totalis.trade/v1/mm/quotes?status=accepted&limit=50" \ -H "X-API-Key: $API_KEY" ``` ```python Python theme={null} import requests # Reconcile on startup or after a WebSocket drop: confirm any # committed quote you haven't confirmed yet. resp = requests.get( "https://api.totalis.trade/v1/mm/quotes", headers={"X-API-Key": API_KEY}, params={"status": "accepted"}, ) for quote in resp.json()["data"]["quotes"]: requests.post( f"https://api.totalis.trade/v1/mm/quotes/{quote['id']}/confirm", headers={"X-API-Key": API_KEY}, ) ``` ```json 200 theme={null} { "data": { "quotes": [ { "id": "d4e5f6a7-8901-bcde-f234-567890abcdef", "parlay_ticket_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "market_maker_id": "did:privy:cm3mm...", "payout_odds": 4.0, "user_cost": 24.75, "total_payout": 99, "mm_cost": 74.25, "taker_fee": 0.25, "valid_until": "2026-06-01T18:45:45.000Z", "status": "accepted", "created_at": "2026-06-01T18:45:30.000Z", "updated_at": "2026-06-01T18:45:30.000Z", "mm_wallet_address": "7sKq...9fGh" } ] }, "meta": { "cursor": null, "has_more": false } } ``` # Get parlay Source: https://docs.totalis.trade/api-reference/parlays/get GET /v1/rfqs/{id} Fetch one parlay (RFQ) by id, with quotes and, once terminal, its settlement detail (per-leg outcomes, payout, and settle/buyback transaction). # List parlays Source: https://docs.totalis.trade/api-reference/parlays/list GET /v1/rfqs List your parlays (RFQs) with cursor pagination and optional status filter. # Bid on a cashout auction Source: https://docs.totalis.trade/api-reference/quote-service-mm/cashout-quote PUT /v1/mm/cashout-requests/{id}/quote Bid one all-in price on a user's early cashout auction. Submit or replace your bid on an early cashout auction. Auctions arrive as a [`cashout_request`](/api-reference/quote-service-mm/stream#cashout_request) event on the [MM stream](/api-reference/quote-service-mm/stream) you already subscribe to. They are broadcast to every market maker, including on positions you do not back. An auction lives about 10 seconds. Bid one all-in `price_micro`: the microUSDC the user receives. There is no direction field and no fee term. The highest live bid wins, and ties go to the earliest bid. To pass, submit nothing; there is no decline endpoint. Re-submitting replaces your bid atomically and also re-times it. Re-sending the same price puts you behind any bid that has since matched it. Do not re-`PUT` an unchanged price just to refresh the TTL: you would hand away a tie you were winning. | If you | A win | | - | - | | Back the position | Closes it. A closing exit carries the profit fee on your side. | | Do not back it | Acquires it. No fee applies. | For the payload and auction mechanics, see [Cashout auctions](/guides/cashout-auctions). ## Authentication API key required, with the `mm:quote` scope. Pass it in the `X-API-Key` header. ``` X-API-Key: ``` ## Path parameters The cashout request ID (UUID). Called `cashout_request_id` on the push. ## Request body The all-in amount the user receives, in microUSDC (`1_000_000` = \$1.00). Integer, minimum `1`. May be below the user's stake: a losing parlay exits for less than it cost. Two ceilings apply, both in microUSDC. `user_stake` and `total_payout` arrive as decimal USDC on the push, so scale them by `1e6` first. Every bid must satisfy `price_micro <= round(total_payout * 1e6)` (`price_above_max_payout`). If you are the position's current counterparty, a tighter fee-aware ceiling binds (`price_above_buyback_bound`). It is derived from `total_payout - user_stake`, not from the `mm_risk` field, because the two drift by up to `0.0001` USDC: ``` S = round(user_stake * 1e6) M = round((total_payout - user_stake) * 1e6) price_micro <= S + M - floor(M * fee_bps / 10_000) ``` How long your bid stays live, in milliseconds. Range 5000-60000. A bid may outlive the auction. One that lapses mid-auction stops being the best bid, and nothing tells you when, so age bids out locally. The body is strict: an unknown field is a `400`. ## Withdrawing a bid `DELETE /v1/mm/cashout-requests/{id}/quote` pulls your live bid. It is idempotent. | Response | Meaning | | - | - | | `204 No Content` | Your bid was removed. | | `200 OK` with `{"status": "already_withdrawn"}` | Your bid had already lapsed. Not an error. | | `409` | The auction is no longer active. | | `404` | The auction is unknown. | There is no partial withdraw. Re-`PUT` to change a price. ## Response Returns `201 Created` for your first bid on an auction, or `200 OK` when replacing your own. The submitted bid. Bid UUID. The auction this bid is for. Your market maker ID. The all-in microUSDC you bid. ISO 8601 timestamp when this bid expires. `"active"` The auction's new book sequence number. `true` if this replaced your previous bid. ## Errors | Status | `details.reason` | Description | | - | - | - | | 400 | validation | Non-integer or non-positive `price_micro`, `expires_in_ms` out of range, unknown field, or malformed JSON. | | 400 | `price_above_max_payout` | Bid exceeds the position's `total_payout`. Response carries `max_price_micro`. | | 400 | `price_above_buyback_bound` | You back this position and the bid exceeds what your locked collateral covers after the profit fee. Skipped when the fee rate is briefly unavailable; the commit then re-checks it and rejects with `amount_over_bound`. | | 401 | none | Missing or invalid API key (`error.code: UNAUTHORIZED`). | | 403 | `insufficient_scope` | Key lacks `mm:quote`. Response carries `required_scope`. | | 404 | none | Auction unknown, or already evicted at its TTL. The ordinary "too slow" outcome, not a fault. | | 409 | `self_quote` | You opened this auction (you are the seller). Bidding on a position you back is allowed; that is the closing case. | | 409 | `not_active` | The auction was committed or cancelled before your bid landed. | | 409 | `expired` | The auction expired as your bid landed. | | 409 | `MARKET_NEAR_CLOSE` | The underlying market is too close to close to exit. | | 409 | `buyback_wallet_missing` | You back this position but no wallet is on record, so a closing exit cannot be built. Only your bids fail; other market makers can still win the same auction by acquiring the position. | | 429 | none | Too many requests (`error.code: RATE_LIMITED`). Honor `Retry-After`. | Winning is not the same as being paid. The user is only filled at a price they explicitly accepted, so a fill can fail if the book moves down between their tap and their commit. If you win by acquiring a position, your funding and eligibility are checked at that commit, not when you bid. An underfunded or ineligible winning bid fails there with `insufficient_buyer_balance`, `buyer_no_vault`, or `buyer_is_counterparty`. ```bash curl theme={null} curl -X PUT https://api.totalis.trade/v1/mm/cashout-requests/$CASHOUT_REQUEST_ID/quote \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"price_micro": 1990000, "expires_in_ms": 15000}' ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.totalis.trade/v1/mm/cashout-requests/${cashoutRequestId}/quote`, { method: 'PUT', headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }, // price_micro is what the USER receives, all-in, in micro-USDC body: JSON.stringify({ price_micro: 1_990_000, expires_in_ms: 15_000 }), }, ); ``` ```python Python theme={null} import requests requests.put( f'https://api.totalis.trade/v1/mm/cashout-requests/{cashout_request_id}/quote', headers={'X-API-Key': API_KEY}, # price_micro is what the USER receives, all-in, in micro-USDC json={'price_micro': 1_990_000, 'expires_in_ms': 15_000}, ) ``` # Stream quote requests Source: https://docs.totalis.trade/api-reference/quote-service-mm/stream GET /v1/mm/quote-requests/stream Subscribe to all active quote requests in real time via Server-Sent Events. Open a Server-Sent Events (SSE) stream of all active quote requests. This is the primary integration path for market makers; use it instead of polling. You receive new requests, version updates, closes, and early cashout auctions. ## Authentication API key required. Pass your API key in the `X-API-Key` header. ``` X-API-Key: ``` ## SSE event types The stream sends seven event names. Match them exactly: `quote_request` and `quote_request_expired` use underscores, but `quote_request:updated` uses a colon. | Event | Fires when | | - | - | | `connected` | Once, immediately after connecting. | | `snapshot_begin` | Start of the initial active request snapshot (on connect or reconnect). | | `quote_request` | A new quote request is created, or an active one is replayed during the snapshot. | | `quote_request:updated` | A request you've already seen bumps to a new `version` (legs or bet amount edited). | | `snapshot_complete` | The initial snapshot finishes; the stream switches to incremental updates only. | | `quote_request_expired` | A previously seen request is no longer active (committed, cancelled, or expired). | | `cashout_request` | An early cashout auction opens on a position. Broadcast to every market maker. | ### `connected` Sent once on connection. Confirms your identity. `mm_id` is your Privy DID. It is the same value as the `user_id` returned by `auth:success` on the [WebSocket](/guides/websocket#authentication), and the key of your `mm:quotes:{mm_id}` channel there. ```json theme={null} { "mm_id": "did:privy:cm3mm..." } ``` ### `snapshot_begin` Sent at the start of the initial snapshot, on a new connection or a reconnect. Every active quote request follows before `snapshot_complete`. ```json theme={null} { "page_size": 500 } ``` ### `quote_request` Sent when a new quote request is created, and once for each active request replayed during the snapshot. This is the event you price. ```json theme={null} { "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "user_id": "did:privy:cm3abc...", "input_legs": [ {"market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi"}, {"market_ticker": "KXBTC-26JUN01-T73000", "side": "no", "venue": "kalshi"} ], "legs": [ { "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6", "event_ticker": "KXBTC-26JUN01", "market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi" }, { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "event_ticker": "KXBTC-26JUN01", "market_ticker": "KXBTC-26JUN01-T73000", "side": "no", "venue": "kalshi" } ], "bet_amount": 25, "user_stake": 24.75, "version": 1, "request_hash": "sha256:9f86d08...", "expires_at": "2026-06-01T18:45:30.000Z" } ``` ### `quote_request:updated` Sent when a request you already saw moves to a new `version` because the user edited the legs or the bet amount. The payload matches `quote_request`, with the incremented `version` and a new `request_hash`. Your quote on the prior version is invalidated automatically. It leaves the book immediately and never blocks a new submission, so you do not need to withdraw it. Re-price and [submit](/api-reference/quote-service-mm/submit-quote) a new quote with the new `version` and `request_hash`. A listener registered only for `quote_request` misses every version bump. Register one for `quote_request:updated` too, or branch on the `event:` field in a raw SSE parser. ```json theme={null} { "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "user_id": "did:privy:cm3abc...", "input_legs": [ "..." ], "legs": [ "..." ], "bet_amount": 50, "user_stake": 49.50, "version": 2, "request_hash": "sha256:3b1f9c2...", "expires_at": "2026-06-01T18:46:10.000Z" } ``` ### `snapshot_complete` Sent when the initial snapshot finishes. The stream then sends incremental updates only. ```json theme={null} { "pages": 1, "total": 87 } ``` ### `quote_request_expired` Sent when a quote request you saw is no longer active. Remove it from your local state. `reason` says why it closed: | `reason` | Meaning | | - | - | | `committed` | The user committed to a quote. Check `won`. Can arrive well before `expires_at`; there is no minimum auction duration. | | `cancelled` | The user abandoned the request before committing to any quote. | | `expired` | The request reached `expires_at` with no commit. | `won` separates a lost auction from an abandoned or timed-out one: | `won` | Meaning | | - | - | | `true` | `reason` is `committed` and your quote was the one selected. | | `false` | Every other close. On a `committed` close, you were out-bid: the user took another market maker's quote. | | absent | A rare `committed` close with an indeterminate outcome, for example an event produced by a previous server instance during a deploy. Treat it as unknown, not as a loss. | The winning maker's identity and odds are never disclosed. ```json theme={null} { "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "reason": "committed", "won": false } ``` ### `cashout_request` Sent when a user opens an [early cashout auction](/guides/cashout-auctions) on a position, and once for each active auction replayed during the snapshot. It is broadcast to every market maker, including on positions you do not back. The auction lives about 10 seconds. Bid with [`PUT /v1/mm/cashout-requests/{id}/quote`](/api-reference/quote-service-mm/cashout-quote). To pass, submit nothing; there is no decline endpoint. This event differs from `quote_request` in name and payload. It carries `cashout_request_id` (not `id`) and the on-chain `position_id`. Branch on the SSE `event:` field, or register a dedicated `cashout_request` listener. ```json theme={null} { "cashout_request_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", "position_id": "9f86d08818844f86d08818844f86d088", "legs": [ { "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6", "event_ticker": "KXBTC-26JUN01", "market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi", "resolution": "unresolved" } ], "user_stake": 24.75, "mm_risk": 74.25, "total_payout": 99, "payout_odds": 4.0, "version": 1, "expires_at": "2026-06-01T18:45:40.000Z" } ``` Every field below is always present. The auction id. Pass it as `{id}` when you [bid](/api-reference/quote-service-mm/cashout-quote). This payload has no `id` field, unlike `quote_request`. The on-chain position id (32 hex chars, no `0x`). Look it up in your own book to see whether you are its counterparty, which determines your economics. The position's legs. Each carries `id`, `event_ticker`, `market_ticker`, `side` (`yes` / `no`), `venue`, and `resolution`. `resolution` is one of `unresolved`, `won`, `lost`, `void`. It is your only protection against pricing an already-decided parlay. `void` overrides the others: any void leg cancels the whole position at settlement and refunds the stake, so the position is worth roughly `user_stake` even when a sibling leg reads `lost`. With no void leg, one `lost` leg makes the parlay worthless. Decimal USDC. The net stake the counterparty underwrites, the same quantity as on `quote_request`. It is the reference point for the exit price: a bid above it means the user leaves in profit, below it at a loss. Decimal USDC. The counterparty's locked risk on the position, for pricing context only. Do not compute the counterparty bid ceiling from it. The server derives that ceiling from `total_payout - user_stake`; the two are stored separately and can disagree in the last decimal. Decimal USDC. What the position pays if every leg wins, and the hard ceiling on any bid: `price_micro <= round(total_payout * 1e6)`, or the bid is rejected `price_above_max_payout`. The accepted quote's payout multiplier. Pricing context, not an amount, and never a bid term. On positions created before this field was carried, it is reconstructed from `total_payout / user_stake` and can be off in the 5th or 6th decimal. Do not reconcile against it exactly. The auction's version, `1` today. A cashout auction is never re-versioned mid-flight the way a forward quote request can be, so there is no `cashout_request:updated` event to handle. ISO 8601 close time of the auction, about 10 s out. Bid before it. It is not the position's market-end horizon, which this payload omits, so you cannot pre-filter for the near-close cutoff. A bid on a position close to its market end is rejected `MARKET_NEAR_CLOSE`. Treat that as an ordinary lost auction. ## Leg fields Legs use the same field names on every event and both venues. The values are venue specific. The examples above are Kalshi. | Field | Kalshi | Polymarket | | - | - | - | | `id` | The Totalis leg id, a UUID. Stable for the life of the leg. Not a venue identifier. | Same. | | `venue` | `kalshi` | `polymarket` | | `market_ticker` | The Kalshi market ticker, for example `KXBTC-26JUN01-T72500`. | The `condition_id`, the `0x` prefixed hex string. Resolve a Polymarket leg by this field. | | `event_ticker` | The Kalshi event ticker, for example `KXBTC-26JUN01`. | The Gamma event slug, for example `will-bitcoin-hit-100k-in-june`. A slug, not a ticker. | | `side` | `yes` or `no` | Same. | | `current_yes_price`, `current_no_price` | On `quote_request` legs: the venue's yes and no prices when the request was created, as probabilities from 0 to 1. A starting point for pricing, not a live feed. | Same. | | `resolution` | On `cashout_request` legs only. See above. | Same. | Legs can carry extra display fields beyond these. The set is additive and differs by event, so parse leniently and ignore keys you do not use. ## Reconnection Every SSE event includes an `id:` field. Persist it. On reconnect, pass it as the `last_event_id` query parameter to ask for the events you missed instead of a fresh snapshot. You get a replay only while that id is still retained on the server; otherwise the full snapshot is sent again, starting with `snapshot_begin`. On production in September 2026, reconnects received the full snapshot even with an id seconds old, so treat every snapshot as authoritative and do not depend on replay. The stream also sends a `: keepalive` comment about every 3 seconds. If nothing arrives for much longer, reconnect. ``` GET /v1/mm/quote-requests/stream?last_event_id=1789161257595-0 ``` The taker-fee rate is read once per connection. On a long-lived stream, `user_stake` reflects the rate in effect when you connected. Reconnect after a fee rate change to get `user_stake` at the new rate. ## Notes * Price `payout_odds` and size collateral against `user_stake`, the stake net of the [taker fee](/guides/limits-and-fees#fees), never the gross `bet_amount`: `user_stake = bet_amount - taker_fee`, where `taker_fee = floor(bet_amount_micro * taker_fee_bps / 10000)` in microUSDC. When the fee is off (`taker_fee_bps` is 0), `user_stake == bet_amount`. * The stream excludes quote requests created by your own user account. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | The credential does not start with `api_` (`details.reason: "wrong_api_key_type"`), or the key lacks `mm:quote`. | ```bash curl theme={null} curl -N https://api.totalis.trade/v1/mm/quote-requests/stream \ -H "X-API-Key: $API_KEY" \ -H "Accept: text/event-stream" ``` ```javascript JavaScript theme={null} // Native EventSource cannot send an X-API-Key header, so read the SSE stream // with fetch instead (works in browsers and Node 18+). const res = await fetch('https://api.totalis.trade/v1/mm/quote-requests/stream', { headers: { 'X-API-Key': API_KEY, Accept: 'text/event-stream' }, }); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let sep; while ((sep = buffer.indexOf('\n\n')) !== -1) { const frame = buffer.slice(0, sep); buffer = buffer.slice(sep + 2); const event = frame.match(/^event: (.+)$/m)?.[1]; const data = frame.match(/^data: (.+)$/m)?.[1]; if (!event || !data) continue; const payload = JSON.parse(data); switch (event) { case 'connected': console.log('Connected as', payload.mm_id); break; case 'quote_request': // payload.bet_amount is the gross wager; price + size against net user_stake console.log('New request:', payload.id, payload.user_stake, 'USDC'); break; case 'quote_request:updated': // Your prior-version quote is invalidated; submit a new one console.log('Request updated to version', payload.version, 're-price:', payload.id); break; case 'quote_request_expired': { // reason: 'committed' | 'cancelled' | 'expired'; won: true only if you were selected. // won may be absent on a committed close (indeterminate). Treat as unknown, not a loss. const { id, reason, won } = payload; const outcome = reason === 'committed' ? (won === undefined ? 'unknown' : won ? 'won' : 'lost (out-bid)') : reason; console.log('Request closed:', id, outcome); break; } case 'cashout_request': // Distinct from quote_request: the key is payload.cashout_request_id (NOT payload.id). // Broadcast to every MM, ~10s to bid. Re-value the position and bid ONE all-in // price_micro (what the user receives) on PUT /v1/mm/cashout-requests/{id}/quote. console.log('Cashout auction:', payload.cashout_request_id, 'position', payload.position_id); break; } } } ``` ```python Python theme={null} import requests import json url = 'https://api.totalis.trade/v1/mm/quote-requests/stream' headers = { 'X-API-Key': API_KEY, 'Accept': 'text/event-stream', } with requests.get(url, headers=headers, stream=True) as resp: event_type = None for line in resp.iter_lines(decode_unicode=True): if line.startswith('event: '): event_type = line[7:] elif line.startswith('data: '): payload = json.loads(line[6:]) if event_type == 'quote_request': print(f"New request: {payload['id']}") # Price the legs and submit a quote elif event_type == 'quote_request:updated': print(f"Request {payload['id']} updated to v{payload['version']}, re-price") # Prior-version quote invalidated; submit a new one elif event_type == 'quote_request_expired': # reason: 'committed' | 'cancelled' | 'expired'; won True only if you were selected. # won may be absent on a committed close (indeterminate). Treat as unknown, not a loss. reason = payload.get('reason', 'expired') won = payload.get('won') if reason == 'committed': outcome = 'unknown' if won is None else ('won' if won else 'lost (out-bid)') else: outcome = reason print(f"Request closed: {payload['id']}: {outcome}") elif event_type == 'cashout_request': # Distinct from quote_request: the key is cashout_request_id (NOT id). # Broadcast to every MM, ~10s to bid. Bid ONE all-in price_micro (what the # user receives) on PUT /v1/mm/cashout-requests/{id}/quote. print(f"Cashout auction: {payload['cashout_request_id']} position {payload['position_id']}") event_type = None ``` ```text SSE stream theme={null} event: connected data: {"mm_id":"did:privy:cm3mm..."} event: snapshot_begin data: {"page_size":500} event: quote_request data: {"id":"1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40","user_id":"did:privy:cm3abc...","input_legs":[{"market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"legs":[{"id":"8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6","event_ticker":"KXBTC-26JUN01","market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"bet_amount":25,"user_stake":24.75,"version":1,"request_hash":"sha256:9f86d08...","expires_at":"2026-06-01T18:45:30.000Z"} event: snapshot_complete data: {"pages":1,"total":1} event: quote_request data: {"id":"f7e8d9c0-1234-5678-abcd-ef0123456789","user_id":"did:privy:cm9xyz...","input_legs":[{"market_ticker":"KXETH-26JUN01-T3000","side":"no","venue":"kalshi"}],"legs":[{"id":"d4e5f6a7-8901-23bc-def4-567890abcdef","event_ticker":"KXETH-26JUN01","market_ticker":"KXETH-26JUN01-T3000","side":"no","venue":"kalshi"}],"bet_amount":10,"user_stake":9.90,"version":1,"request_hash":"sha256:b5c6d7e...","expires_at":"2026-06-01T18:50:00.000Z"} event: quote_request:updated data: {"id":"1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40","user_id":"did:privy:cm3abc...","input_legs":[{"market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"legs":[{"id":"8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6","event_ticker":"KXBTC-26JUN01","market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"bet_amount":50,"user_stake":49.50,"version":2,"request_hash":"sha256:3b1f9c2...","expires_at":"2026-06-01T18:46:10.000Z"} event: quote_request_expired data: {"id":"1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40","reason":"committed","won":false} ``` # Submit or replace quote Source: https://docs.totalis.trade/api-reference/quote-service-mm/submit-quote PUT /v1/mm/quote-requests/{id}/quote Submit a price quote for an active quote request, or replace your existing quote. Submit a quote on an active quote request. You hold at most one active quote per request version. Submitting again for the same version replaces it atomically. When the user edits the legs or bet amount, the request's `version` and `request_hash` change. Listen for `quote_request:updated` on the [SSE stream](/api-reference/quote-service-mm/stream) to detect the change and re-price. A new version needs no withdraw. A quote is scoped to one `(request, version)` pair. When the request advances, your prior-version quote is invalidated automatically: it leaves the book at once, can no longer be accepted, and never blocks a new submission. `PUT` again with the new `request_version` and `request_hash` from the latest `quote_request:updated` event. You don't need to [withdraw](/api-reference/quote-service-mm/withdraw-quote) the old quote first. ## Authentication API key required. Pass your API key in the `X-API-Key` header. ``` X-API-Key: ``` ## Path parameters The quote request ID (UUID). ## Request body The quote request version you priced. Must match the current version from the SSE stream. The `request_hash` from the quote request event. Prevents pricing against stale data. Payout multiplier on the net stake (`user_stake`), never the gross `bet_amount`; see [Fees](/guides/limits-and-fees#fees). At least 1.0001. The server derives `user_cost`, `total_payout`, and `mm_cost` from it. How long this quote is valid, in milliseconds. Range: 5000-60000. Default: 15000. ## Response Returns `201 Created` for a new quote or `200 OK` when replacing an existing quote. The submitted quote. Quote UUID. Your market maker ID. The request version this quote is for. The request hash this quote is for. The payout multiplier. The net stake in USDC (`bet_amount - taker_fee`). Equals the request's `user_stake`. Total payout in USDC (`user_cost * payout_odds`). Your risk in USDC (`total_payout - user_cost`). ISO 8601 timestamp when this quote expires. The new quote book sequence number. `true` if this replaced a previous quote. The taker fee rate this quote was priced at, in basis points (`100` is 1%). Use it to reconcile `user_cost` against the base you priced, and to tell a fee-on quote from a fee-off one. ## Conflict reasons A `409 Conflict` carries the cause in `error.details.reason`: | Reason | Description | | - | - | | `not_found` | Quote request ID does not exist. | | `expired` | Quote request has expired. | | `not_active` | Quote request is committed, cancelled, or otherwise inactive. | | `version_mismatch` | Your `request_version` or `request_hash` (or both) does not match the live request. You priced a stale version, or it changed while you were pricing. Submit both current values from the latest `quote_request` or `quote_request:updated` event. A still-open prior quote never causes this, so there is nothing to withdraw. | | `self_quote` | You cannot quote your own requests. | ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Invalid body. See `details.issues`. | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | The credential does not start with `api_` (`details.reason: "wrong_api_key_type"`), or the key lacks `mm:quote`. | | 409 | `CONFLICT` | Quote rejected. See `details.reason` above. | | 503 | `SERVICE_UNAVAILABLE` | The taker fee rate could not be read (`details.reason: "fee_unavailable"`). The quote was not priced. Retry. | ```bash theme={null} curl -X PUT https://api.totalis.trade/v1/mm/quote-requests/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40/quote \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "request_version": 1, "request_hash": "sha256:9f86d08...", "payout_odds": 4.0, "expires_in_ms": 15000 }' ``` ```json 201 theme={null} { "data": { "quote": { "id": "b2c3d4e5-6789-0abc-def1-234567890abc", "market_maker_id": "did:privy:cm3mm...", "request_version": 1, "request_hash": "sha256:9f86d08...", "payout_odds": 4.0, "user_cost": 24.75, "total_payout": 99, "mm_cost": 74.25, "valid_until": "2026-06-01T18:45:45.000Z" }, "book_seq": 5, "replaced": false, "taker_fee_bps": 100 } } ``` # Withdraw quote Source: https://docs.totalis.trade/api-reference/quote-service-mm/withdraw-quote DELETE /v1/mm/quote-requests/{id}/quote Withdraw your active quote from a quote request before the user commits it. Withdraw your active quote from a quote request. If the quote already expired naturally, you get `already_expired` instead of an error. Withdraw only to retract a current-version quote you no longer want to honor, for example when your price moved before the user committed. A `version` change needs no withdraw: your prior-version quote is invalidated automatically. To quote the new version, [submit](/api-reference/quote-service-mm/submit-quote) again with the new `request_version` and `request_hash`. ## Authentication API key required. Pass your API key in the `X-API-Key` header. ``` X-API-Key: ``` ## Path parameters The quote request ID (UUID). ## Request body (optional) The request version your quote was submitted for. Defaults to the current version. The request hash your quote was submitted for. Defaults to the current hash. ## Response Returns `204 No Content` when the quote is withdrawn, or `200 OK` if it had already expired naturally: ```json theme={null} { "data": { "status": "already_expired" } } ``` ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 404 | `NOT_FOUND` | Quote request not found. | | 409 | `CONFLICT` | No active quote to withdraw, request expired, no longer active, or version mismatch. See `details.reason`. | ```bash theme={null} curl -X DELETE https://api.totalis.trade/v1/mm/quote-requests/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40/quote \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "request_version": 1, "request_hash": "sha256:9f86d08..." }' ``` ```bash 204 theme={null} # No response body ``` # Cancel quote request Source: https://docs.totalis.trade/api-reference/quote-service/cancel POST /v1/quote-requests/{id}/cancel Cancel an active quote request. Cancel a quote request while its status is `active`. Every pending quote is invalidated, and the request is removed from the live stream. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `trading:write` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Path parameters The quote request ID (UUID). ## Response Returns `204 No Content` with no response body. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this quote request. | | 404 | `NOT_FOUND` | Quote request not found. | | 409 | `CONFLICT` | Quote request is already committed, cancelled, or expired. | ```bash theme={null} curl -X POST https://api.totalis.trade/v1/quote-requests/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40/cancel \ -H "X-API-Key: $TOTALIS_API_KEY" ``` ```bash 204 theme={null} # No response body ``` # Commit cashout Source: https://docs.totalis.trade/api-reference/quote-service/cashout-commit POST /v1/cashout-requests/{id}/commit Accept the winning bid on an early cashout auction and drive it on-chain. Accept the winning bid on your early cashout auction and exit on-chain. The bid is the price, and the market maker does not confirm it. You echo back the price you accepted, and the exit fills only at or above it. Totalis obtains signatures server-side via Privy at execution. You receive `min_price_micro` or better either way. Who won decides what happens to the position: | Winning bid from | Position | RFQ status | | - | - | - | | The position's own counterparty | **Closes.** Your stake is released. | Becomes `bought_back` | | Any other market maker | **Transfers** to the winner and stays open under new ownership. You are out of it. | Does not become `bought_back` | The commit is idempotent per (request, user). A committed result is cached for 5 minutes and replayed verbatim with `200 OK`. A retry with a *different* `min_price_micro` still gets the first result, and the new floor is not re-evaluated. Retry with the value you first sent. **Indeterminate commits never double-spend.** If the on-chain outcome is unknown (timeout or transient RPC failure), the request stays `committing` and returns `503` with `details.indeterminate: true`. Totalis resolves it against on-chain state, so do not treat that `503` as a failure. Reopen the [stream](/api-reference/quote-service/cashout-stream) and wait for the terminal `status`. See the [`503` cases](#errors) below. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `trading:write` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Path parameters The cashout request ID (UUID). ## Request body The price you accepted on screen, in [microUSDC](/guides/glossary#units). Echo back the `price_micro` from the `best_quote` frame you were shown. Without it the commit is a `400`, because nothing ever fills at a price the user did not see. The fill uses the best live bid at commit time, floored at this value. A better bid fills at the better price. If the book has moved below it, the commit fails with `409 price_below_accepted` (carrying `best_price_micro`) and the auction stays open to re-tap. The body is strict. An unknown field is a `400`. ## Response Returns `201 Created` on a fresh commit, or `200 OK` when replaying a cached idempotent result. The cashout request UUID. `"committed"` The on-chain transaction signature: `buy_back_position` when the position closed, `transfer_position` when it transferred. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Missing or invalid `min_price_micro` (must be an integer >= 1), or an unknown field. | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this cashout request (`details.reason: not_owner`). | | 404 | `NOT_FOUND` | Cashout request not found. | | 409 | `CONFLICT` | Determinate failure: nothing landed. See `details.reason` below. | | 410 | `GONE` | The auction expired before commit. Open a fresh one. | | 429 | `RATE_LIMITED` | Too many requests. Honor `Retry-After`. | | 503 | `SERVICE_UNAVAILABLE` | Branch on `details.indeterminate`, never on the message. See below. | A `409` means nothing landed. Most reasons are retryable against a live book. `already_transferred` is terminal: stop, and re-read the position instead of reopening an auction. | `409` reason | Meaning | | - | - | | `price_below_accepted` | The best live bid is below `min_price_micro`. Carries `best_price_micro`. | | `quote_expired` | No live bid left. | | `not_active` | The cashout request is not `active`. | | `locked` | A commit for this auction is already in flight. | | `missing_wallet` | The position has no seller wallet on record, so the exit cannot be built. | | `amount_over_bound` | The winning counterparty bid failed the collateral re-check at commit. | | `already_transferred` | **Terminal.** The position has already moved. | | `insufficient_buyer_balance`, `buyer_no_vault`, `buyer_is_seller`, `buyer_is_counterparty` | Buyer-side rejections when the winner is acquiring the position. | | `details.indeterminate` | Meaning | Your move | | - | - | - | | `true` | The exit may have landed. `details.reason` is the execution-layer code, typically `COMMIT_UNREACHABLE` (the lane was not reached), `BAD_RESPONSE` (it answered unusably), or the lane's own error code. It is not a stable taxonomy, so treat every value the same. | Do not retry. Reopen the [stream](/api-reference/quote-service/cashout-stream) and wait for the terminal `status`. | | `false` | Only ever `fee_unavailable`: the profit-fee rate could not be read. Nothing landed, and the auction is still open. | Retry immediately. It is safe. | ```bash theme={null} curl -X POST https://api.totalis.trade/v1/cashout-requests/c3d4e5f6-7890-abcd-ef12-34567890abcd/commit \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"min_price_micro": 66250000}' ``` ```json 201 theme={null} { "data": { "cashout_request_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", "status": "committed", "chain_sig": "5Nx7...buybackSig" } } ``` ```json 503 theme={null} { "error": { "code": "SERVICE_UNAVAILABLE", "message": "Cashout is being processed; please check back", "details": { "reason": "COMMIT_UNREACHABLE", "indeterminate": true } } } ``` # Create cashout request Source: https://docs.totalis.trade/api-reference/quote-service/cashout-create POST /v1/cashout-requests Open an early cashout auction for an on-chain position. Open an **early cashout auction** to exit an active on-chain position before settlement. Every market maker may bid one all-in price. The [stream](/api-reference/quote-service/cashout-stream) shows the highest live bid, and ties go to the earliest. Accept it with [Commit cashout](/api-reference/quote-service/cashout-commit) to exit on-chain at that price or better. You identify the position by its on-chain [`position_id`](/guides/glossary#identifiers). The position must: * be active (`executed`) * be owned by you * have a counterparty market maker assigned * not be within the near-expiry buffer of its market end (the cashout cutoff on [Limits and fees](/guides/limits-and-fees#timing)) ## Authentication Send your API key in the `X-API-Key` header. The key needs the `trading:write` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Request body The position's on-chain id. A 16-byte hex string (32 hex chars), with or without a `0x` prefix. ## Response The opened cashout request. UUID of the cashout request. Use it on the stream, commit, and MM endpoints. Your user ID. The position's original counterparty market maker. Informational only: every market maker can bid, and a win by this one closes the position instead of transferring it. The on-chain position id (hex), normalized without the `0x` prefix. On-chain position address ([PDA](/guides/glossary#custody-terms)), or `null`. Internal UUID of the position record. Distinct from the on-chain `position_id` hex. Your wallet address on the position. The counterparty market maker's wallet address. Your net stake on the position (USDC). Always refunded on a buyback. The counterparty's risk on the position (USDC): `total_payout - user_stake`. Collateral the backing maker locked for this position when it opened (USDC). Makers are margined across their whole book, so it can be less than `mm_risk`. The position's total payout (USDC). The taker fee locked when the position was created (USDC). Request version, starts at `1`. Quote book sequence number, starts at `0`. `"active"` ISO 8601 time the cashout request was opened. ISO 8601 time of the last update to the request. ISO 8601 close time of the auction (\~10 s), distinct from the position expiry. If it lapses before you commit, open a fresh auction. The underlying position's market-end horizon, or `null`. The position's legs (ticker, side, venue, and resolution where known). ## Response codes | Status | Meaning | | - | - | | `201 Created` | A new auction opened. | | `200 OK` | An auction was already live for this position, and you get that one back. | Opening is idempotent per (position, user), so a double-tap resumes the live auction instead of splitting bids across two. Check the status code or `expires_at` before you assume a fresh window. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Body is not valid JSON, or `position_id` is not a 16-byte hex string. See `details.issues`. | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 404 | `NOT_FOUND` | Position not found, or not owned by you. | | 409 | `CONFLICT` | Position is not active, too close to expiry, or has no counterparty MM wallet assigned yet. | | 503 | `SERVICE_UNAVAILABLE` | Eligibility or economics read failed, or no counterparty MM is resolvable yet. Retry. | ```bash theme={null} curl -X POST https://api.totalis.trade/v1/cashout-requests \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "position_id": "9f86d08818844f86d08818844f86d088" }' ``` ```json 201 theme={null} { "data": { "cashout_request": { "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", "user_id": "did:privy:cm3abc...", "counterparty_mm_id": "did:privy:cm3mm...", "position_id": "9f86d08818844f86d08818844f86d088", "position_pda": "7Xc3...PDA", "db_position_id": "3f1c9e88-1a2b-4c3d-9e8f-7a6b5c4d3e2f", "user_wallet": "5Hs9...UserWallet", "mm_wallet": "47NS...MmWallet", "user_stake": 24.75, "mm_risk": 74.25, "mm_collateral_locked": 74.25, "total_payout": 99, "taker_fee_locked": 0.25, "version": 1, "book_seq": 0, "status": "active", "created_at": "2026-06-01T18:45:30.000Z", "updated_at": "2026-06-01T18:45:30.000Z", "expires_at": "2026-06-01T18:45:40.000Z", "position_expires_at": "2026-06-02T00:00:00.000Z", "legs": [ { "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6", "market_ticker": "KXBTC-26JUN01-T72500", "event_ticker": "KXBTC-26JUN01", "side": "yes", "venue": "kalshi" }, { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "market_ticker": "KXBTC-26JUN01-T73000", "event_ticker": "KXBTC-26JUN01", "side": "no", "venue": "kalshi" } ] } } } ``` # Stream cashout quote Source: https://docs.totalis.trade/api-reference/quote-service/cashout-stream GET /v1/cashout-requests/{id}/stream Follow the best live bid and terminal status for an early cashout auction via Server-Sent Events. Open a Server-Sent Events (SSE) stream to follow a [cashout auction](/api-reference/quote-service/cashout-create). You receive the highest live bid as `best_quote` each time the book moves. Ties go to the earliest bid. A terminal `status` frame arrives when the request leaves `active`/`committing`. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `positions:read` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Path parameters The cashout request ID (UUID). ## SSE event types ### `best_quote` Sent when the best bid changes: a new highest bid, the leader replacing or withdrawing its bid, or the leader lapsing. `book_seq` increments monotonically, so a gap means you missed an update. ```json theme={null} { "book_seq": 3, "version": 1, "best_quote": { "id": "q1w2e3r4-5678-90ab-cdef-1234567890ab", "market_maker_id": "did:privy:cm3mm...", "price_micro": 66250000, "valid_until": "2026-06-01T18:46:05.000Z" } } ``` UUID of the winning bid. The bidding market maker's ID. Do not derive the outcome from it. The server decides whether the exit closes or transfers the position, and that does not change what you receive. The all-in amount you receive, in [microUSDC](/guides/glossary#units) (`1_000_000` = \$1.00). No direction and no separate fee term. Echo this exact value back as `min_price_micro` when you [commit](/api-reference/quote-service/cashout-commit). The commit is rejected without it, and you are never filled below it. ISO 8601 timestamp when this bid lapses. A lapsed bid stops being the best bid. When no bid is live, `best_quote` is `null`. That happens before the first bid arrives and after every bid has lapsed. ```json theme={null} { "book_seq": 4, "version": 1, "best_quote": null } ``` ### `status` Sent once the request leaves `active`/`committing`. `status` is the terminal cashout-request state, and the stream closes right after it. Unlike the [quote-request stream](/api-reference/quote-service/stream#status), no second terminal event follows. ```json theme={null} { "status": "committed" } ``` | Terminal `status` | Meaning | | - | - | | `committed` | The exit was driven on-chain. A win by the position's counterparty closed the position, and the RFQ becomes `bought_back`. A win by any other market maker transferred the position, which stays open under new ownership. Either way you received the winning bid, never less than your `min_price_micro`. | | `expired` | The auction's \~10 s TTL elapsed with no commit. | | `cancelled` | The request was cancelled. | ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this cashout request, or the credential does not start with `api_` (`details.reason: "wrong_api_key_type"`). | | 404 | `NOT_FOUND` | Cashout request not found. | ```bash curl theme={null} curl -N https://api.totalis.trade/v1/cashout-requests/c3d4e5f6-7890-abcd-ef12-34567890abcd/stream \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Accept: text/event-stream" ``` ```javascript JavaScript theme={null} // Native EventSource cannot send an X-API-Key header, so read the SSE stream // with fetch instead (works in browsers and Node 18+). async function streamCashout(id, apiKey) { const res = await fetch( `https://api.totalis.trade/v1/cashout-requests/${id}/stream`, { headers: { 'X-API-Key': apiKey, Accept: 'text/event-stream' } } ); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) return; buffer += decoder.decode(value, { stream: true }); let sep; while ((sep = buffer.indexOf('\n\n')) !== -1) { const frame = buffer.slice(0, sep); buffer = buffer.slice(sep + 2); const event = frame.match(/^event: (.+)$/m)?.[1]; const data = frame.match(/^data: (.+)$/m)?.[1]; if (!event || !data) continue; const payload = JSON.parse(data); if (event === 'best_quote') { console.log('Buyback quote:', payload.best_quote); } else if (event === 'status') { console.log('Terminal:', payload.status); return; // the stream closes after the terminal status } } } } ``` ```text SSE stream theme={null} event: best_quote data: {"book_seq":3,"version":1,"best_quote":{"id":"q1w2e3r4-5678-90ab-cdef-1234567890ab","market_maker_id":"did:privy:cm3mm...","price_micro":66250000,"valid_until":"2026-06-01T18:46:05.000Z"}} event: status data: {"status":"committed"} ``` # Commit quote request Source: https://docs.totalis.trade/api-reference/quote-service/commit POST /v1/quote-requests/{id}/commit Lock in the best available quote and create a trade. Commit the best available quote on an active quote request. The server revalidates every market, selects the best quote at or above your `min_payout_odds_seen`, and creates the trade atomically. The trade is an [RFQ](/guides/glossary#the-noun-stack) with its own `rfq_id`. The body ties the commit to exactly what you last saw, so you never fill at a stale price. After a successful commit, the selected market maker receives a `quote:accepted` event and must confirm before `confirmation_deadline`. It may also receive `mm_quote:accepted` on its private `mm:quotes:{mm_id}` channel, with the final quote economics and exact exposure legs. Vault settlement follows. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `trading:write` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Path parameters The quote request ID (UUID). ## Request body The quote request `version` you are committing against. If the request has changed since, the commit is rejected. The `id` of the best quote you saw at commit time. The `book_seq` from the SSE event that delivered the displayed quote. It proves your data was up to date. The minimum payout odds you were shown. Only quotes at or above it are eligible. Must be at least 1.0001. ## Response `"committed"` The quote request ID. The created RFQ ID for vault settlement. The selected quote ID. The payout odds that were locked in. ISO 8601 deadline by which the market maker must confirm. ## Rejection reasons A rejected commit carries its reason in `error.details.reason`. What each reason means and how to respond is on [Errors](/api-reference/errors#committing-a-quote). | Reason | Status | | - | - | | `QUOTE_CHANGED` | `409` | | `MARKET_NOT_LIVE` | `409` | | `QUOTE_EXPIRED` | `409` | | `COMMIT_FAILED` | `409` | | `COMMIT_FAILED_RETRYABLE` | `503` | | `mm_confirmation_timeout` | `409` | ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Missing or invalid commit fields. | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this quote request. | | 404 | `NOT_FOUND` | Quote request not found. | | 409 | `CONFLICT` | Commit rejected. See [Rejection reasons](#rejection-reasons). | | 503 | `SERVICE_UNAVAILABLE` | Transient failure. Retry after a short delay. | ```bash theme={null} curl -X POST https://api.totalis.trade/v1/quote-requests/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40/commit \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "expected_version": 3, "displayed_quote_id": "b2c3d4e5-6789-0abc-def1-234567890abc", "displayed_quote_book_seq": 5, "min_payout_odds_seen": 4.0 }' ``` ```json 201 theme={null} { "data": { "status": "committed", "quote_request_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "rfq_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", "quote_id": "d4e5f6a7-8901-bcde-f234-567890abcdef", "selected_payout_odds": 4.0, "confirmation_deadline": "2026-06-01T18:45:35.000Z" } } ``` # Create quote request Source: https://docs.totalis.trade/api-reference/quote-service/create POST /v1/quote-requests Create a new quote request for a parlay. Create a live [quote request](/guides/glossary#the-noun-stack) for a parlay of 1-5 legs. Every connected market maker receives it immediately and starts quoting. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `trading:write` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Request body 1-5 parlay legs. See [Limits and fees](/guides/limits-and-fees#parlay-construction). The market ticker on the venue, such as `KXBTC-26JUN01-T72500`. `"yes"` or `"no"`. `"kalshi"` (default) or `"polymarket"`. Bet amount in USDC, from 1 to 1000. Up to 50 market maker ids whose quotes you do not want, such as the `failed_mm_id` from a commit that timed out. ## Response The created quote request. UUID of the quote request. Your user ID. Request version, starts at `1`. Hash of the current request parameters. Quote book sequence number, starts at `0`. `"active"` Bet amount in USDC. ISO 8601 expiration timestamp. The validated legs, with added metadata. The legs exactly as you submitted them. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Invalid legs, side, venue, or bet amount. See `details.issues` for specifics. | | 400 | `VALIDATION_ERROR` | Market validation failed (market not found, expired, or exclusion conflict). See `details.leg_errors`. | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | ```bash theme={null} curl -X POST https://api.totalis.trade/v1/quote-requests \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "legs": [ {"market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi"}, {"market_ticker": "KXBTC-26JUN01-T73000", "side": "no", "venue": "kalshi"} ], "bet_amount": 25 }' ``` ```json 201 theme={null} { "data": { "quote_request": { "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "user_id": "did:privy:cm3abc...", "version": 1, "request_hash": "sha256:9f86d08...", "book_seq": 0, "status": "active", "bet_amount": 25, "expires_at": "2026-06-01T18:45:30.000Z", "legs": [ { "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6", "event_ticker": "KXBTC-26JUN01", "market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi" }, { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "event_ticker": "KXBTC-26JUN01", "market_ticker": "KXBTC-26JUN01-T73000", "side": "no", "venue": "kalshi" } ], "input_legs": [ {"market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi"}, {"market_ticker": "KXBTC-26JUN01-T73000", "side": "no", "venue": "kalshi"} ] } } } ``` # Get quote request Source: https://docs.totalis.trade/api-reference/quote-service/get GET /v1/quote-requests/{id} Retrieve a quote request with its active quotes and the current best quote. Retrieve a quote request by ID. You get the request, every active quote for its current version, and the best quote (highest payout odds). For live updates, open the [SSE stream](/api-reference/quote-service/stream) instead. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `positions:read` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Path parameters The quote request ID (UUID). ## Response The quote request, including `id`, `version`, `status`, `bet_amount`, `legs`, and `expires_at`. All active quotes for the current version. Quote UUID. The quoting market maker's ID. Payout multiplier applied to `user_cost`. Your net stake in USDC: `bet_amount` minus the taker fee. See [Fees](/guides/limits-and-fees#fees). Total payout if the parlay wins. USDC the market maker puts at risk. ISO 8601 timestamp when this quote expires. Per-leg pricing with `leg_id` and `leg_odds`. The quote with the highest payout odds, or `null` if no quotes are active. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this quote request. | | 404 | `NOT_FOUND` | Quote request not found. | ```bash theme={null} curl https://api.totalis.trade/v1/quote-requests/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40 \ -H "X-API-Key: $TOTALIS_API_KEY" ``` ```json 200 theme={null} { "data": { "quote_request": { "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "version": 3, "request_hash": "sha256:a3f4b2c...", "book_seq": 5, "status": "active", "bet_amount": 25, "legs": [ { "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6", "event_ticker": "KXBTC-26JUN01", "market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi" } ], "input_legs": [ {"market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi"} ], "expires_at": "2026-06-01T18:46:30.000Z" }, "quotes": [ { "id": "b2c3d4e5-6789-0abc-def1-234567890abc", "market_maker_id": "did:privy:cm3mm...", "payout_odds": 4.0, "user_cost": 24.75, "total_payout": 99, "mm_cost": 74.25, "valid_until": "2026-06-01T18:45:45.000Z", "leg_prices": [ {"leg_id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6", "leg_odds": 0.42} ] } ], "best_quote": { "id": "b2c3d4e5-6789-0abc-def1-234567890abc", "payout_odds": 4.0, "user_cost": 24.75, "total_payout": 99, "mm_cost": 74.25, "valid_until": "2026-06-01T18:45:45.000Z" } } } ``` # Stream quote updates Source: https://docs.totalis.trade/api-reference/quote-service/stream GET /v1/quote-requests/{id}/stream Subscribe to real-time best-quote updates for a quote request via Server-Sent Events. Open a Server-Sent Events (SSE) stream for one quote request. You receive the current best quote each time market makers submit, update, or withdraw quotes. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `positions:read` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Path parameters The quote request ID (UUID). ## SSE event types ### `best_quote` Sent whenever the quote book changes: a quote is submitted, updated, withdrawn, or expires. `book_seq` increments monotonically, so a gap means you missed an update. ```json theme={null} { "book_seq": 5, "version": 3, "request_hash": "sha256:9f86d08...", "best_quote": { "id": "b2c3d4e5-6789-0abc-def1-234567890abc", "payout_odds": 4.0, "user_cost": 24.75, "total_payout": 99, "mm_cost": 74.25, "valid_until": "2026-06-01T18:45:45.000Z", "market_maker_id": "did:privy:cm3mm..." } } ``` When no quotes are active, `best_quote` is `null`: ```json theme={null} { "book_seq": 6, "version": 3, "request_hash": "sha256:9f86d08...", "best_quote": null } ``` ### `status` Sent when the quote request leaves `active` (committed, cancelled, or expired). `committed_rfq_id` is the created RFQ id when `status` is `committed`, and `null` otherwise. ```json theme={null} { "status": "committed", "committed_rfq_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd" } ``` Immediately after `status`, the stream sends one **terminal event** for the outcome (`committed`, `cancelled`, or `expired`) and closes. Listen for either `status` or the terminal event. ### `committed` The quote request was committed into an [RFQ](/guides/glossary#the-noun-stack). Follow the trade through confirmation and settlement on the [WebSocket](/guides/websocket) `rfq:{rfq_id}` channel. ```json theme={null} { "quote_request_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "rfq_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd" } ``` ### `cancelled` You cancelled the quote request with [Cancel](/api-reference/quote-service/cancel). ```json theme={null} { "quote_request_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40" } ``` ### `expired` The quote request reached its `expires_at` without being committed. ```json theme={null} { "quote_request_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40" } ``` ## Integration guide 1. Open the stream after you create or update a quote request. 2. On each `best_quote` event, update the payout odds you display. Store `book_seq` and the best quote `id`. 3. To commit, send `expected_version`, `displayed_quote_id`, `displayed_quote_book_seq`, and `min_payout_odds_seen` to [Commit](/api-reference/quote-service/commit). 4. On a terminal event, close the stream and handle the outcome. On a commit, read the RFQ id from `committed_rfq_id` (`status` event) or `rfq_id` (`committed` event). Follow the trade on the [WebSocket](/guides/websocket) `rfq:{rfq_id}` channel. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this quote request, or the credential does not start with `api_` (`details.reason: "wrong_api_key_type"`). | | 404 | `NOT_FOUND` | Quote request not found. | ```bash curl theme={null} curl -N https://api.totalis.trade/v1/quote-requests/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40/stream \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Accept: text/event-stream" ``` ```javascript JavaScript theme={null} // Native EventSource cannot send an X-API-Key header, so read the SSE stream // with fetch instead (works in browsers and Node 18+). async function streamQuotes(id, apiKey) { const res = await fetch( `https://api.totalis.trade/v1/quote-requests/${id}/stream`, { headers: { 'X-API-Key': apiKey, Accept: 'text/event-stream' } } ); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) return; buffer += decoder.decode(value, { stream: true }); let sep; while ((sep = buffer.indexOf('\n\n')) !== -1) { const frame = buffer.slice(0, sep); buffer = buffer.slice(sep + 2); const event = frame.match(/^event: (.+)$/m)?.[1]; const data = frame.match(/^data: (.+)$/m)?.[1]; if (!event || !data) continue; const payload = JSON.parse(data); if (event === 'best_quote') { console.log('Best quote:', payload.best_quote, 'book_seq:', payload.book_seq); } else if (event === 'committed') { console.log('RFQ created:', payload.rfq_id); return; // the stream closes after a terminal event } else if (event === 'cancelled' || event === 'expired') { console.log('Terminal:', event); return; } } } } ``` ```python Python theme={null} import json import os import requests quote_request_id = '1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40' url = f'https://api.totalis.trade/v1/quote-requests/{quote_request_id}/stream' headers = { 'X-API-Key': os.environ['TOTALIS_API_KEY'], 'Accept': 'text/event-stream', } event_type = None with requests.get(url, headers=headers, stream=True) as resp: for line in resp.iter_lines(decode_unicode=True): if line.startswith('event: '): event_type = line[7:] elif line.startswith('data: '): payload = json.loads(line[6:]) if event_type == 'best_quote': print('Best quote:', payload['best_quote']) elif event_type in ('committed', 'cancelled', 'expired'): print(f'Terminal: {event_type}', payload) break event_type = None ``` ```text SSE stream theme={null} event: best_quote data: {"book_seq":1,"version":1,"request_hash":"sha256:9f86d08...","best_quote":{"id":"b2c3d4e5-6789-0abc-def1-234567890abc","payout_odds":4.0,"user_cost":24.75,"total_payout":99,"mm_cost":74.25,"valid_until":"2026-06-01T18:45:45.000Z","market_maker_id":"did:privy:cm3mm..."}} event: best_quote data: {"book_seq":2,"version":1,"request_hash":"sha256:9f86d08...","best_quote":{"id":"e5f6a7b8-9012-cdef-3456-7890abcdef12","payout_odds":4.4,"user_cost":24.75,"total_payout":108.9,"mm_cost":84.15,"valid_until":"2026-06-01T18:45:50.000Z","market_maker_id":"did:privy:cm9mm..."}} event: status data: {"status":"committed","committed_rfq_id":"c3d4e5f6-7890-abcd-ef12-34567890abcd"} event: committed data: {"quote_request_id":"1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40","rfq_id":"c3d4e5f6-7890-abcd-ef12-34567890abcd"} ``` # Update quote request Source: https://docs.totalis.trade/api-reference/quote-service/update PATCH /v1/quote-requests/{id} Update the legs or bet amount on an active quote request. Replace the legs or bet amount on a quote request while its status is `active`. The update increments `version` and recomputes `request_hash`. Every existing quote is invalidated, and market makers receive the new request to quote again. ## Authentication Send your API key in the `X-API-Key` header. The key needs the `trading:write` scope. ``` X-API-Key: ``` A Privy JWT (`Authorization: Bearer `) is also accepted. See [Authentication](/guides/authentication). ## Path parameters The quote request ID (UUID). ## Request body Same fields as [Create quote request](/api-reference/quote-service/create). Array of 1-5 parlay legs. The market ticker on the venue. `"yes"` or `"no"`. `"kalshi"` (default) or `"polymarket"`. USDC bet amount (1-1000). ## Response The updated quote request with an incremented `version`. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Invalid body or market validation failed. | | 401 | `UNAUTHORIZED` | Missing or invalid API key. | | 403 | `FORBIDDEN` | You do not own this quote request. | | 404 | `NOT_FOUND` | Quote request not found. | | 409 | `CONFLICT` | Quote request is already committed, cancelled, or expired. | ```bash theme={null} curl -X PATCH https://api.totalis.trade/v1/quote-requests/1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40 \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "legs": [ {"market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi"}, {"market_ticker": "KXETH-26JUN01-T3000", "side": "no", "venue": "kalshi"} ], "bet_amount": 50 }' ``` ```json 200 theme={null} { "data": { "quote_request": { "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "user_id": "did:privy:cm3abc...", "version": 2, "request_hash": "sha256:a3f4b2c...", "book_seq": 0, "status": "active", "bet_amount": 50, "expires_at": "2026-06-01T18:46:10.000Z", "legs": [ { "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6", "event_ticker": "KXBTC-26JUN01", "market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi" }, { "id": "d4e5f6a7-8901-23bc-def4-567890abcdef", "event_ticker": "KXETH-26JUN01", "market_ticker": "KXETH-26JUN01-T3000", "side": "no", "venue": "kalshi" } ], "input_legs": [ {"market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi"}, {"market_ticker": "KXETH-26JUN01-T3000", "side": "no", "venue": "kalshi"} ] } } } ``` # Check username Source: https://docs.totalis.trade/api-reference/user/check-username GET /v1/username/check Check if a username is available on Totalis before updating your profile # Create API key Source: https://docs.totalis.trade/api-reference/user/create-api-key POST /v1/api-keys Create an API key for your Totalis account. The full secret is returned only in this response, so save it immediately. Needs a Privy session or a key that holds keys:manage. Every body field is optional: | Field | Meaning | | - | - | | `name` | A label for the key. | | `preset` | `read_only`, `trading` or `full`. With neither `preset` nor `scopes`, the key gets `trading`. | | `scopes` | An explicit scope list. Overrides `preset`. | | `confirm_withdraw` | Must be `true` for a key that would hold `funds:withdraw`. | No key can hold a scope its creator lacks. Keys created here never expire; revoke one to end it. An account can hold 10 active keys. The eleventh returns `409` with reason `api_key_limit`. # Get profile Source: https://docs.totalis.trade/api-reference/user/get-profile GET /v1/me Get the authenticated Totalis user profile. Auto-creates a record on first call. Pass ?ref= on first login to auto-redeem an invite code (auto_redeemed and airdrop fields surface on success). # Get wallet Source: https://docs.totalis.trade/api-reference/user/get-wallet GET /v1/wallet Get the authenticated user wallet snapshot: address, SOL/USDC wallet balance, vault balance, and aggregated locked_amount across active RFQs and vault positions. # List API keys Source: https://docs.totalis.trade/api-reference/user/list-api-keys GET /v1/api-keys List all API keys for your Totalis account. Returns metadata and prefixes, never full secrets # Revoke API key Source: https://docs.totalis.trade/api-reference/user/revoke-api-key DELETE /v1/api-keys/{prefix} Revoke one of your Totalis API keys by prefix. Takes effect immediately on all requests # Submit transaction Source: https://docs.totalis.trade/api-reference/user/submit-tx POST /v1/wallet/submit-tx Submit a client-signed serialized Solana transaction (base64). Used by browser flows that do not have direct RPC access. Rate-limited to 10 requests/minute. # Update username Source: https://docs.totalis.trade/api-reference/user/update-username PUT /v1/username Update your Totalis username. Must be 3-20 alphanumeric characters or underscores # Get webhook config Source: https://docs.totalis.trade/api-reference/webhooks/get-config GET /v1/webhooks Current webhook endpoint config + the subscribable event catalog. # List deliveries Source: https://docs.totalis.trade/api-reference/webhooks/list-deliveries GET /v1/webhooks/deliveries Recent webhook delivery attempts, newest first. # Replay delivery Source: https://docs.totalis.trade/api-reference/webhooks/redeliver POST /v1/webhooks/deliveries/{id}/redeliver Re-queue a delivered or dead-lettered delivery. # Rotate signing secret Source: https://docs.totalis.trade/api-reference/webhooks/rotate-secret POST /v1/webhooks/rotate-secret Mint a fresh HMAC signing secret (returned once). # Set webhook config Source: https://docs.totalis.trade/api-reference/webhooks/set-config PUT /v1/webhooks Set the endpoint URL and subscribed events. # Changelog Source: https://docs.totalis.trade/changelog Changes to the public Totalis API surface: endpoints added, endpoints retired, renames, and behavior changes that affect a running integration. Changes to the public API surface, newest first. This log starts on 18 July 2026. Earlier changes are not tracked here. Every retired path listed below still resolves through a redirect, so old bookmarks keep working, but the old paths are not supported surface. **Removed: `implied_probability` on quote requests.** Quote requests and the market maker stream no longer carry the field. It was always `0` and never used for pricing. Drop it from any parser that requires it. **Added: `position_id` on the market maker position reads.** [`GET /v1/mm/positions`](/api-reference/mm/list-positions) and [`GET /v1/mm/positions/{id}`](/api-reference/mm/get-position) now return the on-chain `position_id`, hex encoded and lowercase. It matches the `position_id` on a broadcast [`cashout_request`](/api-reference/quote-service-mm/stream#cashout_request), so a maker can tell whether an auction is on a position they back without keeping a side index. **Added: `taker_fee_bps` on the submit quote response.** [`PUT /v1/mm/quote-requests/{id}/quote`](/api-reference/quote-service-mm/submit-quote) echoes the fee rate the quote was priced at. **Changed: submitting a quote now fails closed on the fee rate.** If the taker fee rate cannot be read, the request returns `503` with `details.reason: "fee_unavailable"` instead of pricing at 0 bps. Previously a config read failure silently priced the quote against the gross bet instead of the net stake, so the maker underwrote a larger base than intended. Retry on this reason. **Changed: a malformed API key now returns `403` everywhere.** The SSE surface previously returned `401` for a credential that does not start with `api_`, while REST returned `403`. Both now return `403` with `details.reason: "wrong_api_key_type"`. **Changed: every WebSocket `auth:error` now carries a `code`.** The auth timeout path previously sent only a `message`. Branch on `code`, never on `message`. See [WebSocket](/guides/websocket#authentication) for the four values. **Clarified: rate limits are three buckets, not one.** Public market reads are keyed per IP and do not draw down the authenticated per-account budget, and sending an API key to a public read does not raise its limit. The quote service routes have their own per-IP limit. See [Limits and fees](/guides/limits-and-fees#rate-limits). **Removed: World Cup reward pool.** The FIFA World Cup 10k reward pool challenge ended and its page was removed. `/guides/world-cup-reward-pool` now redirects to [Referrals](/referrals). **Renamed: early exit auctions are now early cashout.** The guide moved from `/guides/early-exit-auctions` to [`/guides/early-cashout`](/guides/early-cashout). **Changed: one auction model replaces two flows.** The separate buyback and takeover flows are gone. A single broadcast auction now covers both outcomes. A winning bid from the maker who backs the position closes it. A winning bid from anyone else acquires it. Bids are one all-in `price_micro` figure, with no direction field and no separate fee term. If you still branch on the old buyback and takeover distinction, replace it with a comparison of the winning `market_maker_id` against `counterparty_mm_id`. See [Cashout auctions](/guides/cashout-auctions). **Retired: `GET /v1/wallet/blockhash`.** The endpoint was removed. Transactions are built and sponsored server side, so clients no longer fetch a blockhash. **Retired: `POST /v1/mm/heartbeat`.** Market maker liveness now comes from the SSE stream connection. Remove any heartbeat loop. Holding the [MM stream](/api-reference/quote-service-mm/stream) open is enough. **Added: market maker position reads.** [`GET /v1/mm/positions`](/api-reference/mm/list-positions) and [`GET /v1/mm/positions/{id}`](/api-reference/mm/get-position) return your active positions with their full parlay legs. **Added: [referral program](/referrals).** Earn 20% of the trading fees generated by traders you refer, claimable at 25 USDC. **Added: [`GET /v1/mm/quotes`](/api-reference/mm/list-quotes).** Reconcile your quotes and their lifecycle status after a disconnect. **Renamed: quote service paths.** The quote service and market maker endpoints moved out of the flat `/rfqs` and `/mm` documentation roots: | Old path | New path | | - | - | | `/api-reference/rfqs/create` | [`/api-reference/quote-service/create`](/api-reference/quote-service/create) | | `/api-reference/rfqs/get` | [`/api-reference/quote-service/get`](/api-reference/quote-service/get) | | `/api-reference/rfqs/accept-quote` | [`/api-reference/quote-service/commit`](/api-reference/quote-service/commit) | | `/api-reference/rfqs/cancel` | [`/api-reference/quote-service/cancel`](/api-reference/quote-service/cancel) | | `/api-reference/mm/submit-quote` | [`/api-reference/quote-service-mm/submit-quote`](/api-reference/quote-service-mm/submit-quote) | | `/api-reference/mm/withdraw-quote` | [`/api-reference/quote-service-mm/withdraw-quote`](/api-reference/quote-service-mm/withdraw-quote) | | `/api-reference/mm/list-rfqs` | [`/api-reference/quote-service-mm/stream`](/api-reference/quote-service-mm/stream) | The `/v1/...` request paths did not change. Only the documentation routes moved. # Authentication Source: https://docs.totalis.trade/guides/authentication Scoped API keys: how to get one, how to send it, the scope catalog, and how to manage and revoke keys. Every authenticated request carries your API key. On REST and the quote streams, send it in the `X-API-Key` header. On the WebSocket, send it in the first message. Keys are scoped, so a key can read without being able to trade, and trade without being able to withdraw. ## Getting your API key Open [app.totalis.trade](https://app.totalis.trade) and go to your account settings. Name it and choose its scopes. Grant only what this integration needs. The presets map to the [scopes](#scopes) below: **Read-only** is the four read scopes, **Trading** adds `trading:write` and `funds:deposit`, and **Custom** lets you tick scopes one by one. The API also accepts a `full` preset. See [Create API key](/api-reference/user/create-api-key). The API Keys settings page with a key name, the Trading preset selected, and the read, trading and funds permissions checked The full key is returned exactly once, in `data.key`. The API key generated successfully banner with the new api_live key, a copy button, and the note that it will not be shown again Store the key in a secret manager before you close the dialog. It cannot be retrieved later, only revoked and replaced. Keys match `api_(live|test)_[A-Za-z0-9_-]{28,36}`. Every key you create is a live key and starts `api_live_`. There is no public test environment. Start with small real bets, or email [founders@totalis.trade](mailto:founders@totalis.trade) if you need a staging setup. The web app authenticates with a Privy JWT (`Authorization: Bearer `) instead of a key. Every endpoint accepts either. Creating, listing and revoking keys needs `keys:manage`. A Privy session always holds it. A key holds it only if you granted it. ## Using your API key ```bash theme={null} curl https://api.totalis.trade/v1/portfolio \ -H "X-API-Key: $TOTALIS_API_KEY" ``` On the WebSocket, authenticate after connecting: ```json theme={null} { "type": "auth", "api_key": "" } ``` A credential that does not start with `api_` gets `403` with reason `wrong_api_key_type`. A well-formed but invalid or revoked key gets `401`. See [Errors](/api-reference/errors#authentication). ## Scopes A key holds the scopes you chose at creation. A request that needs a scope the key lacks gets `403` with reason `insufficient_scope`. | Scope | Grants | Risk | | - | - | - | | `account:read` | View account profile and username. | low | | `markets:read` | Browse markets and exclusion groups. Market reads are public today, so no public endpoint checks it. | low | | `positions:read` | View quote requests, parlays, stats, and P\&L. Portfolio also needs `balances:read`. | low | | `balances:read` | View wallet and vault balances and delegation status. | low | | `funds:deposit` | Move USDC into the vault. | low | | `trading:write` | Create, modify, commit, and cancel quote requests. Places parlays. | medium | | `account:write` | Change username and profile settings. | medium | | `mm:quote` | Read quote requests and submit or confirm quotes as a market maker. | medium | | `funds:withdraw` | Move USDC out of the vault or wallet to any address. | **high** | | `tx:sign` | Submit arbitrary signed Solana transactions. | **high** | | `keys:manage` | Create, list, and revoke API keys. | **high** | Each high-risk scope lets a leaked key take everything. `funds:withdraw` sends USDC to any address. `tx:sign` submits arbitrary transactions. `keys:manage` mints more keys, including ones that outlive your revocation of the original. Grant them only to keys that need them, and never to a key that lives in a browser or a CI log. Scopes by integration: | Integration | Scopes | | - | - | | Dashboard or reconciliation, read only | `account:read`, `positions:read`, `balances:read` | | Trading bot | the four read scopes, plus `trading:write`, plus `funds:deposit` if it tops up the vault | | Market maker | the four read scopes, plus `mm:quote`, plus `funds:deposit` if you park collateral in the vault | ## Managing keys | Action | Endpoint | | - | - | | List keys | [`GET /v1/api-keys`](/api-reference/user/list-api-keys) | | Create key | [`POST /v1/api-keys`](/api-reference/user/create-api-key) | | Revoke key | [`DELETE /v1/api-keys/{prefix}`](/api-reference/user/revoke-api-key) | `{prefix}` is the `key_prefix` value returned by both list and create: the short leading fragment that listings show in place of the full secret. The app shows each key's prefix, scopes, request count and state, with a Revoke control on active keys. Four API keys listed with their prefix, scopes, creation date, request count, and an active or revoked state A new key can never grant a scope the creating credential does not already hold. A key with `positions:read` cannot mint a key with `funds:withdraw`. ## Key states | State | Behavior | | - | - | | Active | Valid and accepted. | | Revoked | Manually revoked. Rejected with `401`. | | Expired | Past its expiration date. Rejected with `401`. | Keys you create yourself never expire, so they reach the Expired state only if they were minted with an expiry. Revocation takes effect immediately. Revoking one key among several does not stop webhook delivery, because webhook endpoints belong to your account, not to the key that created them. See [Webhooks](/guides/webhooks). # Cashout auctions Source: https://docs.totalis.trade/guides/cashout-auctions Bid on parlay exits as a market maker: one broadcast auction, one all-in price, two outcomes notified differently. Bid on the auction Totalis opens when a user leaves a parlay early. The bettor's side is on [Early cashout](/guides/early-cashout). ## At a glance * A user can leave a live parlay early. Totalis opens an auction of about 10 seconds and broadcasts it to every maker on the stream you already consume. * You bid one all-in price. The highest live bid wins, and ties go to the earliest bid. * If the maker who backs the position wins, the position closes and the parlay shows as bought back. * If any other maker wins, the position transfers to them and stays open. The buyer gets no event today. Every `*_micro` field is an integer of microUSDC (`1000000` is 1.00 USDC). The money fields beside them (`user_stake`, `mm_risk`, `total_payout`, and the `amount` on the settlement event) are decimal USDC. `payout_odds` is a multiplier, not an amount. Both conventions appear in the same payload, so scale before you compare. See [Glossary](/guides/glossary#units). ## The two outcomes An auction resolves one of two ways. The seller is paid the same either way. Your integration handles the two very differently. Your position goes to a ten second auction; the highest all-in bid wins and either closes the position or transfers it Your position goes to a ten second auction; the highest all-in bid wins and either closes the position or transfers it Do not wait on `bought_back` to detect a win. If you bid on positions you do not already back, a win transfers the position to you and emits no event to you. Reconcile acquisitions from [`GET /v1/rfqs`](/api-reference/parlays/list): an acquired position moves to your account as the bettor side. ## How it works A [`cashout_request`](/api-reference/quote-service-mm/stream#cashout_request) event arrives on [`GET /v1/mm/quote-requests/stream`](/api-reference/quote-service-mm/stream). It carries the position's legs and economics. Auctions are broadcast, so you see exits on positions you have no relationship with. Re-value the parlay at current odds, and read `legs[].resolution` carefully. `void` beats everything. Any void leg cancels the whole position at settlement and refunds the stake, so the position is worth about the stake even if a sibling leg already resolved `lost`. With no void leg, one `lost` leg makes the parlay worthless. Nothing stops an auction opening on a position whose legs are already decided. `resolution` is your only defense. `PUT` your bid to [`/v1/mm/cashout-requests/{id}/quote`](/api-reference/quote-service-mm/cashout-quote) before `expires_at`. `price_micro` is what the user receives, all-in. If you win as the counterparty, you receive `position:bought_back`. If you win as anyone else, you acquire the position and receive no event. See [Knowing you won](#knowing-you-won). ## The auction | Property | Value | | - | - | | Lifetime | about 10 seconds, per `expires_at` on the push | | Visibility | broadcast to every market maker | | Your bid validity | `expires_in_ms`, 5000 to 60000, default 15000 | | Winner | highest `price_micro`, ties to the earliest bid. A replacement re-times your bid. | | Refusing | submit nothing. There is no decline endpoint. | | Pulling a bid | [`DELETE /v1/mm/cashout-requests/{id}/quote`](/api-reference/quote-service-mm/cashout-quote#withdrawing-a-bid) | `price_micro` is a single all-in figure. There is no direction field, no separate fee term, and no gross versus net. What you send is what the user sees on screen and walks away with. It can be below their stake. A losing parlay exits for less than it cost, and the user never pays more than their stake to leave. A bid may outlive the auction. Nothing tells you that your bid lost or lapsed, or that the auction closed, so age bids out locally against `expires_at`. Your bid being best does not guarantee a fill. The user is only ever filled at a price they explicitly accepted. If the book moves down between their tap and their commit, the commit fails and nobody trades. ## Knowing you won There is no per-auction result event on the MM stream. | Outcome | What you get | | - | - | | You won as the counterparty | `position:bought_back` on `mm:quotes:{mm_id}`. A copy also reaches any connection authenticated as you, with no subscription needed, so dedupe on `tx_signature`. The `position.bought_back` webhook also fires; it needs `positions:read`. `amount` is decimal USDC, and `mm_pays_user` tells you which direction applied. | | You won and acquired the position | Nothing today. The position appears in your `GET /v1/rfqs` and pays you `total_payout` if every leg wins. | | You lost, or the auction expired | Nothing. Your bid lapses. | A bought-back parlay reports a realized cashout P\&L, not a win or loss. The exit is a real settlement, not a refund. ## Errors Every rejection reason for a cashout bid, including the commit-time ones you never see as an HTTP response, is on [Errors](/api-reference/errors#bidding-on-a-cashout-auction). | Class | Treatment | | - | - | | `400`, `401`, `403` | Alert. These are bugs on your side. If every bid fails this way, you are silently out of the market. | | `404`, and race class `409` | Log quietly. These are ordinary lost auctions. | ## Checklist Handle `cashout_request` on the MM stream, and reconnect with `last_event_id`. With a 10 second lifetime, a reconnect gap means lost auctions. Price from `legs`, `user_stake`, and `total_payout`, respecting `legs[].resolution`. Those amounts are decimal USDC. Your bid is microUSDC. Match the auction's `position_id` against the `position_id` on [`GET /v1/mm/positions`](/api-reference/mm/list-positions). A hit means you back the position and a win closes it. No hit means a win acquires it. Both ids are lowercase hex with no `0x`, so they compare directly. Bid before `expires_at`, and age your own bids out locally. Keep free balance ahead of outstanding bids. Alert on `400`, `401`, and `403`. Log `404` and race class `409` quietly. Take closing wins from `position:bought_back`. Take acquisitions from `GET /v1/rfqs`, because nothing will tell you. # Early cashout Source: https://docs.totalis.trade/guides/early-cashout Leave a parlay before it settles: a short auction, one all-in price, and what happens to your position afterwards. Once a parlay is live on chain, you can ask market makers to buy you out before it settles. Totalis runs a short auction. You see the best price live and accept it or walk away. ## At a glance * Any live parlay can be cashed out early, until 30 seconds before its markets close. The auction runs about 10 seconds. * Every market maker sees the auction and can bid one all-in price. You see the best one as it changes. * You receive the best live bid, never less than the price you accepted. If the best bid drops below it before you commit, the commit fails and nothing happens. * The price can be below your stake if the parlay has gone against you. Your position goes to a ten second auction; the highest all-in bid wins and either closes the position or transfers it Your position goes to a ten second auction; the highest all-in bid wins and either closes the position or transfers it ## How it works Ask for a cashout on a live position. Totalis opens the auction and gives you a stream to watch it. ```bash theme={null} POST /v1/cashout-requests { "position_id": "" } GET /v1/cashout-requests//stream # SSE: live buyback quote and status ``` `position_id` is on the parlay object once it is live. Every maker sees the auction and can bid one all-in price. You see the best live bid, and it can change until you accept. The [cashout stream](/api-reference/quote-service/cashout-stream) carries the best live bid. Its `price_micro` is the total you would receive, in microUSDC: an integer where `1000000` is 1 USDC. See [Units](/guides/glossary#units). Take the best bid, or walk away. ```bash theme={null} POST /v1/cashout-requests//commit { "min_price_micro": } ``` `min_price_micro` is required. If the best bid has dropped below it, the commit fails and nothing happens. ## What the price means The bid you accept is one all-in figure. Exactly that amount lands in your vault as free balance, not in your wallet. You can trade with it or withdraw it from there. The profit fee is already inside the number. There is no separate fee line and no second taker fee for leaving. See [Fees on an early exit](/guides/limits-and-fees#fees-on-an-early-exit). When the exit closes the position (see the table below), your stake is released. The difference between the price and your stake moves between you and the maker. On the `position.bought_back` webhook, `amount` is that difference before the profit fee, and `mm_pays_user` gives its direction. A parlay that has gone against you exits for less than it cost, but you never pay anything to leave. If no maker bids before the auction ends, nothing happens and your parlay stays open. ## What happens to the position An auction resolves one of two ways. Both pay you the same. They look different in your data. | Winning bidder | What happens | How it shows | | - | - | - | | The maker who backs your position | The position closes. | The parlay reads as bought back, with realized P\&L on its `cashout` object. | | Any other maker | You are out and paid. The position moves to the buyer's account and stays open there. | The parlay leaves your reads. | Do not wait for a bought back status to know a cashout succeeded, because a sale to another maker never shows as bought back. Use the commit response, or the `position.bought_back` webhook when the backing maker won. Market makers: the other side of the auction is on [Cashout auctions](/guides/cashout-auctions). # Event catalog Source: https://docs.totalis.trade/guides/event-catalog The exact event name each channel uses for every moment in a trade, across SSE, WebSocket, webhooks, and REST. Each row below is one moment in a trade. Each column is one channel. Field names, identifiers, and their aliases are in the [Glossary](/guides/glossary), along with the [event naming rules](/guides/glossary#event-naming) for each channel. `quote:*` WebSocket events (`quote:accepted`, `quote:confirmed`, `quote:executed`, `quote:expired`, `quote:rejected`, `quote:withdrawn`) go to the market maker whose quote they concern, on their private `mm:quotes:{mm_id}` channel. The bettor's own authenticated connections also receive `quote:confirmed` and `quote:executed` for their trade. `rfq:*` and `position:*` events go to `rfq:{rfq_id}`, the channel a user or anyone tracking a specific trade subscribes to. `vault:deposited` and `vault:withdrawn` go to the account's own connections and are not in the table. The two families are not interchangeable. Some rows have a WebSocket event for the maker and none for the user, or the reverse. ## One trade, across every channel | Lifecycle moment | SSE, user stream | SSE, MM stream | WebSocket | Webhook | REST status | | - | - | - | - | - | - | | **Quote request created** | No dedicated created event. The stream opens after creation and the first frame is `best_quote`, often `null`. | `quote_request` | None. WebSocket starts at commit. | None. Webhooks are post-trade only. | Quote request `status: "active"` via [`GET /v1/quote-requests/{id}`](/api-reference/quote-service/get) | | **Quote updated** | `best_quote` | `quote_request:updated`, only when the request itself changes via `PATCH`. No MM stream event exists for an individual quote submission, yours or a competitor's. | None | None | `version` and `book_seq` increment. `status` stays `"active"`. | | **Quote accepted, committed** | `status` (`status: "committed"`), then the terminal `committed` event, then the stream closes. | `quote_request_expired` with `reason: "committed"`. `won: true` for the winning maker, `false` for everyone else. | `quote:accepted` and `mm_quote:accepted` to the maker. `rfq:status` with `status: "accepted"` on `rfq:{rfq_id}` for anyone watching the trade. | `parlay.status_changed` with `status: "accepted"` and `accepted_quote_id` set | RFQ `status: "accepted"` | | **Confirmed by maker** | None. The stream closed at commit. | None | `quote:confirmed` to the maker. `rfq:status` with `status: "confirmed"` on `rfq:{rfq_id}`. | `parlay.status_changed` with `status: "confirmed"` | RFQ `status: "confirmed"` | | **Executed on-chain** | None | None | `quote:executed` to the maker. `rfq:status` with `status: "executed"` and `position:created` with `position_pda` on `rfq:{rfq_id}`. A failed creation attempt that will be retried arrives as `position:failed`. | `parlay.status_changed` with `status: "executed"` | RFQ `status: "executed"` | | **Settled** | None | None | `position:settled` with `outcome` and `tx_signature` | `position.settled`, the terminal outcome event. `parlay.status_changed` stops at `executed` and `cancelled` and never carries `"settled"`. | RFQ `status: "settled"` | | **Exited early** | None. The separate [cashout stream](/api-reference/quote-service/cashout-stream) ends with a terminal `status` event reporting `"committed"`. | `cashout_request`, broadcast to every maker when the auction opens. No SSE event marks its completion. | `position:bought_back` on `rfq:{rfq_id}`, only when the winning bid closes the position. Nothing is delivered when a bid acquires it. See [the acquisition gap](#the-acquisition-gap). | `position.bought_back` with `amount`, `mm_pays_user`, `tx_signature` on a close. `position.transferred` goes to the seller only. | Depends on the outcome. A close sets `status: "bought_back"` and populates the `cashout` object. A transfer leaves the position active and repoints ownership, so the RFQ does not become `bought_back`. | | **Cancelled** | `cancelled`, when the quote request is abandoned pre-commit via [Cancel](/api-reference/quote-service/cancel) | `quote_request_expired` with `reason: "cancelled"` | `rfq:cancelled` and `rfq:status` with `status: "cancelled"` on `rfq:{rfq_id}`, plus `position:cancelled` when a funded position is cancelled. A pre-commit cancellation never reaches a position, so only the `rfq:*` events fire. | None pre-commit. Post-commit fires both `parlay.status_changed` (`status: "cancelled"`) and `position.cancelled`, with different event ids, so match the pair on `rfq_id`. | Quote request `status: "cancelled"` pre-commit, RFQ `status: "cancelled"` post-commit | | **Expired** | `expired`, when the TTL lapsed with no commit | `quote_request_expired` with `reason: "expired"` | `rfq:expired` on `rfq:{rfq_id}` when the whole parlay expires. `quote:expired` to a maker means that maker's accepted quote lapsed before confirmation. | None. No webhook event exists for expiry, before or after commit. | Quote request `status: "expired"` pre-commit, RFQ `status: "expired"` post-commit | ## The acquisition gap An early cashout resolves one of two ways. Only one of them notifies you live. ### A closing exit is well covered `position:bought_back` carries `rfq_id`, `position_pda`, `amount`, `mm_pays_user`, `tx_signature`, and `quote_id`. It fans out to four places at once: | Destination | Needs a subscription | | - | - | | The `rfq:{rfq_id}` channel, the canonical one for `position:*` events | Yes. Subscribe to this one. | | The `mm:quotes:{mm_id}` channel of the maker whose exposure closed | Yes | | Any connection authenticated as the seller | No, it arrives on identity alone | | Any connection authenticated as that maker | No | The last two arrive on identity alone. If you also hold the `rfq:{rfq_id}` subscription, you may see the same event twice. Dedupe on `tx_signature`. The `position.bought_back` webhook carries the same moment. ### An acquiring bid notifies the buyer of nothing One cashout outcome reaches the buyer on no channel: a bid that acquires a position instead of closing it. When the winning bid comes from a market maker that was not the position's counterparty, the position transfers to them and stays open. There is no WebSocket event and no buyer-side webhook for that. `position.transferred` goes to the seller. If you bid on auctions for positions you do not already back, reconcile acquisitions from [`GET /v1/rfqs`](/api-reference/parlays/list), where an acquired position appears on your account, rather than waiting for a push. Polling the RFQ status does not help either. A transfer leaves it `executed`, so polling for `bought_back` never fires. ## Delivery guarantees | Channel | Guarantee | On reconnect | | - | - | - | | SSE | Live only, for the life of the request | Resume with `last_event_id` | | WebSocket | Live only, no redelivery | Reconcile against [read endpoints](/guides/reading-data) | | Webhooks | At-least-once, signed, retried | [Replay](/guides/webhooks#replaying-deliveries) by delivery id | | REST | Current truth, on demand | n/a | # FAQ Source: https://docs.totalis.trade/guides/faq Common questions about Totalis: what a parlay is, which markets are listed, how settlement and security work, fees, and regulatory status. ## What is Totalis? Totalis is a derivatives layer for prediction markets. You pick outcomes across multiple markets and venues, combine them into a single bet called a **parlay**, and market makers compete to give you the best payout odds. Trades run on Totalis infrastructure: quote requests that market makers price, and non-custodial Solana vaults for settlement. Kalshi and Polymarket are supported as underlying venues today, with more to come. ## What is a parlay? A parlay puts one or more predictions, called **legs**, into one bet. Every leg has to win for the parlay to pay out. One loss loses the whole bet. With more than one leg, each leg's odds compound into a larger combined payout, so the parlay offers a higher reward. Because every leg must hit, it is also higher risk than a bet on any one of those outcomes. Leg and bet limits are on [Limits and fees](/guides/limits-and-fees). ## What markets does Totalis offer? Totalis lists markets that are recurring, have reliable liquidity on the underlying platform, and settle within 15 days. They span eight categories: politics, sports, crypto, finance, economics, entertainment, weather, and tech. For the current list, query [`GET /markets`](/api-reference/markets/list) and filter with `category` and `subcategory`. No API key needed. ## What chain are you on? Solana. Vaults, positions, and settlements all execute on chain. Each participant has a persistent vault that holds their collateral across positions. ## How does Totalis use its underlying markets? Totalis uses prediction market platforms as data and resolution layers, not as execution venues. It ingests market data (prices, probabilities, metadata) from platforms like Kalshi and Polymarket. Those markets are the source of truth for event definitions and final outcomes. Your parlay does not route to any underlying platform. The position is synthetic: it exists only inside Totalis, which handles execution, clearing, and settlement, then pays out based on the referenced markets' final outcome. That design lets one parlay combine venues, avoids splitting liquidity across platforms, and allows risk management across the whole portfolio. ## Can I not just do this on Kalshi? Kalshi offers Combos. Totalis works differently: | | Kalshi Combos | Totalis | | - | - | - | | What you combine | Kalshi's own contracts, sports first | Sports, crypto, weather, entertainment, and economic events, across venues in one parlay | | How it is priced | Each combination trades as its own market with a dedicated order book, so the catalog is bounded by what the exchange lists | Market makers price any combination of outcomes through a quote request | ## How secure is it? Totalis is non-custodial. Funds sit in on-chain Solana vaults the protocol controls, not in a Totalis account. When a trade is matched, your stake and the maker's collateral lock together, in one transaction, in those vaults. Neither side can move locked collateral unilaterally. Funds release at settlement, on an early cashout, or when a leg is voided. Signing is delegated, not surrendered. You grant Totalis permission to sign your trades inside a secure enclave your private key never leaves, and you can revoke it at any time. [Vaults and custody](/guides/vault-architecture#what-totalis-can-and-cannot-do) lists what Totalis can and cannot do with your funds. ## What happens if a market gets cancelled on the underlying venue? If a market is cancelled or delisted before all legs in a parlay have resolved, Totalis detects the invalid leg, cancels the position, and unlocks both your collateral and the market maker's. Nobody loses funds to an external market cancellation. ## How does Totalis make money? Two fees: a 1% taker fee on your bet amount, taken when the trade is placed and returned if the parlay is voided, and a 1% fee on the profit earned by the winning side of each trade. The formulas and a worked example are on [Limits and fees](/guides/limits-and-fees#fees). ## Is Totalis available to U.S. persons? No. Totalis currently operates offshore and is not available to U.S. persons. Totalis intends to pursue the registrations required to operate as a regulated event derivatives platform in the U.S., including the appropriate CFTC licenses, as the product matures. ## Where do I report a problem? For integration questions, email [founders@totalis.trade](mailto:founders@totalis.trade). Recent changes to the API surface are listed on the [changelog](/changelog). # Funding your wallet Source: https://docs.totalis.trade/guides/funding Send USDC to your Solana wallet, enable trading, and withdraw. Trading funds the vault by itself; a deposit call exists if you want to pre-fund it. You fund your account by sending USDC to your Solana wallet address. Once it lands, you are ready to trade. ## At a glance * Send native Solana USDC only. Placing a parlay moves the stake into your vault by itself, so there is no separate deposit step. * Enable trading once, in the app. The API can only report whether it is on. * Network fees are sponsored, so your wallet only needs USDC. The one exception is a small rent charge, taken in USDC, on a first withdrawal to a new address. * One withdraw call reaches your wallet and your vault's free balance together. Send USDC, enable trading once, trade with the stake moving into the vault by itself, withdraw any time Send USDC, enable trading once, trade with the stake moving into the vault by itself, withdraw any time ## Send USDC to your wallet | Option | Where | Time | Notes | | - | - | - | - | | Card or Apple Pay | In the app | About a minute | Buys USDC straight into your wallet | | Bridge from another chain | In the app | A few minutes | From the chains the app offers. Delivers native Solana USDC | | Send USDC directly | Any Solana wallet or exchange | Seconds | Native Solana USDC only | To send USDC directly: [`GET /v1/wallet`](/api-reference/user/get-wallet) returns your Solana wallet `address` and current balances. ```bash theme={null} curl https://api.totalis.trade/v1/wallet \ -H "X-API-Key: $TOTALIS_API_KEY" ``` The same address is under **Deposit** in the app, as a QR code and a copy button. The dialog watches the chain and updates as soon as USDC lands. The Deposit dialog showing a USDC on Solana address with a QR code, a warning to send only USDC on Solana, and options to pay by card or bridge from another chain Transfer USDC to that address from any wallet or exchange that supports Solana (SPL) tokens. The chain, token, and mint are on [Limits and fees](/guides/limits-and-fees#network). Send only native Solana USDC at the documented mint. Bridged USDC, other tokens, and transfers on another chain are not credited and may be unrecoverable. Poll [`GET /v1/wallet`](/api-reference/user/get-wallet) until the balance reflects your transfer. Then [enable trading](#enable-trading) once, and you are ready to place parlays. ## Enable trading Enable trading before you place a parlay or withdraw. This delegates signing: you let Totalis sign transactions on your behalf inside a secure enclave, the protected environment that holds your wallet's private key. [Vaults and custody](/guides/vault-architecture) covers what that permission does and does not allow. You enable trading by approving a consent dialog in the app, once. The REST API cannot grant it. `POST /v1/wallet/enable-trading` checks that delegation is in place and returns its status. It does not grant the permission. On older accounts it also credits any pending welcome airdrop. Check the current state with [`GET /v1/wallet/delegation-status`](/api-reference/funds/delegation-status). ## How your balance is used Your money sits in two places: your wallet and your vault. You do not have to deposit before you trade. | Where the money is | What happens | | - | - | | Wallet | USDC you send lands here. | | Vault | When you place a parlay, your stake moves from the wallet into the vault and locks, in one step. | Trade eligibility is checked against your combined wallet and vault free balance. If the total covers your bet amount, the trade goes through. Two calls move money between the two on purpose: * [`POST /v1/vault/deposit`](/api-reference/funds/vault-deposit) pre-funds the vault from the wallet. Market makers use it to hold collateral ahead of quoting. * [`POST /v1/vault/withdraw`](/api-reference/funds/vault-withdraw) moves free balance back to the wallet. Field by field definitions of each balance are in the [Glossary](/guides/glossary#balance-fields). ## Withdrawing Use [`POST /v1/wallet/withdraw`](/api-reference/funds/wallet-withdraw) with a recipient and amount. ```bash theme={null} curl -X POST https://api.totalis.trade/v1/wallet/withdraw \ -H "X-API-Key: $TOTALIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipient": "", "amount_usdc": 50 }' ``` ### Where the money comes from One call reaches everything that is not locked in an open position. | Pool | What it holds | Withdrawable | | - | - | - | | Wallet | USDC you sent, minus holds for parlays you committed that have not funded yet | Yes | | Vault free balance | Settled winnings and unlocked collateral: `gross - locked` | Yes, moved to your wallet first | | Vault locked | Collateral behind your `executed` positions | No, until they settle or you cash out | The app shows the withdrawable total as **Available to withdraw** and the locked part as **In open positions**. If the amount exceeds your wallet balance, Totalis first moves the shortfall from your vault to your wallet. It waits for that transaction to confirm, then sends the external transfer. The response then carries `vault_signature` and `vault_amount` beside the transfer `signature`. ### Rules * Trading must be enabled. Withdrawing uses the same delegation as placing a parlay. If it has lapsed the call returns `409`, and re-enabling trading in the app restores it. * The key needs the `funds:withdraw` scope, the highest risk scope on a key. See [Authentication](/guides/authentication#scopes). * `recipient` must be a regular Solana wallet address. Token accounts, program ids, and derived addresses are rejected, because USDC sent to them is unrecoverable. Your own wallet address is rejected too. * The minimum is one base unit, `0.000001` USDC. The maximum is your wallet plus vault free balance. A larger amount returns `400` with the available breakdown in the message. * If the recipient has never held USDC, Totalis creates its token account and fronts the rent, about 0.002 SOL. That rent is billed back in USDC out of the withdrawal at the live SOL price: about 0.29 USDC on 11 September 2026. The response reports it as `chargeback_usdc`, what arrived as `delivered_usdc`, and sets `recipient_ata_created`. An amount too small to cover it is rejected. So is the call if the SOL price is briefly unavailable. Retry in a moment. * `Idempotency-Key` is honored and must be a UUID; anything else returns `400`. A repeat with the same key within 24 hours replays the original success without moving funds again. The vault deposit and withdraw calls follow the same rule. * The call has its own per-account rate limit, listed on [Limits and fees](/guides/limits-and-fees#rate-limits). ### When it fails | Response | Meaning | What to do | | - | - | - | | `400` | Bad recipient, your own address, or more than the available balance | Fix the request | | `409` | Wallet is no longer delegated | Enable trading in the app, then retry | | `502` with `error.code: PARTIAL_VAULT_ONLY` | The vault move succeeded, the external transfer did not. `error.details.vault_signature` shows the vault move | Retry with the same amount. The wallet now covers it, so the vault is not tapped again | | `502` with any other code | The transfer was broadcast but not confirmed in time | Look up the returned signature before retrying, so you do not send twice | The partial case returns `502`, not `200`, on purpose. An idempotent retry against a `502` runs again, where a cached `200` would replay a half-finished result. ### Withdrawing in the app **Withdraw** sits next to Deposit on the portfolio page and offers two routes. The API endpoint above covers only the first. The Withdraw dialog offering Send to a Solana wallet and Withdraw to another chain The Withdraw USDC form showing the amount available to withdraw, a recipient Solana address field, and an amount field The Unifold cash out dialog with token, chain, recipient address, amount with a 3 USD minimum, and processing time # Glossary Source: https://docs.totalis.trade/guides/glossary Every term, field, identifier, and unit convention on Totalis: the noun stack, the money fields and their formulae, microUSDC versus decimal USDC, and the aliases one value travels under. ## The noun stack One trade goes by four names over its life. The name changes at commit, and again when it lands on-chain. Quote request before you commit, parlay from commit, position from executed Quote request before you commit, parlay from commit, position from executed | Term | Stage | Definition | | - | - | - | | **Quote request** | Before commit | The price discovery object. Has its own `id` and `status` (`active`, then `committed`, `cancelled` or `expired`). Once it commits or dies it is gone. It never becomes a queryable record. | | **RFQ** | After commit | The API object created at commit, with its own `rfq_id` and status progression. [`GET /v1/rfqs/{id}`](/api-reference/parlays/get) returns it. | | **Parlay** | Any | The product name for the RFQ. Same object: say parlay in a UI or conversation, use `rfq` in code. | | **Position** | Once funded | The RFQ's on-chain footprint, present once the trade is funded on chain (`status` `executed`). Carries `position_id` and `position_pda`. | | **Leg** | Any | One outcome inside a parlay. Every leg must win for the parlay to pay out. | ## Money fields All of these are decimal USDC unless the name ends in `_micro`. See [Units](#units). | Field | Also called | Definition | | - | - | - | | `bet_amount` | | Your gross wager, before any fee. | | `taker_fee` | | 1% of `bet_amount`, taken upfront. Computed in integer microUSDC: `floor(bet_amount_micro * 100 / 10000)`. | | `user_stake` | `user_cost` before commit | Your net stake: `bet_amount - taker_fee`. Every payout is calculated from it. | | `payout_odds` | | The multiplier applied to `user_stake`, never to `bet_amount`. Range on [Limits and fees](/guides/limits-and-fees#parlay-construction). | | `total_payout` | `payout` at settlement | What the winning side receives before the profit fee: `user_stake * payout_odds`. | | `mm_risk` | `mm_cost` before commit | The maker's amount at risk, which the maker locks: `total_payout - user_stake`. | | `price_micro` | | On a cashout auction, the all-in amount the user receives, in integer microUSDC. | The full chain: ``` # bet_amount_micro is bet_amount in microUSDC (millionths of a USDC: 1000000 = 1 USDC) taker_fee = floor(bet_amount_micro * 100 / 10000) # integer microUSDC user_stake = bet_amount - taker_fee # both in microUSDC total_payout = user_stake * payout_odds mm_risk = total_payout - user_stake ``` Odds apply to the net stake, never to the gross bet. A 25 USDC bet at 4.0 odds pays out 99.00 before the profit fee, not 100.00, because the 1% taker fee comes off first. Pricing against `bet_amount` overprices every quote you submit. See [Limits and fees](/guides/limits-and-fees#fees). ## Units Any field whose name ends in `_micro` is an **integer of microUSDC**, where `1000000` equals 1.00 USDC. Every other money field is a **decimal USDC number**. | Convention | Fields | Example value | | - | - | - | | Integer microUSDC | `price_micro`, `min_price_micro`, `max_price_micro` | `24750000` is 24.75 USDC | | Decimal USDC | `bet_amount`, `user_stake`, `user_cost`, `mm_risk`, `mm_cost`, `total_payout`, `payout`, `amount`, `usdc_balance`, `vault_balance`, `locked_amount` | `24.75` | | Multiplier | `payout_odds` | `4.0` | | Basis points | `taker_fee_bps` | `100` is 1% | | Probability | market prices | `0.62` | Both conventions appear in the same payload on the cashout surface. Scale before you compare. Market prices are decimal probabilities from 0 to 1 on every venue. Kalshi cent prices are converted for you. ## Balance fields | Field | Source | Definition | | - | - | - | | `usdc_balance` | [`GET /v1/wallet`](/api-reference/user/get-wallet) | USDC sitting in your Solana wallet. | | `vault_balance` | `GET /v1/wallet` | Gross USDC in your vault, including collateral that is currently locked. | | `locked_amount` | `GET /v1/wallet` | Everything held or locked: bets you committed that have not funded yet, stakes on live positions, and maker-side collateral. | | `locked_collateral` | [`GET /v1/vault`](/api-reference/funds/get-vault) | The vault's own view of locked collateral. Already counted inside `locked_amount`, so do not subtract it twice. | ``` total USDC you control = usdc_balance + vault_balance available to trade = usdc_balance + vault_balance - locked_amount ``` ## Identifiers | Field | What it identifies | | - | - | | `user_id` | Your account, as a Privy DID (`did:privy:...`). | | `mm_id` | A market maker's account. Equal to that account's `user_id`, so also a Privy DID. Appears in the `mm:quotes:{mm_id}` channel name and the MM stream's `connected` event. | | `market_maker_id` | The maker attached to a specific quote object. Same kind of value as `mm_id`, under a different field name by channel. | | `counterparty_mm_id` | The maker who already backs a position. On a broadcast cashout auction, usually not the maker whose bid wins. | | `rfq_id` | The parlay, after commit. The primary key for looking up trades. | | `quote_id` | One maker's quote on a request. | | `position_id` | The 16-byte on-chain position id, hex encoded (32 lowercase chars, no `0x`). Opens a [cashout request](/api-reference/quote-service/cashout-create). Returned by [`GET /v1/mm/positions`](/api-reference/mm/list-positions) so a maker can match a broadcast cashout auction against their book. | | `position_pda` | The position's on-chain account address, on WebSocket `position:*` events. An address, not a lookup key. | | `cashout_request_id` | One cashout auction. Returned as `id` when you create it, and as `cashout_request_id` on the MM stream. | `counterparty_mm_id` names the maker who backs the position, not the bidder. Compare it with the winning quote's `market_maker_id`. They are equal only when the exit closes the position rather than transferring it. Treating `counterparty_mm_id` as the winner routes an acquisition to the wrong maker. ## Protocol fields | Field | Definition | | - | - | | `venue` | The underlying market platform: `kalshi` or `polymarket`. | | `market_ticker` | Venue-specific. Kalshi: the market ticker (`KXBTC-25FEB07-T100000`). Polymarket: the `condition_id`, a `0x`-prefixed hex string, not the slug or the question id. | | `side` | `yes` or `no`. On Polymarket both sides of a binary share one `condition_id` and differ only by this field. | | `exclusion_keys` | An array on every market. Two markets can share a parlay only if their arrays do not intersect. This is how correlated legs are blocked. | | `version` | Increments every time you edit a quote request. Editing invalidates every quote priced against the old version. | | `request_hash` | A digest of the request at a given `version`. Makers echo it back when quoting, which ties a quote to exactly what was priced. | | `book_seq` | A monotonically increasing sequence number on the SSE (Server-Sent Events) stream. When you commit, you send back the `book_seq` of the event that showed you the price, to prove the price was current. | | `resolution` | A leg's settled state. `void` overrides everything: one void leg cancels the whole position and refunds the stake, even if another leg already resolved `lost`. | | `outcome` | A settled parlay's result, per leg and overall. | ## Custody terms | Term | Definition | | - | - | | **Vault** | Your persistent on-chain Solana account, reused across every parlay. Holds USDC and tracks gross, locked, and free balances. | | **PDA** | Program Derived Address. The Solana account type the protocol's program controls. Your vault, each position, and the config are all PDAs. | | **TEE** | Trusted Execution Environment. The secure enclave that holds your Privy embedded wallet's private key. The key never leaves it. | | **Delegation** | The one-time permission you grant Totalis to sign inside that enclave on your behalf. Required before you can trade or withdraw. Revocable at any time. | | **Non-custodial** | Collateral sits in on-chain vaults the program controls, not in an account Totalis holds. | | **Portfolio margining** | Collateral sized against a maker's incremental worst-case exposure across their whole book, rather than each position in isolation. | ## Aliases A value can change name when a trade moves from pricing to settlement. Each pair below is the same number. | Before commit | After commit | Why they match | | - | - | - | | `user_cost` | `user_stake` | Both are your net stake, `bet_amount` minus the taker fee. | | `mm_cost` | `mm_risk` | Both are the maker's amount at risk. | | `total_payout` | `payout` | Both are the full amount paid to the winner. | | `id` on a cashout request | `cashout_request_id` | Both identify the same auction. | `mm_risk` and `total_payout - user_stake` are stored separately and can disagree in the last decimal place. To reconcile them exactly, work in microUSDC. ## Event naming Each channel uses its own separator. The separator tells you the channel and nothing else. | Channel | Grammar | Examples | | - | - | - | | SSE | bare or underscored | `best_quote`, `quote_request`, `quote_request_expired` | | WebSocket | `resource:verb` | `quote:accepted`, `position:settled` | | Webhook | `resource.verb` | `position.settled`, `parlay.status_changed` | The market maker (MM) SSE stream mixes separators: `quote_request` and `quote_request_expired` use underscores, while `quote_request:updated` uses a colon. Match on the exact strings in the [event catalog](/guides/event-catalog). # How do I Source: https://docs.totalis.trade/guides/how-do-i The ten things integrators ask first, each answered in two lines with the call to make. ## Get an API key In the app, open **Get an API key** in the top bar, or Settings, then API Keys. Name the key, pick a preset or tick scopes, and copy the key when it is shown. It is shown once. See [Authentication](/guides/authentication#getting-your-api-key). ## Get a market price No key needed. Prices are decimal probabilities from 0 to 1 on every venue. ```bash theme={null} curl "https://api.totalis.trade/markets?search=NFL&limit=1" ``` Read `yes_bid`, `yes_ask`, `no_bid`, `no_ask` and `last_price` on each market. See [List markets](/api-reference/markets/list). ## Find markets that can go in the same parlay Every market carries `exclusion_keys`. Two markets can share a parlay only if their arrays do not intersect. Check that yourself before you create the request. See [Limits and fees](/guides/limits-and-fees#parlay-construction). ## Place a parlay Three calls: create the quote request with your legs and a bet amount, watch the quote stream, then commit the quote you want. Funds move only once the maker confirms. See [How parlays work](/guides/lifecycle#place-a-parlay). ## Check my balance Call [`GET /v1/wallet`](/api-reference/user/get-wallet) for the wallet and [`GET /v1/vault`](/api-reference/funds/get-vault) for the vault's gross, locked, and free balances. What you can spend or withdraw is wallet plus vault free. See [Funding](/guides/funding#how-your-balance-is-used). ## Fund the account Send native Solana USDC to the `address` from `GET /v1/wallet`, or scan the QR under **Deposit** in the app. Placing a parlay moves the stake into the vault by itself, and you never need SOL. See [Funding](/guides/funding#send-usdc-to-your-wallet). ## Enable trading Once, in the app, when you first place a parlay. It delegates signing to Totalis inside a secure enclave. The API can only report whether it is on, through [`GET /v1/wallet/delegation-status`](/api-reference/funds/delegation-status). See [Vaults and custody](/guides/vault-architecture#delegated-signing). ## Know when a position settles Configure a webhook and handle `position.settled`. It is signed, retried, and delivered once your key holds `positions:read`. The WebSocket carries the same event live as `position:settled`. See [Webhooks](/guides/webhooks). ## Close a position early Open a cashout request on a live position, watch the stream for the best all-in price, and commit it. The auction runs about 10 seconds. See [Early cashout](/guides/early-cashout). ## Withdraw Call [`POST /v1/wallet/withdraw`](/api-reference/funds/wallet-withdraw) with a Solana recipient and an amount. It reaches your wallet and your vault free balance in one call. It needs trading enabled and the `funds:withdraw` scope. See [Funding](/guides/funding#withdrawing). # How parlays work Source: https://docs.totalis.trade/guides/lifecycle The path a parlay takes from quote request, through market maker pricing and commit, to on-chain settlement, and how to close one early. A parlay starts as a **quote request**: your legs, priced live by market makers. It becomes a real **position** once you commit to a quote, the maker confirms, and it funds on chain, all within seconds. From there it settles automatically when the underlying markets resolve, or you close it early. ## At a glance * A parlay is one bet on one or more legs, priced live by competing market makers. Limits are on [Limits and fees](/guides/limits-and-fees). * Requesting quotes holds nothing. From commit, your bet is held against your balance. It moves on chain once, when the maker confirms. * The position then goes live on chain and settles by itself when the last market resolves. * You can close a live position early through a cashout auction of about 10 seconds. How a parlay works: your slip, makers quote, one position, then it settles or you cash out early How a parlay works: your slip, makers quote, one position, then it settles or you cash out early ## Place a parlay Add your legs and a bet amount. Each leg names a market and a side (`yes` or `no`). The `market_ticker` is venue specific: * **Kalshi**: the market ticker, for example `KXBTC-25FEB07-T100000`. * **Polymarket**: the `condition_id`, the `0x` prefixed hex string. Not the slug and not the question id. Both sides of a binary share one `condition_id` and are told apart by `side`. ```bash theme={null} POST /v1/quote-requests { "legs": [ { "market_ticker": "KXBTC-25FEB07-T100000", "side": "yes", "venue": "kalshi" }, { "market_ticker": "0x4d2...", "side": "no", "venue": "polymarket" } ], "bet_amount": 25 } ``` Totalis checks each market, rejects correlated legs (markets whose exclusion rules conflict), and sends the request to market makers. The full field reference is on [Create quote request](/api-reference/quote-service/create), and the constraints are on [Limits and fees](/guides/limits-and-fees). Odds apply to your bet amount minus a 1% taker fee. A 25 USDC bet carries a 24.75 net stake, so do not expect the stake to match `bet_amount`. The fee locks with the stake and comes back to you if the parlay is voided. Makers receive the request and submit quotes with payout odds. You watch them arrive on the quote request [stream](/api-reference/quote-service/stream). The best one is always on top. | Field | Meaning | | - | - | | `user_cost` | Your net stake, `bet_amount` minus the taker fee. | | `payout_odds` | The multiple applied to your net stake, never to the gross bet. | | `total_payout` | `user_cost * payout_odds`. What you receive if every leg wins. | | `book_seq` | The sequence number of the stream event that showed you the quote. You send it back at commit to prove your pricing was current. | Accept a quote to lock the trade. Totalis selects the best still valid quote for the version of the request you saw, and creates the parlay with that quote already accepted. ```bash theme={null} POST /v1/quote-requests//commit { "expected_version": 3, "displayed_quote_id": "", "displayed_quote_book_seq": 5, "min_payout_odds_seen": 4.0 } ``` The call waits the few seconds it takes the maker to confirm. It then returns the parlay's real `rfq_id`, the accepted quote, and the payout odds, with the parlay already `confirmed`. The response's own `status`, `committed`, is the quote request's. If the commit fails, no funds moved and no trade happened. When the maker misses the confirmation window you get `409` with reason `mm_confirmation_timeout`, and your quote request stays open, so you can commit the next best quote. The rejection reasons are on [Errors](/api-reference/errors). The maker confirms, and your stake and the maker's collateral lock together, in one transaction, in [on-chain Solana vaults](/guides/vault-architecture). Network fees are sponsored, so you only ever need USDC. Status moves to `confirmed`, then `executed` once the position is live on chain. The position settles as soon as the result is decided: when any leg loses, or when every leg has resolved. * **Every leg wins.** You receive the full payout, `total_payout`, minus the profit fee. * **Any leg loses.** Your stake transfers to the maker, minus the fee on their profit. * **A leg is voided.** The whole position is cancelled and both sides' collateral is released. You get your full bet back, taker fee included, and nobody wins or loses. If a leg loses before any void is known, the position has already settled as a loss. Status becomes `settled`. Read the outcome and transaction signatures from [`GET /v1/rfqs/{id}`](/api-reference/parlays/get). ## Closing early You can leave a live parlay before settlement. Totalis runs a short auction among market makers. You see the best all-in price live and accept it or not. You are never filled at a price you did not see. [Early cashout](/guides/early-cashout) has the steps and the two ways an auction can resolve. ## Status reference In the API, a parlay is called an **RFQ** (request for quote). Its `status` field, returned by [`GET /v1/rfqs/{id}`](/api-reference/parlays/get), takes these values: | Status | Meaning | Funds | | - | - | - | | `accepted` | You committed a quote and the maker is confirming. The commit call waits through this. | held, not moved | | `confirmed` | Maker confirmed. Funding on chain. | locking | | `executed` | Live on chain. Eligible for early cashout. | locked | | `settled` | Resolved and paid out. | released | | `bought_back` | Closed early via a cashout that the backing maker won. | released | | `cancelled` | Ended before funding, or a leg was voided. | released or never moved | | `expired` | Ended before funding, for example when the maker missed confirmation. | hold released | The enum also contains `open` and `quoted`. They belong to an older flow. A parlay placed through a quote request is created at `accepted` and never shows them. Each quote has its own `status`: | Quote status | Meaning | | - | - | | `pending` | Live. | | `accepted`, `confirmed`, `executed` | The trade is progressing. | | `withdrawn` | The maker pulled it. | | `expired` | Its validity lapsed, or the maker missed confirmation. | | `rejected` | You accepted a different quote on the same request. | ## Constraints Leg counts, bet range, odds range, and every timing window live on [Limits and fees](/guides/limits-and-fees). # Limits and fees Source: https://docs.totalis.trade/guides/limits-and-fees Every constraint and fee on Totalis: parlay size, bet range, payout odds, timing windows, rate limits, and how the two fees are calculated. ## At a glance * 1 to 5 legs, 1 to 1000 USDC per bet, payout odds of at least 1.0001. * Two fees, both 1%: a taker fee on the bet, and a profit fee on the winner's profit at settlement. A 25 USDC bet at 4.0 odds pays 98.2575 (98.26 rounded) if every leg wins. * Odds apply to the net stake after the taker fee, never to the gross bet. * Every expiring object carries its own timestamp. Honor it over any default on this page. ## Parlay construction | Constraint | Value | Enforced at | | - | - | - | | [Legs](/guides/glossary#the-noun-stack) per parlay | 1 to 5 | [Create quote request](/api-reference/quote-service/create) | | Bet amount | 1 to 1000 USDC | Create quote request | | Payout odds | At least 1.0001 | [Submit quote](/api-reference/quote-service-mm/submit-quote) | | Leg combinations | No two legs' markets may share an `exclusion_keys` entry | Create quote request | Correlated legs are rejected. To avoid the rejection, read each market's [`exclusion_keys`](/guides/glossary#protocol-fields) from [List markets](/api-reference/markets/list-flat) and check for overlap before you create the request. ## Timing | Window | Default | Range | What it governs | | - | - | - | - | | Quote validity | 15 s | 5 to 60 s | How long a maker's quote stays live (`expires_in_ms`) | | Quote request expiry | 90 s | set by the server | How long your request accepts quotes. Each update restarts it. Not settable in the request | | Maker confirmation | 5 s | 5 to 8 s | How long the winning maker has to confirm after you commit. Your commit call waits for it | | Cashout auction | about 10 s | fixed | How long a cashout auction accepts bids | | Cashout cutoff | 30 s | configurable | Within this long of a position's or market's end, no cashout opens and no maker quote is accepted | | Cashout bid validity | 15000 ms | 5000 to 60000 ms | How long your cashout bid stays live | | WebSocket authentication | 10 s | fixed | How long you have to send the auth message after connecting | Do not hardcode these defaults. They are configurable. Every object that expires carries its own `expires_at` or `valid_until` timestamp, and that timestamp is what counts. If the maker misses the confirmation window, the commit returns `409` with reason `mm_confirmation_timeout` and the maker's id in `failed_mm_id`. Nothing moves and your quote request stays open, so you can commit the next best quote, or leave that maker out with `exclude_mm_ids`. ## Rate limits Each route draws from one of three buckets. | Bucket | Covers | Keyed by | Limit | | - | - | - | - | | Authenticated | Every route that needs a key, apart from the two rows below | your account | 300 / minute. Requests without a valid key get 100 per IP | | Public | Market data and referral lookups | source IP | 100 / minute | | Quote service | Quote requests and cashouts, for bettors and makers | source IP | 300 / minute by default | The exact route prefixes: * **Public:** `/markets*`, `/v1/markets*`, `/referral*`. * **Quote service:** `/v1/quote-requests*`, `/v1/cashout-requests*`, `/v1/mm/quote-requests*`, `/v1/mm/cashout-requests*`. * **Authenticated:** everything else under `/v1/*`, plus `/app/*`, `/rfqs*`, `/parlay-shares*`, and the flat `/mm/quotes*` and `/mm/positions*` aliases. They all draw down the same 300, including the maker reads at `/v1/mm/quotes` and `/v1/mm/positions`. These calls move money and carry their own per-account cap, counted on top of the shared 300: | Call | Limit | | - | - | | [`POST /v1/wallet/withdraw`](/api-reference/funds/wallet-withdraw) | 5 / minute | | [`POST /v1/vault/deposit`](/api-reference/funds/vault-deposit), [`POST /v1/vault/withdraw`](/api-reference/funds/vault-withdraw) | 10 / minute each | | [`POST /v1/wallet/submit-tx`](/api-reference/user/submit-tx) | 10 / minute | When you size a client: * Market reads and quote service calls do not draw down your authenticated budget. * Sending an API key to a public read does not raise its limit. Those routes never resolve the key. * Per-IP buckets count the source IP, not the key. A market maker fleet behind one NAT shares one 100 / minute market-read budget and one quote service budget across every process and key. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. A `429` carries a `Retry-After` header, in seconds. Main API routes also put it in `error.retry_after`; quote service routes send only the header. See [Errors](/api-reference/errors). `X-RateLimit-Limit` reports the ceiling that applied, not the bucket. Two routes can both report `100` from different buckets. Watch `X-RateLimit-Remaining` to tell them apart. ## Fees Totalis charges two fees, at different times and on different bases. | Fee | Rate | Base | When | | - | - | - | - | | Taker fee | 1% | Your gross `bet_amount` | Locked with your stake when the trade funds. Kept at settlement or cashout, returned if a leg is voided | | Profit fee | 1% | The winning side's profit | At settlement, or on a closing cashout | The taker fee comes off first. Odds apply to what remains, never to your gross bet. ``` # bet_amount_micro is bet_amount in microUSDC (millionths of a USDC: 1000000 = 1 USDC) taker_fee = floor(bet_amount_micro * 100 / 10000) # 1% = 100 bps, integer microUSDC user_stake = bet_amount - taker_fee # both in microUSDC; called user_cost before commit total_payout = user_stake * payout_odds mm_risk = total_payout - user_stake # called mm_cost before commit ``` The field names are defined in the [Glossary](/guides/glossary#money-fields). A 25 USDC bet: 1% taker fee, 24.75 net stake, 4.0 odds, 99 payout, 98.26 received after the 1% profit fee A 25 USDC bet: 1% taker fee, 24.75 net stake, 4.0 odds, 99 payout, 98.26 received after the 1% profit fee Example: you bet 25 USDC and a maker quotes 4.0 payout odds. Amounts are in USDC. | Step | Amount | | - | - | | Bet amount | 25.00 | | Taker fee (1%, upfront) | 0.25 | | Net stake (`user_stake`) | 24.75 | | Total payout at 4.0 odds | 99.00 | | Maker's locked risk (`mm_risk`) | 74.25 | | Your profit if every leg wins | 74.25 | | Profit fee (1% of profit) | 0.7425 | | You receive | 98.2575 | The fee rate is fixed on the position when it is created. Settlement uses that rate, even if the rate changes while the position is live. ### Fees on an early exit An [early cashout](/guides/early-cashout) is a settlement, not a refund. The bid you accept is one all-in amount with the profit fee already included. No separate fee is deducted, and you pay no second taker fee for leaving. ### Referral share You earn a USDC share of the trading fees generated by traders you refer. The rate, claim minimum and accrual rules are on [Referrals](/referrals). ## Network | Item | Value | | - | - | | Chain | Solana mainnet | | Settlement asset | USDC (SPL) | | Mint | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | | Vault program | `58LfwWEnq1aCG2R18voCBF7zzJNtVSWquQBvdaaDFhme` | | Network fees | Sponsored by Totalis. Your wallet never needs SOL. | Send only native Solana USDC at the mint above. Bridged USDC, other tokens, and transfers on another chain are not credited and may be unrecoverable. # Market making Source: https://docs.totalis.trade/guides/market-maker Stream parlay quote requests, submit payout odds, confirm fills within seconds, bid on early cashouts, and size collateral with portfolio netting. Any account can make markets. You need an API key with the `mm:quote` scope, USDC to back your quotes, trading enabled once in the app (see [Funding](/guides/funding#enable-trading)), and a bot on two channels: the SSE quote request stream for pricing, and the WebSocket for fills. A request arrives, you quote, the user commits, you confirm within five seconds, collateral locks, settlement pays out A request arrives, you quote, the user commits, you confirm within five seconds, collateral locks, settlement pays out ## How it works Open [`GET /v1/mm/quote-requests/stream`](/api-reference/quote-service-mm/stream). You get a snapshot of every active request, then updates. Match event names exactly as the [event table](/api-reference/quote-service-mm/stream#sse-event-types) lists them. Each leg carries `current_yes_price` and `current_no_price`, the venue prices when the request was created, as a starting point for your own pricing. Send `payout_odds` for the exact `version` and `request_hash` you received, with [`PUT /v1/mm/quote-requests/{id}/quote`](/api-reference/quote-service-mm/submit-quote). Odds apply to the net stake, `user_stake`, never to `bet_amount`. Quoting off the gross bet overprices every quote. See [Limits and fees](/guides/limits-and-fees#fees). When a user commits your quote, you get `quote:accepted` on the `mm:quotes:{mm_id}` channel of the [WebSocket](/guides/websocket), with the `quote_id` and a `confirmation_deadline` a few seconds away. Confirm with [`POST /v1/mm/quotes/{quoteId}/confirm`](/api-reference/mm/confirm-quote) before that timestamp. The SSE stream only says a request closed, so keep the socket open. Confirm with the event's `quote_id`, not the draft id your submit returned. After a disconnect or a restart, list [`GET /v1/mm/quotes?status=accepted`](/api-reference/mm/list-quotes) and confirm anything still open. ## Endpoints | Step | Endpoint or event | | - | - | | Check your balance | [`GET /v1/wallet`](/api-reference/user/get-wallet) | | Stream quote requests | [`GET /v1/mm/quote-requests/stream`](/api-reference/quote-service-mm/stream) | | Submit or replace a quote | [`PUT /v1/mm/quote-requests/{id}/quote`](/api-reference/quote-service-mm/submit-quote) | | Withdraw a quote | [`DELETE /v1/mm/quote-requests/{id}/quote`](/api-reference/quote-service-mm/withdraw-quote) | | Receive fills | `quote:accepted` and `mm_quote:accepted` on `mm:quotes:{mm_id}` | | Confirm a fill | [`POST /v1/mm/quotes/{quoteId}/confirm`](/api-reference/mm/confirm-quote) | | Reconcile | [`GET /v1/mm/quotes`](/api-reference/mm/list-quotes) | | Bid on an early cashout | [`PUT /v1/mm/cashout-requests/{id}/quote`](/api-reference/quote-service-mm/cashout-quote) | ## Quoting rules * One live quote per request version. Submitting again replaces it. * Send only `request_version`, `request_hash`, `payout_odds` and, optionally, `expires_in_ms`. The server derives every money field from your odds and the net stake. * A new version invalidates your old quote. Re-price and submit again. There is nothing to withdraw. * `DELETE` a quote only to stand down on the current version before the user commits. * Submit and confirm take no `Idempotency-Key`. After a timeout, read [`GET /v1/mm/quotes?status=accepted`](/api-reference/mm/list-quotes) before you confirm again. Quote validity, the confirmation window, and the quote service's 300 requests a minute per IP are on [Limits and fees](/guides/limits-and-fees#timing). Re-quote when a quote is about to lapse or your price moves, not on a timer. ## Your balance Quotes are backed by wallet USDC plus your vault's free balance. The submit check (`insufficient_collateral`) and the confirm check both read that sum. Your vault is created with your first filled position. Until then keep USDC in the wallet. [`POST /v1/vault/deposit`](/api-reference/funds/vault-deposit) returns `404` before the vault exists, and is optional after. Live quotes do not reserve collateral. Each quote is checked on its own when you submit it, so if several fill at once, a confirm can fail for lack of funds. A missed confirmation loses the fill, and the bettor can leave you out of that request. ## Profit and loss | Outcome | You | | - | - | | Any leg loses | Receive the user's `user_stake`, minus the profit fee on it | | Every leg wins | Pay `mm_risk` | | A leg is voided | Get your collateral back. Nothing else moves | There is no maker P\&L endpoint yet. Record `user_stake` and `mm_risk` when you confirm, and settle them against `mm_result` on the maker `position.settled` webhook. ## Collateral and the ILP You do not lock each position's full `mm_risk` on its own. Totalis locks your worst-case loss across your whole book of open positions. It finds that number exactly by solving an integer linear program (ILP) over every way your markets can resolve, so positions that cannot all pay out at once share collateral. Two parlays that cannot both win: posted in isolation they lock 20,000, margined as a portfolio they lock 12,000 Two parlays that cannot both win: posted in isolation they lock 20,000, margined as a portfolio they lock 12,000 In the picture, two parlays cannot both win. Locked one by one they need 20,000 USDC. As a book they need 12,000, the larger of the two. | When | What locks | | - | - | | Your first open position | Its full `mm_risk` | | Each new position | Only the rise in your book's worst-case loss, from 0 up to that position's `mm_risk`. This is `mm_collateral_locked` on the position | | A position settles, is cancelled, or is bought back | Your worst case is recomputed, and collateral no longer needed returns to your vault free balance | Netting lowers what stays locked, not what you need to quote. Submitting a quote and confirming a fill both require wallet plus vault free balance to cover the quote's full `mm_risk`, plus the `mm_risk` of any fills not yet on chain. The solver treats these as linked, and everything else as independent: * The legs of one position. It pays only if every leg hits. * Markets in one event where only one can resolve yes, such as the winner of a game. * Kalshi strike ladders on the same underlier or game, such as BTC above 100k and above 90k, or a game's totals and spreads. * A team's total over a line and the game's total over the same line. * Markets that have resolved but not yet settled. They are fixed to their result. Treating two markets as independent can only overstate your worst case, never understate it. If a solve runs out of time you are locked a safe upper bound, never more than the plain sum of your `mm_risk`. In Totalis backtests the saving was 10% to 30% for generalist books, 20% to 40% for books on a single league, and over 70% for books of long-tail parlays with four or more legs. ## Early cashouts A user can leave a live position before it settles. Totalis broadcasts a short auction to every maker as a [`cashout_request`](/api-reference/quote-service-mm/stream#cashout_request) event on the stream you already read. Reply with one all-in `price_micro`, the amount the user receives in microUSDC, through [`PUT /v1/mm/cashout-requests/{id}/quote`](/api-reference/quote-service-mm/cashout-quote). The highest live bid wins, and ties go to the earliest. To pass, submit nothing. If you back the position, a win closes it, and the profit fee comes out of whichever side profits. If you do not, a win acquires it: the position moves to your account as the bettor side, and shows in `GET /v1/rfqs`. [Cashout auctions](/guides/cashout-auctions) has the rules, payload and errors. ## Your identity Your `mm_id` is your `user_id`, a Privy DID (`did:privy:...`). `auth:success` on the WebSocket and the [`connected`](/api-reference/quote-service-mm/stream#connected) event on the SSE stream both return it, and it names your `mm:quotes:{mm_id}` channel. `counterparty_mm_id` is not your id and not the bidder's id. It names the maker who already backs a position. See the [Glossary](/guides/glossary#identifiers). ## Get set up A key with the four read scopes and `mm:quote`. Bid on parlay exits and know which outcome you are in. Every rejection reason, and which to alert on. Every maker endpoint and event. To provide liquidity at scale, email [founders@totalis.trade](mailto:founders@totalis.trade). # Quickstart Source: https://docs.totalis.trade/guides/quickstart Call the public market data endpoint with no account, then get a scoped API key and make your first authenticated request. ## 1. Call the API right now Market data needs no key and no account: ```bash theme={null} curl "https://api.totalis.trade/markets?limit=3" ``` Every market in the response carries the `ticker` and `venue` you reference when you build a parlay leg (one outcome inside a parlay). [List markets](/api-reference/markets/list) has the filters for category, venue, and search. Everything past this point needs a key. ## 2. Get an API key Create a key from the [Totalis Dashboard](https://app.totalis.trade), under account settings, then API keys. Give it only the scopes (permissions) your integration needs. For a read-only start, `account:read`, `positions:read`, and `balances:read` are enough. The full scope catalog is on [Authentication](/guides/authentication). The API Keys settings page with a key name field, permission presets, and the scope checkboxes Copy the key when it is shown. It is displayed exactly once and cannot be retrieved later. Store it in an environment variable: ```bash theme={null} export TOTALIS_API_KEY="api_live_..." ``` ## 3. Make your first authenticated call Every request to `https://api.totalis.trade` carries your key in the `X-API-Key` header. Confirm it works by reading your profile: ```bash theme={null} curl https://api.totalis.trade/v1/me \ -H "X-API-Key: $TOTALIS_API_KEY" ``` ```json theme={null} { "data": { "id": "did:privy:...", "username": "satoshi", "wallet_address": "7Xy..." } } ``` Every successful response wraps the resource in a `data` envelope. | Response | Meaning | | - | - | | `200` | The key works. | | `401` | The key is missing, invalid, or revoked. | | `403` | The key is valid but lacks the scope for this endpoint, or does not start with `api_`. | ## 4. Read your account Pull your balances, open positions, and stats in one call: ```bash theme={null} curl https://api.totalis.trade/v1/portfolio \ -H "X-API-Key: $TOTALIS_API_KEY" ``` ## 5. Before you can trade Reads work as soon as your key does. Placing a parlay needs two more things: Send native Solana USDC to the address from [`GET /v1/wallet`](/api-reference/user/get-wallet). When you place a parlay, the stake moves into your on-chain vault by itself. See [Funding](/guides/funding). A one time consent in the app that lets Totalis sign trades on your behalf. You cannot do this from the REST API. Check the current state with [`GET /v1/wallet/delegation-status`](/api-reference/funds/delegation-status). ## Next steps The full path from quote request to settlement. Bet range, leg counts, timing windows, and both fees. Positions, history, settlement outcomes, and P\&L. Every field name and unit convention, defined once. # Quote requests in depth Source: https://docs.totalis.trade/guides/quote-requests The fields that protect a commit, where to find the reasons a commit is refused, the SSE event shape, and the quote request object. A **quote request** is the pre-trade price-discovery object (see the [Glossary](/guides/glossary#the-noun-stack)). For the full lifecycle, start with [How parlays work](/guides/lifecycle). ## Commit protection [Commit](/api-reference/quote-service/commit) requires four fields. Together they make sure you lock in the price you actually saw. | Field | Type | Description | | - | - | - | | `expected_version` | integer | The quote request `version` you're committing against. Rejected if the request was modified since your last update. | | `displayed_quote_id` | string | The `id` of the best quote you were shown. | | `displayed_quote_book_seq` | integer | The `book_seq` from the SSE event that delivered the quote. Proves your pricing was current. | | `min_payout_odds_seen` | number | The minimum payout odds you saw. Only quotes at or above this threshold can be selected. | ```json theme={null} { "expected_version": 3, "displayed_quote_id": "quote-uuid", "displayed_quote_book_seq": 5, "min_payout_odds_seen": 4.0 } ``` ## Rejection codes If a commit fails, `error.details.reason` tells you why. Each reason and what to do about it is on [Errors](/api-reference/errors#committing-a-quote). ## SSE event format Events on the user stream (`/v1/quote-requests/{id}/stream`) look like this: ```json theme={null} { "book_seq": 5, "version": 3, "best_quote": { "id": "quote-uuid", "payout_odds": 4.0, "user_cost": 24.75, "total_payout": 99, "mm_cost": 74.25, "valid_until": "2026-06-01T18:45:45.000Z" } } ``` When no quotes are active, `best_quote` is `null`. Store `book_seq` and `best_quote.id` from the latest event. You send them as `displayed_quote_book_seq` and `displayed_quote_id` when you commit. ## Quote request object ```json theme={null} { "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "version": 3, "request_hash": "sha256...", "book_seq": 5, "status": "active", "bet_amount": 25, "expires_at": "2026-06-01T18:45:30.000Z", "legs": [ { "id": "8ccf2c5d-...", "market_ticker": "KXBTC-26JUN05-T73500", "side": "yes", "venue": "kalshi" } ], "input_legs": [ { "market_ticker": "KXBTC-26JUN05-T73500", "side": "yes", "venue": "kalshi" } ] } ``` # Reading your data Source: https://docs.totalis.trade/guides/reading-data Pull account state on demand: portfolio, parlay history, settlement outcomes, P&L, and balances, for backfill and reconciliation. Read endpoints return your current state on demand. Use them to backfill, reconcile, and display. For events as they happen, use [webhooks](/guides/webhooks) or the [WebSocket](/guides/websocket). [Real time and data channels](/guides/realtime-and-data) compares all four channels. Authenticated reads take a scoped API key and live under `/v1`. Market data at `/markets` is public and needs no key. ## What to read | You want | Endpoint | Scope | | - | - | - | | Balance, stats, and status counts in one call | [`GET /v1/portfolio`](/api-reference/funds/portfolio) | `positions:read` + `balances:read` | | Vault balance and active positions | [`GET /v1/vault`](/api-reference/funds/get-vault) | `balances:read` | | Wallet address, all balances, locked funds | [`GET /v1/wallet`](/api-reference/user/get-wallet) | `balances:read` | | Your parlays, paginated, filterable by status | [`GET /v1/rfqs`](/api-reference/parlays/list) | `positions:read` | | One parlay's detail and settlement outcome | [`GET /v1/rfqs/{id}`](/api-reference/parlays/get) | `positions:read` | | Realized P\&L timeseries | [`GET /v1/pnl`](/api-reference/funds/pnl) | `positions:read` | | Your profile | [`GET /v1/me`](/api-reference/user/get-profile) | `account:read` | | Live market data | [`GET /markets`](/api-reference/markets/list) | none, public | ## Filtering parlays [`GET /v1/rfqs`](/api-reference/parlays/list) takes two query parameters: | Parameter | Behavior | | - | - | | `status` | Comma joined, for example `?status=executed,settled`. See the [status reference](/guides/lifecycle#status-reference). | | `include=quotes` | Attaches each parlay's `quotes` array. Without it, `quotes` is `null`, meaning not requested. With it, an empty array `[]` means no quotes were submitted. | ## P\&L windows [`GET /v1/pnl`](/api-reference/funds/pnl) returns realized P\&L per day. Pick the window with `?period=1D`, `1W`, `1M`, or `ALL`. The default is `1W`. Everything on the read surface is realized. `portfolio.summary.portfolio_value` is your deposited USDC. It does not mark open positions to market, and there is no unrealized P\&L field anywhere in the API. To value open positions, price each leg yourself against live [`/markets`](/api-reference/markets/list) data. ## Balances [`GET /v1/wallet`](/api-reference/user/get-wallet) returns every balance in one call. Each field is defined in the [Glossary](/guides/glossary#balance-fields). ``` total USDC you control = usdc_balance + vault_balance available to trade = usdc_balance + vault_balance - locked_amount ``` Do not subtract `/v1/vault`'s `locked_collateral` on top of `locked_amount`. It is already counted inside `locked_amount`. Subtracting both understates your available balance and makes a bot refuse trades it can afford. ## Settlement outcomes [`GET /v1/rfqs/{id}`](/api-reference/parlays/get) is the source of truth for how a parlay resolved. Once it is terminal, the response carries the full settlement detail: | Field | Contents | | - | - | | per-leg `outcome` | How each leg resolved. | | `status` | `settled`, `cancelled`, `expired`, or `bought_back`. | | payout | The amount paid to the winning side. | | `settle_tx`, `cancel_tx`, `buyback_tx` | On-chain transaction signatures. | A parlay id you do not own returns `404`. ## Reconciling Page through [`GET /v1/rfqs?status=settled`](/api-reference/parlays/list), following `meta.cursor` until `has_more` is false. For each parlay you have not recorded, fetch [`GET /v1/rfqs/{id}`](/api-reference/parlays/get) for the settlement detail and transaction signatures. Compare realized P\&L against [`GET /v1/pnl`](/api-reference/funds/pnl) and current balances against [`GET /v1/vault`](/api-reference/funds/get-vault). Cursors are opaque. Pass the `meta.cursor` value from the previous response as `?cursor=...`. Never parse or construct one. ### What reconciliation is the only way to catch Cashout bids that acquire a position you did not back. See [the acquisition gap](/guides/event-catalog#the-acquisition-gap). # Real time and data channels Source: https://docs.totalis.trade/guides/realtime-and-data Which channel to use when: SSE for live quote pricing, WebSocket for live trade events, webhooks for durable delivery, and REST reads for state on demand. Totalis has four data channels. Pick one by the phase of the trade you are in and the delivery guarantee you need. Which channel covers which stretch of a trade: quote stream before commit, WebSocket live after, webhooks from settlement, REST any time Which channel covers which stretch of a trade: quote stream before commit, WebSocket live after, webhooks from settlement, REST any time | Channel | What it carries | Use it for | | - | - | - | | [SSE streams](/api-reference/quote-service/stream) | Pre-trade pricing. Live `best_quote` updates on a quote request or cashout request. | Watching prices while you decide whether to commit | | [WebSocket](/guides/websocket) | Post-trade events, live. Quote acceptance, confirmation, position created and settled. | Live UIs and bots that hold a connection | | [Webhooks](/guides/webhooks) | Post-trade events, durable. The same lifecycle moments as HMAC signed POSTs with retries and replay. | Server systems of record that must never miss an event | | [REST reads](/guides/reading-data) | State on demand. Portfolio, parlays, settlement detail, P\&L, balances. | Backfill, reconciliation, on-demand display | All four authenticate with the same [API key](/guides/authentication). ## One trade, three phases **Pricing is SSE.** Each quote request has its own stream at [`GET /v1/quote-requests/{id}/stream`](/api-reference/quote-service/stream). Each cashout request has one at [`GET /v1/cashout-requests/{id}/stream`](/api-reference/quote-service/cashout-stream). A stream exists only while its request is live and closes after a terminal event. Market makers watch the firehose variant, [`GET /v1/mm/quote-requests/stream`](/api-reference/quote-service-mm/stream). **From commit onward it is events.** When you commit, the quote request becomes an [RFQ](/guides/glossary#the-noun-stack) and the SSE stream ends. Acceptance, confirmation, on-chain execution, and settlement arrive on the [WebSocket](/guides/websocket) and as [webhooks](/guides/webhooks). **State is always readable.** The [read endpoints](/guides/reading-data) return current truth whenever you ask. Use them to backfill after downtime and to reconcile what the push channels told you. ## WebSocket or webhooks Both deliver post-trade lifecycle events and overlap for most of the lifecycle. They differ in the delivery guarantee. | | WebSocket | Webhooks | | - | - | - | | Delivery | To a connected client, in real time | POST to your server, at-least-once | | Missed while disconnected | Not redelivered | Retried with backoff, then dead-lettered | | Signed | No, the connection is authenticated | Yes, HMAC per delivery | | Replay | No | [Yes](/guides/webhooks#replaying-deliveries) | | Best for | Live UIs and trading bots, where latency matters | Accounting, notifications, any system of record | ## Typical integrations ### Trading bot 1. Create a quote request with [`POST /v1/quote-requests`](/api-reference/quote-service/create). 2. Watch its [SSE stream](/api-reference/quote-service/stream) for `best_quote` events. Store `book_seq` and the quote `id`. 3. [Commit](/api-reference/quote-service/commit) when the price is right. 4. Follow the trade live on the WebSocket [`rfq:{rfq_id}` channel](/guides/websocket#subscribing-to-channels), or durably via the [`parlay.status_changed`](/guides/webhooks#event-catalog) webhook. 5. Record the outcome from the `position.settled` webhook, or read it from [`GET /v1/rfqs/{id}`](/api-reference/parlays/get). ### Read-only dashboard 1. Pull state with [`GET /v1/portfolio`](/api-reference/funds/portfolio), [`GET /v1/rfqs`](/api-reference/parlays/list), and [`GET /v1/pnl`](/api-reference/funds/pnl). 2. Subscribe to [`rfq:{rfq_id}`](/guides/websocket#subscribing-to-channels) for live position updates. 3. On reconnect, [reconcile](/guides/reading-data#reconciling) against the read endpoints. ### Market maker The maker loop, including cashout pricing, is on [Market making](/guides/market-maker). ## Every event, side by side The [event catalog](/guides/event-catalog) gives the exact event name each channel uses for every moment in a trade. # Vaults and custody Source: https://docs.totalis.trade/guides/vault-architecture How Totalis holds collateral: non-custodial on-chain Solana vaults, delegated signing inside a secure enclave, sponsored gas, and automatic settlement. Totalis is **non-custodial**: your funds sit in an on-chain Solana vault that the protocol's program controls, not in a Totalis held account. You enable trading once by delegating signing permission. After that, you place, settle, and close parlays without signing each transaction by hand. ## At a glance * One vault per account, with three balances: gross, locked, and free. * Totalis signs trades inside a secure enclave and can never extract your key. You can revoke the permission at any time. * Your stake and the maker's collateral lock in one atomic transaction when the maker confirms your trade. * Network fees are sponsored. Your wallet only ever needs USDC. Your wallet funds your vault, your stake and the maker's collateral lock in a position, the fee vault takes 1% of profit, and settlement follows the venues Your wallet funds your vault, your stake and the maker's collateral lock in a position, the fee vault takes 1% of profit, and settlement follows the venues ## Your vault Your vault is persistent and reused across every parlay. It tracks three balances: | Balance | Meaning | | - | - | | Gross | Total USDC in the vault. | | Locked | Collateral committed to your active positions. | | Free | `gross - locked`. Available to trade or withdraw. | Read them any time from [`GET /v1/vault`](/api-reference/funds/get-vault). To fund the vault, send USDC to your wallet (see [Funding](/guides/funding)). Placing a parlay moves your stake into the vault and locks it in one atomic step. ## Delegated signing Your wallet is a Privy embedded wallet. Its private key lives inside a Trusted Execution Environment, a secure enclave the key never leaves. Privy cannot extract it either. To trade, you grant Totalis permission to sign on your behalf inside that enclave. This is a one time consent in the app, described under [Enable trading](/guides/funding#enable-trading). While trading is enabled, Totalis can sign transactions for your wallet, including sending unlocked USDC. The API does that only for a withdrawal that you, or a key with `funds:withdraw`, request. You stay in control: | To | In the app | | - | - | | Stop Totalis signing for you | **Settings**, **Trading**, **Revoke**. Trading and API withdrawals stop until you enable trading again. | | Hold your own key | **Settings**, **Wallet**, export the private key, then use Phantom, Backpack or any Solana wallet. Move vault free balance to your wallet first. | ## Gas is sponsored Totalis pays the network fees (gas) for trading and settlement. Your wallet only ever needs to hold USDC, never SOL. ## How a position locks and settles When the maker confirms your trade, your stake and the maker's collateral lock together in a single atomic transaction. There is no window where one side is funded and the other is not. Settlement moves funds in exactly one of three ways: | Outcome | Result | | - | - | | You win | Your stake comes back and you receive the maker's collateral, minus the profit fee on your profit. | | You lose | Your stake transfers to the maker, minus the fee on their profit. | | A leg is voided | The position is cancelled and both sides' collateral is released. You get your full bet back, taker fee included. | The fee rate is written onto each position when it is created, so settlement always uses the rate that applied at trade time. Both fees and the full math are on [Limits and fees](/guides/limits-and-fees). ## What Totalis can and cannot do | Totalis can | Totalis cannot | | - | - | | Sign trading transactions inside the enclave, while your delegation is active | Extract or hold your private key | | Settle each leg to its venue's official result | Move locked collateral to an arbitrary address | | Send your unlocked USDC while trading is enabled, which the API does only on a withdrawal you request | Keep you from revoking trading or exporting your key | | Collect the protocol fee defined on the position | Change the fee rate on a position that is already live | | Cancel a position when a leg is voided | Change what a live position pays | Every vault, position, and settlement is on the Solana blockchain and independently verifiable. The vault program is `58LfwWEnq1aCG2R18voCBF7zzJNtVSWquQBvdaaDFhme`. # Webhooks Source: https://docs.totalis.trade/guides/webhooks Receive HMAC-signed events when your positions settle, get bought back, or your funds move. Configure an endpoint, verify signatures, handle retries, and replay deliveries. Totalis posts signed events to your server as they happen and retries failed deliveries with backoff. You receive only the events your key's scopes allow. Webhook delivery is still being enabled. Email [founders@totalis.trade](mailto:founders@totalis.trade) before you build on it. Until then, follow events on the [WebSocket](/guides/websocket) or by polling. Your endpoint belongs to your **account** (the principal), not to the key that created it. Any of your active keys with the right scope can manage it. Revoking one key among several does not stop delivery. Webhooks are the durable channel. For the same events live on a connection, use the [WebSocket](/guides/websocket). [Real time and data channels](/guides/realtime-and-data) compares the two. An event is signed and delivered to you; 2xx is accepted, 5xx is retried with backoff, other 4xx is dropped for replay An event is signed and delivered to you; 2xx is accepted, 5xx is retried with backoff, other 4xx is dropped for replay ## Configuring your endpoint Set your endpoint URL and the events you want with `PUT /v1/webhooks`. Viewing the endpoint needs `account:read`. Changing it needs `account:write`. Maker endpoints use `mm:quote` instead (see [Maker endpoints](#maker-endpoints)). ```bash theme={null} curl -X PUT https://api.totalis.trade/v1/webhooks \ -H "X-API-Key: " \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.example/totalis/webhooks", "events": ["position.settled", "position.bought_back", "funds.withdrawn"] }' ``` | Field | Rule | | - | - | | `url` | Must be HTTPS. Must not resolve to private or internal infrastructure. | | `events` | Any subset of the [event catalog](#event-catalog). An empty array parks the endpoint without deleting it: nothing matches. | Deliveries start only once a signing secret exists. After the `PUT`, generate one with `POST /v1/webhooks/rotate-secret`. The secret (`whsec_...`) is shown exactly once, so store it securely. You receive an event only if your key holds the scope that event requires. The scope is re-checked at delivery time, not just at subscribe time. A `position.settled` delivery needs `positions:read`. A `funds.*` delivery needs `balances:read`. ### Maker endpoints Market makers manage a separate endpoint by adding `?owner_kind=mm` to each webhook call. Viewing and changing it both need `mm:quote`, in place of `account:read` and `account:write`. A maker endpoint can subscribe only to the three events a backing maker is party to. Each `data` carries the maker's side of the position: | Event | Maker `data` fields | | - | - | | `position.settled` | `rfq_id`, `mm_id`, `mm_result` (`won` or `lost`), `outcome`, `tx_signature`, `occurred_at` | | `position.cancelled` | `rfq_id`, `mm_id`, `reason`, `occurred_at` | | `position.bought_back` | `rfq_id`, `mm_id`, `amount`, `mm_pays_user` (`true` when you paid the user, `false` when you collected), `tx_signature`, `occurred_at` | A `user` endpoint, the default, receives your own bettor-side event data from the catalog below. ## Event catalog | Event | Required scope | `data` fields | | - | - | - | | `position.settled` | `positions:read` | `rfq_id`, `outcome`, `tx_signature`, `occurred_at` | | `position.cancelled` | `positions:read` | `rfq_id`, `reason`, `occurred_at` | | `position.bought_back` | `positions:read` | `rfq_id`, `amount`, `mm_pays_user`, `tx_signature`, `occurred_at` | | `position.transferred` | `positions:read` | `rfq_id`, `price`, `buyer_wallet`, `tx_signature`, `occurred_at` | | `parlay.status_changed` | `positions:read` | `rfq_id`, `status`, `accepted_quote_id`, `occurred_at` | | `funds.deposited` | `balances:read` | `amount_usdc`, `vault_balance`, `occurred_at` | | `funds.withdrawn` | `balances:read` | `amount_usdc`, `vault_balance`, `occurred_at` | * **`position.bought_back`**: `amount` is what moves between you and the maker on top of your released stake, before the profit fee. `mm_pays_user` gives the direction. The seller always receives exactly the accepted price. * **`position.transferred`**: seller side only. Your position was taken over by a winning cashout bid from a maker other than the one backing it. * **`parlay.status_changed`**: tracks an RFQ's live progression. `status` is one of `accepted`, `confirmed`, `executed`, or `cancelled`. `accepted_quote_id` is set once a quote is accepted, else `null`. Settlement comes via `position.settled`, and expiry isn't part of this event, so use the `position.*` events for final outcomes. A cancellation fires both `parlay.status_changed` (`status: "cancelled"`) and `position.cancelled`. The two carry different event ids. If you subscribe to both, match them on `rfq_id`. ## Delivery format Each delivery is an HTTP `POST` with a JSON body and these headers: | Header | Description | | - | - | | `X-Totalis-Signature` | `t=,v1=`. See [verifying](#verifying-signatures). | | `X-Totalis-Event-Id` | Unique event id. Use it to deduplicate. | | `X-Totalis-Event-Type` | The event name, e.g. `position.settled`. | | `Content-Type` | `application/json` | The body is a stable envelope: `id`, `type`, and an event-specific `data` object. ```json theme={null} { "id": "evt_01HZX", "type": "position.settled", "data": { "rfq_id": "11111111-1111-1111-1111-111111111111", "outcome": "user_wins", "tx_signature": "5Hk...", "occurred_at": "2026-06-22T00:00:00.000Z" } } ``` Respond with any `2xx` status to acknowledge. Anything else is treated as a failure and retried. ## Verifying signatures The `X-Totalis-Signature` header has the form `t=,v1=`. The signature is `HMAC-SHA256(secret, ".")`, hex-encoded. To verify: 1. Parse `t` and `v1` from the header. 2. Reject if `t` is outside your tolerance. 5 minutes either side is a good default. This blocks replay. 3. Recompute the HMAC over the exact raw request body joined to the timestamp as `t.body`. 4. Compare to `v1` with a constant-time equality check. Compute the HMAC over the raw bytes of the request body, before any JSON parsing or re-serialization. Re-stringifying parsed JSON can reorder keys or change whitespace and will break the signature. ```ts Node / Express theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyTotalisWebhook( secret: string, signatureHeader: string, rawBody: string | Buffer, // express.raw() gives a Buffer; both are fine toleranceSec = 300, ): boolean { const fields = new Map(); for (const part of signatureHeader.split(",")) { const i = part.indexOf("="); if (i !== -1) fields.set(part.slice(0, i), part.slice(i + 1)); } const t = Number(fields.get("t")); const v1 = fields.get("v1"); if (!Number.isFinite(t) || !v1) return false; if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // Chained .update() signs "." then the raw body bytes, with no string coercion. const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(v1); return a.length === b.length && timingSafeEqual(a, b); } // Express: capture the raw body so you can sign over the exact bytes. // app.use("/totalis/webhooks", express.raw({ type: "application/json" })); ``` ```python Python / Flask theme={null} import hmac, hashlib, time def verify_totalis_webhook(secret: str, signature_header: str, raw_body: bytes, tolerance_sec: int = 300) -> bool: fields = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p) t, v1 = fields.get("t"), fields.get("v1") if not t or not v1: return False if abs(time.time() - int(t)) > tolerance_sec: return False signed = f"{t}.".encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) # Flask: use request.get_data() for the raw body, not request.json. ``` ## Idempotency & ordering * **Deduplicate on `X-Totalis-Event-Id`.** Delivery is at-least-once, so the same event can arrive more than once: a retry of a delivery that actually succeeded, or a [replay](#replaying-deliveries). Treat the event id as the unique key. * **Do not assume ordering.** Process each event on its own. Use `occurred_at` to reason about sequence. ## Retries & failure handling If your endpoint doesn't return `2xx`, the outcome depends on the response: | Response | Delivery status | | - | - | | `5xx`, `408`, `429`, timeout, network error | `failed`. Retried with exponential backoff. Once the retry budget is exhausted, `dead_letter`. | | Any other `4xx` | `dead_letter` immediately. Your endpoint rejected it, so a retry won't help. | Return `2xx` quickly and do heavy work asynchronously. A slow endpoint that exceeds the delivery timeout counts as a failed attempt. ## Inspecting & replaying deliveries `GET /v1/webhooks/deliveries` lists recent deliveries, newest first, with their status, attempt count, and last response code: ```json theme={null} { "deliveries": [ { "id": "...", "event_type": "position.settled", "status": "delivered", "attempts": 1, "response_code": 200, "created_at": "2026-06-22T12:00:00.000Z" } ] } ``` ### Replaying deliveries Re-queue a `delivered` or `dead_letter` delivery with `POST /v1/webhooks/deliveries/{id}/redeliver`. Use it to recover after fixing a bug or an outage on your side. Replays reuse the same `X-Totalis-Event-Id`, so your deduplication recognizes them. A `failed` delivery that is still mid-retry cannot be replayed manually. It is already scheduled for another attempt. Wait for it to land or dead-letter. ## Rotating your signing secret Rotate the secret any time with `POST /v1/webhooks/rotate-secret`. The new secret is shown once. The old one stops signing immediately, with no overlap window where both are valid. Generate the new secret. Capture it immediately, since it is shown only once. Deploy the new secret to your endpoint as fast as possible. Deliveries signed with the new secret fail verification until your verifier has it. From the deliveries list, replay any `dead_letter` deliveries from the swap window, including formerly `failed` ones once they exhaust retries. Replays are re-signed with the current secret. # WebSocket Source: https://docs.totalis.trade/guides/websocket Real-time trade confirmations and position updates via WebSocket. The Totalis WebSocket pushes post-trade events live: quote acceptances, confirmations, and position settlements. You connect, authenticate, subscribe to channels, and handle events as they arrive. For pre-trade pricing, use the SSE streams: the [maker stream](/api-reference/quote-service-mm/stream) for incoming quote requests, and the [quote-request stream](/api-reference/quote-service/stream) for the best current quote. Events reach you only while you are connected. Missed events are not redelivered. For durable delivery to a server, use [webhooks](/guides/webhooks). Connect, authenticate before the timeout, subscribe to your channel, and events arrive while pings keep it open Connect, authenticate before the timeout, subscribe to your channel, and events arrive while pings keep it open ## Connecting ``` wss://api.totalis.trade/ws ``` ## Authentication Send an auth message with your API key as soon as you connect: ```json theme={null} { "type": "auth", "api_key": "" } ``` The server answers with one of: ```json Success theme={null} { "type": "auth:success", "user_id": "did:privy:abc123", "user_type": "unified" } ``` ```json Error theme={null} { "type": "auth:error", "code": "INVALID_CREDENTIALS", "message": "Invalid API key" } ``` Every `auth:error` carries a `code`. Branch on `code`, not on `message`, which is not stable. | `code` | Meaning | Retry | | - | - | - | | `INVALID_CREDENTIALS` | The key is missing, malformed, invalid, or you are already authenticated. | No. Fix the credential. | | `TOKEN_INVALID` | A Privy JWT failed verification. | No | | `AUTH_TIMEOUT` | You connected but sent no auth message within 10 seconds. The socket closes with code `4001`. | Yes. Reconnect and authenticate. | | `SERVICE_UNAVAILABLE` | Authentication could not be completed on our side. | Yes, with backoff | ## Subscribing to channels After you authenticate, subscribe to each channel you want: ```json theme={null} { "type": "subscribe", "channel": "mm:quotes:" } ``` | Channel | Description | | - | - | | `mm:quotes:{mm_id}` | Your quote lifecycle updates (accepted, accepted details, confirmed, expired, withdrawn) | | `rfq:{rfq_id}` | Status changes, cancellation, expiry and position events for one trade | Your **`mm_id`** is the same value as your `user_id`: your Privy DID (`did:privy:...`). `auth:success` returns it, and so does the [`connected`](/api-reference/quote-service-mm/stream#connected) event on the MM SSE stream. ## Events ### Quote events | Event | Description | Data | | - | - | - | | `quote:accepted` | A user committed your quote. Confirm it before `confirmation_deadline`. The default is 5 s from acceptance but it is configurable, so always honor the timestamp. | `{ quote_id, rfq_id, confirmation_deadline }` | | `mm_quote:accepted` | Accepted quote details, sent only to the selected market maker. Use it to reserve local exposure or record analytics immediately. Its `quote_id` is valid for confirmation. Do not depend on its order relative to `quote:accepted`. | `{ rfq_id, quote_id, user_id, quote, exposure_legs, confirmation_deadline, accepted_at }` | | `quote:confirmed` | You confirmed the quote. Vault settlement begins. | `{ quote_id, rfq_id, execution_id }` | | `quote:executed` | On-chain vault position created. | `{ quote_id, rfq_id }` | | `quote:expired` | Quote expired before confirmation. | `{ quote_id, rfq_id }` | | `quote:rejected` | Another quote was accepted instead. | `{ quote_id, rfq_id, reason }` | | `quote:withdrawn` | You withdrew the quote. | `{ quote_id, rfq_id }` | `mm_quote:accepted` payload: ```json theme={null} { "rfq_id": "550e8400-e29b-41d4-a716-446655440000", "quote_id": "7d3a2f1e-4b8c-4d6e-9f0a-1b2c3d4e5f60", "market_maker_id": "did:privy:cm3mm...", "user_id": "did:privy:cm3abc...", "source": "quote_request_commit", "quote_request_id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40", "draft_quote_id": "b2c3d4e5-6789-0abc-def1-234567890abc", "request_version": 3, "request_hash": "sha256:...", "confirmation_deadline": "2026-06-05T12:00:05.000Z", "accepted_at": "2026-06-05T12:00:00.000Z", "quote": { "payout_odds": 7.08, "user_cost": 49.50, "total_payout": 350.46, "mm_cost": 300.96, "valid_until": "2026-06-05T12:00:10.000Z" }, "exposure_legs": [ { "venue": "kalshi", "event_ticker": "KXCAR", "market_ticker": "KXCAR-YES", "side": "yes" } ] } ``` `draft_quote_id` is the `quote.id` you got back when you [submitted your draft quote](/api-reference/quote-service-mm/submit-quote). Use it to see which of your drafts was lifted. Use `quote_id`, the accepted quote, for the [confirmation call](/api-reference/mm/confirm-quote). ### Parlay events Delivered on `rfq:{rfq_id}`. These are the user-facing mirror of the maker's `quote:*` events. | Event | Description | Data | | - | - | - | | `rfq:status` | The parlay moved to a new status: `accepted`, `confirmed`, `executed` or `cancelled`. Same moments as the `parlay.status_changed` webhook. | `{ rfq_id, status, updated_at, accepted_quote_id? }` | | `rfq:cancelled` | The parlay was cancelled. | `{ rfq_id, reason? }` | | `rfq:expired` | The parlay expired before it was funded. | `{ rfq_id }` | ### Position events `position_pda` is the position's on-chain account address, not a lookup key. To fetch or reference a position through the API, use `rfq_id`, or `position_id` once it is [set on the RFQ](/api-reference/parlays/get). | Event | Description | Data | | - | - | - | | `position:created` | Vault position created, collateral locked. | `{ rfq_id, position_pda, timestamp }` | | `position:settled` | Position settled with outcome. | `{ rfq_id, position_pda, outcome, tx_signature, timestamp }` | | `position:cancelled` | Position cancelled, collateral unlocked. | `{ rfq_id, position_pda, reason, timestamp }` | | `position:failed` | One attempt to create the position on chain failed and will be retried. Delivered on `rfq:{rfq_id}`, to the user, and to the maker. | `{ rfq_id, reason, retry_count, timestamp }` | | `position:error` | Position execution error. | `{ rfq_id, error, timestamp }` | | `position:bought_back` | A winning cashout bid closed the position early. See the note below. | `{ rfq_id, position_pda, amount, mm_pays_user, tx_signature, quote_id, timestamp }` | `position:bought_back` fans out to the `rfq:{rfq_id}` channel, to the `mm:quotes:{mm_id}` channel of the maker whose exposure closed, and directly to any connection authenticated as the seller or as that maker. Subscribe on `rfq:{rfq_id}` and dedupe on `tx_signature`. `amount` is decimal USDC. `quote_id` is the position's original accepted quote, not the winning bid. For bids that acquire a position instead, see [the acquisition gap](/guides/event-catalog#the-acquisition-gap). ### Balance events Delivered to every connection authenticated as you, with no subscription. Amounts are decimal USDC. | Event | Description | Data | | - | - | - | | `vault:deposited` | USDC moved into your vault, whether by a trade or by `POST /v1/vault/deposit`. | `{ amount, vault_balance, free_balance }` | | `vault:withdrawn` | USDC moved out of your vault. | `{ amount, vault_balance, free_balance }` | ## Keep-alive Send periodic pings to keep the connection open: ```json theme={null} { "type": "ping" } ``` The server replies: ```json theme={null} { "type": "pong", "timestamp": "2026-06-01T12:00:00.000Z" } ``` ## Example ```javascript theme={null} const ws = new WebSocket('wss://api.totalis.trade/ws'); ws.onopen = () => { ws.send(JSON.stringify({ type: 'auth', api_key: '' })); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'auth:success') { // Your mm_id is your user_id (Privy DID) ws.send(JSON.stringify({ type: 'subscribe', channel: `mm:quotes:${msg.user_id}` })); } if (msg.type === 'quote:accepted') { console.log('Quote accepted! Confirm before:', msg.data.confirmation_deadline); // POST https://api.totalis.trade/v1/mm/quotes/{quoteId}/confirm } if (msg.type === 'mm_quote:accepted') { console.log('Accepted quote details:', msg.data.exposure_legs); } if (msg.type === 'position:settled') { console.log('Position settled:', msg.data.outcome); } }; setInterval(() => { ws.send(JSON.stringify({ type: 'ping' })); }, 30_000); ``` # Get balances Source: https://docs.totalis.trade/hyperliquid/api-reference/account/get-balances /hyperliquid/openapi.json get /v1/me/balances Get your Totalis balance: your wallet's USDC on HyperEVM plus your free vault balance. Requires a key for your account with `account:read`. # Get identity Source: https://docs.totalis.trade/hyperliquid/api-reference/account/get-identity /hyperliquid/openapi.json get /v1/me Get the account your key acts for. Requires a key for your account with `identity:read`. # Get operation Source: https://docs.totalis.trade/hyperliquid/api-reference/account/get-operation /hyperliquid/openapi.json get /v1/me/operations/{operation_id} Get an on-chain operation and its latest chain status. Requires one of: - a key for your account with `account:read` - a key for a maker with `vault:read` and the maker permissions `READ_ACTIVITY`, `READ_CAPITAL`, `READ_PERFORMANCE`, `READ_POSITIONS` # List activity Source: https://docs.totalis.trade/hyperliquid/api-reference/account/list-activity /hyperliquid/openapi.json get /v1/me/activity List your account's activity. Requires one of: - a key for your account with `account:read` - a key for a maker with `vault:read` and the maker permission `READ_ACTIVITY` # List operations Source: https://docs.totalis.trade/hyperliquid/api-reference/account/list-operations /hyperliquid/openapi.json get /v1/me/operations List your account's on-chain operations. Requires one of: - a key for your account with `account:read` - a key for a maker with `vault:read` and the maker permissions `READ_ACTIVITY`, `READ_CAPITAL`, `READ_PERFORMANCE`, `READ_POSITIONS` # Get deployment Source: https://docs.totalis.trade/hyperliquid/api-reference/deployment/get-deployment /hyperliquid/openapi.json get /v1/deployment Get the deployment your signatures are made against: chain, vault, fees and USDC domain. No API key required; callers without one receive the public projection. A key, when sent, must be one of: - a key for your account - a key for a maker # Create collateral reduction Source: https://docs.totalis.trade/hyperliquid/api-reference/makers/create-collateral-reduction /hyperliquid/openapi.json post /v1/makers/{maker_id}/collateral-reductions Start a job that lowers your collateral requirement against correlated open positions. Requires a key for a maker with `vault:write`. # Get collateral reduction Source: https://docs.totalis.trade/hyperliquid/api-reference/makers/get-collateral-reduction /hyperliquid/openapi.json get /v1/makers/{maker_id}/collateral-reductions/{job_id} Get a collateral-reduction job. Requires a key for a maker with `vault:read` and the maker permission `READ_CAPITAL`. # Get maker capital Source: https://docs.totalis.trade/hyperliquid/api-reference/makers/get-maker-capital /hyperliquid/openapi.json get /v1/makers/{maker_id}/capital Get your maker vault's capital, reservations and free collateral. Requires a key for a maker with `vault:read` and the maker permission `READ_CAPITAL`. # Submit self-funded transaction Source: https://docs.totalis.trade/hyperliquid/api-reference/makers/submit-self-funded-transaction /hyperliquid/openapi.json post /v1/makers/{maker_id}/operations/{operation_id}/self-funded-transaction Submit a transaction your own gas wallet signed and pays for, such as a collateral reduction. Requires a key for a maker with `operations:submit`. # Get market Source: https://docs.totalis.trade/hyperliquid/api-reference/markets/get-market /hyperliquid/openapi.json get /v1/markets/{outcome_id} Get one market with its YES and NO sides, product details, current round and 24-hour stats. No API key required. # Get market history Source: https://docs.totalis.trade/hyperliquid/api-reference/markets/get-market-history /hyperliquid/openapi.json get /v1/markets/{outcome_id}/history Get up to 24 hours of one-minute price history for one side of a market. No API key required. # List markets Source: https://docs.totalis.trade/hyperliquid/api-reference/markets/list-markets /hyperliquid/openapi.json get /v1/markets List active markets, grouped by question. No API key required. # Cash out position Source: https://docs.totalis.trade/hyperliquid/api-reference/positions/cash-out-position /hyperliquid/openapi.json post /v1/me/positions/{position_id}/cashout Cash out a position by opening a cash-out RFQ. Requires a key for your account with `rfq:write`. # Get position Source: https://docs.totalis.trade/hyperliquid/api-reference/positions/get-position /hyperliquid/openapi.json get /v1/me/positions/{position_id} Get one of your account's positions. Requires one of: - a key for your account with `account:read` - a key for a maker with `vault:read` and the maker permission `READ_POSITIONS` # List positions Source: https://docs.totalis.trade/hyperliquid/api-reference/positions/list-positions /hyperliquid/openapi.json get /v1/me/positions List your account's positions. Requires one of: - a key for your account with `account:read` - a key for a maker with `vault:read` and the maker permission `READ_POSITIONS` # Accept quote Source: https://docs.totalis.trade/hyperliquid/api-reference/quotes/accept-quote /hyperliquid/openapi.json post /v1/quotes/{quote_id}/accept Accept a quote to open a position or cash one out. Requires a key for your account with `quote:accept`. Also requires `sponsorship:use` when Totalis pays the gas. Also requires `funding:deposit` when the request includes a funding authorization. Also requires `position:cashout` when the quote is a cash-out. # Cancel quote Source: https://docs.totalis.trade/hyperliquid/api-reference/quotes/cancel-quote /hyperliquid/openapi.json delete /v1/quotes/{quote_id} Cancel your quote. Requires a key for a maker with `quote:write`. # Confirm quote Source: https://docs.totalis.trade/hyperliquid/api-reference/quotes/confirm-quote /hyperliquid/openapi.json post /v1/quotes/{quote_id}/confirm Confirm or decline an acceptance of your quote. Requires a key for a maker with `quote:write`. # Create quote Source: https://docs.totalis.trade/hyperliquid/api-reference/quotes/create-quote /hyperliquid/openapi.json post /v1/rfqs/{rfq_id}/quotes Create a quote in response to an RFQ. Requires a key for a maker with `quote:write`. # Get acceptance Source: https://docs.totalis.trade/hyperliquid/api-reference/quotes/get-acceptance /hyperliquid/openapi.json get /v1/quotes/{quote_id}/acceptance Get a quote's acceptance, for example after a lost response. Requires a key for your account with `quote:read`. # List acceptances Source: https://docs.totalis.trade/hyperliquid/api-reference/quotes/list-acceptances /hyperliquid/openapi.json get /v1/makers/{maker_id}/acceptances List the acceptances of your quotes that wait for your confirm. Requires a key for a maker with `quote:read`. # List quotes Source: https://docs.totalis.trade/hyperliquid/api-reference/quotes/list-quotes /hyperliquid/openapi.json get /v1/rfqs/{rfq_id}/quotes List the live quotes on your RFQ. Requires a key for your account with `quote:read`. # Cancel RFQ Source: https://docs.totalis.trade/hyperliquid/api-reference/rfqs/cancel-rfq /hyperliquid/openapi.json post /v1/rfqs/{rfq_id}/cancel Cancel your RFQ. Requires a key for your account with `rfq:write`. # Create RFQ Source: https://docs.totalis.trade/hyperliquid/api-reference/rfqs/create-rfq /hyperliquid/openapi.json post /v1/rfqs Create an RFQ that asks makers to quote a new position. Requires a key for your account with `rfq:write`. # Get RFQ Source: https://docs.totalis.trade/hyperliquid/api-reference/rfqs/get-rfq /hyperliquid/openapi.json get /v1/rfqs/{rfq_id} Get an RFQ. No API key required; callers without one receive the public projection. A key, when sent, must be one of: - a key for your account with `rfq:read` - a key for a maker with `rfq:read` # List RFQs Source: https://docs.totalis.trade/hyperliquid/api-reference/rfqs/list-rfqs /hyperliquid/openapi.json get /v1/rfqs List open RFQs. No API key required; callers without one receive the public projection. A key, when sent, must be one of: - a key for your account with `rfq:read` - a key for a maker with `rfq:read` # Open stream Source: https://docs.totalis.trade/hyperliquid/api-reference/streams/open-stream /hyperliquid/openapi.json get /v1/stream Open the WebSocket that streams your account's or your maker's updates. Requires one of: - a key for your account with `account:stream` - a key for a maker with `rfq:read` - a key for a maker with `vault:read` and the maker permissions `READ_ACTIVITY`, `READ_CAPITAL`, `READ_PERFORMANCE`, `READ_POSITIONS` - a key for a maker with `quote:read` - a key for your account with `rfq:read` Also requires `replay:read` to resume from a cursor or replay retained events. Each key receives only the frames its scopes allow, and other frame classes are denied: | Frames | Key | Needs | | --- | --- | --- | | Open RFQ discovery | key for your account | `rfq:read` | | Account | key for your account | `account:stream` | | RFQ | key for a maker | `rfq:read` | | Quote | key for a maker | `quote:read` | | Account | key for a maker | `vault:read` and the maker permissions `READ_CAPITAL`, `READ_POSITIONS`, `READ_ACTIVITY`, `READ_PERFORMANCE` | # Create withdrawal Source: https://docs.totalis.trade/hyperliquid/api-reference/withdrawals/create-withdrawal /hyperliquid/openapi.json post /v1/me/withdrawals Withdraw USDC from your Totalis balance to an external address. Requires a key for your account with `funding:withdraw`, `sponsorship:use`. # Get withdrawal Source: https://docs.totalis.trade/hyperliquid/api-reference/withdrawals/get-withdrawal /hyperliquid/openapi.json get /v1/me/withdrawals/{withdrawal_id} Get a withdrawal. Requires a key for your account with `account:read`. # Authentication & signing Source: https://docs.totalis.trade/hyperliquid/authentication Scoped API keys, the scopes each route needs, and the EIP-712 and EIP-3009 signatures that move funds. ```bash theme={null} curl https://hip4-api.totalis.trade/v1/me \ -H "Authorization: Bearer $TOTALIS_API_KEY" ``` A key authenticates the call. The vault contract only moves funds with a signature from their owner, sent in the request body. Market reads need no key; an expired or revoked key on them still returns `401`. ## Keys There is one kind of key. Each key acts for one account, yours or a maker's, and holds only scopes that account can use. | Account | Approved by | Signs trades | Reads | | - | - | - | - | | Yours | Your Totalis wallet | Your Totalis wallet (`wallet` in [Get identity](/hyperliquid/api-reference/account/get-identity)) | `/v1/me/...` | | A maker (`maker_id`) | Your Totalis wallet | Your Totalis wallet (`signer` in [Get maker capital](/hyperliquid/api-reference/makers/get-maker-capital)) | `/v1/me/...` for the maker, plus `/v1/makers/{maker_id}/...` | * The key alone selects the account, chain and vault. No header or parameter does. * A key for a maker naming another `maker_id` gets `404`. * Keys start with `ttk_v1_prd_` (production) or `ttk_v1_stg_` (staging) and work only there. ## Create, rotate, revoke In the [Totalis app](https://hip4.totalis.trade/account/api-keys), under Account, then API keys: 1. Under **Keys act for**, choose **Your account** or a maker. 2. Enter a name, the [scopes](#scopes) and an expiry: 1, 7, 30 or 90 days, or Forever. Only the account's scopes are offered. 3. Your wallet approves with a Sign-In with Ethereum message. 4. Copy the key. It is shown once. Rotating takes the same approval and keeps the name and scopes. The old key can overlap for none, one hour or 24 hours. Revoking needs no signature and takes effect at once. A key that reports `SECRET_UNAVAILABLE` is lost: revoke it and create another. An API key cannot create, rotate or revoke keys. ## Scopes | Scope | Account | Grants | | - | - | - | | `identity:read` | Yours | Your identity and wallet | | `account:read` | Yours | Balances, positions, activity, operations, withdrawals | | `account:stream` | Yours | The [account channel](/hyperliquid/websocket/account) | | `rfq:read` | Either | Open RFQs and RFQ events | | `rfq:write` | Yours | Create and cancel RFQs | | `quote:read` | Either | Yours: quotes on your RFQs, your acceptances. A maker: acceptances of its quotes. | | `quote:accept` | Yours | Accept a quote | | `position:cashout` | Yours | Accept a cash-out quote | | `funding:deposit` | Yours | Fund an accept's shortfall from your wallet | | `funding:withdraw` | Yours | Withdraw | | `sponsorship:use` | Yours | Let Totalis pay the gas: accepts, cash-outs, withdrawals | | `quote:write` | Maker | Create, cancel and confirm quotes | | `replay:read` | Either | Resume the WebSocket from a cursor | | `vault:read` | Maker | Capital, positions, activity, operations, collateral reductions | | `vault:write` | Maker | Request a collateral reduction | | `operations:submit` | Maker | Submit a self-funded transaction | | Integration | Account | Scopes | | - | - | - | | Taker | Yours | `identity:read`, `account:read`, `account:stream`, `rfq:write`, `quote:read`, `quote:accept`, `position:cashout`, `sponsorship:use`, `funding:deposit` | | Taker that withdraws | Yours | The taker scopes plus `funding:withdraw` | | Maker | The maker | `rfq:read`, `quote:read`, `quote:write`, `replay:read`, `vault:read` | | Maker collateral | The maker | `vault:read`, `vault:write`, `operations:submit` | A change of the maker's owner invalidates every key of that maker. ## Wallet key To sign from code, export your Totalis wallet's key: Account, then Private key, then **Reveal private key**. Check its address equals `wallet` in `GET /v1/me`. The exported key also signs withdrawals. Anyone who has it can withdraw everything the account holds. Keep only the balance your bot needs. A maker bot signs its confirmations with the same key. To keep it off the bot, move signing to a key that holds no funds with [`setQuoteSigner`](/hyperliquid/market-making#move-maker-funds). ## Who signs what | Action | Signer | Message | Body field | | - | - | - | - | | Accept an entry quote | Your wallet | `TakerAccept` | `taker_signature` | | Accept a cash-out quote | The owner | `SellBack` or `CashoutTransfer` | `taker_signature` | | Confirm an entry quote | Your wallet | `Quote` | `maker_signature` | | Confirm a cash-out quote | Your wallet | `SellBack` or `CashoutTransfer` | `maker_signature` | | Fund an accept | Your wallet | USDC `ReceiveWithAuthorization` | `funding.authorization_signature` | | Withdraw: vault shortfall | Your wallet | `Exit` | `vault_withdrawal.signature` | | Withdraw: payout | Your wallet | USDC `TransferWithAuthorization` | `payout_authorization.authorization_signature` | Settling and claiming need no signature. Send signatures as lowercase hex with `v` of 27 or 28. EOA and ERC-1271 signatures are accepted. ## Domain Read [Get deployment](/hyperliquid/api-reference/deployment/get-deployment) before you sign; re-read the fees before each quote. ```json theme={null} { "chain_id": "999", "vault_address": "0xedf7131c57e0ac4eade509e1bf56a9ab7af43745", "fee_bps": 100, "taker_fee_bps": 20, "usdc": { "address": "0xb88339cb7199b77e23db6e890353e22632ba630f", "eip712_domain": { "name": "USDC", "version": "2", "chain_id": "999", "verifying_contract": "0xb88339cb7199b77e23db6e890353e22632ba630f" } } } ``` | Messages | EIP-712 domain | | - | - | | `Quote`, `TakerAccept`, `SellBack`, `CashoutTransfer`, `Exit` | `{ name: "TotalisParlay", version: "1", chainId: chain_id, verifyingContract: vault_address }` | | `ReceiveWithAuthorization`, `TransferWithAuthorization` | `usdc.eip712_domain` | ## Types ```text theme={null} Quote(uint64 makerId,Leg[] legs,uint128 stake,uint128 payout,uint16 feeBps,uint16 takerFeeBps,address taker,uint64 expiry,uint256 salt) TakerAccept(uint64 makerId,Leg[] legs,uint128 stake,uint128 payout,uint16 feeBps,uint16 takerFeeBps,address taker,uint64 expiry,uint256 salt) Leg(uint32 outcomeId,uint8 side) SellBack(bytes32 ticketId,uint128 price,address owner,uint64 expiry,uint256 salt,bytes32 legState) CashoutTransfer(bytes32 ticketId,uint128 price,address seller,address buyer,uint64 expiry,uint256 salt,bytes32 legState) Exit(address account,address to,uint128 amount,uint64 expiry,uint256 salt) ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce) TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce) ``` ## API names and signed names Signed field names differ from the API's. This is the only place they do. | API | Signed | | - | - | | `position_id` | `ticketId` | | `entry` terms | `Quote` and `TakerAccept` | | `sell_back`, `transfer`, `vault_withdrawal` | `SellBack`, `CashoutTransfer`, `Exit` | | `quote_id` | The EIP-712 digest of the quote's terms. For an entry quote it is also the new `position_id`. | | Leg `side`: `YES`, `NO` | `side`: `0`, `1` | | `maker_id`, `fee_bps`, `taker_fee_bps`, `leg_state`, `outcome_id` | `makerId`, `feeBps`, `takerFeeBps`, `legState`, `outcomeId` | ## USDC nonces | Authorization | Nonce | | - | - | | Accept funding | `keccak256(abi.encode(vault, chainId, from, salt, account))`, with `from` and `account` your wallet. Send `salt`, not the nonce. | | Withdrawal payout | `keccak256(abi.encode(keccak256("TOTALIS_PAYOUT_NONCE_V1"), chainId, vault, account, uint128(Idempotency-Key)))`. Not sent. | Signature windows are on [Limits](/hyperliquid/errors#limits). # Errors, pagination & limits Source: https://docs.totalis.trade/hyperliquid/errors Request conventions, idempotency, pagination, every limit and rate limit, and every error code of the Hyperliquid API. ## Requests | Kind | Encoding | Example | | - | - | - | | Amount | String, USDC atomic units (6 decimals). Never a JSON number. | `"12500000"` is 12.5 USDC | | Large number | Decimal string: outcome, chain, block, sequence | `"1209"` | | Leg | `outcome_id` and `side` | `{ "outcome_id": "1209", "side": "YES" }` | | Address | Lowercase `0x` hex. Input may be checksummed. | `"0xedf7131c57e0ac4eade509e1bf56a9ab7af43745"` | | `quote_id`, `position_id`, hash, signature | Lowercase `0x` hex. Opaque. | `"0x5e5e...5e5e"` | | `rfq_id`, `acceptance_id`, `withdrawal_id` | UUIDv7 | `"01a0df4e-5e07-7a3c-8f21-d4e6b7a90c1a"` | | Timestamp | RFC 3339 UTC. Signed terms use unix seconds. | `"2026-09-26T20:00:50Z"` | | Enum | Upper snake case. Ignore unknown values on reads. | `"ACCEPTED"` | An omitted optional field and `null` are not interchangeable unless the field says so. ### Idempotency A command `POST` sends an `Idempotency-Key`: a lowercase UUIDv7, new for every command. The routes in the table below are the exceptions. ```http theme={null} POST /v1/rfqs HTTP/1.1 Authorization: Bearer ttk_v1_prd_... Idempotency-Key: 01a0df4e-5dc8-71f2-a3d4-c5b6a7988771 Content-Type: application/json { "stake": "25000000", "legs": [{ "outcome_id": "1209", "side": "YES" }] } ``` | You send | You get | | - | - | | Same key, same body | The original response, with `Idempotency-Replayed: true` | | Same key, different body | `409 IDEMPOTENCY_KEY_REUSED` | | Route | Key | | - | - | | [Accept quote](/hyperliquid/api-reference/quotes/accept-quote) | Required. It becomes the `acceptance_id`, unique across all your quotes. | | [Confirm quote](/hyperliquid/api-reference/quotes/confirm-quote) | None. Sending one returns `INVALID_IDEMPOTENCY_KEY`. | | [Cancel quote](/hyperliquid/api-reference/quotes/cancel-quote) | None. | ## Pagination ```bash theme={null} curl "https://hip4-api.totalis.trade/v1/markets?limit=50&cursor=eyJnIjo2NDM3LCJhIjoxMjExLCJrIjoibWFya2V0In0" ``` ```json theme={null} { "items": [], "next_cursor": "eyJnIjo2NDM3LCJhIjoxMjYwLCJrIjoibWFya2V0In0", "has_more": true } ``` | Field | Rule | | - | - | | `limit` | 1 to 200, default 50. [Get market history](/hyperliquid/api-reference/markets/get-market-history) allows 1,440. | | `cursor` | Omit for the first page, then send `next_cursor` unchanged. Never parse it. | | `next_cursor` | `null` exactly when `has_more` is `false`. | | `has_more` | Keep paging until `false`. A page may hold fewer than `limit` items, even none. | ## Limits Current fees are in [Get deployment](/hyperliquid/api-reference/deployment/get-deployment). | Trading | Value | | - | - | | Legs per RFQ | 1 to 50, strictly ascending `outcome_id` | | Minimum stake | Net stake (after the entry fee) of at least 1 USDC | | Maximum payout | 100,000 USDC | | Fees | `taker_fee_bps` on the stake at accept, `fee_bps` on profit at claim; currently 20 and 100 | | Entry RFQ open | 20 seconds | | Cash-out RFQ open | 30 seconds | | Entry quote `expiry` | At most 30 seconds ahead. Quotes within 2 seconds of expiry are left out. | | Cash-out quote `expiry` | 25 to 60 seconds ahead. Quotes within 22 seconds of expiry are left out. | | Confirm deadline | 2 seconds after the accept. The accept responds at most 1 second later. | | Settlement and claim | About every 5 minutes | | Signing | Window | | - | - | | Accept funding | `valid_before` at least 5 seconds ahead and at most 900 seconds after `valid_after` | | Withdrawal `Exit` | `expiry` 5 to 3,600 seconds ahead, less a 5 second margin | | Withdrawal payout | At most 4,500 seconds from `valid_after` to `valid_before`; `valid_before` at least 300 seconds after the `Exit` `expiry`, or after now with no `Exit` | | Maker vault withdrawal delay | 86,400 seconds | | WebSocket and keys | Value | | - | - | | Replay window | 24 hours. An older cursor resets to a snapshot. | | Snapshot | 100 items per class | | Heartbeat | 15 seconds, or 1 second with `publication_freshness` | | Market data | 1 to 256 outcomes per subscription, frames up to 1 MiB | | API keys | 20 live per account; 1 to 90 days, or Forever | | Key approval message | 5 minutes, single use | | Request body | 64 KiB | ### Rate limits Over quota returns `429 RATE_LIMITED` with `Retry-After` in seconds. Each route also has a total limit across all callers, so it can arrive below these figures. | Route | Per account, per minute | | - | - | | [Create RFQ](/hyperliquid/api-reference/rfqs/create-rfq), [Cash out position](/hyperliquid/api-reference/positions/cash-out-position) | 30, shared | | [Accept quote](/hyperliquid/api-reference/quotes/accept-quote) | 30 | | [Create quote](/hyperliquid/api-reference/quotes/create-quote), [Cancel quote](/hyperliquid/api-reference/quotes/cancel-quote), [Confirm quote](/hyperliquid/api-reference/quotes/confirm-quote) | 120 each | | [List quotes](/hyperliquid/api-reference/quotes/list-quotes), [Get acceptance](/hyperliquid/api-reference/quotes/get-acceptance) | 600 each | | Position and capital reads | 120, shared | | [List activity](/hyperliquid/api-reference/account/list-activity) | 120 | Too many open RFQs on one account returns `429 RFQ_INTENT_LIMIT`. ## Errors ```json theme={null} { "error": { "code": "INVALID_REQUEST", "message": "The stake must be a positive integer.", "retry": "NEVER", "request_id": "0198f0a2-5c1e-7b3a-9f10-2d4c6e8a0b12", "field_violations": [{ "field": "/stake", "code": "INVALID" }] } } ``` | Field | Meaning | | - | - | | `code` | Stable. Branch on it. | | `message` | For logs. May change. | | `retry` | What to do next. | | `request_id` | This request's ID, also in `X-Request-ID`. | | `field_violations` | JSON pointers to invalid inputs, each with `REQUIRED`, `INVALID` or `UNEXPECTED`. | | `retry` | Do | | - | - | | `NEVER` | Stop. | | `BACKOFF` | Resend the same request, same `Idempotency-Key`, after `Retry-After`. | | `REFRESH` | Re-read the state, then send a new request with a new key. | | HTTP | Meaning | | -: | - | | `400` | Malformed request | | `401` | Missing, invalid, expired or revoked key | | `403` | Missing scope or permission | | `404` | Not found, or not visible to this key | | `409` | Conflicts with current state | | `422` | A business or signature rule rejects it | | `429` | Rate limited. Always `BACKOFF`. | | `503` | A dependency is unavailable. Always `BACKOFF`. | ### Codes Each [reference](/hyperliquid/api-reference/markets/list-markets) page lists its codes per status. WebSocket closes use the same codes. | Code | HTTP | Stream close | Retry | Meaning | | - | -: | -: | - | - | | `INVALID_REQUEST` | 400 | - | `NEVER` | The body, a path parameter or a header is malformed or fails validation. `field_violations` names invalid body fields. | | `INVALID_QUERY` | 400 | - | `NEVER` | A query parameter is malformed, out of range or not accepted by this operation. `field_violations` names it. | | `INVALID_CURSOR` | 400 | - | `NEVER` | The cursor is malformed or was issued for a different query. | | `INVALID_IDEMPOTENCY_KEY` | 400 | - | `NEVER` | The command needs exactly one lowercase UUIDv7 `Idempotency-Key` header; confirm takes none. | | `SUBPROTOCOL_REQUIRED` | 400 | 1008 | `NEVER` | The stream request did not negotiate a supported subprotocol or extension. | | `UNAUTHENTICATED` | 401 | - | `NEVER` | The credential is missing, invalid, expired or revoked. | | `FORBIDDEN` | 403 | - | `NEVER` | The credential is valid but lacks the scope, permission or role this operation needs. | | `NOT_FOUND` | 404 | - | `NEVER` | The resource does not exist or is not visible to this credential. | | `IDENTITY_NOT_FOUND` | 404 | - | `NEVER` | The account has no venue identity yet; bind the embedded wallet first. | | `QUOTE_NOT_FOUND` | 404 | - | `NEVER` | The quote does not exist. | | `IDEMPOTENCY_KEY_REUSED` | 409 | - | `NEVER` | The idempotency key was already used with different input. | | `STATE_CONFLICT` | 409 | - | `REFRESH` | The resource changed and no longer admits this request. | | `STALE_CURSOR` | 409 | - | `REFRESH` | The cursor belongs to an older catalog or browse generation; restart from the first page. | | `CURSOR_EXPIRED` | 409 | - | `REFRESH` | The account-read snapshot behind this cursor expired or its scope changed; restart from the first page. | | `CURSOR_RESET` | 409 | 1008 | `REFRESH` | The realtime cursor is no longer replayable; take a fresh snapshot. | | `RFQ_NOT_CANCELLABLE` | 409 | - | `NEVER` | The RFQ has progressed past cancellation. | | `RFQ_NOT_OPEN` | 409 | - | `REFRESH` | The RFQ is no longer open. | | `RFQ_GENERATION_STALE` | 409 | - | `REFRESH` | The RFQ generation the quote targets was superseded. | | `RFQ_GENERATION_IN_FLIGHT` | 409 | - | `REFRESH` | The RFQ generation is changing; re-read the RFQ. | | `QUOTE_EXPIRED` | 409 | - | `REFRESH` | The quote expired. | | `QUOTE_CANCELLED` | 409 | - | `REFRESH` | The maker cancelled the quote. | | `QUOTE_REPLACED` | 409 | - | `REFRESH` | A newer quote from the same maker replaced this one. | | `QUOTE_SIGNER_CHANGED` | 409 | - | `REFRESH` | The maker's quote signer changed after the quote was signed. | | `QUOTE_CAPACITY_EXCEEDED` | 409 | - | `REFRESH` | The maker's current capacity does not cover the quote. | | `CORE_FUNDING_CONFLICT` | 409 | - | `REFRESH` | Another Core funding is active for this account. | | `CORE_VAULT_RESERVATION` | 409 | - | `REFRESH` | The vault reservation for this Core funding changed. | | `CORE_MOVE_IN_FLIGHT` | 409 | - | `REFRESH` | A HyperCore transfer for this account is still in flight. | | `INVALID_SIGNATURE` | 422 | - | `NEVER` | The signature does not verify for the signed terms. | | `WRONG_AUTHORITY` | 422 | - | `REFRESH` | The signature is not from the current authority: the taker wallet for an accept, or the maker's current quote signer for a confirm. | | `RATE_LIMITED` | 429 | - | `BACKOFF` | The request quota for this credential or route is exhausted. | | `CONNECTION_LIMIT` | 429 | - | `BACKOFF` | The stream connection limit is reached. | | `RFQ_INTENT_LIMIT` | 429 | - | `BACKOFF` | The account has too many open RFQs. | | `QUOTE_EXPOSURE_LIMIT` | 429 | - | `BACKOFF` | The maker's open quote exposure is at its limit. | | `DEPENDENCY_STALE` | 503 | 1013 | `BACKOFF` | A projection or upstream the request depends on (chain projection, HyperCore state, database) is behind its freshness bound or unavailable. | | `AUTHORIZATION_UNAVAILABLE` | 503 | - | `BACKOFF` | Identity or credential authority storage is unavailable. | | `SOURCE_UNAVAILABLE` | 503 | 1013 | `BACKOFF` | The account-read or stream source is unavailable. | | `CONTRACT_UNAVAILABLE` | 503 | - | `BACKOFF` | This producer is disabled in this environment. | | `MARKET_DATA_UNAVAILABLE` | 503 | - | `BACKOFF` | No complete market catalog is available. | | `FINANCIAL_ACTIONS_DISABLED` | 503 | - | `BACKOFF` | Financial actions are disabled for this deployment. | | `CORE_FUNDING_UNAVAILABLE` | 503 | - | `BACKOFF` | Core funding cannot be admitted now. | | `SPONSORSHIP_UNAVAILABLE` | 503 | - | `BACKOFF` | Venue gas sponsorship is unavailable. | | `CONTRACT_SIGNATURE_OVERLOADED` | 503 | - | `BACKOFF` | Contract signature verification is at capacity. | | `QUOTE_EXPOSURE_STALE` | 503 | - | `BACKOFF` | The maker's exposure projection is stale. | | `COMBO_AUTOMATIC_DISABLED` | 503 | - | `BACKOFF` | Automatic combo quoting is disabled. | | `EMBEDDED_WALLET_UNAVAILABLE` | 503 | - | `BACKOFF` | The bound embedded wallet is unavailable. | | `CONNECTION_DRAIN` | 503 | 1012 | `BACKOFF` | The stream replica is draining; reconnect and resume from the last applied cursor. | | `INVALID_SUBSCRIPTION` | - | 1008 | `NEVER` | The first stream frame is not a valid subscribe. | | `SUBSCRIPTION_DENIED` | - | 1008 | `NEVER` | The credential may not subscribe to the requested channels or outcomes. | | `AUTHORIZATION_REVOKED` | - | 1008 | `NEVER` | The credential that opened the stream lost its authorization. | | `SLOW_CONSUMER` | - | 1008 | `BACKOFF` | The client fell behind; reconnect and resume from the last applied cursor. | | `RESNAPSHOT_REQUIRED` | - | 1008 | `REFRESH` | The stream cannot continue from this state; reload HTTP state and resubscribe. | | `SNAPSHOT_TOO_LARGE` | - | 1013 | `BACKOFF` | The snapshot exceeds the frame ceiling. | # Market making Source: https://docs.totalis.trade/hyperliquid/market-making Quote RFQs, confirm accepted quotes before the deadline, and manage your maker capital and collateral. Your Totalis wallet runs your maker. It owns the maker vault, the USDC that backs every confirmed quote, and signs your bot's confirms. ## Set up At [hip4.totalis.trade/mm](https://hip4.totalis.trade/mm): 1. Under **Your wallet**, add a little HYPE for gas and the USDC you will quote with. 2. Choose **Create maker account**: one transaction. 3. Choose **Fund vault**. 4. Create a key for the maker with the [maker scopes](/hyperliquid/authentication#scopes). 5. Run your bot with that key, your `maker_id` and your [wallet key](/hyperliquid/authentication#wallet-key). ## Quote 1. Read [Get maker capital](/hyperliquid/api-reference/makers/get-maker-capital). `free_collateral` backs new fills. 2. Subscribe to the [maker channel](/hyperliquid/websocket/maker). 3. Receive `RFQ_CREATED`. Price it from your own Hyperliquid market data. 4. [Create quote](/hyperliquid/api-reference/quotes/create-quote). It is unsigned. 5. Replace it with a new quote, or [cancel it](/hyperliquid/api-reference/quotes/cancel-quote). 6. Receive `QUOTE_ACCEPTED` with a `deadline`. 7. [Confirm quote](/hyperliquid/api-reference/quotes/confirm-quote) with your signature before the deadline, or decline. 8. Re-read capital on every `MAKER_CAPITAL_UPDATED`. ### Create quote ```bash theme={null} curl https://hip4-api.totalis.trade/v1/rfqs/$RFQ_ID/quotes \ -H "Authorization: Bearer $TOTALIS_MAKER_KEY" \ -H "Idempotency-Key: 01a0df4e-5f11-7a2b-9c3d-4e5f6a7b8c9d" \ -H "Content-Type: application/json" \ -d '{ "quote_id": "0x5e5e...5e5e", "entry": { "maker_id": "7", "legs": [{ "outcome_id": "1209", "side": "YES" }], "stake": "25000000", "payout": "98500000", "fee_bps": 100, "taker_fee_bps": 20, "taker": "0x1111111111111111111111111111111111111111", "expiry": "1790452875", "salt": "48151623429108" } }' ``` ```json theme={null} { "quote_id": "0x5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e" } ``` | Field | Rule | | - | - | | `legs`, `stake`, `taker` | Copied from the RFQ. | | `payout` | What you pay if every leg wins. | | `fee_bps`, `taker_fee_bps` | The current values in [Get deployment](/hyperliquid/api-reference/deployment/get-deployment). | | `expiry` | Unix seconds. | | `salt` | A random uint256. | | `quote_id` | The EIP-712 digest of `entry` as `Quote` ([Signing](/hyperliquid/authentication#types)). It becomes the `position_id`. | For a cash-out RFQ, price the position at `decision_block` and send `sell_back` terms if your maker backs it, or `transfer` terms if not. A `transfer` pays from your own vault balance, not the maker vault, and the position becomes yours. Keep that balance at zero to sell back only. ### Cancel quote ```bash theme={null} curl -X DELETE https://hip4-api.totalis.trade/v1/quotes/$QUOTE_ID \ -H "Authorization: Bearer $TOTALIS_MAKER_KEY" ``` ```json theme={null} { "status": "CANCELLED" } ``` `CANCELLED` or `REPLACED` means it is off the RFQ. `409` with `ACCEPTED` means you already confirmed it. ### Confirm quote `QUOTE_ACCEPTED` carries the exact terms, named by `kind`. Sign them (`entry` as `Quote`, `sell_back` as `SellBack`, `transfer` as `CashoutTransfer`) and answer with no `Idempotency-Key`: ```bash theme={null} curl https://hip4-api.totalis.trade/v1/quotes/$QUOTE_ID/confirm \ -H "Authorization: Bearer $TOTALIS_MAKER_KEY" \ -H "Content-Type: application/json" \ -d '{ "decision": "CONFIRM", "maker_signature": "0xcdcd...cd1c" }' ``` ```json theme={null} { "quote_id": "0x5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e", "rfq_id": "01a0df4e-5e07-7a3c-8f21-d4e6b7a90c1a", "status": "ACCEPTED", "deadline": "2026-09-26T20:00:53.352Z", "operation_id": "0x9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a" } ``` | `status` | Meaning | | - | - | | `ACCEPTED` | Queued as `operation_id`. | | `PENDING_FUNDING` | Waits for the taker's HyperCore payment, then `ACCEPTED` or `CANCELLED`. | | `DECLINED`, `TIMED_OUT`, `CANCELLED` | Nothing filled. | `{ "decision": "DECLINE" }` needs no signature. Resend the same answer if a response is lost; until you know the outcome, treat the quote as filled. ## Collateral ### Capital ```bash theme={null} curl https://hip4-api.totalis.trade/v1/makers/$MAKER_ID/capital \ -H "Authorization: Bearer $TOTALIS_MAKER_KEY" ``` ```json theme={null} { "context": { "as_of": "2026-09-26T12:00:02Z", "status": "CURRENT" }, "owner": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "signer": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "vault_gross": "5250000000", "stake_escrow": "150000000", "payable_owed": "40000000", "gross_open": "300000000", "requirement": "300000000", "free_collateral": "4760000000" } ``` A confirm fails when `free_collateral` cannot cover the fill. ### Move maker funds Your wallet calls the vault directly and pays the gas. Totalis does not relay these. | Vault call | Does | | - | - | | USDC `approve(vault, amount)`, then `depositMaker` | Deposits into the maker vault. | | `requestMakerWithdraw`, then `executeMakerWithdraw` | Withdraws after the [delay](/hyperliquid/errors#limits). | | `cancelMakerWithdraw` | Cancels a pending withdrawal. | | `setQuoteSigner` | Moves confirm signing to another key at once, so your bot never holds your wallet key. The zero address stops all fills. | | `resetDiscount` | Sets your collateral discount to zero. | ### Reduce collateral 1. [Create collateral reduction](/hyperliquid/api-reference/makers/create-collateral-reduction), with no body. 2. Follow `COLLATERAL_REDUCTION_UPDATED`, or [Get collateral reduction](/hyperliquid/api-reference/makers/get-collateral-reduction), until the job holds its `transaction`. 3. Sign that transaction with any gas wallet and send it to [Submit self-funded transaction](/hyperliquid/api-reference/makers/submit-self-funded-transaction). ```bash theme={null} curl -X POST https://hip4-api.totalis.trade/v1/makers/$MAKER_ID/collateral-reductions \ -H "Authorization: Bearer $TOTALIS_MAKER_KEY" \ -H "Idempotency-Key: 01998713-2f4a-7c3e-9b1d-6a0e4f5c8d20" ``` ```json theme={null} { "job_id": "01998713-2f4a-7c3e-9b1d-6a0e4f5c8d21", "status": "QUEUED", "next_eligible_at": "2026-09-26T14:07:00Z", "status_url": "/v1/makers/7/collateral-reductions/01998713-2f4a-7c3e-9b1d-6a0e4f5c8d21" } ``` # Overview Source: https://docs.totalis.trade/hyperliquid/overview Trade singles and parlays on Hyperliquid HIP-4 prediction markets through Totalis, as a taker or a market maker. Totalis lets you trade Hyperliquid HIP-4 prediction markets, one outcome (a single) or several (a parlay), at a price market makers quote. You accept a quote with a wallet signature, and the position settles on-chain in the Totalis vault on HyperEVM. This API is separate from the [Solana API](/guides/quickstart) and has its own keys. ## How an RFQ works | Step | Taker | Maker | | - | - | - | | 1 | [Create RFQ](/hyperliquid/api-reference/rfqs/create-rfq): legs and a stake. | Receives `RFQ_CREATED`. | | 2 | Receives `QUOTE_CREATED`. | [Create quote](/hyperliquid/api-reference/quotes/create-quote): the payout. | | 3 | [Accept quote](/hyperliquid/api-reference/quotes/accept-quote) with a signature. | Receives `QUOTE_ACCEPTED`. | | 4 | The accept response carries the maker's answer. | [Confirm quote](/hyperliquid/api-reference/quotes/confirm-quote) with a signature, or decline. | | 5 | Holds the position. | Backs it from the maker vault. | Totalis executes, settles and claims every position. There is no deposit step and no claim step. Guides: [Trading](/hyperliquid/trading) and [Market making](/hyperliquid/market-making). ## Base URL | Environment | URL | Chain | | - | - | - | | Production | `https://hip4-api.totalis.trade` | HyperEVM, chain ID `999` | | Staging | `https://hip4-api-staging.totalis.trade` | HyperEVM testnet, chain ID `998` | Send `Authorization: Bearer ` ([Authentication](/hyperliquid/authentication)). Market reads need no key. Amounts are strings in USDC atomic units: `"25000000"` is 25 USDC. ## Glossary | Term | Meaning | | - | - | | Market | One HIP-4 outcome (`outcome_id`) with a `YES` and a `NO` side. | | Leg | One outcome and one side: `{ "outcome_id": "1209", "side": "YES" }`. One leg is a single; two or more is a parlay. | | Taker | The account that creates an RFQ and accepts a quote. For a cash-out, the position's owner. | | Maker | A market maker (`maker_id`) that quotes, confirms and backs positions from its maker vault. | | RFQ | Request for quote. `ENTRY` prices new legs for a stake; `CASHOUT` prices an existing position. | | Quote | A maker's unsigned offer on one RFQ: `entry` terms (a `payout`), or `sell_back` or `transfer` terms (a `price`). | | Acceptance | The record of one accept on one quote, named by the accept's `Idempotency-Key` (`acceptance_id`). A quote has at most one. | | Confirm | The maker's answer to an acceptance before its `deadline`: `CONFIRM` with a signature, or `DECLINE`. | | Position | A filled trade on-chain: legs, stake, payout, owner and backing maker. An entry position's `position_id` is its `quote_id`. | | Stake, payout | What the taker pays, before the entry fee; what the position pays if every leg wins, before the profit fee. | | Cash-out | Selling an open position: a `sell_back` to its maker, or a `transfer` to another maker. | | Settlement, claim | Grading a resolved position, then paying its winnings into the owner's vault balance. Totalis runs both. | | Operation | One on-chain action Totalis relays for you, with a `status` (`operation_id`, `OPERATION_UPDATED`). | | Vault | The Totalis contract on HyperEVM that holds USDC and every position (`vault_address`). | | Totalis balance | Your wallet's USDC on HyperEVM plus your free vault balance. | | Wallet | Your Totalis embedded wallet. It approves your keys and signs your trades. | | Deployment | The chain, vault, fees and USDC domain every signature binds to ([Get deployment](/hyperliquid/api-reference/deployment/get-deployment)). | | Cursor | Your place in the [WebSocket](/hyperliquid/websocket/connection). Send it back to resume. | # Quickstart Source: https://docs.totalis.trade/hyperliquid/quickstart Get a Hyperliquid API key, make your first reads, and request your first quotes. ## 1. Get a key In the [Totalis app](https://hip4.totalis.trade/account/api-keys), go to Account, then API keys, choose **Your account** under **Keys act for**, pick the [taker scopes](/hyperliquid/authentication#scopes), and approve with your wallet. The key is shown once. ```bash theme={null} export TOTALIS_API_KEY="ttk_v1_prd_..." ``` ## 2. Read Markets need no key: ```bash theme={null} curl "https://hip4-api.totalis.trade/v1/markets?limit=1" ``` ```json theme={null} { "items": [ { "outcome_id": "1209", "title": "HYPE touches 100 by 20261001-0000", "state": "ACTIVE", "sides": [ { "side": "YES", "coin": "#12090" }, { "side": "NO", "coin": "#12091" } ] } ], "next_cursor": "eyJnIjo2NDM3LCJhIjoxMjExLCJrIjoibWFya2V0In0", "has_more": true } ``` Your identity checks the key: ```bash theme={null} curl https://hip4-api.totalis.trade/v1/me \ -H "Authorization: Bearer $TOTALIS_API_KEY" ``` ```json theme={null} { "account_id": "0198a6f6-82a5-7abc-9f2a-4f0fb4374941", "status": "ACTIVE", "wallet": "0x1111111111111111111111111111111111111111" } ``` `wallet` is your Totalis wallet. It signs your trades. ## 3. Request quotes Subscribe to your account first, so no quote is missed: ```bash theme={null} wscat -c wss://hip4-api.totalis.trade/v1/stream -s totalis.realtime.v2 \ -H "Authorization: Bearer $TOTALIS_API_KEY" > {"method":"subscribe","subscription":{"type":"account"}} ``` Then create an RFQ for 5 USDC on `YES` of outcome `1209`: ```bash theme={null} curl https://hip4-api.totalis.trade/v1/rfqs \ -H "Authorization: Bearer $TOTALIS_API_KEY" \ -H "Idempotency-Key: 01a0df4e-5dc8-71f2-a3d4-c5b6a7988771" \ -H "Content-Type: application/json" \ -d '{ "stake": "5000000", "legs": [{ "outcome_id": "1209", "side": "YES" }] }' ``` ```json theme={null} { "rfq_id": "01a0df4e-5e07-7a3c-8f21-d4e6b7a90c1a" } ``` Quotes arrive on the socket as `QUOTE_CREATED`, each with a `payout`. Generate a new lowercase UUIDv7 `Idempotency-Key` for every command. ## 4. Accept Sign a quote and accept it: [Trading](/hyperliquid/trading#accept). # TypeScript SDK Source: https://docs.totalis.trade/hyperliquid/sdk The @totalistrading/hip4-client SDK 0.14.0: install, the taker and maker flows, signing, the WebSocket, retries, and changes from 0.13.0. `@totalistrading/hip4-client` 0.14.0 wraps every route, both flows, every signed message and the WebSocket. It never stores a key: every signature goes to the signer you pass in. ## Install The SDK is private. Email [founders@totalis.trade](mailto:founders@totalis.trade) with the GitHub account that should read it, then: ```ini .npmrc theme={null} @totalistrading:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} ``` ```bash theme={null} npm install @totalistrading/hip4-client@0.14.0 viem ``` ESM, Node 22 or later, and browsers. `viem` or ethers only turns a private key into a signer. | Import | Holds | | - | - | | `@totalistrading/hip4-client` | `createTotalis`, `createClient`, `bestQuote`, `bestBid`, `ApiError`, `NetworkError`, `pages`, `items`, `unwrap`, `toTotalisSigner`, `TotalisStream`, `uuidv7` | | `.../maker` | `createMaker`, `createMakerClient`, the maker's vault calls | | `.../realtime` | `TotalisStream` and frame types | | `.../typed-data` | Builders and hashing for every signed message | | `.../signer` | `toTotalisSigner` for viem, EIP-1193 and ethers signers | | `.../errors` | The error registry, `ApiError`, `NetworkError` | | `.../api-keys` | Key issuance for apps with a signed-in user session | ## Clients ```ts theme={null} import { createClient, unwrap } from "@totalistrading/hip4-client"; const client = createClient("production", { accessToken: process.env.TOTALIS_API_KEY }); const me = unwrap(await client.GET("/v1/me")); ``` | Function | Calls | Signer | | - | - | - | | `createClient(environment, options)` | Any route for your account, or any public route, typed | None | | `createTotalis(options)` | `openPosition`, `cashOut`, `withdraw`, `signWithdrawal`, `sendWithdrawal`, `waitForWithdrawal`, `waitForOperation`, `watchQuotes`, `deployment` | Your Totalis wallet | | `createMakerClient(environment, options)` | Any maker route, typed | None | | `createMaker(options)` | `onRfq`, `quote`, `bid`, `cancelQuote`, `onAcceptance`, `confirm`, `deployment`, `controller`, `reduceCollateral` | Your wallet | `environment` is `"production"` or `"staging"`; `baseUrl` overrides it. Money is atomic USDC as a `bigint` or decimal string. `createTotalis` and `createMaker` expose `client` and `stream`; call `close()` when done. ## Open a position ```ts theme={null} import { createTotalis } from "@totalistrading/hip4-client"; import { privateKeyToAccount } from "viem/accounts"; const totalis = createTotalis({ environment: "production", accessToken: process.env.TOTALIS_API_KEY!, signer: privateKeyToAccount(process.env.TOTALIS_WALLET_KEY as `0x${string}`), }); const opened = await totalis.openPosition({ legs: [{ outcome_id: "1209", side: "YES" }], stake: 25_000_000n, quoteTimeoutMs: 10_000, }); if (opened.status === "ACCEPTED") await totalis.waitForOperation(opened.operationId!); ``` `openPosition` subscribes, creates the RFQ, picks a quote, signs, accepts with any funding, and recovers a lost accept. It returns `rfqId`, `acceptanceId`, `quoteId`, `positionId`, `entry`, `status` and `operationId`. | Option | Default | Does | | - | - | - | | `choose(quotes, serverNowMs)` | `bestQuote()`: highest payout with 5 seconds left | Returns one quote, or `undefined` to wait | | `funding` | `"wallet"` | `"wallet"` signs the exact shortfall; `"none"` never funds; a funding command is sent as is | | `quoteTimeoutMs` | | Cancels the RFQ and throws `TimeoutError` if nothing acceptable arrives | `status` can also be `NOT_ADMITTED`: every response was lost, and the SDK cancelled the RFQ. `watchQuotes(rfqId, listener)` reports live quotes, and `[]` while the stream is down. ## Cash out and withdraw ```ts theme={null} const sale = await totalis.cashOut({ positionId }); // choose defaults to bestBid(): 25 seconds left const done = await totalis.withdraw({ destination: "0x2222222222222222222222222222222222222222", amount: 15_000_000n, onStarted: (withdrawalId) => save(withdrawalId), }); ``` After a restart, `waitForWithdrawal(withdrawalId)` follows a started withdrawal. To store the signed request first, `signWithdrawal` returns `{ idempotencyKey, account, body }` and `sendWithdrawal(signed)` sends it. ## Make markets ```ts theme={null} import { createMaker } from "@totalistrading/hip4-client/maker"; const maker = createMaker({ environment: "production", apiKey: process.env.TOTALIS_MAKER_KEY!, makerId: process.env.TOTALIS_MAKER_ID!, signer: privateKeyToAccount(process.env.TOTALIS_WALLET_KEY as `0x${string}`), }); maker.onRfq(async (rfq) => { if (rfq.kind === "ENTRY") await maker.quote(rfq, { payout: (BigInt(rfq.stake) * 18n) / 10n }); else await maker.bid(rfq, { price: 18_000_000n }); }); maker.onAcceptance((acceptance) => (Date.parse(acceptance.deadline) > Date.now() ? "CONFIRM" : "DECLINE")); ``` * `onRfq` runs once per open RFQ, from events and the snapshot, and pages a truncated snapshot. * `quote` builds `entry` terms with the deployment's fees and a 20 second life (`lifetimeSeconds`). `bid` builds `sell_back` or `transfer` terms with a 45 second life. * `onAcceptance` checks the terms, signs them once, and resends a lost answer byte for byte. It never signs terms this maker did not create. * `published` stores the `quote_id`s this maker created (`record`, `forget`, `expiry`). Back it with durable storage so a restarted process can confirm them. * Pass `stream: { cursor }` to resume, and save `maker.stream.cursor` after each event. ```ts theme={null} const tx = await maker.controller(); // { chain_id, to, data, value } for your wallet to send await send(tx.approveUsdc(1_000_000_000n)); await send(tx.deposit(1_000_000_000n)); const { job, submitted } = await maker.reduceCollateral({ signTransaction: async ({ to, data }) => gasWallet.signTransaction(await gasWallet.prepareTransactionRequest({ to, data, type: "eip1559" })), }); ``` Vault call builders: `approveUsdc`, `deposit`, `requestWithdraw`, `executeWithdraw`, `cancelWithdraw`, `setQuoteSigner`, `resetDiscount`. ## Signing | Builder | Message | | - | - | | `takerAcceptTypedData(deployment, entry)` | `TakerAccept` | | `quoteTypedData(deployment, entry)` | `Quote` | | `sellBackTypedData(deployment, sellBack)` | `SellBack` | | `cashoutTransferTypedData(deployment, transfer)` | `CashoutTransfer` | | `exitTypedData(deployment, exit)` | `Exit` | | `receiveWithAuthorizationTypedData(deployment, authorization)` | USDC `ReceiveWithAuthorization` | | `transferWithAuthorizationTypedData(deployment, payout)` | USDC `TransferWithAuthorization` | `hashTypedData` gives a digest, `canonicalSignature` the form the API accepts, `depositNonce` and `payoutNonce` the USDC nonces. ## WebSocket and pagination ```ts theme={null} import { TotalisStream } from "@totalistrading/hip4-client/realtime"; const stream = new TotalisStream({ environment: "production", accessToken: process.env.TOTALIS_API_KEY!, subscription: { type: "account" }, // or { type: "maker", maker_id: "7" } }); stream.on("event", (event) => console.log(event.event_type, event.payload)); stream.start(); ``` It applies the snapshot or replay, drops duplicates, and reconnects from the last applied cursor. `status` is `live` only while caught up. ```ts theme={null} for await (const position of items( (query) => client.GET("/v1/me/positions", { params: { query } }).then(unwrap), (page) => page.items, )) console.log(position.position_id); ``` ## Retries and errors Each command is built once with its `Idempotency-Key` and resent byte for byte after a network failure or a `BACKOFF` error, up to five sends (`retry: { attempts }`, or `retry: false`). `unwrap` throws `ApiError` with `code`, `retry`, `status`, `requestId`, `fieldViolations` and `retryAfterMs`. ```ts theme={null} import { ApiError, NetworkError } from "@totalistrading/hip4-client"; try { await totalis.openPosition({ legs: [{ outcome_id: "1209", side: "YES" }], stake: 25_000_000n }); } catch (error) { if (error instanceof NetworkError || (error instanceof ApiError && error.retry === "BACKOFF")) { await error.resend?.(); // same bytes, same key } else throw error; } ``` `NetworkError` means the outcome is unknown. Call `resend()`, never a new command. ## Changes in 0.14.0 Breaking; needs contract 118. Signed messages and digests are unchanged. The full list ships as `CHANGELOG.md`. | 0.13.0 | 0.14.0 | | - | - | | `placeBet` | `openPosition` | | `cashOut({ ticketId })` | `cashOut({ positionId })` | | `watchBook` | `watchQuotes` | | `release()` | `deployment()` | | `maker.withdraw(rfqId, digest)` | `maker.cancelQuote(quoteId)` | | `maker.onConfirmation` | `maker.onAcceptance` | | `state` | `status` | | `{ outcome_id: 1209, side: 0 }` | `{ outcome_id: "1209", side: "YES" }` | | `ticket_id`, `quote_digest`, `attempt_id` | `position_id`, `quote_id`, `acceptance_id` | | `BookQuote`, `Confirmation`, `Release`, `MakerConfirmationRequest` | `Quote`, `Acceptance`, `Deployment`, `MakerAcceptance` | | Stream option `topic` | `subscription: { type: "account" }` or `{ type: "maker", maker_id }` | | `ticket:accept`, `ticket:cashout` | `quote:accept`, `position:cashout` | # Trading Source: https://docs.totalis.trade/hyperliquid/trading Open a position, cash it out, and let Totalis settle it: the taker flow on the Hyperliquid API. You need a key for your account with the [taker scopes](/hyperliquid/authentication#scopes), your [wallet key](/hyperliquid/authentication#wallet-key) to sign, and USDC on HyperEVM in your wallet (`usdc.address` in [Get deployment](/hyperliquid/api-reference/deployment/get-deployment)). Totalis pays the gas. ## Open a position 1. Subscribe to the [account channel](/hyperliquid/websocket/account) and wait for the snapshot. 2. [Create RFQ](/hyperliquid/api-reference/rfqs/create-rfq). 3. Collect `QUOTE_CREATED` events. 4. Sign one quote and [accept](/hyperliquid/api-reference/quotes/accept-quote) it. 5. Follow `OPERATION_UPDATED` until `COMMITTED`. ### Create RFQ Legs go in strictly ascending `outcome_id` order. One leg is a single; more is a parlay. ```bash theme={null} curl https://hip4-api.totalis.trade/v1/rfqs \ -H "Authorization: Bearer $TOTALIS_API_KEY" \ -H "Idempotency-Key: 01a0df4e-5dc8-71f2-a3d4-c5b6a7988771" \ -H "Content-Type: application/json" \ -d '{ "stake": "25000000", "legs": [{ "outcome_id": "1209", "side": "YES" }] }' ``` ```json theme={null} { "rfq_id": "01a0df4e-5e07-7a3c-8f21-d4e6b7a90c1a" } ``` ### Collect quotes Each `QUOTE_CREATED` carries one quote: ```json theme={null} { "quote_id": "0x5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e", "rfq_id": "01a0df4e-5e07-7a3c-8f21-d4e6b7a90c1a", "kind": "ENTRY", "sequence": 48213, "entry": { "maker_id": "7", "legs": [{ "outcome_id": "1209", "side": "YES" }], "stake": "25000000", "payout": "98500000", "fee_bps": 100, "taker_fee_bps": 20, "taker": "0x1111111111111111111111111111111111111111", "expiry": "1790452875", "salt": "48151623429108" } } ``` `payout` is what the position pays if every leg wins. `QUOTE_REMOVED` names a quote that left. A quote also stops at `expiry` (unix seconds), which sends no event. ### Accept Sign the quote's `entry` terms as `TakerAccept` ([Signing](/hyperliquid/authentication#who-signs-what)). The `Idempotency-Key` becomes the `acceptance_id`: choose it before the first send and reuse it on every retry. ```bash theme={null} curl https://hip4-api.totalis.trade/v1/quotes/$QUOTE_ID/accept \ -H "Authorization: Bearer $TOTALIS_API_KEY" \ -H "Idempotency-Key: 01a0df4e-6269-78d2-a4f6-a1b3c5d7e9f8" \ -H "Content-Type: application/json" \ -d '{ "taker_signature": "0xabab...ab1b" }' ``` The response waits for the maker's answer: ```json theme={null} { "acceptance_id": "01a0df4e-6269-78d2-a4f6-a1b3c5d7e9f8", "quote_id": "0x5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e", "rfq_id": "01a0df4e-5e07-7a3c-8f21-d4e6b7a90c1a", "status": "ACCEPTED", "deadline": "2026-09-26T20:00:53.352Z", "operation_id": "0x9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a" } ``` | `status` | Meaning | | - | - | | `ACCEPTED` | Confirmed. The trade is queued as `operation_id`. | | `PENDING_FUNDING` | Confirmed; waits for a HyperCore payment. Becomes `ACCEPTED` or `CANCELLED`. | | `DECLINED`, `TIMED_OUT`, `CANCELLED` | Nothing traded. Create a new RFQ. | Read later, an acceptance can also be `FILLED` or `FAILED`. ### Funding If your free vault balance is short of the stake, add `funding` to the accept. The vault draws exactly the shortfall in the same transaction as the trade. It needs `funding:deposit`. ```json theme={null} { "taker_signature": "0xabab...ab1b", "funding": { "kind": "WALLET", "value": "20000000", "valid_after": "1790452800", "valid_before": "1790453400", "salt": "0x0000000000000000000000000000000000000000000000000000000000000042", "authorization_signature": "0x9e9e...9e1b" } } ``` `value` is the stake minus `VAULT_FREE` in [Get balances](/hyperliquid/api-reference/account/get-balances), signed as USDC `ReceiveWithAuthorization`. `kind` is `WALLET`, `CORE` (HyperCore spot USDC) or `COMBINED`. ### Lost response Resend the identical request with the same `Idempotency-Key`. Or read [Get acceptance](/hyperliquid/api-reference/quotes/get-acceptance): a `404` means no accept was admitted; [cancel the RFQ](/hyperliquid/api-reference/rfqs/cancel-rfq) and read once more. A `404` after the cancel is final. Never accept the same quote under a second key. ### Follow the fill | Operation `status` | Meaning | | - | - | | `QUEUED`, `IN_FLIGHT`, `RETRYABLE`, `UNKNOWN` | In progress. | | `COMMITTED` | The position is on-chain. `POSITION_OPENED` follows. | | `REVERTED`, `EXPIRED_UNEXECUTED` | Nothing traded. No stake was taken. | The position's ID is the `quote_id` you accepted: ```bash theme={null} curl https://hip4-api.totalis.trade/v1/me/positions/$QUOTE_ID \ -H "Authorization: Bearer $TOTALIS_API_KEY" ``` ## Cash out [Cash out position](/hyperliquid/api-reference/positions/cash-out-position) opens a cash-out RFQ on an `OPEN` position you own. The body is empty. ```bash theme={null} curl https://hip4-api.totalis.trade/v1/me/positions/$POSITION_ID/cashout \ -H "Authorization: Bearer $TOTALIS_API_KEY" \ -H "Idempotency-Key: 01a0df58-a825-7f0e-9d2c-3b4a59687701" \ -H "Content-Type: application/json" \ -d '{}' ``` ```json theme={null} { "rfq_id": "01a0df58-a825-7f0e-9d2c-3b4a5968778f" } ``` Quotes arrive as `QUOTE_CREATED`. `kind` is `SELL_BACK` or `TRANSFER` and names the terms it carries: ```json theme={null} { "quote_id": "0x7c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d", "rfq_id": "01a0df58-a825-7f0e-9d2c-3b4a5968778f", "kind": "SELL_BACK", "sequence": 48240, "sell_back": { "position_id": "0xd9d9664d8d5996d51a8aea5a199441e519d09b797ea4b2a370016b03ce5a4e11", "price": "18000000", "owner": "0x1111111111111111111111111111111111111111", "expiry": "1790452905", "salt": "90210331458871", "leg_state": "0x0000000000000000000000000000000000000000000000000000000000000002" } } ``` | `kind` | Terms | Buyer | The position | | - | - | - | - | | `SELL_BACK` | `sell_back` | The maker that backs it | Closes (`close_reason: SELL_BACK`) | | `TRANSFER` | `transfer` | Another maker | Moves to the buyer | Sign `sell_back` as `SellBack` or `transfer` as `CashoutTransfer`, then accept it like an entry quote, with no `funding`. On `COMMITTED` you receive `POSITION_CLOSED` or `POSITION_TRANSFERRED`, and the price lands in your vault balance. ## Settlement Automatic. Totalis settles every resolved position, then claims a win into your vault balance. Nothing to call or sign. `SETTLED_UNCLAIMED` is the short wait between the two. | Position `status` | Meaning | | - | - | | `OPEN` | A leg is unresolved. You can cash out. | | `AWAITING_SETTLEMENT` | Resolved, or a leg lost; settlement has not landed. | | `SETTLED_UNCLAIMED` | A `WIN` with profit not yet claimed. | | `CLOSED` | Claimed (`CLAIM`), sold back (`SELL_BACK`), or settled with nothing to claim (`null`), per `close_reason`. | | `settlement_kind` | Meaning | | - | - | | `WIN` | Pays at least the net stake. The profit is claimed after. | | `PARTIAL` | Pays less than the net stake. | | `LOSS` | Pays nothing. | | `EARLY_LOST` | A leg lost before the others resolved. Pays nothing. | Each leg's result is `result_numerator / result_denominator`. The position pays its `payout` times the product of its legs' results. # Account channel Source: https://docs.totalis.trade/hyperliquid/websocket/account The account subscription: your RFQs, the quotes on them, your acceptances, operations, positions, balances and withdrawals. Needs a key for your account with `account:stream`. Connecting and resuming are on [Connection](/hyperliquid/websocket/connection). ## Subscribe ```json theme={null} { "method": "subscribe", "subscription": { "type": "account", "publication_freshness": true } } ``` | Field | Required | Meaning | | - | - | - | | `type` | Yes | `account`: the account your key acts for. | | `cursor` | No | Your last applied cursor. Needs `replay:read`. | | `open_rfqs` | No | `true` for every open RFQ, not only yours. Needs `rfq:read`. | | `account_reads` | No | `true` for `readInvalidated` frames. | | `publication_freshness` | No | `true` for a 1 second heartbeat and `publication` checkpoints. | ## Event ```json theme={null} { "channel": "event", "data": { "cursor": { "stream_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a60", "generation_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a61", "sequence": "184243" }, "event_id": "0199a1c9-3d44-7f60-8b53-8e0fbacd0004", "event_type": "QUOTE_CREATED", "occurred_at": "2026-09-26T20:00:05.412Z", "payload": { "quote_id": "0x2b7c9d1e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c", "rfq_id": "0199a1c9-0a11-7c3e-9f20-5b7d8e9fa001", "kind": "ENTRY", "sequence": 8812, "entry": { "maker_id": "3", "legs": [{ "outcome_id": "1209", "side": "YES" }], "stake": "25000000", "payout": "61000000", "fee_bps": 500, "taker_fee_bps": 20, "taker": "0x1111111111111111111111111111111111111111", "expiry": "1790193625", "salt": "73100245913027741890236401853207113049" } } } } ``` | `event_type` | When | `payload` | | - | - | - | | `RFQ_CREATED` | You created an RFQ, or any RFQ opened (with `open_rfqs`) | The open RFQ, as [Get RFQ](/hyperliquid/api-reference/rfqs/get-rfq), without `status` | | `RFQ_ACCEPTED`, `RFQ_CANCELLED`, `RFQ_FAILED` | An RFQ ended | `rfq_id`, `status`, `terminal_reason`, `created_at`, `expires_at` | | `QUOTE_CREATED` | A maker quoted your RFQ | The quote: `quote_id`, `rfq_id`, `kind`, `sequence`, and `entry`, `sell_back` or `transfer` | | `QUOTE_REMOVED` | A quote left your RFQ | `rfq_id`, `quote_id`, `status`: `CANCELLED`, `REPLACED` or `ACCEPTED`. Expiry sends no event. | | `OPERATION_UPDATED` | An operation changed status | `operation_id`, `kind`, `status`, `transaction_hash`, `updated_at` | | `POSITION_OPENED`, `POSITION_TRANSFERRED`, `POSITION_SETTLED`, `POSITION_CLOSED`, `POSITION_UPDATED` | A position changed on-chain | `position_id`, `block_number`, `log_index`. Read the position. | | `VAULT_BALANCE_CHANGED` | Your vault balance changed | `address`, `amount` (the new balance), `transaction_hash`, `status` | | `WALLET_BALANCE_CHANGED` | A USDC transfer touched your wallet | `address`, `transaction_hash`, `block_number` | | `WALLET_FUNDING_UPDATED`, `CORE_FUNDING_UPDATED` | An acceptance's funding changed | `acceptance_id`, `status`, `updated_at` | | `WITHDRAWAL_UPDATED` | A withdrawal changed | The withdrawal, as [Get withdrawal](/hyperliquid/api-reference/withdrawals/get-withdrawal) | | `CORE_EXIT_UPDATED` | A HyperCore exit changed | `exit_id`, `status`, `updated_at` | | `HYPERCORE_WITHDRAWAL_UPDATED` | A HyperCore withdrawal changed | `operation_id`, `status`, `updated_at` | ## Snapshot ```json theme={null} { "channel": "snapshot", "data": { "cursor": { "stream_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a60", "generation_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a61", "sequence": "184240" }, "resources": { "rfqs": [], "quotes": [], "acceptances": [], "operations": [] } } } ``` | Class | Holds | Pages the rest when `truncated` | | - | - | - | | `rfqs` | Your RFQs | [List RFQs](/hyperliquid/api-reference/rfqs/list-rfqs) | | `quotes` | Live quotes on your open RFQs | [List quotes](/hyperliquid/api-reference/quotes/list-quotes), per RFQ | | `acceptances` | Your acceptances in `PENDING_CONFIRM` or `PENDING_FUNDING` | [Get acceptance](/hyperliquid/api-reference/quotes/get-acceptance), per quote | | `operations` | Your operations | [List operations](/hyperliquid/api-reference/account/list-operations) | | `open_rfqs` | Every open RFQ, with `open_rfqs: true` | [List RFQs](/hyperliquid/api-reference/rfqs/list-rfqs) | # Connection Source: https://docs.totalis.trade/hyperliquid/websocket/connection Connect to the Hyperliquid WebSocket, send one subscribe frame, resume from a cursor, and handle heartbeats, drains and resets. | | Value | | - | - | | URL | `wss://hip4-api.totalis.trade/v1/stream` (staging: `wss://hip4-api-staging.totalis.trade/v1/stream`) | | Subprotocol | `totalis.realtime.v2` | | Auth | `Authorization: Bearer ` on the upgrade. Browsers send a second subprotocol, `auth.`, instead. | | Channels | [Account](/hyperliquid/websocket/account) (a key for your account), [Maker](/hyperliquid/websocket/maker) (a key for a maker) | Public market prices use a separate socket: [Market data](/hyperliquid/websocket/market-data). ## Subscribe Send one frame after the upgrade: ```json theme={null} { "method": "subscribe", "subscription": { "type": "account" } } ``` Response: ```json theme={null} { "channel": "subscriptionResponse", "data": { "method": "subscribe", "subscription": { "type": "account" }, "connection_id": "0199a1c8-7f01-7d10-9a2b-3c4d5e6f7081", "server_time": "2026-09-26T20:00:00.412Z", "heartbeat_interval_ms": 15000, "resume": "SNAPSHOT", "cursor": { "stream_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a60", "generation_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a61", "sequence": "184240" } } } ``` A first frame that is not a valid subscribe, or arrives late, closes with `INVALID_SUBSCRIPTION`. A subscription your key may not use closes with `SUBSCRIPTION_DENIED`. There is no unsubscribe: close the socket. ## Frames Every server frame is `{ "channel": ..., "data": ... }`. | `channel` | When | `data` | | - | - | - | | `subscriptionResponse` | Once, after your subscribe | Your subscription, `connection_id`, `server_time`, `heartbeat_interval_ms`, `resume`, `cursor` | | `snapshot` | After the response, when `resume` is `SNAPSHOT` or `RESET` | `cursor`, `resources`, and `truncated` when a class was cut short | | `event` | Each committed change, in order | `cursor`, `event_id`, `event_type`, `occurred_at`, `payload` | | `publication` | Each heartbeat, with `publication_freshness` | `cursor`, `source_cut_at`, `server_time` | | `readInvalidated` | With `account_reads`, when an account read changed | The `resources` to read again | | `marketRefresh` | After the snapshot, and when the market catalog changes | `catalog_generation`, `resource: "/v1/markets"` | | `drain` | The server is restarting | `deadline`, `retry_after_ms` | | `reset` | Right before a close with `CURSOR_RESET` | None | ```json theme={null} { "channel": "event", "data": { "cursor": { "stream_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a60", "generation_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a61", "sequence": "184241" }, "event_id": "0199a1c9-1b22-7d4e-8f31-6c8d9eafb002", "event_type": "OPERATION_UPDATED", "occurred_at": "2026-09-26T20:00:02.905114Z", "payload": { "operation_id": "0x5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e", "kind": "ACCEPT", "status": "COMMITTED", "transaction_hash": "0xa7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7", "updated_at": "2026-09-26T20:00:02.905114Z" } } } ``` `event_type` selects the shape of `payload`. Ignore event types you do not recognize. ## Snapshot The snapshot is your current state and the cursor it matches. Replace your local state with it, then apply events after that cursor. A class cut short is listed in `truncated` with the route that pages the rest: ```json theme={null} { "resource": "acceptances", "href": "/v1/makers/7/acceptances" } ``` Page that route once from the first page. Balances, positions and maker capital are not snapshot classes: read them over HTTP when you connect, and again on their events. ## Apply events * Delivery is at least once. Skip an event whose `event_id` you have seen, or whose `cursor.sequence` is not above yours. * Apply events in `sequence` order. Sequences skip numbers; a skipped number is not a gap. * Save the event's `cursor` after you apply it. ## Resume Send your last applied cursor. Resuming needs `replay:read`. ```json theme={null} { "method": "subscribe", "subscription": { "type": "account", "cursor": { "stream_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a60", "generation_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a61", "sequence": "184241" } } } ``` | `resume` | Do | | - | - | | `REPLAY` | Apply the events that follow, in order. | | `SNAPSHOT` | You sent no cursor. Apply the snapshot that follows. | | `RESET` | Your cursor is too old, ahead, or from another generation. Discard local state and apply the snapshot that follows. | The stream is the only replay; there is no HTTP event log. The replay window is on [Limits](/hyperliquid/errors#limits). ## Heartbeats, drain and reset * Keepalive is WebSocket ping and pong, every `heartbeat_interval_ms`. * On `drain`, open a new connection, resume from your cursor, then close the old one. The server closes it with `CONNECTION_DRAIN` by `deadline`. * On `reset`, discard local state and reconnect without a cursor. Every close carries a code from [Errors](/hyperliquid/errors#codes) as its reason. Its `retry` says what to do: | `retry` | Do | Codes | | - | - | - | | `BACKOFF` | Reconnect with backoff and resume from your cursor. | `SLOW_CONSUMER`, `CONNECTION_DRAIN` | | `REFRESH` | Reconnect without a cursor and apply the fresh snapshot. | `CURSOR_RESET` | | `NEVER` | Stop. | `SUBSCRIPTION_DENIED`, `AUTHORIZATION_REVOKED` | # Maker channel Source: https://docs.totalis.trade/hyperliquid/websocket/maker The maker subscription: every open RFQ to quote, the acceptances to confirm, and your maker operations, positions and capital. Needs a key for this `maker_id`. Connecting and resuming are on [Connection](/hyperliquid/websocket/connection). ## Subscribe ```json theme={null} { "method": "subscribe", "subscription": { "type": "maker", "maker_id": "7" } } ``` | Field | Required | Meaning | | - | - | - | | `type` | Yes | `maker`. | | `maker_id` | Yes | Your key's maker. Another maker's ID closes with `SUBSCRIPTION_DENIED`. | | `cursor` | No | Your last applied cursor. Needs `replay:read`. | | `account_reads` | No | `true` for `readInvalidated` frames. | | `publication_freshness` | No | `true` for a 1 second heartbeat and `publication` checkpoints. | A key receives only the frames its scopes allow: | Frames | Needs | | - | - | | `open_rfqs`, `RFQ_*` | `rfq:read` | | `acceptances`, `QUOTE_ACCEPTED` | `quote:read` | | `operations`, `OPERATION_UPDATED`, `POSITION_*`, `MAKER_CAPITAL_UPDATED`, `COLLATERAL_REDUCTION_UPDATED` | `vault:read` and the maker permissions `READ_ACTIVITY`, `READ_CAPITAL`, `READ_PERFORMANCE`, `READ_POSITIONS` | ## Event ```json theme={null} { "channel": "event", "data": { "cursor": { "stream_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a60", "generation_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a61", "sequence": "184244" }, "event_id": "0199a1c9-4e55-7071-9c64-9f10cbde0005", "event_type": "QUOTE_ACCEPTED", "occurred_at": "2026-09-26T20:00:06.101Z", "payload": { "quote_id": "0x2b7c9d1e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c", "rfq_id": "0199a1c9-0a11-7c3e-9f20-5b7d8e9fa001", "kind": "ENTRY", "deadline": "2026-09-26T20:00:08.101Z", "entry": { "maker_id": "3", "legs": [{ "outcome_id": "1209", "side": "YES" }], "stake": "25000000", "payout": "61000000", "fee_bps": 500, "taker_fee_bps": 20, "taker": "0x1111111111111111111111111111111111111111", "expiry": "1790193625", "salt": "73100245913027741890236401853207113049" } } } } ``` | `event_type` | When | `payload` | | - | - | - | | `RFQ_CREATED` | An RFQ opened | Entry: `rfq_id`, `kind: ENTRY`, `taker`, `stake`, `legs`, `created_at`, `expires_at`. Cash-out: `rfq_id`, `kind: CASHOUT`, `position_id`, `owner`, `maker_id`, `legs`, `legs_hash`, `leg_state`, `decision_block`, `decision_read_gas`, `created_at`, `expires_at`. | | `RFQ_ACCEPTED`, `RFQ_CANCELLED`, `RFQ_FAILED` | An RFQ ended. Stop quoting it. | `rfq_id`, `status`, `terminal_reason`, `created_at`, `expires_at` | | `QUOTE_ACCEPTED` | A taker accepted your quote | `quote_id`, `rfq_id`, `kind` (`ENTRY`, `SELL_BACK` or `TRANSFER`), `deadline`, and one terms object: `entry`, `sell_back` or `transfer`, per `kind`. Answer with [Confirm quote](/hyperliquid/api-reference/quotes/confirm-quote) before `deadline`. | | `OPERATION_UPDATED` | A maker operation changed status | `operation_id`, `kind`, `status`, `transaction_hash`, `updated_at` | | `POSITION_OPENED`, `POSITION_TRANSFERRED`, `POSITION_SETTLED`, `POSITION_CLOSED`, `POSITION_UPDATED` | A position your maker backs changed | `position_id`, `block_number`, `log_index` | | `MAKER_CAPITAL_UPDATED` | Your capital changed on-chain | `maker_id`, `block_number`, `log_index`. Read [Get maker capital](/hyperliquid/api-reference/makers/get-maker-capital). | | `COLLATERAL_REDUCTION_UPDATED` | A collateral reduction changed | `job_id`, `status`, `operation_id`, `updated_at` | An RFQ stops taking quotes at `expires_at`, which sends no event. ## Snapshot ```json theme={null} { "channel": "snapshot", "data": { "cursor": { "stream_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a60", "generation_id": "0199a1c2-3b4d-7e5f-8a6b-1c2d3e4f5a61", "sequence": "184240" }, "resources": { "open_rfqs": [], "acceptances": [], "operations": [] } } } ``` | Class | Holds | Pages the rest when `truncated` | | - | - | - | | `open_rfqs` | Every open RFQ | [List RFQs](/hyperliquid/api-reference/rfqs/list-rfqs) | | `acceptances` | Acceptances waiting for your confirm, earliest deadline first | [List acceptances](/hyperliquid/api-reference/quotes/list-acceptances) | | `operations` | Your maker's operations | [List operations](/hyperliquid/api-reference/account/list-operations) | # Market data Source: https://docs.totalis.trade/hyperliquid/websocket/market-data The public market data WebSocket: catalog entries and reference prices for the outcomes you select, as a snapshot then deltas. | | Value | | - | - | | URL | `wss://hip4-api.totalis.trade/v1/market-stream` (staging: `wss://hip4-api-staging.totalis.trade/v1/market-stream`) | | Subprotocol | `totalis.market.v2` | | Auth | None. The upgrade needs an allowed browser `Origin`, or it is refused with `ORIGIN_NOT_ALLOWED`. | Server integrations read [List markets](/hyperliquid/api-reference/markets/list-markets) and [Get market](/hyperliquid/api-reference/markets/get-market) instead, and price from their own Hyperliquid feed. ## Subscribe ```json theme={null} { "method": "subscribe", "subscription": { "type": "market", "outcome_ids": ["1209", "1211"] } } ``` | Field | Meaning | | - | - | | `type` | `market`. | | `outcome_ids` | 1 to 256 unique outcome IDs, as decimal strings ([Limits](/hyperliquid/errors#limits)). More is refused with `SUBSCRIPTION_DENIED`. To change them, open a new connection. | Response: ```json theme={null} { "channel": "subscriptionResponse", "data": { "method": "subscribe", "subscription": { "type": "market", "outcome_ids": ["1209", "1211"] }, "server_time": "2026-08-07T20:00:00.118Z", "heartbeat_interval_ms": 15000, "max_frame_bytes": 1048576 } } ``` ## Snapshot Sent at once, and again after the server repairs a source outage. Replace all local state with it. ```json theme={null} { "channel": "snapshot", "data": { "schema_version": "1", "producer_id": "019fd6f1-d200-7000-8000-000000000001", "catalog_generation": "7", "feed_generation": "12", "complete": true, "version": "44", "catalog": [ { "outcome_id": "1209", "native_question_id": "166", "title": "HYPE touches 100 by 20261001-0000", "state": "ACTIVE", "sides": [ { "side": "YES", "coin": "#12090", "asset_id": "100012090" }, { "side": "NO", "coin": "#12091", "asset_id": "100012091" } ], "version": "44" } ], "references": [ { "outcome_id": "1209", "side": "YES", "reference": { "availability": "UNAVAILABLE", "source": "HYPERLIQUID_ALL_MIDS", "reason": "NOT_OBSERVED", "freshness": "UNKNOWN", "received_at": "2026-08-07T20:00:00Z" }, "version": "44" } ] } } ``` | Field | Meaning | | - | - | | `version` | This state's version. The next delta's `previous_version` equals it. | | `catalog` | Each selected outcome's catalog entry, trimmed above. The full shape is [Get market](/hyperliquid/api-reference/markets/get-market). | | `references` | The reference price of each side, with its source and `freshness`. | | `catalog_generation`, `producer_id` | A change to either resets the stream. | | `feed_generation` | The reference-price feed generation. | | `complete` | `true` when every selected outcome is covered. | ## Delta ```json theme={null} { "channel": "delta", "data": { "schema_version": "1", "producer_id": "019fd6f1-d200-7000-8000-000000000001", "catalog_generation": "7", "feed_generation": "12", "previous_version": "44", "version": "52", "catalog_upserts": [], "catalog_removals": [], "reference_upserts": [ { "outcome_id": "1209", "side": "YES", "reference": { "availability": "UNAVAILABLE", "source": "HYPERLIQUID_ALL_MIDS", "reason": "NOT_OBSERVED", "freshness": "UNKNOWN", "received_at": "2026-08-07T20:00:05Z" }, "version": "52" } ], "reference_removals": [] } } ``` | Field | Meaning | | - | - | | `previous_version` | Must equal the `version` you last applied. If not, reconnect. | | `version` | Higher than `previous_version`, not always by one. | | `catalog_upserts`, `reference_upserts` | Entries added or changed. Replace them whole. | | `catalog_removals`, `reference_removals` | Entries removed. | A change that only moves a timestamp is not sent. Read each reference's `freshness`. ## Reset and close No cursor, no replay. When the stream cannot continue, the server sends `{"channel":"reset"}` and closes with `RESNAPSHOT_REQUIRED`. On `drain`, reconnect after `retry_after_ms`. | Close reason | Do | | - | - | | `RESNAPSHOT_REQUIRED` | Reload markets over HTTP and reconnect. | | `CONNECTION_DRAIN`, `SLOW_CONSUMER`, `SOURCE_UNAVAILABLE` | Reconnect with backoff. | | `INVALID_SUBSCRIPTION`, `SUBSCRIPTION_DENIED` | Fix the subscribe frame. | # Withdrawals Source: https://docs.totalis.trade/hyperliquid/withdrawals Send your Totalis USDC to any HyperEVM address in one signed request. Totalis pays the gas. Needs `funding:withdraw` and `sponsorship:use`. ```bash theme={null} curl https://hip4-api.totalis.trade/v1/me/withdrawals \ -H "Authorization: Bearer $TOTALIS_API_KEY" \ -H "Idempotency-Key: 01998683-79f0-7c21-8d3e-5a6b7c8d9e0f" \ -H "Content-Type: application/json" \ -d '{ "destination": "0x2222222222222222222222222222222222222222", "amount": "15000000", "vault_withdrawal": { "amount": "10000000", "expiry": "1790434800", "salt": "4919131752989213764", "signature": "0xefef...efef" }, "payout_authorization": { "valid_after": "1790433540", "valid_before": "1790435100", "authorization_signature": "0x9e9e...9e1b" } }' ``` ```json theme={null} { "withdrawal_id": "01998683-7a31-7e5a-a3c1-6f2b8d4e9a17", "account": "0x1111111111111111111111111111111111111111", "destination": "0x2222222222222222222222222222222222222222", "amount": "15000000", "shortfall_amount": "10000000", "status": "VAULT_WITHDRAWAL_QUEUED", "vault_operation_id": "0x3a7d1e9c5b2f8a4e6d1c9b3f7a5e2d8c4b1f6a9e3d7c2b5f8a1e4d9c6b3f7a20", "payout_operation_id": null, "payout_transaction_hash": null, "updated_at": "2026-09-26T14:30:00.412000Z" } ``` ## What you sign The payout always leaves from your wallet. | Part | When | Message | | - | - | - | | `vault_withdrawal` | Your wallet holds less than `amount`. Moves exactly the shortfall from the vault to your wallet. | `Exit`, with your wallet as `account` and `to` | | `payout_authorization` | Always. Pays `amount` from your wallet to `destination`. | USDC `TransferWithAuthorization` | The shortfall is `amount` minus `WALLET_USDC` minus `pending.wallet_reserved` in [Get balances](/hyperliquid/api-reference/account/get-balances), never below zero. The payout nonce derives from the `Idempotency-Key` ([USDC nonces](/hyperliquid/authentication#usdc-nonces)), so choose the key before you sign. ## Statuses `202` means admitted, not paid. Follow `WITHDRAWAL_UPDATED` on the [account channel](/hyperliquid/websocket/account). | `status` | Meaning | | - | - | | `VAULT_WITHDRAWAL_QUEUED` | The shortfall is moving to your wallet. | | `PAYOUT_SUBMITTED` | The payout is queued or relayed. | | `PAYOUT_COMMITTED` | Done. `payout_transaction_hash` is set. | | `FAILED` | Nothing left your Totalis balance. USDC already moved from the vault stays in your wallet. | A resend with the same key and body returns the same withdrawal. A `409 STATE_CONFLICT` means your balance changed: re-read it and sign again under a new key. # Introduction Source: https://docs.totalis.trade/index APIs for building and settling custom parlays across prediction markets, including Kalshi and Polymarket, on Solana. Totalis turns individual prediction market bets into custom parlays. A **parlay** is one bet that pays out only if every outcome in it happens. Pick outcomes on Kalshi or Polymarket and combine them into one position. Market makers compete to price it, and trades settle on Solana. Totalis is not available to U.S. persons. Your first authenticated call in five minutes. Every endpoint and stream, with request and response shapes. Stream quote requests, price them, and earn the spread. ## Try it without an account Market data is public. This call needs no key and no signup: ```bash theme={null} curl "https://api.totalis.trade/markets?limit=3" ``` Everything else needs an API key. Start with the [Quickstart](/guides/quickstart). ## How it works Pick outcomes from any supported market and set your bet amount. Open a quote request: your outcomes and amount, sent to market makers for pricing. They compete in real time to offer the best payout odds. Accept a quote. When the maker confirms, your stake and the maker's collateral lock in on-chain Solana vaults. The position settles automatically when the markets resolve. ## Core concepts The path from quote request to settlement, and how to close early. Every constraint and both fees, with the math. Send USDC, enable trading once, withdraw any time. Where collateral sits and what Totalis can and cannot do with it. Scoped API keys, the scope catalog, and key management. Every term, field, and unit convention. ## Need help? Integration questions and liquidity. Every API change, dated. What integrators ask most. # Privacy Policy Source: https://docs.totalis.trade/privacy-policy Totalis Privacy Policy describing how UCALLEDIT, Inc. collects, uses, and shares personal information through the Totalis trading platform and related Service. **Last Updated:** May 25th, 2026 This Privacy Policy describes the privacy practices of UCALLEDIT, Inc. ("Totalis", "we", "us", or "our"), and how we handle personal information that we collect through our trading platform (described below) and related application interfaces, feeds, smart contract interactions, documentation and related content to you through our website located at [https://totalis.trade/](https://totalis.trade/) (the "Site") and through our related technologies (such platform and related technology, together with the Site, including any updated or new features, functionality and technology, collectively, the "Service"). ## Personal Information We Collect The information that you provide to us depends on how you interact with the Services and may include: * **Contact information**, such as your first and last name, email and mailing addresses, phone number, and professional title. * **Account information**, such as the username and password you use to access our Services and all other information you add or upload to your account profile. * **Payment information** needed to complete your transactions with us, including name, payment card information, and billing information. This information is processed by our payment service providers, who may handle your payment information in accordance with their own privacy policies. When you make a payment through the Services, we do not have access to your full payment card information. * **Communications** that we exchange with you, including when you contact us with questions, feedback, or otherwise. * **Usage information**, such as information about how you use the Services and interact with us, including information associated with any content you upload to the Services or otherwise submit to us, and information you provide automatically when you use any interactive features of the Services. * **Marketing information** such as your preferences for receiving our marketing communications and details about your engagement with them. * **Other information** not specifically listed here, which we will use as described in this Privacy Policy or as otherwise disclosed at the time of collection. **Automatic data collection.** We, our service providers, and our advertising partners may automatically log and combine information about you, your computer or mobile device, and your interactions over time with the Services, online resources, and our communications, such as: * **Device data**, including your computer's or mobile device's operating system type and version, manufacturer and model, browser type, screen resolution, device type (e.g., phone, tablet), IP address, unique identifiers (including identifiers used for advertising purposes), language settings, and general location information such as city, state, or geographic area. * **Online activity data**, including pages, screens and/or features you viewed, accessed and/or used, how long you spent on a page, screen and/or feature, the website you visited before browsing to the Services, navigation paths between pages or screens, information about your activity on a page, screen and/or feature, access times, duration of access, and whether you have opened or otherwise engaged with our communications, such as our marketing emails, or clicked links or files within them. We use cookies, web beacons (e.g., pixel tags), and local and session storage technologies to collect some of this information. For more information on how we use these technologies, please visit our Cookie Policy. **Information we obtain from other sources:** * **Other sources.** We may obtain personal information from other third parties, such as marketing partners, publicly-available sources, and data providers. ## How We Use Personal Information We use personal information for the following purposes or as otherwise described at the time of collection: **To provide our Services.** We use personal information to operate, maintain, and provide you with our Services, including to facilitate payments. In particular, we use personal information to perform our contractual obligations under our terms of service. **To communicate with you about our Services.** It is in our legitimate business interests to use personal information to respond to your requests, provide customer support, and communicate with you about our Services, including by sending announcements, updates, security alerts, and support and administrative messages. **To improve, monitor, personalize, and protect our Services.** It is in our legitimate business interests to improve and keep our Services safe for our users, which includes: * understanding your needs and interests, and personalizing your experience with the Services and our communications; * troubleshooting, testing and researching, and keeping the Services secure; and * investigating and protecting against fraudulent, harmful, unauthorized or illegal activity. **For research and development.** We may use personal information for research and development purposes where it is in our legitimate business interests, including to analyze and improve the Services and our business. As part of these activities, we may create or use aggregated, de-identified, or other anonymized data from personal information we collect. We make personal information into anonymized data by removing information that makes the data personally identifiable to you. We may use this anonymized data and share it with third parties for our lawful business purposes, including to analyze and improve the Services and promote our business. **For marketing and advertising.** We, our service providers, and our third-party advertising partners may collect and use personal information for the following marketing and advertising purposes: * **Direct marketing.** We may send you direct marketing communications as permitted by law, including by email. You may opt-out of our marketing communications as described in the "Opt-out of marketing communications" section below. * **Interest-based advertising.** We may engage third-party advertising companies, such as Google, to display our ads on their online services. We may also share information about our users with these companies to facilitate advertising to them or similar users on other online platforms. For more information, or to understand your choices, please visit our Cookie Policy. Except where consent is required, we undertake such marketing and advertising on the basis of our legitimate business interests. Where we seek your consent, you may withdraw your consent at any time, including by following the opt-out instructions in the "Privacy Rights and Choices" section below. **For compliance and protection.** We may use personal information to comply with legal obligations, and it is in our legitimate business interests to enforce our terms of service and to defend our family of companies against legal claims or disputes, including to: * protect our, your or others' rights, privacy, safety, or property (including by making and defending legal claims); * audit our internal processes for compliance with legal and contractual requirements and internal policies; * enforce the terms and conditions that govern the Services; * prevent, identify, investigate, and deter fraudulent, harmful, unauthorized, unethical, or illegal activity, including cyberattacks and identity theft; and * comply with applicable laws, lawful requests, and legal process, such as to respond to subpoenas or requests from government authorities. ## How We Share Personal Information We may share personal information with the following individuals or entities: **Affiliates.** Within our family of companies for purposes consistent with this Privacy Policy. **Service providers.** Companies and individuals that provide services on our behalf or help us operate the Services or our business (such as hosting, information technology, customer support, email delivery, and website analytics services). **Professional advisors.** Professional advisors, such as lawyers, auditors, bankers, and insurers, where necessary in the course of the professional services that they render to us. **Other users of the Services.** We may share Brands' information with Influencers, and Influencers' information with Brands in order to facilitate marketing partnerships or marketing relationships between the parties as part of our Services. **Authorities and others.** Law enforcement, government authorities and private parties, as we believe in good faith to be necessary or appropriate for the compliance and protection purposes described above. **Business transferees.** Acquirers and other relevant participants in business transactions (or negotiations for such transactions) involving a corporate divestiture, merger, consolidation, acquisition, reorganization, sale or other disposition of all or any portion of the business or assets of, or equity interests in, our business (including, in connection with a bankruptcy or similar proceedings). **Advertising partners.** Third party advertising companies, including for the interest-based advertising purposes described above, which may collect information on the website through cookies and other automated technologies. ## Privacy Rights and Choices **Opt-out of marketing communications.** You may opt out of marketing-related emails and other communications by following the opt-out or unsubscribe instructions in the communications you receive from us or by contacting us as provided in the "How to Contact Us" section below. You may continue to receive Services-related and other non-marketing emails. **Account deactivation and deletion.** You may deactivate and delete your account by contacting us as described in the "How to Contact Us" section below. Note that deactivating your account may not cause us to fully delete all of your personal information. To request that we do so, please submit a data deletion request as described in the "Personal information requests" section. **Personal information requests.** We also offer you choices that affect how we handle the personal information that we control. Depending on your location and the nature of your interactions with our Services, you may request the following in relation to personal information: * **Information** about how we have collected and used personal information. We have made this information available to you without having to request it by including it in this Privacy Policy. * **Access** to a copy of the personal information that we have collected about you. Where applicable, we will provide the information in a portable, machine-readable, readily usable format. * **Correction** of personal information that is inaccurate or out of date. * **Deletion** of personal information that we no longer need to provide the Services or for other lawful purposes. * **Withdrawal of consent**, where we have collected and processed your personal information with your consent. Withdrawing your consent will not affect the lawfulness of any processing we conducted prior to your withdrawal, nor will it affect processing of your personal information conducted in reliance on lawful processing grounds other than consent. * **Additional rights**, such as to object to and request that we restrict our use of personal information. To make a request, please email us or write to us as provided in the "How to Contact Us" section below. We may ask for specific information from you to help us confirm your identity. Depending on where you reside, you may be entitled to empower an authorized agent to submit requests on your behalf. We will require authorized agents to confirm their identity and authority, in accordance with applicable laws. You are entitled to exercise the rights described above free from discrimination. **Limits on your privacy rights and choices.** In some instances, your choices may be limited, such as where fulfilling your request would impair the rights of others, our ability to provide a service you have requested, or our ability to comply with our legal obligations and enforce our legal rights. If you are not satisfied with how we address your request, you may submit a complaint by contacting us as provided in the "How to Contact Us" section below. Depending on where you reside, you may have the right to complain to a data protection regulator where you live or work, or where you feel a violation has occurred. **Do Not Track.** Some Internet browsers may be configured to send "Do Not Track" signals to the online services that you visit. We currently do not respond to "Do Not Track" or similar signals. To find out more about "Do Not Track," please visit [http://www.allaboutdnt.com](http://www.allaboutdnt.com). ## Other Sites and Services Our Services may contain links to websites and other online services operated by third parties. In addition, our content may be integrated into web pages or other online services that are not associated with us. These links and integrations are not an endorsement of, or representation that we are affiliated with, any third party. We do not control websites or online services operated by third parties, and we are not responsible for their actions. This Privacy Policy does not apply to such third-party sites or services. ## Security We use reasonable organizational, technical, and administrative measures designed to protect against unauthorized access, misuse, loss, disclosure, alteration, and destruction of personal information we maintain. Unfortunately, data transmission over the Internet cannot be guaranteed as completely secure. Therefore, while we strive to protect your personal information, we cannot guarantee the security of personal information. ## Children's Privacy Our Services are not intended for use by children under 13 years of age. If we learn that we have collected personal information through the Services from a child under 13 without the consent of the child's parent or guardian as required by law, we will delete it. ## International Data Transfers You will provide personal information directly to our Services in Canada. We may also transfer personal information to our affiliates and service providers in other jurisdictions. Please note that such jurisdictions may not provide the same protections as the data protection laws in your home country. When we engage in cross-border data transfers, we will ensure that relevant safeguards are in place to afford adequate protection for personal information. For more information about how we transfer personal information internationally, please contact us as set out in the "How to Contact Us" section below. ## Retention of Personal Information Where required under applicable laws, we retain personal information only for as long as is necessary to fulfil the purposes for which it was collected and processed, in accordance with our retention policies, and in accordance with applicable laws and regulatory obligations or until you withdraw your consent (where applicable). To determine the appropriate retention period for personal information, we consider the amount, nature, and sensitivity of the personal information, the potential risk of harm from unauthorized use or disclosure of personal information, the purposes for which we use personal information and whether we can achieve those purposes through other means, and the applicable legal and regulatory requirements. ## Changes to This Privacy Policy We reserve the right to modify this Privacy Policy at any time. Where we do so, we will update the date of this Privacy Policy and post it on the website. If we make material changes to this Privacy Policy, we will let you know. ## How to Contact Us **Responsible entity.** Totalis is the entity responsible for the processing of personal information under this Privacy Policy (as a controller, where provided under applicable law). **Contact us.** If you have any questions or comments about this Policy, our privacy practices, or if you would like to exercise your rights with respect to your personal information, please contact us by email at [legal@totalis.trade](mailto:legal@totalis.trade). # Referrals Source: https://docs.totalis.trade/referrals Earn 20% of the trading fees generated by traders you refer, paid in USDC and claimable at 25 USDC. Refer other traders and earn a cut of the fees they generate. Anyone who signs up through your referral link is tied to your account. | | | | - | - | | Your share | 20% of the trading fee on each referred trade | | Paid in | USDC, to the wallet linked to your account | | Claim minimum | 25 USDC | | Tiers | None. You earn only from people you refer directly. | ## How to refer someone Open Settings and go to the Referrals tab. Copy your referral link and share it. When someone signs up through that link, they appear in your referred users list. They start earning you rewards the moment they trade. ## What you earn You earn your share on the fee charged when a referred user's trade fills, so your reward is a straight cut of fees that were actually collected. It builds up as a claimable balance in your account. * Rewards accrue on both settled parlays and early cashouts, since both charge a trading fee. * Your rate is fixed per reward at the time it accrues. A later rate change never touches what you have already earned. ## Claiming your rewards The Referrals tab shows three numbers: what you have earned in total, what is available to claim right now, and what you have already been paid. Once your claimable balance reaches the minimum, the Claim button turns on. Press it and the USDC is sent to your wallet. The payout usually confirms within a few seconds, and the tab updates once it lands. Below the minimum, nothing is lost. The balance stays in your account and keeps growing as your referred users trade. ## Before you claim * Payouts go to the wallet linked to your account. Make sure one is set up. * You can have one claim in flight at a time. If a payout is still processing, let it finish before starting another. * A claim that sits as pending for a little while is safe. Your balance is never lost, and the payout completes once it settles. # Terms of Service Source: https://docs.totalis.trade/terms-of-service Totalis Terms of Service governing access to and use of the Totalis trading platform, RFQ system, vaults, and related technologies. **Date of Last Revision:** April 30th, 2026 ## Acceptance of These Terms of Service UCALLEDIT, Inc. ("Totalis," "we," "us," or "our") provides our trading platform (described below) and related application interfaces, feeds, smart contract interactions, documentation and related content to you through our website located at [https://totalis.trade/](https://totalis.trade/) (the "Site") and through our related technologies (such platform and related technology, together with the Site, including any updated or new features, functionality and technology, collectively, the "Service"). All access and use of the Service is subject to the terms and conditions contained in these Terms of Service (as amended from time to time, these "Terms of Service"). By accessing, browsing, connecting a wallet to, generating or using an API key for, enabling a delegated signer or session signer for, submitting an RFQ through, quoting on, accepting a quote through, or otherwise accessing or using any part of the Service, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service and by any additional terms, policies, schedules, or rules that are expressly incorporated into these Terms of Service. If you do not accept the terms and conditions of these Terms of Service, you will not access, browse, or otherwise use the Service. We reserve the right, at our sole discretion, to change or modify portions of these Terms of Service at any time. If we do this, we will post the changes on this page and will indicate at the top of this page the date these Terms of Service were last revised. You may read a current, effective copy of these Terms of Service by visiting the "Terms of Service" link on the Site. We will also notify you of any material changes, either through the Service user interface, a pop-up notice, email, or through other reasonable means. Your continued use of the Service after the date any such changes become effective constitutes your acceptance of the new Terms of Service. You should periodically visit this page to review the current Terms of Service so you are aware of any revisions. If you do not agree to abide by these or any future Terms of Service, you will not access, browse, or use (or continue to access, browse, or use) the Service. PLEASE READ THESE TERMS OF SERVICE CAREFULLY, AS THEY CONTAIN AN AGREEMENT TO ARBITRATE AND OTHER IMPORTANT INFORMATION REGARDING YOUR LEGAL RIGHTS, REMEDIES, AND OBLIGATIONS. THE AGREEMENT TO ARBITRATE REQUIRES (WITH LIMITED EXCEPTION) THAT YOU SUBMIT CLAIMS YOU HAVE AGAINST US TO BINDING AND FINAL ARBITRATION, AND FURTHER (1) YOU WILL ONLY BE PERMITTED TO PURSUE CLAIMS AGAINST TOTALIS ON AN INDIVIDUAL BASIS, NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY CLASS OR REPRESENTATIVE ACTION OR PROCEEDING, (2) YOU WILL ONLY BE PERMITTED TO SEEK RELIEF (INCLUDING MONETARY, INJUNCTIVE, AND DECLARATORY RELIEF) ON AN INDIVIDUAL BASIS, AND (3) YOU MAY NOT BE ABLE TO HAVE ANY CLAIMS YOU HAVE AGAINST US RESOLVED BY A JURY OR IN A COURT OF LAW. **Your Privacy:** At Totalis, we respect the privacy of our users. For more information please see our Privacy Policy, located at [https://totalis.trade/privacy](https://totalis.trade/privacy) (the "Privacy Policy"). By using the Service, you consent to our collection, use and disclosure of personal data and other data as outlined therein. **Additional Terms:** In addition, when using certain features through the Service, you will be subject to any additional terms applicable to such features that may be posted on or within the Service from time to time ("Additional Terms"). All such Additional Terms are hereby incorporated by reference into these Terms of Service. ## Access and Use of the Service **Service Description:** The Service is designed to allow eligible users to create or respond to request-for-quote opportunities relating to structured positions, including custom parlays, that reference outcomes from supported third party prediction market venues. The Service may also interact with supported blockchains, smart contracts, wallets, data providers, and other service providers in order to facilitate position creation, operational workflows, and settlement-related actions. Nothing in the Service is intended to constitute, and the Service does not provide, legal, tax, accounting, investment, financial, brokerage, clearing, custody, escrow, fiduciary, or gambling advice. Totalis is not your agent, broker, dealer, exchange, clearing organization, futures commission merchant, designated contract market, bank, trustee, investment adviser, or fiduciary. Nothing in the Service is intended to constitute, investment, financial, brokerage, clearing, custody, escrow, fiduciary, or gambling services. **Eligibility; Restricted Persons; Geographic Restrictions:** To access or use the Service, you represent, warrant, and covenant that: (a) you are at least eighteen (18) years old or the age of legal majority in your jurisdiction, whichever is higher; (b) you have full legal capacity and authority to enter into these Terms of Service; (c) you are not a U.S. Person, are not located in the United States, and are not accessing or using the Service from within the United States; (d) you are not located in, organized in, resident in, or otherwise subject to any jurisdiction that Totalis designates as restricted or prohibited or in which the Services would be deemed illegal under applicable laws; (e) you are not the subject of any sanctions administered or enforced by any governmental authority and are not owned or controlled by, or acting on behalf of, any sanctioned or otherwise restricted person; and (f) your access to and use of the Service does not violate any law, regulation, judgment, order, contractual restriction, or other obligation applicable to you. For purposes of these Terms of Service, "U.S. Person" includes any natural person resident in the United States, any citizen of the United States, any person physically located in the United States, any entity organized or incorporated under the laws of the United States or any state or territory thereof, any entity having its principal place of business in the United States, and any person acting for the account or benefit of any of the foregoing. You are solely responsible for determining whether your access to and use of the Service is lawful in your jurisdiction and under any laws applicable to you. You may not access or use the Service on behalf of, for the benefit of, at the direction of, or in coordination with any U.S. Person or other restricted or ineligible person. You may not use a virtual private network (VPN), proxy service, anonymization service, remote desktop tool, location-masking technology, or any similar method to obscure your location or to circumvent geographic, sanctions, or other eligibility restrictions. Any such conduct constitutes a material breach of these Terms of Service. Totalis may use any technical, geolocation, wallet-screening, device-screening, or other methods it determines appropriate to assess eligibility and compliance. Totalis may require you to provide information or documentation regarding your identity, location, authority, source of funds, wallet ownership, eligibility, or compliance status at any time, and may suspend or terminate access to the Service pending review. Totalis may also place accounts or wallets into close-only mode, refuse to facilitate new positions, or block access altogether if it determines, in its sole discretion, that doing so is advisable. **Your Registration Obligations:** You may be required to register with Totalis or provide information about yourself (e.g., name and email address) in order to access and use certain features of the Service. If you choose to register for the Service, you agree to provide and maintain true, accurate, current, and complete information about yourself as prompted by the Service's registration form. Registration data and certain other information about you are governed by our Privacy Policy. **Member Account, Password, and Security:** You are responsible for maintaining the confidentiality of your password and account details, if any, and are fully responsible for any and all activities that occur under your password or account. You agree to (a) immediately notify Totalis of any unauthorized use of your password or account or any other breach of security, and (b) ensure that you exit from your account at the end of each session when accessing the Service. Totalis and its current and future affiliates (collectively, "Totalis Affiliates") will not be liable for any loss or damage arising from your failure to comply with this paragraph. **Modifications to Service:** Totalis is not required to make any venue, market, asset, token, network, wallet integration, API endpoint, quote source, or feature available at any time. Totalis may add, remove, disable, restrict, suspend, or modify any aspect of the Service, in whole or in part, at any time and for any reason, with or without notice, and without liability to you or any third party. Totalis reserves the right to modify or discontinue, temporarily or permanently, the Service (or any part thereof) with or without notice. You agree that Totalis and Totalis Affiliates will not be liable to you or to any third party for any modification, suspension, or discontinuance of the Service. **Wallets, Credentials, API Keys, and Account Security:** To use certain features of the Service, you may need to connect a compatible wallet, create credentials, generate or receive an API key, or enable one or more delegated-signing or session-signing permissions through a supported provider. You are solely responsible for ensuring that you use only wallets, devices, software, and integrations that are compatible with the Service and that are properly configured. You are solely responsible for maintaining the confidentiality and security of your wallet credentials, private keys, seed phrases, account credentials, device security, API keys, delegated-signing permissions, and any session signer or similar authorization that you enable. Totalis will not be liable for any loss, damage, unauthorized access, unauthorized transaction, or other consequence arising from your failure to maintain adequate security or from the compromise, misuse, theft, disclosure, or loss of any credentials or permissions associated with you. If you enable delegated or session-based signing through a supported provider or integration, you authorize Totalis and/or its third-party service providers to request and facilitate signatures within the scope of the permissions you grant. You are solely responsible for reviewing, granting, monitoring, and revoking those permissions. Any instruction, request, signature, message, or transaction initiated through your connected wallet, account, device, API key, delegated signer, or session signer will be deemed authorized by you unless and until you revoke the relevant access and Totalis has had a reasonable opportunity to process that revocation. You agree to notify Totalis promptly of any suspected or actual unauthorized access to your account, wallet connection, API key, delegated signer, or other credentials. Totalis may rotate, suspend, revoke, or invalidate API keys or access credentials at any time and for any reason. You may not sell, rent, lease, transfer, sublicense, or otherwise make your account, API key, or wallet access available to any third party except your own authorized personnel acting within the scope of their authority. You remain fully responsible for all acts and omissions associated with your account or of any person or system using credentials associated with you. **No Custody:** Totalis is intended to operate on a non-custodial basis. Totalis does not take independent custody of your digital assets and does not independently hold your private keys. However, the Service may interact with third-party wallet infrastructure, delegated-signing providers and similar technologies that facilitate transactions or signatures authorized by you. Totalis is not responsible for any act, omission, downtime, failure, security incident or other issue attributable to any such third party. **Supported Assets and Networks:** The Service currently is intended to support only USDC on Solana mainnet, unless Totalis states otherwise. Totalis may change the supported assets, tokens, stablecoins, networks, bridges, vault mechanisms, wallet types and other technical parameters of the Service at any time in its sole discretion. **Deposits and Withdrawals:** You bear all risk associated with deposits, withdrawals, transfers, wallet configuration and network selection, including any loss arising from the use of an incorrect wallet address, incorrect token, unsupported token, unsupported network, bridging failure or any other operational or technical failure. Unsupported or misdirected transfers may be irreversible and may be permanently lost. Totalis has no obligation to assist with or recover unsupported or erroneous transfers, although it may attempt to do so in its sole discretion. Totalis may choose, but is not obligated, to sponsor gas or other transaction costs for some operations. Any such sponsorship may be modified, limited, suspended, or discontinued at any time. Where owner-signed or wallet-owner actions require network-native assets or fees, you are solely responsible for maintaining sufficient balances to complete those actions. **RFQs and Quotes; Trade Formation:** The Service may enable users to submit request-for-quote opportunities ("RFQs") referencing supported markets and to receive quotes from market makers or other counterparties. The Service is designed to assist users engaging in user-initiated transactions by enabling users to interact with Totalis supported blockchain protocols (or blockchain-based smart contracts) by converting user-identified RFQ parameters (e.g., buy/sell, volume, crypto asset security, and price or price range) into actionable, blockchain-legible commands for signature and transmission via the user's Totalis compatible wallet. Totalis does not guarantee that any RFQ will receive any quote, that any quote will be available for any duration, or that any quote will be accepted, confirmed, executed, or settled. An RFQ is a request for liquidity only. A quote is indicative or firm only to the extent expressly designated by the Service, and in all cases remains subject to expiry, withdrawal, replacement, risk checks, operational constraints, compliance checks, technical availability and successful execution of the relevant on-chain and off-chain workflows. A user's acceptance of a quote constitutes a request to proceed and does not, by itself, create a completed, enforceable, executed, or settled position. No RFQ, quote, acceptance, confirmation, message or other communication creates a completed position unless and until the relevant transaction or transactions have been successfully created, signed as required, submitted, accepted by the applicable network, and recorded in the manner recognized by the Service, and any additional conditions or checks required by Totalis have been satisfied. Until a position has been formed in the manner recognized by the Service, Totalis and/or the relevant market maker may reject, cancel, expire, withdraw or otherwise decline to proceed with any RFQ, quote, acceptance or confirmation for any reason or no reason. If an RFQ expires, a quote expires, a market maker misses any confirmation or response window, a signature cannot be obtained, a transaction fails or is dropped, the blockchain or supporting infrastructure is unavailable or delayed or any other condition to formation is not satisfied, the relevant attempted transaction may fail to form and neither Totalis nor any market maker will have any obligation to honor the prior RFQ or quote. **Market Makers:** Users acting as market makers agree that Totalis may impose API, connectivity, performance, quoting, monitoring, collateral, throttling and other operational requirements at any time. Totalis may reject, delay, throttle, suspend, deactivate, reorder or remove quotes or market maker access in its sole discretion and without liability. **Underlying Venues:** The Service interacts with markets listed or created on third-party venues (each, an "Underlying Venue"). Totalis may add, remove, suspend or stop supporting any Underlying Venue or portion thereof at any time and for any reason. You acknowledge and agree that the rules, market definitions, market status, corrections, clarifications, cancellations, delistings, voids, pushes, suspensions, disputes and final published outcomes of the applicable Underlying Venue may affect the status, value, or settlement of any position created through the Service. Totalis may rely conclusively on an Underlying Venue's published or otherwise recognized rules, status designations, and outcomes and has no duty to independently verify them. Totalis may determine, in its sole discretion, how to map or translate an Underlying Venue's market definitions, corrections, clarifications, status changes, outcomes and settlement conventions into the Service. **Resolution, Settlement, and Error Handling:** Without limiting the foregoing, Totalis may suspend, delay, reject, cancel, void, reprice, unwind, reverse, partially settle or otherwise modify any RFQ, quote, leg, position, payout or settlement, in its sole discretion, including if Totalis determines that any of the following has occurred or may occur: invalid, ambiguous, unavailable, discontinued, or delisted markets; contradictory, stale, incomplete, or missing venue data; pricing or display errors; technical or network failures; chain congestion; dropped or conflicting transactions; exploits; security incidents; fraud or market abuse; legal or regulatory concerns; sanctions concerns; unsupported states; manifest error; or force majeure. If a leg or market becomes invalid, cancelled, void, delisted, pushed, or otherwise unusable, Totalis may cancel the affected position in whole or in part, unlock collateral, adjust exposure or payout calculations, apply the applicable Underlying Venue rules as Totalis interprets them for purposes of the Service, or take any other action that Totalis deems appropriate in the circumstances. Settlement timing is not guaranteed. Settlement may be delayed if an Underlying Venue, blockchain, smart contract, wallet provider, delegated-signing provider, data source, oracle, RPC, or other service is unavailable, congested, under maintenance, subject to dispute, correction, review, or investigation or otherwise impaired. To the maximum extent permitted by applicable law, Totalis' determinations regarding market support, position status, settlement handling, error correction, operational sequencing and other remedial action will be final and binding. **General Practices Regarding Use and Storage:** You acknowledge that Totalis may establish general practices and limits concerning use of the Service, including the maximum period of time that data or other content will be retained by the Service and the maximum storage space that will be allotted on Totalis's or its third-party service providers' servers on your behalf. You agree that Totalis has no responsibility or liability for the deletion or failure to store any data or other content maintained or transmitted by the Service. You acknowledge that Totalis reserves the right to terminate accounts that are inactive for an extended period of time. You further acknowledge that Totalis reserves the right to change these general practices and limits at any time, in its sole discretion, with or without notice. **Automation Access:** The Service may provide APIs, WebSocket feeds, SDKs, documentation, developer tools or other machine-readable interfaces. Access to such interfaces may require registration, approval, an API key, technical configuration or compliance with additional operational requirements of Totalis. All API, WebSocket and other automation access is provided on an "as is" and "as available" basis. Totalis does not guarantee uptime, performance, continued availability or any other performance of any endpoint, message type, schema, interface or other automation access. Totalis may modify, throttle, suspend, rotate, revoke or discontinue any interface or credentials at any time and without liability. You may use automation and similar tools only to the extent permitted by the Service. You may not use the Service in any manner that is abusive, excessive, disruptive, manipulative, misleading, unlawful or inconsistent with Totalis' technical or operational requirements. You are solely responsible for all messages, requests, quotes, orders and other actions initiated by or through your systems or account. **Third-Party Services and Blockchain Infrastructure:** The Service depends on third-party services, software, infrastructure and data sources, including wallets, delegated-signing providers, trusted execution environments, blockchain networks, smart contracts and Underlying Venues (collectively, "Third-Party Services"). Totalis does not control and is not responsible for any Third-Party Service. Your access to and use of any Third-Party Service may be subject to separate terms and policies between you and the applicable third party. Totalis does not endorse, guarantee, or warrant any Third-Party Service, and Totalis will not be liable for any loss, damage, delay, error, downtime, exploit, change, insolvency, security incident, or other issue arising out of or relating to any Third-Party Service. You acknowledge that blockchain transactions may fail, be delayed, be dropped, be replaced, be front-run or otherwise behave unpredictably. Totalis does not guarantee that any transaction will be mined, finalized, sequenced or recognized in any particular timeframe or at all. **Assumption of Risk:** USING THE SERVICE INVOLVES SUBSTANTIAL RISK. YOU MAY LOSE ALL OR PART OF THE DIGITAL ASSETS OR VALUE ASSOCIATED WITH A POSITION, AND YOU MAY SUFFER LOSSES AS A RESULT OF PRICE MOVEMENTS, ADVERSE OUTCOMES, OPERATIONAL FAILURES, TECHNICAL FAILURES, MARKET CANCELLATIONS, INVALID LEGS, SETTLEMENT DELAYS, OR REGULATORY EVENTS. Without limiting the foregoing, you understand and agree that: (a) prediction markets and structured positions are inherently risky and speculative; (b) parlays and other structured positions can magnify loss relative to single-market exposure; (c) digital assets are volatile; (d) blockchain networks and smart contracts may malfunction or behave in unexpected ways; (e) delegated-signing systems, session signers, wallets, devices and credentials may be compromised; (f) Underlying Venues may clarify, correct, cancel, void, delist, suspend or otherwise change markets or outcomes; (g) data displayed by the Service may be delayed, incomplete, stale or inaccurate; and (h) the legal and regulatory treatment of digital assets, prediction markets, routing activity and related services is uncertain and may change rapidly. **No Advice:** The Service does not provide, and nothing on the Service should be construed as, investment advice, financial advice, brokerage advice, legal advice, tax advice, accounting advice, gambling advice, or a recommendation or solicitation to enter into any transaction. You are solely responsible for evaluating the suitability and legality of any position or activity for yourself. You are solely responsible for conducting your own diligence, using your own judgment and determining the legality, appropriateness and risk of your use of the Service. You should access and use the Service only with assets you can afford to lose. ## Conditions of Access and Use **Representations and Warranties:** By accessing or using the Service, you represent, warrant, and covenant that: (a) you satisfy all eligibility requirements in these Terms of Service; (b) all information you provide to Totalis is true, accurate, current, and complete; (c) you own or validly control the wallets, credentials and digital assets that you use in connection with the Service, or are duly authorized to use them; (d) you have all authority necessary to enter into these Terms of Service and to engage in the activities you undertake through the Service; (e) your use of the Service will comply with all Laws and all obligations applicable to you; and (f) you understand and accept the risks described in these Terms of Service. If you access or use the Service on behalf of an entity or another person, you represent and warrant that you have all authority necessary to bind that entity or person to these Terms of Service, and the words "you" and "your" in these Terms of Service will refer to both you and that entity or person. **User Conduct:** You are solely responsible for all images, information, data, text, software, messages and other materials (collectively, "content") that you make available to Totalis, including by transmitting, posting, publishing or displaying (hereinafter, "transmit(ting)") via the Service or to other users of the Service (collectively, "User Content"). The following are examples of the kinds of content and/or uses that are illegal or prohibited by Totalis. Totalis reserves the right to investigate and take appropriate legal action against anyone who, in Totalis's sole discretion, violates this provision, including removing the offending content from the Service, suspending or terminating the account of such violators, and reporting the violator to law enforcement authorities. You agree to not use the Service to: * transmit any content that (i) infringes any intellectual property or other proprietary rights of any party; (ii) you do not have a right to transmit under any law or under contractual or fiduciary relationships; (iii) contains software viruses or any other computer code, files, or programs designed to interrupt, destroy, or limit the functionality of any computer software or hardware or telecommunications equipment; (iv) poses or creates a privacy or security risk to any person; (v) constitutes unsolicited or unauthorized advertising, promotional materials, commercial activities and/or sales, "junk mail," "spam," "chain letters," "pyramid schemes," "contests," "sweepstakes," or any other form of solicitation; (vi) is unlawful, harmful, threatening, abusive, harassing, tortious, excessively violent, defamatory, vulgar, obscene, pornographic, libelous, invasive of another's privacy, hateful, discriminatory, or otherwise objectionable; or (vii) in the sole judgment of Totalis, is objectionable or which restricts or inhibits any other person from using or enjoying the Service or which may expose Totalis or its users to any harm or liability of any type; * interfere with or disrupt the Service or servers or networks connected to the Service, or disobey any requirements, procedures, policies, or regulations of networks connected to the Service, including through malware, denial-of-service activity, harmful code, or unauthorized scraping or crawling; * violate any applicable local, state, national, or international law, or any rule, guidance, or regulations having the force of law ("Laws"); * impersonate any person or entity, or falsely state or otherwise misrepresent your affiliation with a person or entity; * solicit personal information from anyone under the age of 18; * harvest or collect email addresses or other contact information of other users from the Service by electronic or other means for the purposes of sending unsolicited emails or other unsolicited communications; * advertise or offer to sell or buy any goods or services for any business purpose that is not specifically authorized; * further or promote any criminal activity or enterprise or provide instructional information about illegal activities; * use the Service in connection with money laundering, terrorist financing, sanctions evasion, fraud, or any other unlawful activity; * engage in fraud, deception, market manipulation, wash trading, spoofing, layering, collusion, abusive quoting, quote stuffing, or any other abusive, misleading, or manipulative conduct; * obtain or attempt to access or otherwise obtain any content or information through any means not intentionally made available or provided for through the Service; * circumvent, remove, alter, deactivate, degrade, or thwart any of the content protections in or geographic restrictions on any content (including Service Content (as defined below)) available on or through the Service, including through the use of virtual private networks; * exploit bugs, vulnerabilities, or unintended behavior in the Service, any smart contract, any API, any delegated-signing system, any wallet integration, or any third-party service; * use a VPN, proxy, anonymization service, remote desktop tool, or any similar tool to conceal your location, identity, or eligibility status or to circumvent Service restrictions; * use the Service after Totalis has suspended, blocked, or terminated your access unless Totalis expressly authorizes you in writing to do so; or * engage in or use any data mining, robots, scraping, or similar data gathering or extraction methods. Totalis may monitor activity on the Service and may investigate any actual, suspected, or potential violation of these Terms of Service. Totalis may cooperate with any governmental authority, regulator, law-enforcement body, court, or other third party as it deems appropriate. If you are blocked by Totalis from accessing the Service (including by blocking your IP address), you agree not to implement any measures to circumvent such blocking (e.g., by masking your IP address or using a proxy IP address or virtual private network). **Ownership; Restrictions:** The technology and software underlying the Service or distributed in connection therewith are the property of Totalis, Totalis Affiliates, and their licensors (the "Software"). You agree not to copy, modify, create a derivative work of, reverse engineer, reverse assemble, or otherwise attempt to discover any source code, or sell, assign, sublicense, or otherwise transfer any right in the Software. Any rights not expressly granted herein are reserved by Totalis. **Competitors:** No employee, independent contractor, agent, or affiliate of any competing company is permitted to view, access, or use any portion of the Service without express written permission from Totalis. By viewing, using, or accessing the Service, you represent and warrant that you are not a competitor of Totalis or any Totalis Affiliate, or acting on behalf of a competitor of Totalis or any Totalis Affiliate in using or accessing the Service. **Fees and Economics:** Your use of the Service may be subject to protocol fees, service fees, spreads, market-maker compensation, routing fees, settlement-related charges, blockchain or network fees, gas sponsorship limitations and other charges disclosed by the Service from time to time. Except as otherwise displayed on the Service, Totalis may charge a protocol fee equal to one percent (1%) of the profit realized by the winning side of a settled position. Fees may be displayed in the user interface, documentation, API responses or other Service materials. Totalis may change any fee, introduce new fees, change fee calculation methodologies, discontinue fee waivers or rebates or modify economic terms at any time in its sole discretion, and any such change will become effective when posted or otherwise made available through the Service. All fees are non-refundable except to the extent required by applicable law or expressly determined otherwise by Totalis in its sole discretion. Totalis may deduct fees automatically from amounts otherwise payable or transferable through the Service. You are solely responsible for determining, reporting and paying any taxes, duties, levies, assessments or similar governmental charges arising out of or relating to your use of the Service, your positions, your digital assets or any payments, profits, or losses associated with the Service. Totalis does not provide tax advice and does not undertake to determine whether taxes apply to you. ## Intellectual Property Rights **Service Content:** You acknowledge and agree that the Service may contain content or features ("Service Content") that are protected by copyright, patent, trademark, trade secret, or other proprietary rights and Laws. Except as expressly authorized by Totalis, you agree not to modify, copy, frame, scrape, rent, lease, loan, sell, distribute, or create derivative works based on the Service or the Service Content, in whole or in part, except that the foregoing does not apply to your own User Content that you transmit to or make available through the Service in accordance with these Terms of Service. Any use of the Service or the Service Content other than as specifically authorized herein is strictly prohibited. **Trademarks:** The Totalis name and logos are trademarks and service marks of Totalis (collectively the "Totalis Trademarks"). Other company, product, and service names and logos used and displayed via the Service may be trademarks or service marks of their respective owners who may or may not endorse or be affiliated with or connected to Totalis. Nothing in these Terms of Service or the Service should be construed as granting, by implication, estoppel, or otherwise, any license or right to use any of Totalis Trademarks displayed on the Service without our prior written permission in each instance. All goodwill generated from the use of Totalis Trademarks will inure to our exclusive benefit. **Third-Party Material:** Under no circumstances will Totalis or any Totalis Affiliate be liable in any way for any content or materials of any third parties (including users), including for any errors or omissions in any content, or for any loss or damage of any kind incurred as a result of the use of any such content. You acknowledge that Totalis does not pre-screen content, but that Totalis and its designees will have the right (but not the obligation) in their sole discretion to refuse or remove any content that is available via the Service. Without limiting the foregoing, Totalis and its designees will have the right to remove any content that violates these Terms of Service or is deemed by Totalis, in its sole discretion, to be otherwise objectionable. You agree that you must evaluate, and bear all risks associated with, the use of any content, including any reliance on the accuracy, completeness, or usefulness of such content. **User Content:** You represent and warrant that you own all right, title, and interest in and to your User Content, including all copyrights and rights of publicity contained therein. You hereby grant Totalis and Totalis Affiliates, and their successors and assigns, a non-exclusive, worldwide, royalty-free, fully paid-up, transferable, sublicensable (directly and indirectly through multiple tiers), perpetual, and irrevocable license to copy, display, transmit, perform, distribute, store, modify, and otherwise use your User Content, in any form, medium or technology now known or later developed, (a) in connection with the operation of the Service; (b) to provide, develop and improve the Service and other offerings of Totalis and/or Totalis Affiliates; (c) for the promotion, advertising or marketing of the foregoing; and (d) as otherwise set forth in our Privacy Policy. You also agree that Totalis may remove metadata associated with your User Content and you irrevocably waive any claims and assertions of moral rights or attribution with respect to your User Content. You assume all risk associated with your User Content and the transmission of your User Content and you have sole responsibility for the accuracy, quality, legality, and appropriateness of your User Content. You hereby authorize Totalis and Totalis Affiliates and their third-party service providers to collect and analyze User Content and other data and information relating to the Service and related systems and technologies and derive statistical and usage data relating thereto (collectively, "Usage Data"). Totalis and Totalis Affiliates may use Usage Data for any purpose in accordance with applicable Law and our Privacy Policy. Any questions, comments, suggestions, ideas, feedback, reviews, or other information about the Service ("Submissions"), provided by you to Totalis or any Totalis Affiliate are non-confidential and Totalis and Totalis Affiliates will be entitled to the unrestricted use and dissemination of these Submissions for any purpose, commercial or otherwise, without acknowledgment, attribution, or compensation to you. You acknowledge and agree that Totalis may preserve User Content and may also disclose User Content if required to do so by Law or in the good faith belief that such preservation or disclosure is reasonably necessary to: (a) comply with legal process, applicable Law, or government requests; (b) enforce these Terms of Service; (c) respond to claims that any content violates the rights of third parties; or (d) protect the rights, property, or personal safety of Totalis, its users, or the public. You understand that the technical processing and transmission of the Service, including your User Content, may involve (i) transmissions over various networks; and (ii) changes to conform and adapt to technical requirements of connecting networks or devices. **Intellectual Property Complaints:** Totalis respects the intellectual property of others, and we ask our users to do the same. If you believe that your work has been copied in a way that constitutes copyright infringement, or that your intellectual property rights have been otherwise violated, you should notify Totalis of your infringement claim in accordance with the procedure set forth below. Totalis will process and investigate notices of alleged infringement and will take appropriate actions under the Digital Millennium Copyright Act ("DMCA") and other applicable intellectual property Laws with respect to any alleged or actual infringement. A notification of claimed copyright infringement should be emailed to Totalis's Copyright Agent at [legal@totalis.trade](mailto:legal@totalis.trade) (Subject line: "DMCA Takedown Request"). You may also contact the Copyright Agent by mail or facsimile at: 174 Connecticut St, Unit 2, San Francisco, CA, 94107, USA To be effective, the notification must be in writing and contain the following information: * a physical or electronic signature of a person authorized to act on behalf of the owner of the copyright or other intellectual property interest that is allegedly infringed; * identification of the copyrighted work or other intellectual property that you claim has been infringed, or, if multiple copyrighted works or other intellectual property are covered by a single notification, a representative list of such works or other intellectual property; * identification of the content that is claimed to be infringing or to be the subject of infringing activity, and where the content that you claim is infringing is located on the Service, with enough detail that we may find it on the Service; * your address, telephone number, and email address; * a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright or intellectual property owner, its agent, or the Law; and * a statement by you that the information in your notice is accurate and, under penalty of perjury, that you are the copyright or intellectual property owner or are authorized to act on the behalf of the owner of the copyright or intellectual property that is allegedly infringed. If you believe that the relevant content that was removed (or to which access was disabled) is not infringing, or that you have the authorization from the copyright owner, the copyright owner's agent, or pursuant to the law, to post and use such Content, you may send a written counter-notice containing the following information to the Copyright Agent: * your physical or electronic signature; * identification of the content that has been removed or to which access has been disabled and the location at which the content appeared before it was removed or disabled; * a statement that you have a good faith belief that the content was removed or disabled as a result of mistake or a misidentification of the content; and * your name, address, telephone number, and e-mail address, a statement that you consent to the jurisdiction of the federal court located within the Northern District of California and a statement that you will accept service of process from the person who provided notification of the alleged infringement. If a counter-notice is received by the Copyright Agent, Totalis will send a copy of the counter-notice to the original complaining party informing that person that it may replace the removed content or cease disabling it in 10 business days. Unless the copyright owner files an action seeking a court order against the content provider, member or user, the removed content may be replaced, or access to it restored, in 10 to 14 business days or more after receipt of the counter-notice, at Totalis's sole discretion. In accordance with the DMCA and other Law, Totalis has adopted a policy of terminating, in appropriate circumstances and at Totalis's sole discretion, members who are deemed to be repeat infringers. Totalis may also at its sole discretion limit access to the Service and/or terminate the memberships of any users who infringe any intellectual property rights of others, whether or not there is any repeat infringement. ## Indemnification To the extent permitted under applicable Law, you agree to defend, indemnify, and hold harmless Totalis and Totalis Affiliates, and its and their respective officers, employees, directors, service providers, licensors, and agents (collectively, the "Totalis Parties"), from any and all losses, damages, expenses, including reasonable attorneys' fees, rights, claims, actions of any kind, and injury (including death) arising out of or relating to (a) your access to or use of the Service; (b) your wallets, credentials, delegated-signing permissions, API keys, systems or integrations; (c) your violation of these Terms of Service or any Law; (d) your infringement or misappropriation of any right of any person or entity; or (e) any dispute between you and any other user, market maker, Underlying Venue or third party. Totalis will provide notice to you of any such claim, suit, or proceeding. Totalis reserves the right to assume the exclusive defense and control of any matter which is subject to indemnification under this section, and you agree to cooperate with any reasonable requests assisting Totalis's defense of such matter. You may not settle or compromise any claim against the Totalis Parties without Totalis's written consent. ## Disclaimer of Warranties THE SERVICE IS PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS, WITH ALL FAULTS, AND WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE TOTALIS PARTIES DISCLAIM ALL WARRANTIES, INCLUDING ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, ACCURACY, AVAILABILITY, QUIET ENJOYMENT, SECURITY AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING, COURSE OF PERFORMANCE, OR USAGE OF TRADE. WITHOUT LIMITING THE FOREGOING, THE TOTALIS PARTIES DO NOT WARRANT THAT THE SERVICE, ANY QUOTE, ANY RFQ, ANY MARKET DATA, ANY WALLET INTEGRATION, ANY DELEGATED-SIGNING PROCESS, ANY API, ANY WEBSOCKET FEED, ANY SMART CONTRACT INTERACTION, ANY POSITION STATUS OR ANY SETTLEMENT FLOW WILL BE AVAILABLE, TIMELY, ACCURATE, COMPLETE, RELIABLE, SECURE OR ERROR-FREE; THAT DEFECTS WILL BE CORRECTED; THAT ANY TRANSACTION WILL BE EXECUTED, MINED, FINALIZED, OR SETTLED; OR THAT ANY POSITION WILL PRODUCE ANY PARTICULAR RESULT. NO ORAL OR WRITTEN INFORMATION OR ADVICE PROVIDED BY TOTALIS OR ANY OTHER TOTALIS PARTY WILL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THESE TERMS OF SERVICE. ## Limitation of Liability TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE TOTALIS PARTIES WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, PUNITIVE, OR ENHANCED DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, GOODWILL, DATA, USE, OPPORTUNITY, OR DIGITAL ASSETS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Without limiting the foregoing, the Totalis Parties will not be liable for losses or damages arising out of or relating to: (a) any adverse market movement, failed position or losing position; (b) any action or inaction by an Underlying Venue or any Third-Party Service; (c) any pricing discrepancy, market-data error, display error, stale information or mapping decision; (d) any bug, exploit, vulnerability, hack, malware, denial-of-service event or other security incident; (e) any failure, delay, or interruption of any blockchain, validator, sequencer, RPC, wallet, delegated-signing provider or smart contract; (f) any incorrect network, incorrect asset, incorrect address, failed transfer, insufficient balance or insufficient assets for owner-signed actions; (g) any suspension, cancellation, void, push, repricing, reversal, partial settlement or delay affecting any RFQ, quote, leg, position or payout; (h) any unauthorized access or use of your credentials, wallet, device, account, API key, delegated signer or session signer; or (i) any failure or delay resulting from causes beyond its or their reasonable control, including acts of God, natural disasters, war, terrorism, civil unrest, labor disputes, sanctions, government actions, legal or regulatory changes, power failures, telecommunications failures, internet failures, blockchain failures, validator or sequencer failures, bugs, exploits, denial-of-service events, security incidents or failures of any Third-Party Service. To the maximum extent permitted by applicable law, the aggregate liability of the Totalis Parties to you for all claims arising out of or relating to these Terms of Service, the Service and any positions or attempted positions will not exceed the greater of: (i) the total fees actually paid by you to Totalis during the six (6) months immediately preceding the event giving rise to the claim; or (ii) one hundred U.S. dollars (US\$100). The limitations in this Section apply regardless of the form of action, whether in contract, tort (including negligence), strict liability, statute, or otherwise, and regardless of whether any limited remedy fails of its essential purpose. ## Dispute Resolution By Binding Arbitration PLEASE READ THIS SECTION CAREFULLY AS IT AFFECTS YOUR RIGHTS. ### Agreement to Arbitrate This Dispute Resolution by Binding Arbitration section is referred to in these Terms of Service as the "Arbitration Agreement." You agree that any and all disputes or claims that have arisen or may arise between you and Totalis, whether arising out of or relating to these Terms of Service (including any alleged breach thereof), the Service, any advertising, or any aspect of the relationship or transactions between us, will be resolved exclusively through final and binding arbitration, rather than a court, in accordance with the terms of this Arbitration Agreement, except that you may assert individual claims in small claims court, if your claims qualify. Further, this Arbitration Agreement does not preclude you from bringing issues to the attention of federal, state, or local agencies, and such agencies can, if the law allows, seek relief against us on your behalf. You agree that, by entering into these Terms of Service, you and Totalis are each waiving the right to a trial by jury or to participate in a class action. Your rights will be determined by a neutral arbitrator, not a judge or jury. The Federal Arbitration Act governs the interpretation and enforcement of this Arbitration Agreement. ### Prohibition of Class and Representative Actions and Non-Individualized Relief YOU AND TOTALIS AGREE THAT EACH OF US MAY BRING CLAIMS AGAINST THE OTHER ONLY ON AN INDIVIDUAL BASIS AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE ACTION OR PROCEEDING. UNLESS BOTH YOU AND TOTALIS AGREE OTHERWISE, THE ARBITRATOR MAY NOT CONSOLIDATE OR JOIN MORE THAN ONE PERSON'S OR PARTY'S CLAIMS AND MAY NOT OTHERWISE PRESIDE OVER ANY FORM OF A CONSOLIDATED, REPRESENTATIVE, OR CLASS PROCEEDING. ALSO, THE ARBITRATOR MAY AWARD RELIEF (INCLUDING MONETARY, INJUNCTIVE, AND DECLARATORY RELIEF) ONLY IN FAVOR OF THE INDIVIDUAL PARTY SEEKING RELIEF AND ONLY TO THE EXTENT NECESSARY TO PROVIDE RELIEF NECESSITATED BY THAT PARTY'S INDIVIDUAL CLAIM(S), EXCEPT THAT YOU MAY PURSUE A CLAIM FOR AND THE ARBITRATOR MAY AWARD PUBLIC INJUNCTIVE RELIEF UNDER APPLICABLE LAW TO THE EXTENT REQUIRED FOR THE ENFORCEABILITY OF THIS PROVISION. ### Pre-Arbitration Dispute Resolution Totalis is always interested in resolving disputes amicably and efficiently, and most user concerns can be resolved quickly and to the user's satisfaction by emailing user support at [support@totalis.trade](mailto:support@totalis.trade). If such efforts prove unsuccessful, a party who intends to seek arbitration must first send to the other, by certified mail, a written Notice of Dispute ("Notice"). The Notice to Totalis should be sent to [legal@totalis.trade](mailto:legal@totalis.trade) ("Notice Address"). The Notice must (i) describe the nature and basis of the claim or dispute and (ii) set forth the specific relief sought. If Totalis and you do not resolve the claim within sixty (60) calendar days after the Notice is received, you or Totalis may commence an arbitration proceeding. During the arbitration, the amount of any settlement offer made by Totalis or you will not be disclosed to the arbitrator until after the arbitrator determines the amount, if any, to which you or Totalis is entitled. ### Arbitration Procedures Arbitration will be conducted by a neutral arbitrator in accordance with the American Arbitration Association's ("AAA") rules and procedures, including the AAA's Consumer Arbitration Rules (collectively, the "AAA Rules"), as modified by this Arbitration Agreement. For information on the AAA, please visit its website, [https://www.adr.org](https://www.adr.org). Information about the AAA Rules and fees for consumer disputes can be found at the AAA's consumer arbitration page, [https://www.adr.org/consumer](https://www.adr.org/consumer). If there is any inconsistency between any term of the AAA Rules and any term of this Arbitration Agreement, the applicable terms of this Arbitration Agreement will control unless the arbitrator determines that the application of the inconsistent Arbitration Agreement terms would not result in a fundamentally fair arbitration. The arbitrator must also follow the provisions of these Terms of Service as a court would. All issues are for the arbitrator to decide, including issues relating to the scope, enforceability, and arbitrability of this Arbitration Agreement. Although arbitration proceedings are usually simpler and more streamlined than trials and other judicial proceedings, the arbitrator can award the same damages and relief on an individual basis that a court can award to an individual under these Terms of Service and applicable law. Decisions by the arbitrator are enforceable in court and may be overturned by a court only for very limited reasons. Unless Totalis and you agree otherwise, any arbitration hearings will take place in a reasonably convenient location for both parties with due consideration of their ability to travel and other pertinent circumstances. If the parties are unable to agree on a location, the determination will be made by AAA. If your claim is for \$10,000 or less, Totalis agrees that you may choose whether the arbitration will be conducted solely on the basis of documents submitted to the arbitrator, through a telephonic hearing, or by an in-person hearing as established by the AAA Rules. If your claim exceeds \$10,000, the right to a hearing will be determined by the AAA Rules. Regardless of the manner in which the arbitration is conducted, the arbitrator will issue a reasoned written decision sufficient to explain the essential findings and conclusions on which the award is based. ### Costs of Arbitration Payment of all filing, administration, and arbitrator fees (collectively, the "Arbitration Fees") will be governed by the AAA Rules, unless otherwise provided in this Arbitration Agreement. To the extent any Arbitration Fees are not specifically allocated to either Totalis or you under the AAA Rules, Totalis and you shall split them equally; provided that if you are able to demonstrate to the arbitrator that you are economically unable to pay your portion of such Arbitration Fees or if the arbitrator otherwise determines for any reason that you should not be required to pay your portion of any Arbitration Fees, Totalis will pay your portion of such fees. In addition, if you demonstrate to the arbitrator that the costs of arbitration will be prohibitive as compared to the costs of litigation, Totalis will pay as much of the Arbitration Fees as the arbitrator deems necessary to prevent the arbitration from being cost-prohibitive. Any payment of attorneys' fees will be governed by the AAA Rules. ### Confidentiality All aspects of the arbitration proceeding, and any ruling, decision, or award by the arbitrator, will be strictly confidential for the benefit of all parties. ### Severability If a court or the arbitrator decides that any term or provision of this Arbitration Agreement (other than the subsection (b) above titled "Prohibition of Class and Representative Actions and Non-Individualized Relief" above) is invalid or unenforceable, the parties agree to replace such term or provision with a term or provision that is valid and enforceable and that comes closest to expressing the intention of the invalid or unenforceable term or provision, and this Arbitration Agreement will be enforceable as so modified. If a court or the arbitrator decides that any of the provisions of subsection (b) above titled "Prohibition of Class and Representative Actions and Non-Individualized Relief" are invalid or unenforceable, then the entirety of this Arbitration Agreement will be null and void, unless such provisions are deemed to be invalid or unenforceable solely with respect to claims for public injunctive relief. The remainder of these Terms of Service will continue to apply. ### Future Changes to Arbitration Agreement Notwithstanding any provision in these Terms of Service to the contrary, Totalis agrees that if it makes any future change to this Arbitration Agreement (other than a change to the Notice Address) while you are a user of the Service, you may reject any such change by sending Totalis written notice within thirty (30) calendar days of the change to the Notice Address provided above. By rejecting any future change, you are agreeing that you will arbitrate any dispute between us in accordance with the language of this Arbitration Agreement as of the date you first accepted these Terms of Service (or accepted any subsequent changes to these Terms of Service). ## Suspension and Termination Totalis may, at any time and in its sole discretion, with or without notice, suspend, restrict, limit, geoblock, rate-limit, disable or terminate your access to all or any part of the Service, or refuse to support any wallet, position, venue, market, token, asset, network, API key, delegated signer or session signer associated with you, for any reason or no reason, including any actual, suspected or potential breach of these Terms of Service, eligibility concerns, compliance concerns, legal or regulatory concerns, sanctions concerns, security concerns, market-integrity concerns, operational concerns, technical concerns, inactivity or disputes. Totalis may also cancel, reject or decline to facilitate pending RFQs, quotes, acceptances, position-creation steps, or settlement-related actions where Totalis determines that doing so is advisable. Nothing in these Terms of Service obligates Totalis to continue to provide the Service or any feature of the Service to you or to any other person. Termination or suspension of your access does not limit any other right or remedy available to Totalis. Any provision of these Terms of Service that by its nature should survive suspension or termination will survive, including provisions relating to representations, risk disclosures, intellectual property, indemnification, disclaimers, limitation of liability, dispute resolution and general provisions. ## User Disputes You agree that you are solely responsible for your interactions with any other user in connection with the Service, and Totalis and Totalis Affiliates will have no liability or responsibility with respect thereto. Totalis reserves the right, but has no obligation, to become involved in any way with disputes between you and any other user of the Service. ## General These Terms of Service (together with the terms incorporated by reference herein) constitute the entire agreement between you and Totalis governing your access and use of the Service, and supersede any prior agreements between you and Totalis with respect to the Service. You also may be subject to additional terms and conditions that may apply when you use Third-Party Services, third-party content or third-party software. These Terms of Service will be governed by the laws of the State of California without regard to its conflict of law provisions. With respect to any disputes or claims not subject to arbitration, as set forth above, you and Totalis submit to the personal and exclusive jurisdiction of the state and federal courts located within San Francisco, California. The failure of Totalis to exercise or enforce any right or provision of these Terms of Service will not constitute a waiver of such right or provision. If any provision of these Terms of Service is found by a court of competent jurisdiction to be invalid, the parties nevertheless agree that the court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of these Terms of Service remain in full force and effect. You agree that regardless of any Law to the contrary, any claim or cause of action arising out of or related to use of the Service or these Terms of Service must be filed within one (1) year after such claim or cause of action arose or be forever barred. A printed version of these Terms of Service and of any notice given in electronic form will be admissible in judicial or administrative proceedings based upon or relating to these Terms of Service to the same extent and subject to the same conditions as other business documents and records originally generated and maintained in printed form. You may not assign these Terms of Service without the prior written consent of Totalis, but Totalis may assign or transfer these Terms of Service, in whole or in part, without restriction. The section titles in these Terms of Service are for convenience only and have no legal or contractual effect. As used in these Terms of Service, the words "include" and "including," and variations thereof, will not be deemed to be terms of limitation, but rather will be deemed to be followed by the words "without limitation." Notices to you may be made via either email or regular mail. The Service may also provide notices to you of changes to these Terms of Service or other matters by displaying notices or links to notices generally on the Service. You consent to receive communications from Totalis electronically, including by email, through the Service, through API responses, or by other electronic means. You agree that all agreements, notices, disclosures, and other communications that Totalis provides to you electronically satisfy any legal requirement that such communications be in writing. ## Contact Information Please contact us at [legal@totalis.trade](mailto:legal@totalis.trade) to report any violations of these Terms of Service or to pose any questions regarding these Terms of Service or the Service.