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