{ "openapi": "3.0.3", "info": { "title": "Totalis RFQ API", "version": "2.1.0", "description": "Public REST surface for the Totalis parlay RFQ platform \u2014 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 \u2014 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 \u2014 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 \u00d7 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" } } } }, "/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" } } } }, "/markets/exclusion-groups": { "get": { "operationId": "getExclusionGroups", "summary": "Get Exclusion Groups", "description": "Returns the configured leg-conflict rules. Each group is a set of tickers that cannot co-exist as legs in the same parlay (e.g. correlated outcomes on the same underlying asset). Each entry carries a `scope` (`event` or `series`). No authentication required.", "tags": [ "Markets" ], "security": [], "responses": { "200": { "description": "Exclusion groups configuration", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "exclusion_groups": { "type": "array", "items": { "$ref": "#/components/schemas/ExclusionGroup" } }, "conflict_mode": { "type": "string" } } } } } } } } } } }, "/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\u201320 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\u201320 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\u201320 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` \u2014 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 \u2014 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/blockhash": { "get": { "operationId": "getBlockhash", "summary": "Get Blockhash", "description": "Latest Solana blockhash for client-side transaction construction. Useful for browser clients that do not have direct RPC access.", "tags": [ "User" ], "security": [ { "PrivyJWT": [] }, { "ApiKey": [] } ], "responses": { "200": { "description": "Latest blockhash", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "required": [ "blockhash", "last_valid_block_height" ], "properties": { "blockhash": { "type": "string" }, "last_valid_block_height": { "type": "integer" } } } } } } } }, "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" } } } } }, "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 \u2014 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, "description": "Opaque cursor for the next page; null on the final page." }, "has_more": { "type": "boolean" } } }, "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" ], "description": "Lifecycle status of an RFQ." }, "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; `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", "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\u2026`); 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` \u2014 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", "required": [ "market_ticker", "event_ticker", "side", "venue", "market_title" ], "properties": { "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\u2026`); not the gamma id, question id, or market slug. Yes/no CLOB token ids are not returned \u2014 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" }, "event_title": { "type": "string" }, "yes_sub_title": { "type": "string", "nullable": true, "description": "Snapshot of the market's yes-side subtitle at RFQ creation." }, "no_sub_title": { "type": "string", "nullable": true, "description": "Snapshot of the market's no-side subtitle at RFQ creation." }, "venue_url": { "type": "string", "nullable": true, "description": "Canonical URL of the leg's market on its venue." }, "image_url": { "type": "string", "nullable": true, "description": "Image URL for the leg's market." }, "expected_expiration_time": { "type": "string", "format": "date-time", "nullable": true, "description": "Expected market resolution time for the leg." }, "current_yes_price": { "type": "number", "description": "Yes ask price at RFQ creation, decimal (0-1)." }, "current_no_price": { "type": "number", "description": "No ask price at RFQ creation, decimal (0-1)." }, "outcome": { "$ref": "#/components/schemas/LegOutcome" } } }, "Rfq": { "type": "object", "required": [ "id", "user_id", "status", "legs", "bet_amount", "user_stake", "implied_probability", "created_at", "updated_at", "expires_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "user_id": { "type": "string", "description": "Privy DID of the bettor." }, "status": { "$ref": "#/components/schemas/RfqStatus" }, "bet_amount": { "type": "number", "description": "USDC. Capped at 100 USDC per RFQ." }, "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": "Combined implied probability of all legs (0-1)." }, "legs": { "type": "array", "items": { "$ref": "#/components/schemas/Leg" } }, "quotes": { "type": "array", "nullable": true, "items": { "$ref": "#/components/schemas/Quote" }, "description": "`null` when not requested (listRfqs without `?include=quotes`). `[]` means 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_status": { "nullable": true, "description": "Position status from the 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'`." }, "cancellation_reason": { "type": "string", "nullable": true }, "is_failed": { "type": "boolean", "description": "True when the vault position errored on-chain. Derived from `position_status === 'error'`. Omitted on 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 }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "expires_at": { "type": "string", "format": "date-time" } } }, "MmRfqLeg": { "type": "object", "description": "Subset of a leg surfaced to market makers \u2014 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\u2026`); not the gamma id, question id, or market slug. Yes/no CLOB token ids are not returned \u2014 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\u2026`); not the gamma id, question id, or market slug. Look this up via `GET /markets` (`Market.ticker`) \u2014 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 \u2013 1000x). Must be a finite number." }, "valid_for_seconds": { "type": "integer", "minimum": 10, "maximum": 600, "default": 60, "description": "Quote validity window. 10s \u2013 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 \u2014 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`** \u2014 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`** \u2014 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 \u2014 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 \u2014 equals `user_stake + mm_risk` (i.e. `user_stake * payout_odds`). For MM loss-side PnL, use `-(payout - user_stake) = -mm_risk`." }, "user_stake": { "type": "number" }, "mm_risk": { "type": "number" }, "fee_amount": { "type": "number", "description": "Protocol fee deducted from the winner's payout." }, "settle_tx": { "type": "string" }, "settled_at": { "type": "string", "format": "date-time" } } } } } }