{ "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" } } } }, "/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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2192 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 \u2014 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 \u2264 the caller's total spendable pool\n(ATA + vault free_balance \u2212 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 \u2192 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\u2192 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 \u2212 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 \u2014 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` \u2014 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` \u2014 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 \u2014 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 \u2014 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" ] } } } } } } } } } } } } } }, "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", "bought_back" ], "description": "Lifecycle status of an RFQ. `bought_back` is the terminal status for a position closed early via the buyback (early-cashout) flow, distinct from `settled`." }, "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\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** \u2014 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 }, "cashout": { "nullable": true, "description": "Set on bought_back (early-cashout) RFQs only — realized buyback P&L.", "allOf": [ { "$ref": "#/components/schemas/Cashout" } ] }, "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 buyback analogue of Settlement. realized_pnl is the user's net P&L (stake nets out; the directional `amount` is the realized delta, less the profit fee on the profiting side and the create-time taker fee).", "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. Absent on legacy rows without a fee." } } }, "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" } } }, "AirdropDrainOutcome": { "type": "object", "description": "Outcome of the pending-airdrop drain performed by\n`POST /v1/wallet/enable-trading`. Narrower than\n`AirdropOutcome` \u2014 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 }` \u2014 the staged amount minted\nsuccessfully on-chain. `{ success: false, amount }` \u2014 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 \u2014 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 \u2014 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" } } } } } }