{ "openapi": "3.0.3", "info": { "title": "Totalis RFQ API", "version": "2.1.0", "description": "Public REST surface for the Totalis parlay RFQ platform — a decentralized request-for-quote marketplace for parlay bets across Kalshi and Polymarket, with on-chain Solana vault settlement.\n\n**Wire format**: snake_case JSON. All successful responses are wrapped in `{ \"data\": ... }`; list endpoints add `meta` with cursor-based pagination. Errors use a `{ \"error\": { code, message, details? } }` envelope.\n\n**Authentication**: programmatic clients send `X-API-Key`; the web dashboard uses Privy JWTs (`Authorization: Bearer ...`). Any authenticated user can both place parlays and quote as a market maker — there is no separate MM role. Admin endpoints are out of scope for this public reference.\n\n**Rate limiting**: 100 req/min anonymous, 300 req/min authenticated. Responses include `X-RateLimit-*` headers. Request body limit: 256KB.\n\n**Pagination**: list endpoints use opaque cursor pagination. Pass `meta.cursor` from the previous response as `?cursor=...` to fetch the next page; do not parse or construct cursors client-side." }, "servers": [ { "url": "https://api.totalis.trade", "description": "Production" } ], "security": [ { "ApiKey": [] } ], "tags": [ { "name": "Markets", "description": "Cached Kalshi and Polymarket market data." }, { "name": "User", "description": "User profile, wallet, and devnet helpers." }, { "name": "API Keys", "description": "Manage programmatic access keys for the authenticated user." } ], "paths": { "/markets": { "get": { "operationId": "listMarkets", "summary": "List Markets", "description": "List active markets grouped by event across supported venues (Kalshi and Polymarket). Filters: `category`, `venue`, `subcategory` (validated against the selected category), `frequency` (`daily`/`weekly`/`monthly`/`hourly`), `date_filter` (time bucket), and `search`. Pagination is cursor-based at the event level. The response includes `available_subcategories` and `available_frequencies` so clients can drive filter chips from the same result set.", "tags": [ "Markets" ], "security": [], "parameters": [ { "name": "category", "in": "query", "description": "Filter by category. Pass `all` to disable the default category filter.", "schema": { "type": "string", "enum": [ "politics", "sports", "crypto", "finance", "economics", "entertainment", "weather", "tech", "all" ] } }, { "name": "venue", "in": "query", "description": "Filter by venue. Omit to include all venues.", "schema": { "$ref": "#/components/schemas/Venue" } }, { "name": "subcategory", "in": "query", "description": "Filter events by subcategory. Valid values depend on the selected `category` (requires `category` to also be set).", "schema": { "$ref": "#/components/schemas/SubcategorySlug" } }, { "name": "frequency", "in": "query", "description": "Filter events by the cadence of the underlying series. `other` is intentionally rejected — it's a residual response bucket, not a filter target.", "schema": { "type": "string", "enum": [ "daily", "weekly", "monthly", "hourly" ] } }, { "name": "date_filter", "in": "query", "description": "Time bucket filter. If omitted, a default 7-day horizon is applied. Pass `all` to retrieve all active markets regardless of close date.", "schema": { "type": "string", "enum": [ "1h", "1h-1d", "1d-7d", "today", "this_week", "this_month", "all" ] } }, { "name": "search", "in": "query", "description": "Full-text search across event/market titles.", "schema": { "type": "string" } }, { "$ref": "#/components/parameters/CursorParam" }, { "name": "limit", "in": "query", "description": "Events per page. Default 21, max 100. The product `limit × markets_per_event` must not exceed 2000.", "schema": { "type": "integer", "default": 21, "minimum": 1, "maximum": 100 } }, { "name": "markets_per_event", "in": "query", "description": "Max markets returned per event group. Default 10, max 50.", "schema": { "type": "integer", "default": 10, "minimum": 1, "maximum": 50 } } ], "responses": { "200": { "description": "Events grouped by event_ticker, plus subcategory/frequency facets for the current result set", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "events": { "type": "array", "items": { "$ref": "#/components/schemas/EventGroup" } } } }, "meta": { "allOf": [ { "$ref": "#/components/schemas/PaginationMeta" }, { "type": "object", "properties": { "total_events": { "type": "integer" }, "available_subcategories": { "type": "array", "items": { "$ref": "#/components/schemas/SubcategorySlug" } }, "available_frequencies": { "type": "array", "items": { "type": "string", "enum": [ "daily", "weekly", "monthly", "hourly" ] } } } } ] } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/markets/list": { "get": { "operationId": "listMarketsFlat", "security": [], "tags": [ "Markets" ], "summary": "List markets (flat)", "description": "Returns a paginated list of individual markets, most-traded first. Unlike the event-grouped `GET /markets`, results aren't grouped by event and are never capped per event, so every market is reachable. Filter by venue, category, subcategory, or one or more series.\n\nPaginate with `cursor` until it returns `null`; `meta.total` is the total number of matches. For a complete sync, de-duplicate by `ticker` — markets with equal volume aren't guaranteed a stable order between requests. Scoping to a `subcategory` or `series_ticker` set avoids this entirely.", "parameters": [ { "name": "category", "in": "query", "description": "Filter by category, e.g. `sports` or `crypto`.", "schema": { "type": "string", "enum": [ "crypto", "economics", "finance", "sports", "politics", "tech", "entertainment", "weather", "all" ] } }, { "name": "venue", "in": "query", "description": "Filter by venue: `kalshi` or `polymarket`.", "schema": { "$ref": "#/components/schemas/Venue" } }, { "name": "subcategory", "in": "query", "description": "Filter by subcategory, e.g. `world_cup`. Requires `category`.", "schema": { "$ref": "#/components/schemas/SubcategorySlug" } }, { "name": "frequency", "in": "query", "description": "Filter by series cadence: `daily`, `weekly`, `monthly`, or `hourly`.", "schema": { "type": "string", "enum": [ "daily", "weekly", "monthly", "hourly" ] } }, { "name": "series_ticker", "in": "query", "description": "Filter by one or more series, comma-separated and case-sensitive, e.g. `KXWCAST,KXWCTCORNERS`. Up to 100.", "schema": { "type": "string" } }, { "name": "status", "in": "query", "description": "Filter by status. Defaults to tradeable markets (`active,open`); pass a comma-separated list of `active`, `open`, `closed`, `settled`, `finalized`, or `all`. An empty or unrecognised value returns 400.", "schema": { "type": "string", "default": "active,open" } }, { "$ref": "#/components/parameters/CursorParam" }, { "name": "limit", "in": "query", "description": "Number of markets per page, from 1 to 500. Defaults to 100. Only the first 10,000 matches are reachable — narrow the filters to page further.", "schema": { "type": "integer", "default": 100, "minimum": 1, "maximum": 500 } } ], "responses": { "200": { "description": "OK", "headers": { "Cache-Control": { "schema": { "type": "string", "example": "public, max-age=15, stale-while-revalidate=60" } } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "object", "required": [ "markets" ], "properties": { "markets": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } } } }, "meta": { "allOf": [ { "$ref": "#/components/schemas/PaginationMeta" }, { "type": "object", "properties": { "total": { "type": "integer", "description": "Total markets matching the filters across all pages." } } } ] } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "description": "Market index unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } } } }, "/markets/{ticker}": { "get": { "operationId": "getMarket", "summary": "Get Market", "description": "Fetch a single Kalshi market by ticker or a single Polymarket market by condition ID. When `venue` is omitted, the API infers Polymarket for condition IDs that look like `0x...`; otherwise it looks up the market as Kalshi.", "tags": [ "Markets" ], "security": [], "parameters": [ { "name": "ticker", "in": "path", "required": true, "description": "Kalshi market ticker or Polymarket `condition_id`.", "schema": { "type": "string", "maxLength": 160, "pattern": "^[A-Za-z0-9_.:-]+$" } }, { "name": "venue", "in": "query", "required": false, "description": "Optional venue hint. Use this when the caller already knows whether the ticker belongs to Kalshi or Polymarket.", "schema": { "$ref": "#/components/schemas/Venue" } } ], "responses": { "200": { "description": "Market details", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Market" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/v1/me": { "get": { "operationId": "getOrCreateUser", "summary": "Get Profile", "description": "Get the authenticated user's profile. Auto-creates a user record on first call. Pass `?ref=` on first login to auto-redeem an invite code; when redemption succeeds the response includes `auto_redeemed: true` and, where configured, an `airdrop` outcome.", "tags": [ "User" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "ref", "in": "query", "description": "Referral / invite code to auto-redeem on first login.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "User profile", "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/User" } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/v1/username": { "put": { "operationId": "updateUsername", "summary": "Update Username", "description": "Change the authenticated user's display name. Must be 3–20 characters, alphanumeric and underscores only.", "tags": [ "User" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "username" ], "properties": { "username": { "type": "string", "minLength": 3, "maxLength": 20, "pattern": "^[a-zA-Z0-9_]+$", "description": "New username (3–20 chars, alphanumeric + underscores)." } } }, "example": { "username": "trader_123" } } } }, "responses": { "200": { "description": "Username updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "username": { "type": "string" } } } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "409": { "$ref": "#/components/responses/Conflict" } } } }, "/v1/username/check": { "get": { "operationId": "checkUsername", "summary": "Check Username", "description": "Check whether a username is available. Must match 3–20 chars, alphanumeric + underscores.", "tags": [ "User" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "username", "in": "query", "required": true, "description": "Candidate username.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Availability result", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "available": { "type": "boolean" } } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/v1/api-keys": { "post": { "operationId": "createApiKey", "summary": "Create API Key", "description": "Create a new API key for the authenticated user. The full secret is returned **once** in `data.key` — save it immediately. Subsequent listings only show the prefix. Requires Privy JWT auth (web dashboard); cannot be called with an existing API key.", "tags": [ "API Keys" ], "security": [ { "PrivyJWT": [] } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "description": "Optional display name for the key." } } } } } }, "responses": { "201": { "description": "API key created (full key visible once)", "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/ApiKeyFull" } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } }, "get": { "operationId": "listApiKeys", "summary": "List API Keys", "description": "List the authenticated user's API keys. Only metadata and prefixes are returned — the original secret is never re-exposed.", "tags": [ "API Keys" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "responses": { "200": { "description": "List of API keys", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "keys": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKeySummary" } } } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/v1/api-keys/{prefix}": { "delete": { "operationId": "revokeApiKey", "summary": "Revoke API Key", "description": "Revoke one of the authenticated user's API keys by prefix. Takes effect immediately on all subsequent requests. Returns `204 No Content`.", "tags": [ "API Keys" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "prefix", "in": "path", "required": true, "description": "Key prefix returned by `listApiKeys` / `createApiKey` (`key_prefix`).", "schema": { "type": "string" } } ], "responses": { "204": { "description": "API key revoked" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/v1/wallet": { "get": { "operationId": "getWallet", "summary": "Get Wallet", "description": "Get the authenticated user's embedded Solana wallet address, wallet/vault balances, and the aggregated `locked_amount` across active RFQs and vault positions.", "tags": [ "User" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "responses": { "200": { "description": "Wallet snapshot", "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Wallet" } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/v1/wallet/submit-tx": { "post": { "operationId": "submitTransaction", "summary": "Submit Transaction", "description": "Submit a client-signed serialized Solana transaction. Used for wallet delegation and other user-signed flows from browser clients without direct RPC access. Rate-limited to 10 requests/minute.", "tags": [ "User" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "transaction" ], "properties": { "transaction": { "type": "string", "description": "Base64-encoded serialized Solana transaction." } } }, "example": { "transaction": "AQAAAA..." } } } }, "responses": { "200": { "description": "Transaction broadcast", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "required": [ "signature" ], "properties": { "signature": { "type": "string", "description": "Solana transaction signature." } } } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/v1/vault": { "get": { "operationId": "getUserVault", "tags": [ "Vault" ], "summary": "Get user vault state and active positions", "description": "Returns the user's vault balance and active positions. Returns data: null\n(200 OK) if the user has no vault yet (vault is created on first trade).\n", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "responses": { "200": { "description": "OK — vault state or null if no vault exists", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "nullable": true, "allOf": [ { "$ref": "#/components/schemas/UserVaultResponse" } ] } } } } } } } } }, "/v1/portfolio": { "get": { "operationId": "getUserPortfolio", "tags": [ "User" ], "summary": "Consolidated portfolio (balance, stats, counts, summary)", "description": "Returns the user's full portfolio in a single batched query: vault balance,\ntrading stats, RFQ status counts, and active-bet summary. Balance is null\nwhen the user has no vault (never traded).\n", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/PortfolioData" } } } } } } } } }, "/v1/wallet/delegation-status": { "get": { "operationId": "getDelegationStatus", "tags": [ "User" ], "summary": "Check TEE wallet delegation status", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DelegationStatus" } } } } } } } } }, "/v1/wallet/enable-trading": { "post": { "operationId": "enableTrading", "tags": [ "User" ], "summary": "Confirm TEE wallet delegation AND drain any pending welcome airdrop", "description": "Two responsibilities, kept in one round-trip so the frontend can\nrender a single combined success toast:\n\n1. **Delegation status.** Confirms whether the embedded wallet\n is currently delegated to our key quorum. The actual\n delegation is a USER-SIDE action (frontend `addSigners()`);\n this endpoint only reads the current state — it does NOT\n perform the delegation itself.\n2. **Pending welcome-airdrop drain.** Under the deferred-airdrop\n flow (TT-airdrop-bug 2026-05-13), the welcome airdrop amount\n is staged on `users.welcome_airdrop_pending_amount` at\n approval time. This endpoint atomically claims that amount,\n mints the USDC from treasury to the canonical wallet, and\n clears the column. Idempotent: a second call after a\n successful drain returns `airdrop: null` (nothing left to\n drain) without re-minting. On mint failure, the pending\n amount is restored so the user can retry on their next call.\n", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/DelegationStatus" }, { "type": "object", "properties": { "airdrop": { "allOf": [ { "$ref": "#/components/schemas/AirdropDrainOutcome" } ], "nullable": true, "description": "Outcome of the pending-airdrop drain.\n`null` when nothing was staged for this\nuser (most common — pre-approval, or\nalready drained on a prior call). See\n`AirdropDrainOutcome` for the non-null\nshape.\n" } } } ] } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/v1/wallet/withdraw": { "post": { "operationId": "withdrawFromWallet", "tags": [ "User" ], "summary": "Withdraw USDC from the embedded wallet to an external address", "description": "Sends USDC out of the caller's embedded wallet ATA to an arbitrary\non-curve Solana address. Uses Privy's session-signer delegation\n(set up via /v1/wallet/enable-trading) and Privy's native gas\nsponsorship — the user is not prompted to sign and never needs SOL.\n\nChained vault-tap: when the requested `amount_usdc` exceeds the\ncaller's ATA-available balance, the handler first issues\n`withdraw_from_vault` (vault PDA → owner ATA) for the shortfall,\nwaits for that TX to confirm, then issues the external SPL transfer.\nThe `vault_signature` / `vault_amount` fields surface the intermediate\nTX when this happens. If the vault tap succeeds but the external\ntransfer fails to construct or broadcast, the response is **502**\nwith `error.code: PARTIAL_VAULT_ONLY` and the vault TX surfaced under\n`error.details.vault_signature` / `error.details.vault_amount`. The\ncaller can retry with the same amount; the pre-flight will see the\nATA is now sufficient and skip the vault tap. 502 (rather than 200)\nis deliberate: the idempotency middleware caches only 2xx, so\nsame-key retries against a 502 re-enter the handler instead of\nreplaying a cached partial response.\n\nIdempotency-Key is honored: a duplicate request with the same key\nreplays the cached 2xx response without re-issuing on-chain TXs.\n", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "recipient", "amount_usdc" ], "properties": { "recipient": { "type": "string", "description": "Destination Solana wallet address (base58). Must be\non-curve — PDAs, program IDs, and token-account\naddresses are rejected because funds sent to a\nderived ATA-of-an-ATA are unrecoverable.\n" }, "amount_usdc": { "type": "number", "minimum": 1e-06, "description": "USDC amount to send. Minimum is one USDC base unit\n(1e-6). Must be ≤ the caller's total spendable pool\n(ATA + vault free_balance − in-flight RFQ holds);\nrequests exceeding this are rejected with 400.\n" } } } } } }, "responses": { "200": { "description": "Withdrawal submitted. `signature` is the external SPL transfer\nsignature. `vault_signature` and `vault_amount` are present\nonly when the request required a vault → ATA hop.\n", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "signature": { "type": "string", "description": "External SPL transfer signature." }, "explorer_url": { "type": "string", "description": "Solscan URL for the external SPL signature." }, "vault_signature": { "type": "string", "description": "On-chain `withdraw_from_vault` signature.\nPresent only when the request required a vault\n→ ATA hop to cover a shortfall.\n" }, "vault_amount": { "type": "number", "description": "USDC amount moved from vault to ATA before the\nexternal transfer. Present only when\n`vault_signature` is present.\n" }, "recipient_ata_created": { "type": "boolean", "description": "Present (true) when the recipient had no USDC token\naccount and this transaction created one (the\ncreate-and-chargeback path, gated by\nWITHDRAW_ATA_AUTOCREATE). The fee-payer fronts the\n~0.002 SOL account rent and bills it back in USDC, so\nthe recipient receives `delivered_usdc` and the treasury\nkeeps `chargeback_usdc`. The create is non-idempotent, so\na confirmed response means this tx created the account\n(a race where it already existed reverts and retries\nvia the standard path, with no chargeback).\n" }, "chargeback_usdc": { "type": "number", "description": "USDC withheld from the requested amount to reimburse\nthe one-time recipient-ATA rent. Present only with\n`recipient_ata_created`.\n" }, "delivered_usdc": { "type": "number", "description": "USDC actually delivered to the recipient\n(`amount_usdc − chargeback_usdc`). Present only with\n`recipient_ata_created`.\n" } } } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "409": { "description": "The caller's embedded wallet is no longer delegated for\nserver-side signing. Re-enabling trading from the app restores\nthe session signer.\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "502": { "description": "Ambiguous partial state — returned as 5xx (not 200) so\nIdempotency-Key callers retry against the handler instead of\nreplaying a cached partial response. Two codes:\n\n`PARTIAL_VAULT_ONLY` — the vault tap succeeded but the external SPL\ntransfer failed to construct/broadcast. Funds moved from vault to\nATA; retry the same request to complete the external leg (the\npre-flight sees the updated ATA and skips the vault tap). Returned\nonly when the vault tap fired this request; a same-key retry against\nan already-topped-up ATA skips the tap (`shortfall = 0`) and a\nsubsequent SPL failure surfaces as a normal 500.\n\n`WITHDRAW_SUBMITTED_UNCONFIRMED` — only on the recipient-ATA\ncreate-and-chargeback path (WITHDRAW_ATA_AUTOCREATE), where broadcast\nand confirmation are separate steps: the transaction was broadcast\n(`details.signature` present) but confirmation timed out, so it may\nhave landed. Do NOT blind-retry — verify the recipient balance first,\nsince a retry would re-send via the now-existing ATA and double-pay.\n", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "PARTIAL_VAULT_ONLY", "WITHDRAW_SUBMITTED_UNCONFIRMED" ] }, "message": { "type": "string" }, "details": { "type": "object", "properties": { "signature": { "type": "string", "description": "The broadcast transaction signature. Present on\nWITHDRAW_SUBMITTED_UNCONFIRMED — the tx may have\nlanded; check it before retrying.\n" }, "vault_signature": { "type": "string" }, "vault_amount": { "type": "number" }, "chargeback_usdc": { "type": "number", "description": "On WITHDRAW_SUBMITTED_UNCONFIRMED, the USDC that\nwould have been withheld for the recipient-ATA rent.\n" }, "delivered_usdc": { "type": "number", "description": "On WITHDRAW_SUBMITTED_UNCONFIRMED, the USDC that\nwould have been delivered to the recipient.\n" }, "partial": { "type": "string", "enum": [ "vault_only", "submitted_unconfirmed" ] } } } } } } } } } } } } }, "/v1/rfqs": { "get": { "operationId": "listRfqsV1", "tags": [ "RFQs" ], "summary": "List parlays", "description": "List your parlays (RFQs), newest first, with cursor pagination. Filter by status (`?status=settled`, or comma-separated `?status=open,quoted`); `?include=quotes` embeds each parlay's quotes.", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "status", "in": "query", "description": "Filter by RFQ status. Repeatable values are joined with commas (e.g. ?status=open,quoted,accepted).", "style": "form", "explode": false, "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RfqStatus" } }, "example": [ "open", "quoted", "accepted" ] }, { "name": "include", "in": "query", "schema": { "type": "string", "enum": [ "quotes" ] }, "description": "Include quotes array (otherwise null)" }, { "$ref": "#/components/parameters/LimitParam" }, { "$ref": "#/components/parameters/CursorParam" } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "rfqs": { "type": "array", "items": { "$ref": "#/components/schemas/Rfq" } } } }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } } } } } } } } }, "/v1/rfqs/{id}": { "get": { "operationId": "getRfqV1", "tags": [ "RFQs" ], "summary": "Get parlay", "description": "Fetch one of your parlays (RFQs) by id, with its quotes. Once the parlay is terminal the response also carries its settlement detail — per-leg outcomes, final status, payout, and the settle/buyback transaction signature. An id you don't own returns 404.", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Rfq" } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/v1/pnl": { "get": { "operationId": "getPnlV1", "tags": [ "Portfolio" ], "summary": "P&L timeseries", "description": "Realized P&L per day over the requested window (`1D` / `1W` / `1M` / `ALL`; default `1W`). Stats totals (wins / losses / realized P&L) are on `GET /v1/portfolio`.", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "period", "in": "query", "schema": { "type": "string", "enum": [ "1D", "1W", "1M", "ALL" ], "default": "1W" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "points": { "type": "array", "items": { "$ref": "#/components/schemas/PnlPoint" } } } } } } } } } } } }, "/v1/webhooks": { "get": { "operationId": "getWebhookConfig", "tags": [ "Webhooks" ], "summary": "Get webhook config", "description": "Returns the current webhook endpoint config (or nulls if never configured) plus the subscribable event catalog. The signing secret is never returned — only whether one is set.", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "owner_kind", "in": "query", "description": "Which endpoint to address: `user` (default) or `mm` (market-maker).", "schema": { "type": "string", "enum": [ "user", "mm" ], "default": "user" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/WebhookConfig" } } } } } } } }, "put": { "operationId": "setWebhookConfig", "tags": [ "Webhooks" ], "summary": "Set webhook config", "description": "Replace the endpoint URL and subscribed events. Preserves the signing secret. A fresh endpoint is undeliverable until a secret is set (see rotate-secret).", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "owner_kind", "in": "query", "description": "Which endpoint to address: `user` (default) or `mm` (market-maker).", "schema": { "type": "string", "enum": [ "user", "mm" ], "default": "user" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookConfigInput" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/WebhookConfig" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" } } } }, "/v1/webhooks/rotate-secret": { "post": { "operationId": "rotateWebhookSecret", "tags": [ "Webhooks" ], "summary": "Rotate signing secret", "description": "Mint a fresh HMAC signing secret, returned ONCE (stored encrypted, never retrievable again). Requires an existing endpoint (set a URL first). Deliveries signed with the old secret stop verifying immediately.", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "owner_kind", "in": "query", "description": "Which endpoint to address: `user` (default) or `mm` (market-maker).", "schema": { "type": "string", "enum": [ "user", "mm" ], "default": "user" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "signing_secret": { "type": "string", "description": "The new secret (`whsec_…`). Shown only once." } } } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/v1/webhooks/deliveries": { "get": { "operationId": "listWebhookDeliveries", "tags": [ "Webhooks" ], "summary": "List recent deliveries", "description": "Recent delivery attempts (status/attempt metadata, newest first). Empty if no endpoint is configured.", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "owner_kind", "in": "query", "description": "Which endpoint to address: `user` (default) or `mm` (market-maker).", "schema": { "type": "string", "enum": [ "user", "mm" ], "default": "user" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50 } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookDelivery" } } } } } } } } } } } }, "/v1/webhooks/deliveries/{id}/redeliver": { "post": { "operationId": "redeliverWebhookDelivery", "tags": [ "Webhooks" ], "summary": "Replay a delivery", "description": "Re-queue a settled (delivered or dead_letter) delivery, scoped to your own endpoint. Replays reuse the same X-Totalis-Event-Id.", "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "parameters": [ { "name": "owner_kind", "in": "query", "description": "Which endpoint to address: `user` (default) or `mm` (market-maker).", "schema": { "type": "string", "enum": [ "user", "mm" ], "default": "user" } }, { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "requeued": { "type": "boolean" } } } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } } }, "components": { "securitySchemes": { "PrivyJWT": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Privy JWT issued to the web dashboard. Sent as `Authorization: Bearer `. The Privy session signer underpins all wallet-signed actions." }, "ApiKey": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "Programmatic API key. Sent as `X-API-Key: `. Generate one from the Totalis dashboard. The same header is accepted on the WebSocket auth message." } }, "parameters": { "CursorParam": { "name": "cursor", "in": "query", "description": "Opaque pagination cursor from a previous response's `meta.cursor`. Do not construct manually.", "schema": { "type": "string" } }, "LimitParam": { "name": "limit", "in": "query", "description": "Page size.", "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 100 } }, "MmLimitParam": { "name": "limit", "in": "query", "description": "Page size (MM endpoints default to 50, max 100).", "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 100 } }, "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "description": "Client-generated UUID. Successful (2xx) responses are cached for 24h and replayed on retry with the same key. The request body is not part of the cache key — use a fresh UUID whenever the payload differs (e.g. updated odds), otherwise the original response is replayed. Non-2xx responses are not cached.", "schema": { "type": "string", "format": "uuid" } } }, "responses": { "BadRequest": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "Unauthorized": { "description": "Missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }, "example": { "error": { "code": "UNAUTHORIZED", "message": "Missing or invalid authentication" } } } } }, "Forbidden": { "description": "Authenticated caller is not allowed to perform this action", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "NotFound": { "description": "Resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }, "example": { "error": { "code": "NOT_FOUND", "message": "Resource not found" } } } } }, "Conflict": { "description": "Resource conflict (duplicate, already taken, invalid status transition, etc.)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "RateLimited": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "InternalError": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "ServiceUnavailable": { "description": "Temporary upstream or infrastructure failure", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } }, "schemas": { "ErrorEnvelope": { "type": "object", "required": [ "error" ], "properties": { "error": { "$ref": "#/components/schemas/ErrorBody" } } }, "ErrorBody": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "enum": [ "VALIDATION_ERROR", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "CONFLICT", "RATE_LIMITED", "PAYLOAD_TOO_LARGE", "INTERNAL_ERROR", "SERVICE_UNAVAILABLE" ], "description": "Machine-readable error code." }, "message": { "type": "string", "description": "Human-readable error message." }, "details": { "type": "object", "description": "Additional error context." }, "retry_after": { "type": "integer", "nullable": true, "description": "Seconds until retry; set on 429 responses." } } }, "PaginationMeta": { "type": "object", "properties": { "cursor": { "type": "string", "nullable": true }, "has_more": { "type": "boolean" }, "limit": { "type": "integer", "description": "Maximum number of items requested for this page." }, "offset": { "type": "integer", "description": "Number of items skipped before this page." }, "total": { "type": "integer", "description": "Total number of items available across all pages, when returned by the endpoint." } } }, "Venue": { "type": "string", "enum": [ "kalshi", "polymarket" ], "description": "Source venue for a market." }, "Side": { "type": "string", "enum": [ "yes", "no" ], "description": "Bet side." }, "WinnerSide": { "type": "string", "enum": [ "user", "mm" ], "description": "Winner of a settled parlay position." }, "LegOutcome": { "type": "string", "enum": [ "win", "loss", "push", "pending" ] }, "RfqStatus": { "type": "string", "enum": [ "open", "quoted", "accepted", "confirmed", "executed", "cancelled", "expired", "settled", "bought_back" ] }, "QuoteStatus": { "type": "string", "enum": [ "pending", "accepted", "confirmed", "executed", "rejected", "expired", "withdrawn" ], "description": "Lifecycle status of a quote." }, "PositionStatus": { "type": "string", "description": "Vault position lifecycle. `pending`/`processing` are intermediate states before on-chain creation; `settling`/`cancelling` are intermediate states for settlement/cancellation; `settled_win`/`settled_loss`/`cancelled` are terminal; the `*_mm_release` and `expired` states cover the permissionless-expiry path; the buyback path is `active → bought_back_pending_db → bought_back` (with `reconciling_bought_back_db` as the short-lived reconciler sentinel); `error` means retries are exhausted and manual intervention is required.", "enum": [ "pending", "processing", "active", "settling", "cancelling", "settled_win", "settled_loss", "cancelled", "reconciling_mm_release", "expired_pending_mm_release", "expired", "reconciling_bought_back_db", "bought_back_pending_db", "bought_back", "error" ] }, "SubcategorySlug": { "type": "string", "description": "Subcategory wire format (lowercase snake_case). Valid values depend on the selected `category` and are enforced server-side.", "enum": [ "nba", "ncaa", "nfl", "nhl", "mlb", "ufc", "lol", "epl", "efl", "la_liga", "world_cup", "bundesliga", "liga_portugal", "ligue_1", "f1", "ipl", "btc", "eth", "sol", "stocks", "indices", "etfs", "earnings", "rates", "inflation", "commodities", "fx", "jobs", "ai", "companies", "other" ] }, "Market": { "type": "object", "properties": { "ticker": { "type": "string", "description": "Unique market identifier on its venue. **Kalshi**: market ticker (e.g. `KXBTC-25FEB07-T100000`). **Polymarket**: `condition_id`, the 0x-prefixed hex string from Polymarket's CTF (e.g. `0x4d2…`); not the gamma id, question id, or market slug. Yes/no CLOB token ids are not returned by this API." }, "event_ticker": { "type": "string", "description": "Event identifier the market belongs to. **Kalshi**: event ticker (e.g. `KXBTC-25FEB07`). **Polymarket**: Gamma `event_slug` (falls back to `market_slug` when no parent event exists)." }, "series_ticker": { "type": "string", "description": "Series identifier the event belongs to. **Kalshi**: series ticker (e.g. `KXBTC`). **Polymarket**: Gamma `market_slug` — Polymarket has no native series concept, so we key whitelisting on the market slug." }, "title": { "type": "string" }, "subtitle": { "type": "string", "nullable": true }, "yes_sub_title": { "type": "string", "nullable": true }, "no_sub_title": { "type": "string", "nullable": true }, "bet_group": { "type": "string", "nullable": true, "description": "Polymarket-only: section label for the row (e.g. \"Match O/U 21.5\"). Always null on Kalshi rows." }, "category": { "type": "string" }, "subcategory": { "type": "string", "nullable": true }, "frequency": { "type": "string", "nullable": true, "description": "Cadence of the underlying series: daily/weekly/monthly/hourly/other. Null for categories without timeframe pills." }, "venue": { "$ref": "#/components/schemas/Venue" }, "status": { "type": "string" }, "yes_bid": { "type": "number" }, "yes_ask": { "type": "number" }, "no_bid": { "type": "number" }, "no_ask": { "type": "number" }, "last_price": { "type": "number" }, "price_to_beat": { "type": "number", "nullable": true, "description": "Reference price for Polymarket up/down markets. Null on Kalshi and on non-up/down Polymarket markets." }, "volume": { "type": "integer" }, "volume_24h": { "type": "integer" }, "open_interest": { "type": "integer" }, "venue_url": { "type": "string", "nullable": true, "description": "Canonical market URL on its source venue." }, "open_time": { "type": "string", "format": "date-time" }, "close_time": { "type": "string", "format": "date-time" }, "expiration_time": { "type": "string", "format": "date-time" }, "exclusion_keys": { "type": "array", "items": { "type": "string" }, "description": "Parlay-correlation keys. Two markets **cannot co-exist in the same parlay if (and only if) their `exclusion_keys` arrays intersect** — i.e. share at least one string. Apply a pure set-intersection; there is no rule table to maintain client-side. The keys encode every correlation rule (cross-series exclusion groups, threshold ladders, same-game sports family/kind/participant correlation, Polymarket sub-events). Keys are opaque and namespaced by reason and scope (e.g. `kp::moneyline|spread`, `grp::`, `fam::`), and are derived solely from the market itself, so a selected leg can snapshot its keys and compare them later. The plain one-leg-per-event rule (same `event_ticker`) is intentionally not encoded here. Always present; an empty array means the market participates in no correlation rule." } } }, "EventGroup": { "type": "object", "required": [ "event_ticker", "series_ticker", "category", "total_markets", "markets" ], "properties": { "event_ticker": { "type": "string", "description": "Event identifier. **Kalshi**: event ticker (e.g. `KXBTC-25FEB07`). **Polymarket**: Gamma `event_slug` (falls back to `market_slug` when no parent event exists)." }, "series_ticker": { "type": "string", "description": "Series identifier. **Kalshi**: series ticker (e.g. `KXBTC`). **Polymarket**: Gamma `market_slug`." }, "series_title": { "type": "string", "nullable": true }, "event_title": { "type": "string", "nullable": true }, "event_sub_title": { "type": "string", "nullable": true }, "category": { "type": "string" }, "image_url": { "type": "string", "nullable": true }, "total_markets": { "type": "integer" }, "markets": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } } } }, "ExclusionGroup": { "type": "object", "properties": { "name": { "type": "string" }, "scope": { "type": "string", "enum": [ "event", "series" ] }, "tickers": { "type": "array", "description": "Identifiers belonging to this exclusion group. When `scope=event`, these are event identifiers (Kalshi event ticker like `KXBTC-25FEB07`, or Polymarket Gamma `event_slug`). When `scope=series`, these are series identifiers (Kalshi series ticker like `KXBTC`, or Polymarket Gamma `market_slug`). Values are compared against `Leg.event_ticker` / `Market.series_ticker` to detect conflicts.", "items": { "type": "string" } }, "description": { "type": "string" } } }, "Leg": { "type": "object", "properties": { "leg_index": { "type": "integer", "description": "Zero-based ordering index for the leg within the RFQ" }, "market_ticker": { "type": "string" }, "event_ticker": { "type": "string" }, "side": { "$ref": "#/components/schemas/Side" }, "venue": { "$ref": "#/components/schemas/Venue" }, "market_title": { "type": "string" }, "event_title": { "type": "string" }, "yes_sub_title": { "type": "string", "nullable": true, "description": "Snapshot of `Market.yes_sub_title` at RFQ creation (e.g. team name on a sports market). Lets clients render the side-specific subtitle after the underlying market has rolled out of the live `/markets` cache. Null on binary markets that carry no per-side subtitle and on legs created before this field was persisted." }, "no_sub_title": { "type": "string", "nullable": true, "description": "Snapshot of `Market.no_sub_title` at RFQ creation — opposing-side counterpart to `yes_sub_title`. Same null semantics." }, "venue_url": { "type": "string", "nullable": true, "description": "Canonical URL of the leg's market on its source venue (Kalshi or Polymarket). Hydrated from marketd Redis hashes, falling back to the parlay-leg snapshot when the market hash has been purged." }, "image_url": { "type": "string", "nullable": true, "description": "Image URL for the leg's market. Hydrated from the marketd Redis hash and whitelist metadata, falling back to the parlay-leg snapshot when the market hash has been purged." }, "expected_expiration_time": { "type": "string", "format": "date-time", "nullable": true, "description": "Expected market resolution time for the leg. Hydrated from marketd Redis hashes, falling back to the parlay-leg snapshot when the market hash has been purged." }, "current_yes_price": { "type": "number", "minimum": 0, "maximum": 1, "description": "Yes ask price at RFQ creation time, decimal probability (0-1)" }, "current_no_price": { "type": "number", "minimum": 0, "maximum": 1, "description": "No ask price at RFQ creation time, decimal probability (0-1)" }, "outcome": { "$ref": "#/components/schemas/LegOutcome" } } }, "Rfq": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "user_id": { "type": "string" }, "status": { "$ref": "#/components/schemas/RfqStatus" }, "bet_amount": { "type": "number" }, "implied_probability": { "type": "number" }, "legs": { "type": "array", "items": { "$ref": "#/components/schemas/Leg" } }, "quotes": { "type": "array", "nullable": true, "items": { "$ref": "#/components/schemas/Quote" }, "description": "null = not requested (fetched without include=quotes). [] = requested but no quotes yet." }, "accepted_quote_id": { "type": "string", "format": "uuid", "nullable": true, "description": "Set once status >= accepted" }, "market_maker_id": { "type": "string", "nullable": true, "description": "Set once status >= accepted" }, "position_pda": { "type": "string", "nullable": true, "description": "On-chain position address. Set once status >= confirmed." }, "position_id": { "type": "string", "nullable": true, "description": "On-chain position id — the 16-byte vault_positions.position_id, hex-encoded (32 chars). Distinct from position_pda (a base58 address): this is the id the early-cashout endpoints key on (/internal/positions/cashout-eligibility, POST /v1/cashout-requests). Set once status >= confirmed." }, "position_status": { "nullable": true, "description": "Position status from vault_positions table", "allOf": [ { "$ref": "#/components/schemas/PositionStatus" } ] }, "position_error": { "type": "string", "nullable": true, "description": "Last error recorded on the vault position. Populated when position_status === 'error'; null otherwise." }, "cancellation_reason": { "type": "string", "nullable": true, "description": "Why the ticket was cancelled." }, "is_failed": { "type": "boolean", "description": "True when the vault position errored (on-chain execution failed). Derived from position_status === 'error'. Omitted (not false) for non-failed parlays." }, "fee_amount": { "type": "number", "nullable": true, "description": "Set on settled RFQs only" }, "settlement": { "nullable": true, "description": "Set on settled RFQs only", "allOf": [ { "$ref": "#/components/schemas/Settlement" } ] }, "settled_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Set on settled RFQs only" }, "cashout": { "nullable": true, "description": "Set on bought_back (early-cashout) RFQs only — realized buyback P&L.", "allOf": [ { "$ref": "#/components/schemas/Cashout" } ] }, "create_position_tx": { "type": "string", "nullable": true, "description": "create_position transaction signature. Set once the on-chain vault position is created (status >= confirmed)." }, "settle_tx": { "type": "string", "nullable": true, "description": "settle_position transaction signature — the on-chain proof a settled RFQ paid out. Set on settled RFQs (the settle analogue of buyback_tx)." }, "cancel_tx": { "type": "string", "nullable": true, "description": "cancel_position transaction signature. Set on cancelled RFQs." }, "buyback_tx": { "type": "string", "nullable": true, "description": "Buyback (early-cashout) transaction signature, surfaced at the RFQ root by the serializer. Set on bought_back RFQs — the on-chain proof of the cashout, the buyback analogue of settle_tx." }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "expires_at": { "type": "string", "format": "date-time" } } }, "Cashout": { "type": "object", "description": "Early-cashout (buyback) summary attached to a bought_back RFQ — the\nbuyback analogue of Settlement. realized_pnl is the user's net P&L\n(stake nets out; the directional `amount` is the realized delta, less\nthe profit fee on the profiting side and the create-time taker fee).\n", "required": [ "realized_pnl", "amount", "mm_pays_user" ], "properties": { "realized_pnl": { "type": "number", "description": "User's realized P&L from the early cashout (USDC, signed)." }, "amount": { "type": "number", "description": "Directional buyback transfer (USDC, >= 0)." }, "mm_pays_user": { "type": "boolean", "description": "True when the MM paid the user (user was up on the position)." }, "cashed_out_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Time the position was bought back / cashed out (the buyback fee insert = finalize moment). The buyback analogue of settled_at; clients sort the History cashed-out row by this. Absent on legacy rows without a fee." } } }, "MmRfqLeg": { "type": "object", "description": "Subset of a leg surfaced to market makers — drops display-only fields, keeps the price snapshot needed for quoting.", "required": [ "leg_index", "market_ticker", "event_ticker", "side", "venue", "market_title", "current_yes_price", "current_no_price" ], "properties": { "leg_index": { "type": "integer", "description": "Zero-based leg position within the RFQ." }, "market_ticker": { "type": "string", "description": "Unique market identifier on the leg's venue. **Kalshi**: market ticker (e.g. `KXBTC-25FEB07-T100000`). **Polymarket**: `condition_id`, the 0x-prefixed hex string from Polymarket's CTF (e.g. `0x4d2…`); not the gamma id, question id, or market slug. Yes/no CLOB token ids are not returned — resolve them client-side via Polymarket's CLOB if needed." }, "event_ticker": { "type": "string", "description": "Event identifier the leg's market belongs to. **Kalshi**: event ticker (e.g. `KXBTC-25FEB07`). **Polymarket**: Gamma `event_slug` (falls back to `market_slug` when no parent event exists)." }, "side": { "$ref": "#/components/schemas/Side" }, "venue": { "$ref": "#/components/schemas/Venue" }, "market_title": { "type": "string" }, "current_yes_price": { "type": "number", "description": "Yes price at RFQ creation, decimal (0-1)." }, "current_no_price": { "type": "number", "description": "No price at RFQ creation, decimal (0-1)." } } }, "MmRfq": { "type": "object", "description": "Open RFQ visible to market makers.", "required": [ "id", "user_id", "status", "bet_amount", "user_stake", "implied_probability", "expires_at", "created_at", "legs" ], "properties": { "id": { "type": "string", "format": "uuid" }, "user_id": { "type": "string", "description": "Privy DID of the bettor (intentionally surfaced to MMs)." }, "status": { "type": "string", "enum": [ "open" ], "description": "MMs only see RFQs available for quoting." }, "bet_amount": { "type": "number", "description": "The user's gross wager (USDC)." }, "user_stake": { "type": "number", "description": "Net stake the MM underwrites = bet_amount - taker_fee, where taker_fee = floor(bet_amount_micro * taker_fee_bps / 10000) is computed in integer microUSDC (1 USDC = 1e6 micro). Price and size against this, not bet_amount, when the taker fee is on." }, "implied_probability": { "type": "number", "description": "Decimal probability (0-1)." }, "expires_at": { "type": "string", "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" }, "legs": { "type": "array", "items": { "$ref": "#/components/schemas/MmRfqLeg" } } } }, "Quote": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "rfq_id": { "type": "string", "format": "uuid" }, "payout_odds": { "type": "number", "minimum": 1.0001, "maximum": 1000, "description": "Multiplier on the net stake (`user_stake`) that produces the total payout if the parlay wins. When the taker fee is off, `user_stake == bet_amount`." }, "user_cost": { "type": "number", "description": "Net stake the MM quotes on = bet_amount - taker_fee (== bet_amount when the taker fee is off)." }, "mm_cost": { "type": "number", "description": "Market maker's max risk = `total_payout - user_cost`." }, "total_payout": { "type": "number", "description": "Total payout on a win = `user_cost + mm_cost`." }, "leg_prices": { "type": "array", "description": "Per-leg price snapshot captured at submit time. `leg_odds` is a decimal (0-1).", "items": { "type": "object", "required": [ "leg_id", "leg_odds" ], "properties": { "leg_id": { "type": "string", "format": "uuid" }, "leg_odds": { "type": "number", "description": "Implied per-leg price, decimal (0-1)." } } } }, "status": { "$ref": "#/components/schemas/QuoteStatus" }, "valid_until": { "type": "string", "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "QuoteMmView": { "description": "Quote with MM-only fields surfaced on MM endpoints.", "allOf": [ { "$ref": "#/components/schemas/Quote" }, { "type": "object", "properties": { "market_maker_id": { "type": "string" }, "mm_wallet_address": { "type": "string" } } } ] }, "CreateRfqRequest": { "type": "object", "required": [ "legs", "bet_amount" ], "properties": { "legs": { "type": "array", "minItems": 2, "maxItems": 5, "description": "Parlay legs (2-5 required). Each leg references a market by ticker on a specific venue.", "items": { "type": "object", "required": [ "market_ticker", "side", "venue" ], "properties": { "market_ticker": { "type": "string", "description": "Identifier of the underlying market on its venue. **Kalshi**: market ticker (e.g. `KXBTC-25FEB07-T100000`). **Polymarket**: `condition_id`, the 0x-prefixed hex string from Polymarket's CTF (e.g. `0x4d2…`); not the gamma id, question id, or market slug. Look this up via `GET /markets` (`Market.ticker`) — there is no separate yes/no CLOB token id field; both sides of a Polymarket binary share the same `condition_id` and are distinguished by `side`." }, "side": { "$ref": "#/components/schemas/Side" }, "venue": { "$ref": "#/components/schemas/Venue" } } } }, "bet_amount": { "type": "number", "minimum": 1, "maximum": 100, "description": "USDC. Max 100. Must be a positive finite number." }, "expires_in_seconds": { "type": "integer", "minimum": 30, "maximum": 86400, "default": 300, "description": "RFQ lifetime in seconds. Floor 30s, cap 24h. Defaults to 300." } } }, "CancelRfqRequest": { "type": "object", "properties": { "reason": { "type": "string", "maxLength": 500, "description": "Optional cancellation reason. Defaults to \"User cancelled\". Truncated to 500 chars." } } }, "SubmitQuoteRequest": { "type": "object", "required": [ "rfq_id", "payout_odds" ], "properties": { "rfq_id": { "type": "string", "format": "uuid", "description": "RFQ to quote." }, "payout_odds": { "type": "number", "minimum": 1.0001, "maximum": 1000, "description": "Multiplier (1.0001x – 1000x). Must be a finite number." }, "valid_for_seconds": { "type": "integer", "minimum": 10, "maximum": 600, "default": 60, "description": "Quote validity window. 10s – 10min. Defaults to 60s." }, "leg_prices": { "type": "array", "description": "Optional per-leg price snapshot. Each entry is `{ leg_id, odds }` where `odds` is a decimal (0-1). Note: the response shape (`Quote.leg_prices[]`) uses `leg_odds` for the same value — the request field is named `odds`, the response field is named `leg_odds`.", "items": { "type": "object", "required": [ "leg_id", "odds" ], "properties": { "leg_id": { "type": "string", "format": "uuid" }, "odds": { "type": "number", "minimum": 0, "maximum": 1, "description": "Implied per-leg price, decimal (0-1)." } } } } } }, "AcceptQuoteResponse": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "object", "properties": { "status": { "type": "string", "example": "accepted" }, "quote_id": { "type": "string", "format": "uuid" }, "confirmation_deadline": { "type": "string", "format": "date-time", "description": "Deadline by which the market maker must call `POST /mm/quotes/{quoteId}/confirm`. If the MM misses this deadline, the RFQ expiry sweep transitions both rows based on the RFQ's own `expires_at`:\n\n- **RFQ still within `expires_at`** — RFQ reverts to `open` (with `accepted_quote_id` and `market_maker_id` cleared so other MMs can quote again) and the accepted quote becomes `withdrawn`.\n- **RFQ past `expires_at`** — RFQ becomes `expired` and the accepted quote becomes `expired`.\n\nThe accepted quote never transitions to `rejected` via the deadline path. `rejected` is reserved for sibling pending quotes auto-rejected when the user accepts a different quote, and for explicit user rejection via `POST /rfqs/{id}/quotes/{quoteId}/reject`." } } } } }, "ConfirmQuoteResponse": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "object", "properties": { "status": { "type": "string", "example": "confirmed" }, "quote_id": { "type": "string", "format": "uuid" }, "execution_id": { "type": "string", "format": "uuid", "description": "Identifier for the queued vault-position-creation job." } } } } }, "User": { "type": "object", "properties": { "id": { "type": "string", "description": "Privy DID — internal identifier, not exposed on public endpoints." }, "username": { "type": "string", "description": "Display name. Auto-set to the wallet address on first login; can be updated via `PUT /v1/username` to a 3-20 char alphanumeric." }, "email": { "type": "string", "nullable": true }, "wallet_address": { "type": "string", "nullable": true }, "approved": { "type": "boolean", "description": "True once the user has redeemed a valid invite code or has been admin-approved." }, "waitlisted": { "type": "boolean", "description": "True when `approved=false` and the user is on the waitlist; false otherwise. Always present." }, "auto_redeemed": { "type": "boolean", "description": "Only present when the `?ref` param successfully redeemed an invite code in this exact request." }, "airdrop": { "nullable": true, "description": "Welcome-airdrop outcome from the auto-redeem path. Present alongside `auto_redeemed: true`. `null` when the invite has no airdrop configured or the user was already credited.", "allOf": [ { "$ref": "#/components/schemas/AirdropOutcome" } ] }, "created_at": { "type": "string", "format": "date-time" } } }, "AirdropOutcome": { "type": "object", "description": "Welcome airdrop outcome. `success: true` with `pending: true` means the amount was staged and will mint on the user's next `POST /v1/wallet/enable-trading` call. `success: false` means staging failed.", "required": [ "success", "amount", "pending" ], "properties": { "success": { "type": "boolean" }, "amount": { "type": "number", "description": "USDC (whole dollars)." }, "pending": { "type": "boolean" } } }, "Wallet": { "type": "object", "properties": { "address": { "type": "string", "nullable": true, "description": "Solana wallet address (base58). Null if no wallet is linked yet." }, "sol_balance": { "type": "number" }, "usdc_balance": { "type": "number" }, "vault_balance": { "type": "number", "description": "Gross USDC balance currently held in the caller's Totalis vault. Resolved by the returned `address`; returns 0 when no vault row exists for that wallet. Includes both free vault cash and locked collateral. Add to `usdc_balance` for total on-chain USDC across wallet and vault; subtract `locked_amount` from that total for an available-cash view." }, "locked_amount": { "type": "number", "description": "Every USDC the caller has committed across roles: in-flight RFQs, bettor-side vault position stakes, and maker-side `locked_collateral`. Because `vault_balance` is gross, vault locked collateral is intentionally represented in both `vault_balance` and `locked_amount`; compute available cash as `usdc_balance + vault_balance - locked_amount` and do not add `/v1/vault.locked_collateral` again." }, "has_traded": { "type": "boolean", "description": "True once any of the caller's parlays has reached `executed` or `settled`. Informational account state; withdrawals are allowed regardless of this value." } } }, "ApiKeyFull": { "type": "object", "description": "Returned only on creation; the full `key` is visible exactly once.", "properties": { "key": { "type": "string", "description": "Full API key (only shown once)." }, "key_prefix": { "type": "string" }, "name": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" }, "expires_at": { "type": "string", "format": "date-time" } } }, "ApiKeySummary": { "type": "object", "properties": { "key_prefix": { "type": "string" }, "name": { "type": "string" }, "status": { "type": "string", "enum": [ "active", "revoked", "expired" ] }, "created_at": { "type": "string", "format": "date-time" }, "expires_at": { "type": "string", "format": "date-time" }, "last_used_at": { "type": "string", "format": "date-time", "nullable": true }, "request_count": { "type": "integer" } } }, "Settlement": { "type": "object", "required": [ "id", "rfq_id", "winner", "payout", "user_stake", "mm_risk", "fee_amount", "settle_tx", "settled_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "rfq_id": { "type": "string", "format": "uuid" }, "winner": { "$ref": "#/components/schemas/WinnerSide" }, "payout": { "type": "number", "description": "Full pot transferred to the winner — equals user_stake + mm_risk\n(i.e. bet_amount * payout_odds). NOT just the MM-side movement.\nFor MM PnL on a loss, use -(payout - user_stake) = -mm_risk.\n" }, "user_stake": { "type": "number", "description": "Bettor's stake at settle time. Surfaced so the MM dashboard can\nderive PnL without a second fetch — MM win = +user_stake - fee,\nMM loss = -(payout - user_stake) = -mm_risk.\n" }, "mm_risk": { "type": "number", "description": "Maker's max risk at settle time. Equal to payout - user_stake; the\nMM dashboard renders loss-side PnL directly off this field.\n" }, "fee_amount": { "type": "number", "description": "Protocol fee deducted from the winner's payout." }, "settle_tx": { "type": "string" }, "settled_at": { "type": "string", "format": "date-time" } } }, "AirdropDrainOutcome": { "type": "object", "description": "Outcome of the pending-airdrop drain performed by\n`POST /v1/wallet/enable-trading`. Narrower than\n`AirdropOutcome` — there is no `pending` field because the drain\nis the terminal step (either the mint landed or it didn't; the\namount is never left in a staged state by this endpoint).\n`{ success: true, amount }` — the staged amount minted\nsuccessfully on-chain. `{ success: false, amount }` — the mint\nattempt failed and the pending amount has been restored; the\nuser can retry on their next call.\n", "required": [ "success", "amount" ], "properties": { "success": { "type": "boolean" }, "amount": { "type": "number", "description": "USDC amount (whole dollars)" } } }, "DelegationStatus": { "type": "object", "properties": { "ready": { "type": "boolean", "description": "True if a wallet is linked and delegation can proceed" }, "delegated": { "type": "boolean", "description": "True if TEE wallet delegation is active (server can sign)" }, "address": { "type": "string", "description": "Solana wallet address (base58). Empty string if no wallet linked." }, "wallet_id": { "type": "string", "description": "Privy embedded wallet ID (used for delegation API calls)" } } }, "PortfolioBalance": { "type": "object", "description": "Caller's vault balance (DB-cached, no RPC). Null only when the\ncaller has no vault row at all. For dual-role accounts the\nbettor-side vault is preferred and the maker-side row is used\nas a fallback (same rules as `GET /v1/vault`).\n", "properties": { "gross_balance": { "type": "number" }, "locked_collateral": { "type": "number" }, "free_balance": { "type": "number" }, "vault_pda": { "type": "string" }, "vault_token_account": { "type": "string" } } }, "PortfolioData": { "type": "object", "description": "Consolidated portfolio response from GET /v1/portfolio.", "properties": { "balance": { "nullable": true, "description": "Null when user has no vault (never traded)", "allOf": [ { "$ref": "#/components/schemas/PortfolioBalance" } ] }, "stats": { "$ref": "#/components/schemas/UserStats" }, "counts": { "$ref": "#/components/schemas/RfqCounts" }, "summary": { "$ref": "#/components/schemas/PortfolioSummary" } } }, "PortfolioSummary": { "type": "object", "description": "Derived portfolio-level aggregates from active positions.", "properties": { "active_count": { "type": "integer", "description": "Number of active (non-terminal) RFQs" }, "active_bet_total": { "type": "number", "description": "Sum of bet_amount across active RFQs" }, "portfolio_value": { "type": "number", "description": "Gross vault balance (deposited USDC). Does not include unrealized P&L." } } }, "RfqCounts": { "type": "object", "properties": { "open": { "type": "integer" }, "quoted": { "type": "integer" }, "accepted": { "type": "integer" }, "confirmed": { "type": "integer" }, "executed": { "type": "integer", "description": "Positions on-chain but not yet resolved. Distinct from total_settled — settled means market resolution + payout distributed." }, "total_settled": { "type": "integer" }, "total_cancelled": { "type": "integer" }, "total_expired": { "type": "integer" }, "total_failed": { "type": "integer", "description": "Parlays where the vault position errored on-chain. Derived from vault_positions.status = 'error'." } } }, "UserStats": { "type": "object", "properties": { "total_parlays": { "type": "integer" }, "wins": { "type": "integer" }, "losses": { "type": "integer" }, "total_wagered": { "type": "number" }, "realized_pnl": { "type": "number" }, "updated_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Null when the user has never settled a trade (no rfq.user_stats row yet)." } } }, "UserVaultResponse": { "type": "object", "description": "Vault state with active position summaries (GET /v1/vault response).\nReturns the caller's bettor-side vault (owner_type='user') when one\nexists; falls back to their maker-side vault (owner_type='mm') for\npure-MM accounts so the portfolio surface reflects every USDC the\ncaller controls. `positions` is the bettor-side active list when\nreturning the user vault, or the maker-side active list (joined via\n`q.market_maker_id`) when returning the MM vault.\n", "properties": { "vault_pda": { "type": "string" }, "vault_token_account": { "type": "string" }, "gross_balance": { "type": "number" }, "locked_collateral": { "type": "number" }, "free_balance": { "type": "number" }, "positions": { "type": "array", "items": { "$ref": "#/components/schemas/VaultPositionSummary" } } } }, "VaultPositionSummary": { "type": "object", "description": "Lightweight position summary for the vault response.\n\nCaller-relative reading: `user_stake` and `mm_risk` are\nside-of-trade labels, not caller-relative ones. When\n`UserVaultResponse` returns the bettor-side vault, `user_stake`\nis the caller's own stake. When it returns the maker-side\nvault (pure-MM accounts), `user_stake` is the counterparty\nbettor's stake and `mm_risk` is the caller's own collateral.\nDon't display `user_stake` as \"my stake\" without branching on\nwhich vault was returned — for aggregate \"USDC committed\",\nprefer `wallet.locked_amount`, which is role-agnostic.\n", "properties": { "position_id": { "type": "string", "description": "16-byte position ID (hex encoded)" }, "rfq_id": { "type": "string", "format": "uuid" }, "user_stake": { "type": "number", "description": "Bettor side of the trade. The caller's stake on the\nbettor-side vault response; the counterparty's stake on\nthe maker-side vault response.\n" }, "mm_risk": { "type": "number", "description": "Maker side of the trade. The counterparty's collateral on\nthe bettor-side vault response; the caller's own\ncollateral on the maker-side vault response.\n" }, "total_payout": { "type": "number" }, "status": { "$ref": "#/components/schemas/PositionStatus" }, "created_at": { "type": "string", "format": "date-time" } } }, "PnlPoint": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "pnl": { "type": "number" } } }, "WebhookConfig": { "type": "object", "properties": { "owner_kind": { "type": "string", "enum": [ "user", "mm" ] }, "url": { "type": "string", "nullable": true, "description": "Configured HTTPS endpoint, or null if never set." }, "events": { "type": "array", "items": { "type": "string" }, "description": "Subscribed event names." }, "status": { "type": "string", "nullable": true, "description": "Endpoint status, or null if never configured." }, "has_signing_secret": { "type": "boolean", "description": "Whether a signing secret is set (deliveries require one)." }, "event_catalog": { "type": "array", "items": { "type": "string" }, "description": "The events this endpoint kind may subscribe to. (GET only.)" } } }, "WebhookConfigInput": { "type": "object", "required": [ "url", "events" ], "properties": { "url": { "type": "string", "description": "HTTPS endpoint. Must not resolve to private/internal infrastructure." }, "events": { "type": "array", "items": { "type": "string" }, "description": "Event names to subscribe to (subset of the catalog). Empty parks the endpoint." } } }, "WebhookDelivery": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "event_type": { "type": "string" }, "status": { "type": "string", "enum": [ "pending", "failed", "delivered", "dead_letter" ] }, "attempts": { "type": "integer" }, "response_code": { "type": "integer", "nullable": true }, "created_at": { "type": "string", "format": "date-time" } } } } } }