> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kairos.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Get merged PnL data for a user across one or more wallets/providers

> Fetches and merges PnL records/summary across the wallets supplied for this user via the general-purpose PnL path — not the real-time pipeline used by `/pnl/hover` and `/pnl/wallet-totals`. At least one of `polymarket_wallet`, `kalshi_wallet`, or `hyperliquid_wallet` is required — the request is rejected with 400 if none are supplied. `polymarket_wallet` and `hyperliquid_wallet` must be valid Ethereum addresses (normalized/lower-cased); `kalshi_wallet` is treated as an opaque user-id string (Kalshi is a CEX with internal, non-address identifiers). `provider` (repeatable query param, up to 16 items) filters the merged result down to specific provider(s); when omitted, all providers implied by the supplied wallets are used. Kalshi and Hyperliquid PnL are served only through this aggregate endpoint; neither is available from `/pnl/hover` or `/pnl/wallet-totals`. Hyperliquid open-position and unrealized PnL use HIP-4 balances and mark prices, while realized PnL and fee fields are currently zero. The caller-supplied `{user_id}` MUST match the authenticated principal's own id (JWT `sub`, or `user_id` for API-key auth) — mismatches are rejected with 403; this endpoint can only ever return the caller's own PnL. `limit`/`offset` paginate the merged record list; internally up to `min(limit + offset, 1000)` records per provider are fetched, so `total`/`has_more` in the response describe the page relative to that ceiling, not an authoritative full-history count. For API-key callers, each requested provider must additionally have its API access enabled; JWT/admin callers are exempt.




## OpenAPI

````yaml /openapi/data-api.yaml get /pnl/{user_id}
openapi: 3.1.0
info:
  title: Kairos Data API
  version: 1.0.0
  summary: >-
    The full Kairos data plane — markets, candles, trades, search, discover,
    sports, trader analytics, and PnL.
  description: |
    The Kairos Data API at `data.kairos.trade` is the platform's primary data
    plane: market metadata and prices, OHLCV candles, live venue trade proxies,
    normalized trade history, full-text search, discovery feeds, the sports
    catalog, public trader analytics, and account PnL.

    ## Authentication

    Most endpoints accept a Kairos **API key** (`X-Client-Id` + `X-Api-Key` +
    `X-Api-Secret`, all three) or a first-party session JWT. Endpoints tagged
    `public` (trader analytics, provider configs, several sports feeds) need
    no credentials at all. Some endpoints additionally require an API-key
    **scope** (`trade:read`, `position:read`) — stated per operation.

    For high-volume market-data consumption (candles/trades/metadata at
    scale), prefer the dedicated **Market Data API** at `md.kairos.trade` —
    it has higher budgets, ETag caching, and a binary candle format.

    ## Rate limits

    Rate limits use a sliding window keyed on the authenticated user (JWT
    `sub`) or, for anonymous callers, the trusted client IP. Routes belong to
    a named **bucket** (`x-kairos-bucket`) whose per-minute budget is shared
    by every route in it; routes with no bucket run under the service default
    of 100/minute. `x-kairos-rate-limit` states the bucket's compile-time
    default — operators can raise or lower a bucket at runtime, so treat the
    documented number as the baseline, not a contract. 429 responses carry
    `Retry-After` and `X-RateLimit-*`.

    ## Conventions

    - Prices are on the **0–100 cents scale** unless a field says otherwise
      (trader-analytics position/trade prices use the 0–1 scale; each field's
      description states its scale).
    - Errors: `{"detail": "<message>"}` for handler errors; FastAPI's
      standard validation envelope for 422s. Errors surfaced from a venue
      adapter instead use `{"error", "provider", "operation", "message"}` —
      see `DataProviderError`. Unhandled failures always return the generic
      500 body; internal details are logged, never returned.
    - Every request body is capped at 8 MiB service-wide (413).
  contact:
    name: Kairos
    url: https://app.kairos.trade/docs/api-reference
  termsOfService: https://kairos.trade/terms
