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

# List the key owner's ruby ledger

> Every ruby movement, newest first: settlement payouts (and rubies
taken back when a settled topic is taken down or re-ruled), positions
sold in the SeeSaw app, opinion paybacks and creator fees (and their
clawbacks), and the one-off conversion of the old sapphire balance.
Added in 2.1.0; read scope, counts against the read window.

Only the ruby balance is covered. Sapphire rows from before the
sapphire→ruby merge are not listed — the balance they left behind
arrives as one `sapphire_conversion` row.

Filtering happens in the ledger query, so pages and `has_more` are
exact. A combination that cannot match (e.g. `flow=out&source=sell`)
returns an empty page. Titles in `topic` follow `Accept-Language`.




## OpenAPI

````yaml /api-reference/openapi.yaml get /me/ruby-ledger
openapi: 3.1.0
info:
  title: SeeSaw Open API
  version: 2.1.0
  description: |
    The **open API** of SeeSaw — a dedicated surface for AI agents and
    third-party integrations, exposed both as REST (`/open/v1/*`) and as an
    **MCP server** (`/mcp`, Streamable HTTP, stateless) in the same process.

    Every key can **read**; keys created with the `write` scope can also act
    on the owner's behalf: place a position, publish an opinion, back an
    opinion. Writes always run in two phases — a `preview_*` call returns the
    summary the user is meant to confirm plus a short-lived `confirm_token`,
    and the execute call accepts only that token.

    This contract is independent from the app-facing API:
    it does not inherit app DTO shapes and follows its own conventions.

    ## Conventions
    - Field names are `snake_case`; enums are lowercase strings.
    - Decimals (coins, gems, prices) are **strings with 4 decimal places**
      (`"100.0000"`) — never floats. Rounding for display is the client's call.
      One deliberate exception: `avg_price` (coins per ruby, a ratio rather
      than a price) carries **6 decimal places**, and says so at the field.
    - Timestamps are RFC 3339 UTC (`2026-09-09T12:00:00Z`).
    - Every resource carries a human-visitable `url`.
    - Lists use **opaque cursor pagination**: `{data, next_cursor, has_more}`.
      `limit` defaults to 20, max 100. Reusing a cursor with different query
      params does not guarantee continuity.
    - Single resources are returned bare (no wrapper); errors use
      `{"error": {"code", "message", "details"}}`.
    - Vocabulary follows the 2.0 glossary: **opinion** (not comment), **back**,
      **position**, **stake**, **payout**.

    ## Authentication and scopes
    Every request (public data included) requires a **personal API key** (PAT):
    `Authorization: Bearer sspat_…`. Keys are issued per user (max 5 active)
    and can be revoked anytime. Key management lives on the app API
    (`/v1/users/me/api-keys`, JWT auth), NOT on this surface.

    Two scope tiers, chosen when the key is created and **fixed for the life of
    the key** (there is no upgrade path — create a new key instead):

    | Scopes | Grants |
    | --- | --- |
    | `["read"]` | The whole read surface. The default. |
    | `["read","write"]` | The read surface **plus** the six write operations. |

    A read-only key hitting a write endpoint gets `403 FORBIDDEN_SCOPE`; over
    MCP it never sees the write tools at all.

    ## The write flow
    1. Call `POST …/order-preview`, `…/opinion-preview` or `…/back-preview`.
       Nothing is charged; the response carries a `summary` plus, when the
       action is currently possible, a `confirm_token` and its `expires_at`.
    2. **Show the summary to the user and get explicit confirmation.**
    3. Call `POST /orders`, `/opinions` or `/backs` with only that token.

    Tokens are valid **5 minutes**, are bound to the issuing user, key, action
    and parameters, and are single-use. `place_order` and `back_opinion` are
    idempotent on retry with the same token (`replayed: true`);
    `publish_opinion` is strictly single-use and answers a retry with
    `409 CONFIRM_TOKEN_USED`.

    ## Rate limits (per key)
    - **Read window**: 120 requests/min and 10,000 requests/day across the
      surface. The three `preview_*` operations count only against this window.
    - **Search**: additionally capped at 30 requests/min — a dedicated per-key
      budget shared by REST `/search` and the MCP `search_topics` tool.
    - **Write window**: the three executing operations (`/orders`, `/opinions`,
      `/backs`) are additionally capped at **10/min and 200/day**; they also
      consume the read window.
    - `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`
      response headers reflect the binding minute window; 429 responses
      carry `Retry-After`. Over MCP, exceeding a quota surfaces as a tool
      error naming the retry delay.

    ## MCP server
    `POST https://api.seesaw.fun/mcp` (Streamable HTTP, stateless, same
    `Authorization` header). The server reports the contract version as its
    `serverInfo.version` (`2.1.0`). A read key sees **12 tools**; a
    read+write key sees **18**:

    - Read: `search_topics`, `list_topics`, `list_topic_categories`,
      `get_topic`, `list_topic_opinions`, `get_my_profile`,
      `list_my_positions`, `list_my_opinions`, `list_my_backed_opinions`,
      `list_my_watchlist`, `list_my_coin_ledger`, `list_my_ruby_ledger`.
    - Write: `preview_order` / `place_order`, `preview_opinion` /
      `publish_opinion`, `preview_back` / `back_opinion`.

    Read and preview tools are annotated `readOnlyHint: true`; the three
    executing tools carry `destructiveHint: true` and
    `_meta["anthropic/requiresUserInteraction"] = true`, which compatible
    clients turn into a mandatory confirmation prompt. Tool results carry the
    corresponding REST response JSON.

    Tools that return titles, options or opinions take an optional `language`
    argument; REST reads the `Accept-Language` header instead (unknown values
    fall back to `en`). Supported: `en` (default), `zh-TW`, `zh-CN`, `vi`,
    `id`, `th`, `ja`, `ko`, `ms`. Over MCP, aliases such as `zh` or `en-US`
    fold onto the closest supported code and any other value is rejected —
    which is why the tool schemas carry no JSON-schema `enum` for it.

    ## Trading mechanisms
    2.0 topics run one of two mechanisms. `v2_share` topics are legacy
    share-pool markets — readable, but not tradable through this surface
    (`preview_order` answers `MECHANISM_UNSUPPORTED`). `v21_lmsr` is the live
    one; list and search endpoints default to it.

    - **Stake rules are fixed per topic.** Each `v21_lmsr` topic freezes its
      own stake range, market depth and rules version when it is created;
      they never change afterwards, and a later platform change only affects
      new topics. The range is published as the topic's `stake_rules`
      (`min_coins`..`max_coins`, whole coins; older topics are 100..100).
    - **The user chooses the stake.** `preview_order` takes `amount_coins`
      within that range. If the user stated an amount, pass exactly that;
      if they did not, ask them — an agent must never choose or infer an
      amount. An amount outside the range is refused with
      `STAKE_OUT_OF_RANGE`, never adjusted. Omitting `amount_coins` stakes
      `stake_rules.default_coins`, a fallback rather than a suggestion.
    - **One order per topic, no fees.** A user places at most one order on a
      topic; the whole stake buys units.
    - **Opening prices.** A topic can open at initial probabilities set when
      it is published, rather than at an even split across its options;
      read the current `implied_price` instead of assuming 1/n.
    - **Payout.** A correct option pays `units × ruby multiplier` **rubies**;
      the stake itself is not returned. The multiplier is 1 unless a ruby
      boost (`ruby_boost` on the topic) is live at one of three moments: when
      the order is placed, when the topic settles, or (in the app) when the
      position is sold — whichever gives the highest multiplier applies. The
      order records the multiplier live when it is placed; a boost live at
      settlement raises it. So every `payout_if_correct_rubies` figure is a
      **guaranteed minimum** (units × the order's current multiplier), not
      the final amount.
    - **Selling is app-only.** Users can sell a whole position for rubies in
      the SeeSaw app; this surface offers no sell operation. A sold position
      shows `sold: true` with `sold_rubies`, and is never settled or refunded
      afterwards — `won`, `payout_rubies` and `payout_if_correct_rubies`
      stay null. Selling still counts as the topic's one order.

    ## Rubies and the retired sapphire
    Sapphires were merged 1:1 into rubies; opinion paybacks now pay rubies.
    `list_my_ruby_ledger` shows every ruby movement — settlement payouts,
    positions sold in the app, opinion paybacks and the one-off conversion
    of the old sapphire balance. The sapphire-named fields remain on the
    wire with the same values as their `ruby_*` replacements and are marked
    `deprecated`; they will be removed in contract **3.0.0**:

    | Deprecated | Use instead |
    | --- | --- |
    | `MyProfile.sapphires` (always `"0.0000"`) | `rubies` |
    | `sapphire_payback` | `ruby_payback` |
    | `sapphire_total` | `ruby_total` |
    | `max_relay_payback_sapphires` | `max_relay_payback_rubies` |
servers:
  - url: https://api.seesaw.fun/open/v1
    description: Production
security:
  - apiKey: []
tags:
  - name: Topics
    description: 2.0 prediction topics — browse, search, detail, opinions
  - name: Me
    description: Data of the authenticated key owner
  - name: Trading
    description: Place a position (preview then execute, `write` scope)
  - name: Opinions
    description: Publish and back opinions (preview then execute, `write` scope)
paths:
  /me/ruby-ledger:
    get:
      tags:
        - Me
      summary: List the key owner's ruby ledger
      description: |
        Every ruby movement, newest first: settlement payouts (and rubies
        taken back when a settled topic is taken down or re-ruled), positions
        sold in the SeeSaw app, opinion paybacks and creator fees (and their
        clawbacks), and the one-off conversion of the old sapphire balance.
        Added in 2.1.0; read scope, counts against the read window.

        Only the ruby balance is covered. Sapphire rows from before the
        sapphire→ruby merge are not listed — the balance they left behind
        arrives as one `sapphire_conversion` row.

        Filtering happens in the ledger query, so pages and `has_more` are
        exact. A combination that cannot match (e.g. `flow=out&source=sell`)
        returns an empty page. Titles in `topic` follow `Accept-Language`.
      operationId: listMyRubyLedger
      parameters:
        - name: flow
          in: query
          description: '`in` = rubies received, `out` = rubies taken back.'
          schema:
            type: string
            enum:
              - all
              - in
              - out
            default: all
        - name: source
          in: query
          description: |
            Optional business-line filter (omit for all). `other` matches
            only the registered other types (today `sapphire_conversion`);
            rows of unregistered types appear only when unfiltered. An
            unknown value is `400 INVALID_ARGUMENT` with
            `details.field = source`.
          schema:
            type: string
            enum:
              - settlement
              - sell
              - opinion_payback
              - other
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Ruby ledger entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RubyLedgerEntryList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  parameters:
    Cursor:
      name: cursor
      in: query
      description: Opaque pagination cursor from a previous response's `next_cursor`.
      schema:
        type: string
    Limit:
      name: limit
      in: query
      description: Page size (max 100).
      schema:
        type: integer
        default: 20
        minimum: 1
        maximum: 100
  schemas:
    RubyLedgerEntryList:
      allOf:
        - $ref: '#/components/schemas/ListEnvelope'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/RubyLedgerEntry'
    ListEnvelope:
      type: object
      description: Uniform list shape. `next_cursor` is null on the last page.
      required:
        - data
        - next_cursor
        - has_more
      properties:
        data:
          type: array
          items: {}
        next_cursor:
          type:
            - string
            - 'null'
        has_more:
          type: boolean
    RubyLedgerEntry:
      type: object
      description: >
        One ruby movement. `amount` is always positive and `direction` says

        which way it moved the balance. `type` is the raw ledger type, passed

        through as-is — new types may appear and must not break a client;

        a type the server has not registered comes back with

        `direction: unknown` and `source: other` rather than a guess.


        | `source` | `type` values |

        | --- | --- |

        | `settlement` | `ruby_settlement` (in) · `ruby_revocation` (out —
        settlement rubies taken back on a takedown or re-ruling) |

        | `sell` | `ruby_trade_sell` (in — a position sold in the SeeSaw app) |

        | `opinion_payback` | `sapphire_back_relay` · `sapphire_creator_fee`
        (in) · `sapphire_opinion_takedown` (out — clawed back with the opinion)
        |

        | `other` | `sapphire_conversion` (in — the one-off 1:1 merge of the old
        sapphire balance) and any unregistered type |


        The `sapphire_*` type strings are historical names of ledger types

        that now pay rubies.
      required:
        - id
        - amount
        - direction
        - source
        - type
        - topic
        - opinion_id
        - created_at
      properties:
        id:
          type: string
          format: uuid
        amount:
          type: string
          description: Always positive, 4 places.
        direction:
          type: string
          enum:
            - in
            - out
            - unknown
        source:
          type: string
          enum:
            - settlement
            - sell
            - opinion_payback
            - other
        type:
          type: string
          description: Raw ledger type (see the table above); open-ended.
        topic:
          description: |
            The topic of a settlement, revocation or sell row; null on other
            rows. `title` follows the requested language and is `""` when it
            cannot be resolved (`id` and `url` are still set).
          oneOf:
            - $ref: '#/components/schemas/TopicRef'
            - type: 'null'
        opinion_id:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            The opinion of an opinion payback, creator fee or takedown row; null
            on other rows.
        created_at:
          type: string
          format: date-time
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - UNAUTHORIZED
                - FORBIDDEN
                - FORBIDDEN_SCOPE
                - NOT_FOUND
                - OPINION_NOT_FOUND
                - INVALID_ARGUMENT
                - RATE_LIMITED
                - SERVICE_DISABLED
                - INTERNAL
                - CONFIRM_TOKEN_INVALID
                - CONFIRM_TOKEN_EXPIRED
                - CONFIRM_TOKEN_USED
                - CONFIRM_UNAVAILABLE
                - MECHANISM_UNSUPPORTED
                - PREVIEW_UNAVAILABLE
                - TOPIC_ALREADY_STAKED
                - TOPIC_CLOSED
                - TOPIC_QUOTE_EXPIRED
                - TOPIC_QUOTE_INVALID
                - TOPIC_MECHANISM_MISMATCH
                - STAKE_OUT_OF_RANGE
                - RETRY_IDEMPOTENCY_KEY
                - OPINION_ALREADY_BACKED
                - OPINION_BACK_PRICE_MOVED
                - OPINION_BACK_RETRY
                - OPINION_BACK_FULL
                - OPINION_BACK_CHAIN_BROKEN
                - OPINION_SIDE_REQUIRED
                - OPINION_SIDE_INVALID
                - OPINION_TOPIC_CLOSED
                - OPINION_TOPIC_HIDDEN
                - OPINIONS_UNAVAILABLE
                - INSUFFICIENT_BALANCE
                - CONTENT_INVALID
                - CONTENT_MODERATED
                - USER_PUNISHED
              description: |
                Stable contract values. `SERVICE_DISABLED` (503) is the
                emergency kill switch; `FORBIDDEN_SCOPE` (403) means the key
                lacks the `write` scope. The `CONFIRM_TOKEN_*` and rule codes
                only occur on the write surface. `OPINION_SIDE_REQUIRED` /
                `OPINION_SIDE_INVALID` are rare but reachable: `preview_opinion`
                already answers a missing or invalid side with
                `can_publish: false` (reason `SIDE_REQUIRED`), so they surface
                as a 400 from `publish_opinion` only when the owner's position
                changed between preview and publish (e.g. no position at
                preview, then an order on the other side before publishing) and
                the side in the token no longer fits.
            message:
              type: string
            details:
              type: object
              description: >-
                Optional context, e.g. `{"field": "limit"}`, `{"retry_after":
                12}` or `{"required_scope": "write"}`.
              additionalProperties: true
    TopicRef:
      type: object
      required:
        - id
        - title
        - url
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        url:
          type: string
  responses:
    BadRequest:
      description: Invalid argument (`details.field` names the offending parameter)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: >-
        Missing, invalid, revoked, or expired API key (carries
        `WWW-Authenticate`)
      headers:
        WWW-Authenticate:
          schema:
            type: string
          description: Bearer realm="seesaw-open-api"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: >-
        Rate limit exceeded (see `Retry-After` and `details.retry_after`
        seconds)
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: sspat_ personal API key
      description: |
        Personal API key issued per SeeSaw user account:
        `Authorization: Bearer sspat_…`. Manage keys on the app API
        (`/v1/users/me/api-keys`). Revocation propagates within ~60 seconds
        (validation cache window).

        The key's **scopes** decide what it may do: every key has `read`; a key
        created with `["read","write"]` may also use the six write operations
        (marked `x-required-scope: write` in this document). Scopes are fixed at
        creation — to get write access, create a new key. A read-only key
        calling a write operation gets `403 FORBIDDEN_SCOPE`.

````