servers:
  - url: https://data.kairos.trade
    description: Production
  - url: https://staging-data.kairos.trade
    description: Staging
security:
  - apiKeyClientId: []
    apiKeyKey: []
    apiKeySecret: []
paths:
  /pnl/{user_id}:
    get:
      tags:
        - PnL
      summary: Get merged PnL data for a user across one or more wallets/providers
      description: >
        Fetches and merges PnL records/summary across the wallets supplied for
        this user via the general-purpose PnL path — not the real-time pipeline
        used by `/pnl/hover` and `/pnl/wallet-totals`. At least one of
        `polymarket_wallet`, `kalshi_wallet`, or `hyperliquid_wallet` is
        required — the request is rejected with 400 if none are supplied.
        `polymarket_wallet` and `hyperliquid_wallet` must be valid Ethereum
        addresses (normalized/lower-cased); `kalshi_wallet` is treated as an
        opaque user-id string (Kalshi is a CEX with internal, non-address
        identifiers). `provider` (repeatable query param, up to 16 items)
        filters the merged result down to specific provider(s); when omitted,
        all providers implied by the supplied wallets are used. Kalshi and
        Hyperliquid PnL are served only through this aggregate endpoint; neither
        is available from `/pnl/hover` or `/pnl/wallet-totals`. Hyperliquid
        open-position and unrealized PnL use HIP-4 balances and mark prices,
        while realized PnL and fee fields are currently zero. The
        caller-supplied `{user_id}` MUST match the authenticated principal's own
        id (JWT `sub`, or `user_id` for API-key auth) — mismatches are rejected
        with 403; this endpoint can only ever return the caller's own PnL.
        `limit`/`offset` paginate the merged record list; internally up to
        `min(limit + offset, 1000)` records per provider are fetched, so
        `total`/`has_more` in the response describe the page relative to that
        ceiling, not an authoritative full-history count. For API-key callers,
        each requested provider must additionally have its API access enabled;
        JWT/admin callers are exempt.
      operationId: getUserPnl
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
            maxLength: 128
          description: >-
            Kairos internal user id. Must match the authenticated caller's own
            id.
          example: usr_9f3c2a1b
        - name: polymarket_wallet
          in: query
          required: false
          schema:
            type: string
            maxLength: 128
          description: Polymarket (Ethereum) wallet address.
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        - name: kalshi_wallet
          in: query
          required: false
          schema:
            type: string
            maxLength: 128
          description: Kalshi user ID (opaque internal identifier, not a wallet address).
        - name: hyperliquid_wallet
          in: query
          required: false
          schema:
            type: string
            maxLength: 128
          description: >-
            Hyperliquid EVM address (HIP-4 positions key off this same
            deposit/trade address).
          example: '0x2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c'
        - name: provider
          in: query
          required: false
          style: form
          explode: true
          schema:
            type: array
            maxItems: 16
            items:
              type: string
          description: Filter merged PnL to these provider(s). Repeatable query param.
          example:
            - polymarket
            - hyperliquid
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
          description: Records per page.
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 10000
            default: 0
          description: Records to skip.
      responses:
        '200':
          description: Merged PnL summary and records for the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PnlUserPnlResponse'
        '400':
          description: >
            Missing/oversized `user_id`, an invalid wallet address, an invalid
            `provider` filter, or no wallet address supplied at all.
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: >-
            The `user_id` does not match the authenticated caller, the API key
            is missing the `position:read` scope, or platform API access is
            disabled.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
components:
  schemas:
    PnlUserPnlResponse:
      type: object
      description: >
        Full merged PnL response for a user, across the requested
        wallets/providers.
      required:
        - user_id
        - summary
        - records
        - filters_applied
      properties:
        user_id:
          type: string
          example: usr_9f3c2a1b
        summary:
          $ref: '#/components/schemas/PnlSummary'
        records:
          type: array
          items:
            $ref: '#/components/schemas/PnlRecord'
        filters_applied:
          type: object
          description: >-
            Echoes the effective filters used to build this response (wallets,
            providers, etc.).
          additionalProperties: true
        total:
          type: integer
          default: 0
          description: >
            Count of merged records the aggregator returned before pagination
            slicing, bounded by the per-provider fetch ceiling
            (min(limit+offset, 1000)) — not an authoritative full-history count.
          example: 143
        has_more:
          type: boolean
          default: false
          description: >-
            True only when the aggregator definitively saw more records than
            this page returned.
    DataError:
      type: object
      description: Handler error envelope (FastAPI HTTPException).
      required:
        - detail
      properties:
        detail:
          type: string
          description: Human-readable error message.
          example: Invalid provider
    PnlSummary:
      type: object
      description: Merged PnL summary across all providers/wallets supplied for a user.
      required:
        - user_id
        - total_realized_pnl
        - total_fees
        - total_cost_basis
        - total_winning_positions
        - total_losing_positions
        - by_exchange
      properties:
        user_id:
          type: string
          example: usr_9f3c2a1b
        total_realized_pnl:
          type: number
          format: double
          example: 1875.1
        total_fees:
          type: number
          format: double
          example: 18.2
        total_cost_basis:
          type: number
          format: double
          example: 12400
        total_winning_positions:
          type: integer
          example: 25
        total_losing_positions:
          type: integer
          example: 11
        by_exchange:
          type: object
          description: Keyed by provider name.
          additionalProperties:
            $ref: '#/components/schemas/PnlExchangeSummary'
    PnlRecord:
      type: object
      description: A single merged PnL record.
      required:
        - provider
        - market_id
        - timestamp
        - realized_pnl
        - fees
      properties:
        provider:
          type: string
          example: polymarket
        market_id:
          type: string
          example: 0xabc123condition
        timestamp:
          type: string
          description: >-
            ISO-8601 timestamp string (empty string if the underlying record has
            no timestamp).
          example: '2026-07-20T09:14:02+00:00'
        realized_pnl:
          type: number
          format: double
          example: 24.15
        fees:
          type: number
          format: double
          example: 0.35
    PnlExchangeSummary:
      type: object
      description: Per-exchange PnL summary within a merged PnL response.
      required:
        - provider
        - wallet_address
        - total_realized_pnl
        - total_fees
        - total_cost_basis
        - winning_positions
        - losing_positions
      properties:
        provider:
          type: string
          example: polymarket
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        total_realized_pnl:
          type: number
          format: double
          example: 1250.4382
        total_fees:
          type: number
          format: double
          example: 12.5
        total_cost_basis:
          type: number
          format: double
          example: 8400
        winning_positions:
          type: integer
          example: 18
        losing_positions:
          type: integer
          example: 9
  responses:
    DataUnauthorized:
      description: >
        No valid credential presented — missing/invalid API-key headers, or an
        invalid/expired session token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DataError'
          example:
            detail: Not authenticated
    DataValidationError:
      description: Request failed FastAPI parameter validation.
      content:
        application/json:
          schema:
            type: object
            required:
              - detail
            properties:
              detail:
                type: array
                description: >-
                  One entry per failed field, with location, message, and error
                  type.
                items:
                  type: object
                  additionalProperties: true
    DataRateLimited:
      description: >
        Rate limit exceeded for this route's sliding window, keyed on the
        session `sub` when

        authenticated and on the trusted client IP otherwise. Honor
        `Retry-After`. A per-API-key

        data ceiling (set per credential) rejects with `{"detail": "API key data
        rate limit

        exceeded"}` instead of the `error` envelope below.
      headers:
        Retry-After:
          schema:
            type: integer
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: 'Rate limit exceeded: 100 per 1 minute'
  securitySchemes:
    apiKeyClientId:
      type: apiKey
      in: header
      name: X-Client-Id
      description: >-
        Credential client id (`kairos_ck_...`). Must be sent together with
        X-Api-Key and X-Api-Secret.
    apiKeyKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: 64-char hex API key.
    apiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: 64-char hex API secret.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.