openapi: 3.1.0

info:
  title: deepstate-live
  version: 0.1.0
  description: |
    Live layer (layer B) for the Deepstate v3 site on Robinhood Chain (4663).

    `deepstate-live` is the Go BFF described in `odap/DEEPSTATE_BUILD_RUNBOOK.md` §0.1
    (layer B) and built by runbook phase 9. It sits between `robinhood-node` (the
    chain client), the `prod.robinhood.*` OpenSearch indexes written by ODAP (layer A)
    and the Next.js site (layer C).

    **This document is the contract.** The frontend builds against it; the service is
    written to satisfy it. Where the two disagree, this file is the bug report.

    ## Reading the schemas

    * **Every `uint256` amount is a decimal string** in a field suffixed `_raw`
      (runbook D10, spec §8). A sibling `number` field without the suffix may be
      present for display; it is `x-lossy: true` and must never be used for a
      customer-facing figure. The frontend formats `_raw` with `viem`'s `formatUnits`.
    * **`finality` is on every row that carries a `block_number`** and is one of
      `head` / `safe` / `finalized`. It is computed per response from the node's
      `safe` and `finalized` tags, never stored (runbook §3.2). Rows at `head` can
      still be reorged away (runbook §4.5); render them as unconfirmed.
    * **`side` is always the display side** (`bid` / `ask`): the side that buys or
      sells the market's *base* token. The raw ABI flag is exposed separately as
      `is_bid_token0`, which is token0-relative and therefore flips between books
      (spec §2.4.1): `side = bid iff is_bid_token0 == (base_token == token0)`.
      On NVDA/USDG `is_bid_token0 = true` is a display **ask**; on DEEP/USDG it is a
      display **bid**. Never derive the display side yourself from the raw flag
      without the market's `token0`.
    * **Market-scoped responses carry `book_id`, `book_label`, `base_token` and
      `quote_token`** so a client never has to hold a side table.
    * **`market_session`** labels each timestamp with the US equity session it falls
      in (runbook §5). The chain trades 24/7; NYSE does not.

    ## What is live today

    `/v1/health`, `/v1/markets`, `/v1/gas`, `/v1/stream` and `/rpc` are served from
    the node and the two indexes that exist (`prod.robinhood.blocks`,
    `prod.robinhood.robinhood_gas_1m`). Every other route is fully specified here but
    answers **501** with a `NotReady` body naming the index it waits on, until the
    corresponding runbook phase lands. The 200 schema and example on those routes are
    the contract the frontend builds against in the meantime, and `--mock` serves
    exactly those examples.

    ## What this service will not do

    `POST /rpc` is a **read-only, allow-listed** JSON-RPC pass-through. It does not
    carry `eth_sendRawTransaction` (runbook D8/P9-9): `robinhood-node` runs with
    `--execution.forwarding-target` pointed at Robinhood's rate-limited public RPC, so
    proxying writes would funnel every user of the site through one IP against that
    limit. Wallet **writes** go through the user's own wallet connector. It also does
    not carry `eth_getLogs`, `debug_*` or `trace_*`.
  contact:
    name: OneSource
    url: https://onesource.io
  license:
    name: Proprietary

servers:
  - url: http://localhost:8080
    description: local (`go run ./cmd/deepstate-live`, or `--mock`)
  - url: http://deepstate-live.deepstate.svc.cluster.local
    description: in-cluster
  - url: https://deepstate-live.onesource.io
    description: direct public host (free, unauthenticated, browser/SSE)
  - url: https://api.onesource.io/deepstate
    description: |
      paid public host via the OneSource API gateway (runbook
      odap/DEEPSTATE_API_RUNBOOK.md, API-D17: one canonical front door;
      formerly its own host, markets.onesource.io, retired 2026-09-06). The
      gateway strips the `/deepstate`
      prefix before forwarding, so every path below is unchanged on the wire;
      it also injects `X-Auth-Method`, `X-License-ID` and `X-License-Tier` on
      authenticated calls. See `components.securitySchemes.bearerAuth`.

tags:
  - name: health
    description: Liveness, chain head and index freshness.
  - name: market
    description: Book, tape and market metadata.
  - name: analytics
    description: Derived series (runbook §0.3 E1-E14).
  - name: protocol
    description: Rewards, governance and vault.
  - name: stream
    description: Server-sent events.
  - name: rpc
    description: Allow-listed JSON-RPC read proxy.
  - name: meta
    description: The contract itself.

paths:
  /openapi.yaml:
    get:
      tags: [meta]
      operationId: getOpenAPI
      summary: This document, byte-for-byte.
      description: |
        Serves `openapi.yaml` itself — the exact file this operation is defined
        in, embedded into the binary at build time (`spec.go`'s `//go:embed`) so
        it can never drift from what the running process actually serves.
        Compiled-in configuration, so it carries the same long cache-control as
        `GET /v1/markets`. Available on every host this service answers on,
        including the paid host behind the gateway
        (`https://api.onesource.io/deepstate/openapi.yaml`, formerly
        `https://markets.onesource.io/deepstate/openapi.yaml`).
      responses:
        "200":
          description: The contract.
          headers:
            Cache-Control: { $ref: "#/components/headers/LongCache" }
          content:
            application/yaml:
              schema: { type: string }

  /v1/health:
    get:
      tags: [health]
      operationId: getHealth
      summary: Chain head, finality tags and per-index freshness.
      description: |
        Served from the node plus one `max(block_number)` aggregation per Deepstate
        index. `indexes[].not_ready` is `true` for indexes that do not exist yet;
        those are the same indexes named by the `501` bodies elsewhere in this spec.
        Readiness probes use this route: it is `200` whenever the process can answer,
        `503` when the node is unreachable.
      responses:
        "200":
          description: Service is up.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
        "503":
          description: The node is unreachable; the service cannot answer for the chain.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /v1/markets:
    get:
      tags: [market]
      operationId: listMarkets
      summary: The books this service serves, with their token layout.
      description: |
        Static, compiled-in configuration (runbook §7, "two books, one code path").
        `token0`/`token1` are the **address-sorted** pair the protocol registers;
        `base_token`/`quote_token` are the display assignment this service makes.
        The two differ, and that difference is what makes `is_bid_token0` flip between
        books — see the `side` note in the API description.
      responses:
        "200":
          description: The configured markets.
          headers:
            Cache-Control: { $ref: "#/components/headers/LongCache" }
          content:
            application/json:
              schema:
                type: object
                required: [markets]
                additionalProperties: false
                properties:
                  markets:
                    type: array
                    items: { $ref: "#/components/schemas/Market" }
              example:
                markets:
                  - book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
                    book_slug: "NVDA-USDG"
                    book_label: "NVDA/USDG"
                    pool_id: "0x42819cadfbb25aab80543236e280fba4e61aa61e0b5b777541de54ae69da35e4"
                    epoch: 0
                    token0:
                      address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
                      symbol: "USDG"
                      decimals: 6
                    token1:
                      address: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
                      symbol: "NVDA"
                      decimals: 18
                    base_token:
                      address: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
                      symbol: "NVDA"
                      decimals: 18
                    quote_token:
                      address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
                      symbol: "USDG"
                      decimals: 6
                    base_is_token0: false
                    size_token:
                      address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
                      symbol: "USDG"
                      decimals: 6
                    router: "0x6cf19308C22FC82ea620Fa0B3E94948d20f27B96"
                    rewarder: "0xE85ADBC03a6b52a2c9894c1BB525eC883ea156D7"
                    has_rewarder: true
                    deployed_block: 36932573
                    deployed_at: "2026-08-15T07:23:57Z"
                    status: "live"
                  - book_id: "0xcde1e9c260cbf55986bf267eafbf57c03aa9bff03e45e96b900f35d3d083d1b6"
                    book_slug: "DEEP-USDG"
                    book_label: "DEEP/USDG"
                    pool_id: "0xbd11e0ec02d8fb9b08dfd465e892cb71e2cb9b2d4697a58baf13bdc1e8753786"
                    epoch: 0
                    token0:
                      address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
                      symbol: "DEEP"
                      decimals: 18
                    token1:
                      address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
                      symbol: "USDG"
                      decimals: 6
                    base_token:
                      address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
                      symbol: "DEEP"
                      decimals: 18
                    quote_token:
                      address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
                      symbol: "USDG"
                      decimals: 6
                    base_is_token0: true
                    size_token:
                      address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
                      symbol: "DEEP"
                      decimals: 18
                    router: "0x6cf19308C22FC82ea620Fa0B3E94948d20f27B96"
                    has_rewarder: false   # poolHook(DEEP/USDG) is the zero address
                    deployed_block: 36932573
                    deployed_at: "2026-08-15T07:23:57Z"
                    status: "live"

  /v1/gas:
    get:
      tags: [analytics]
      operationId: getGas
      summary: Base-fee series at 1-minute resolution, session-labelled.
      description: |
        Rolled-up minutes come from `prod.robinhood.robinhood_gas_1m`, which is built
        only from blocks at or below `safe` so it never needs rollback (runbook §3.1).
        The last few minutes are not in the rollup yet, so the tail is computed live
        from `prod.robinhood.blocks` and marked `source: "tail"`. Base fee moves every
        block on this chain (0.458–0.617 gwei over the last 1,024 blocks on
        2026-09-02, 873 distinct values), so this axis is real, not flat.
      parameters:
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: res
          in: query
          description: Resolution. Only `1m` exists today.
          schema: { type: string, enum: ["1m"], default: "1m" }
      responses:
        "200":
          description: Gas series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GasSeries" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501":
          description: |
            `prod.robinhood.robinhood_gas_1m` does not exist yet (runbook P2-5).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NotReady" }

  /v1/book/{book}:
    get:
      tags: [market]
      operationId: getBook
      summary: Order-book snapshot - bids descending, asks ascending.
      description: |
        Served from **Router state**, not from replaying logs. Runbook P6 measured
        the log-derived model against the chain and falsified it: the resting set at
        any height is whatever `roots()` / `tree()` say at that height, and each
        side's reverse in-order walk is priority order, best first — `topOrder()`
        returns exactly its first element, 40/40 across both books and 20 random
        heights. Price comes out of the order id itself: the leading 4 bytes are a
        fixed-point base-2 logarithm of the pre-fee price (spec §3.4).

        The walk is expensive (~680 batched `eth_call`s per book), so it runs on a
        timer and every request is served from the last completed read. **A snapshot
        is therefore up to one refresh interval old**, and `block_number` names the
        exact height it was read at — the walk pins every call to that number rather
        than to `latest`, so the two sides cannot straddle a fill.

        `501` here no longer means an index is missing. It means no state read has
        completed yet: a cold start, or the node not answering.

        `size_raw` is denominated in the book's **`token0`** on both sides — see
        `Market.size_token`. Depth totals are therefore token0-denominated without a
        side-dependent scale, but token0 is *not* the quote token on every book: it
        is USDG on NVDA/USDG and DEEP on DEEP/USDG.
      parameters:
        - $ref: "#/components/parameters/BookPath"
        - name: depth
          in: query
          description: Maximum price levels returned per side.
          schema: { type: integer, minimum: 1, maximum: 500, default: 50 }
      responses:
        "200":
          description: Book snapshot.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BookSnapshot" }
        "404": { $ref: "#/components/responses/NotFound" }
        "501": { $ref: "#/components/responses/NotReadyBookState" }

  /v1/trades/{book}:
    get:
      tags: [market]
      operationId: listTrades
      summary: Trade tape, newest first.
      description: |
        Backed by `prod.robinhood.deepstate_trades` (runbook P5), newest first. The
        tape's own `attribution: window_watermark` marker rows (one per no-fill
        window, ~2,500 of them) are filtered out here, not passed through as trades.

        `price_raw` is the pre-fee price; `price_net_of_fee_raw` nets the 10 bps the
        Router takes from the taker's output token — populated only for a
        reconciled sole fill (roughly half of priced trades on the live index), so
        its absence does not mean the fee was zero. `attribution` records how the
        trade's legs were resolved from the transaction's transfers (spec §4);
        `exact` is a single match with unambiguous legs, the other two are derived.
      parameters:
        - $ref: "#/components/parameters/BookPath"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - name: before
          in: query
          description: Return trades strictly below this block number (paging).
          schema: { type: integer, format: int64, minimum: 0 }
      responses:
        "200":
          description: Trades.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TradePage" }
        "404": { $ref: "#/components/responses/NotFound" }
        "501": { $ref: "#/components/responses/NotReadyTrades" }

  /v1/candles/{book}:
    get:
      tags: [market]
      operationId: listCandles
      summary: OHLCV candles.
      description: |
        Backed by `prod.robinhood.deepstate_trades` (runbook P5): an OpenSearch
        `date_histogram` over the trade tape, excluding both `window_watermark` and
        `unpriced` rows (an unpriced fill has no price to put in a candle). High,
        low, open and close are each the real `price_raw` string off the matching
        document — never recomputed — selected via the lossy `price` double field's
        sort order. Volumes are exact `uint256` sums computed server-side. The open
        bucket is cached for 5 s; closed buckets are immutable and cached long.
      parameters:
        - $ref: "#/components/parameters/BookPath"
        - name: tf
          in: query
          required: true
          schema: { type: string, enum: ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"] }
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
      responses:
        "200":
          description: Candles.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CandleSeries" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "501": { $ref: "#/components/responses/NotReadyTrades" }

  /v1/stats/{book}:
    get:
      tags: [market]
      operationId: getStats
      summary: Rolling 24h / 7d / 30d volume and change.
      description: |
        Backed by `prod.robinhood.deepstate_trades` (runbook P5). Volume is the exact
        `uint256` sum of `quote_amount_raw` over the window, computed server-side
        rather than through the lossy double `sum` aggregation OpenSearch also
        offers. `24h` volume trails the site's own headline while the tape's
        upstream events stream is still catching up to head (see the runbook's
        current freshness note) — it is not a data-quality bug in this route.
      parameters:
        - $ref: "#/components/parameters/BookPath"
      responses:
        "200":
          description: Stats.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MarketStats" }
        "404": { $ref: "#/components/responses/NotFound" }
        "501": { $ref: "#/components/responses/NotReadyTrades" }

  /v1/stats/revenue:
    get:
      tags: [analytics]
      operationId: getRevenue
      summary: Protocol fee revenue per bucket.
      description: |
        Backed by `prod.robinhood.deepstate_trades` (runbook P5). The Router takes
        `floor(output × 10 / 10000)` — 10 bps — from the taker's **output** token
        (`side: bid` settled means the taker sold and paid in the quote leg;
        `side: ask` settled means the taker bought and paid in the base leg), so
        revenue arrives in whichever token the taker received. This route derives
        both legs from the trade's own settled amount at the compiled-in 10 bps
        rate rather than reading the tape's `fee_amount_raw` field back — that field
        is populated only for a reconciled sole fill (about half of priced trades on
        the live index), so using it alone would understate revenue by roughly half.
        Both legs are reported separately as `_raw` and only then combined into a
        lossy `fee_usd` (USDG at $1, base legs converted at that bucket's VWAP).

        **`book` omitted combines both configured books' USDG-denominated fee leg
        and trade count, but withholds `fee_base_raw`/`vwap_raw`**: NVDA/USDG's base
        leg is NVDA and DEEP/USDG's is DEEP, and summing two different ERC-20s'
        raw amounts would produce a number with no meaning.

        Because the rate is fixed, the fee is also a check on the volume figure:
        gross notional should be 1,000 × fee. A cross-check against fee transfers
        into the STATE vault in `deepstate_events` catches the case where the tape
        misses a leg — the vault is `feeConfig().recipient`, confirmed on-chain.
      parameters:
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: res
          in: query
          schema: { type: string, enum: ["1h", "1d"], default: "1d" }
        - $ref: "#/components/parameters/BookQuery"
      responses:
        "200":
          description: Revenue series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RevenueSeries" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyTrades" }

  /v1/users/active:
    get:
      tags: [analytics]
      operationId: getActiveUsers
      summary: Distinct addresses transacting with the protocol per bucket.
      description: |
        Backed by `prod.robinhood.deepstate_trades` and `prod.robinhood.deepstate_events`
        (runbooks P5 and P3), queried together in one request.

        Three populations, deliberately counted separately because they barely
        overlap: `takers` cross the spread, `makers` rest orders, `farmers` receive
        `RewardsDistributed`. `total_unique` is the union, so it is **not** the sum —
        one address can be all three, and the interesting ones are. It is computed
        as its own cardinality over the whole window rather than derived from the
        bucketed series, because summing the buckets' `total_unique` would double
        count an address active on more than one day.

        **Sources, corrected from an earlier draft of this description:**
        `deepstate_trades.taker_address` for `takers` — the tape's `maker_address`
        field is never populated (the join it would need is the book reducer's
        state, not a document lookup; see `listTrades`), so `makers` comes from
        `deepstate_events`' `OrderRested.maker` instead, and `farmers` from
        `RewardsDistributed.rewardee`.
      parameters:
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: res
          in: query
          schema: { type: string, enum: ["1h", "1d"], default: "1d" }
      responses:
        "200":
          description: Active-user series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ActiveUsersSeries" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyTrades" }

  /v1/flow/sources:
    get:
      tags: [analytics]
      operationId: getFlowSources
      summary: Taker flow attribution - where the order flow comes from.
      description: |
        Backed by `prod.robinhood.deepstate_trades` and `prod.robinhood.deepstate_events`
        (runbooks P5 and P3). `tx_to` is not a field the trade tape carries — it is a
        receipt field the events step joins onto the Router's own match-event
        documents — so this route joins the two indexes on `transaction_hash` inside
        one OpenSearch `scripted_metric` aggregation rather than fetching every
        matching document into the BFF to join client-side.

        The question a block explorer and a generic dashboard cannot answer: which
        interface or contract routed each fill. Three sources, in decreasing
        confidence:

        * `direct` — the taker's own address called the Router (`tx_to == Router`).
        * `integrator:<address>` — the fill came through
          `fillWithIntegratorFee`, so the interface identified itself and took a fee.
          This is authoritative.
        * `contract:<tx_to>` — `tx_to` is neither the taker nor the Router, so an
          aggregator or a bot contract sat in between. The address is reported as
          found; naming it is a separate labelling exercise.

        `book` omitted combines both configured books; every book's quote token is
        USDG, so unlike `getRevenue` there is no mixed-token combination to avoid.
      parameters:
        - $ref: "#/components/parameters/BookQuery"
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: res
          in: query
          schema: { type: string, enum: ["1h", "1d"], default: "1d" }
      responses:
        "200":
          description: Flow attribution.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowSources" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyTrades" }

  /v1/analytics/open-orders-vs-gas:
    get:
      tags: [analytics]
      operationId: getOpenOrdersVsGas
      summary: Resting-order count against base fee, session-shaded (E1).
      description: |
        Backed by `prod.robinhood.deepstate_book_summary` (runbook P6), one document
        per change block per book. Runbook §0.3 E1. The join is made at write time —
        the step carries `base_fee_per_gas` from the block header — so this route is
        one query, not a correlation. It answers whether makers pull quotes when gas
        spikes and whether the book thins when the reference market is closed.

        `book` defaults to the first configured market (NVDA/USDG) rather than to
        "every configured book": the response is one book's points, not a per-book
        list, so an omitted `book` has to resolve to something singular.
      parameters:
        - $ref: "#/components/parameters/BookQuery"
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
      responses:
        "200":
          description: Joined series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OpenOrdersVsGas" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyBookSummary" }

  /v1/rewards/leaderboard:
    get:
      tags: [protocol]
      operationId: getRewardsLeaderboard
      summary: Cumulative DEEP distributed per rewardee.
      description: |
        Backed by `prod.robinhood.deepstate_events` (runbook P3). `RewardsDistributed`
        aggregated by rewardee. The event's `side_token` is the token the rewarded
        order *sold* (its escrow), which is what makes the row's `side` derivable:
        `bid` when `side_token == quote_token`, else `ask`.

        Only NVDA/USDG has a Rewarder — `poolHook(DEEP/USDG)` is the zero address
        (runbook §1.2/P10-4b) — so `book` must resolve to it; naming DEEP/USDG or any
        other non-rewarded book is a `400`, never a `200` with zeroed totals.
        `total_distributed_raw` sums every matching event in the window regardless of
        `limit`, so `share_of_total` on a capped page still means something.
      parameters:
        - name: epoch
          in: query
          description: |
            Book epoch. Only epoch 0 exists today. Mutually exclusive with
            `from`/`to`.
          schema: { type: integer, format: int64, minimum: 0, default: 0 }
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - $ref: "#/components/parameters/BookQuery"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        "200":
          description: Leaderboard.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RewardsLeaderboard" }
        "501": { $ref: "#/components/responses/NotReadyEvents" }

  /v1/rewards/params:
    get:
      tags: [protocol]
      operationId: getRewardsParams
      summary: Rewarder emission constants and per-side state.
      description: |
        Backed by live Rewarder/ERC20 view calls over the node (runbook P9-4),
        cached ~60s in-process so a page polling this route does not turn into a
        standing load of eth_calls. There is no OpenSearch dependency at all.

        The Rewarder addresses sides **by token**, not by a bool (runbook §1.2), so
        every per-side figure here is keyed by the side's token address. Emission
        totals are `uint96` on chain; they are still returned as `_raw` strings.
        Only NVDA/USDG carries a Rewarder (DEEP/USDG's `poolHook` is the zero
        address), so `sides` always has exactly the two NVDA/USDG sides.
      responses:
        "200":
          description: Rewarder parameters.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RewardsParams" }
        "501": { $ref: "#/components/responses/NotReadyRewarderCache" }

  /v1/rewards/emissions:
    get:
      tags: [protocol]
      operationId: getRewardsEmissions
      summary: Emission ceiling against what was actually distributed, per side.
      description: |
        Backed by the hourly `prod.robinhood.deepstate_rewarder_snapshots` index
        (runbook P7-1), bucketed against `prod.robinhood.deepstate_events`.

        Three quantities that are easy to conflate and mean different things:

        * `ceiling_raw` — what the emission curve *allowed* in the bucket, the delta
          of `cumulativeEmissionsAt(token, ts)` across it.
        * `accrued_raw` — what the Rewarder has booked to the side, from
          `totalAccrued(token)` at the snapshot.
        * `distributed_raw` — what was actually paid out, summing
          `RewardsDistributed` events.

        `capture_ratio` is `distributed / ceiling`. It is below 1 whenever makers
        left rewards on the table — no eligible order at the top of book — which is
        the number that says whether the incentive is doing anything. 167.2M of the
        1B DEEP prefund had gone out in the protocol's first 17 days.

        Sides are addressed by token, not by a bool, because the Rewarder is
        (`emissionStart(token)`, `totalAccrued(token)`, ...) throughout.
      parameters:
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: res
          in: query
          schema: { type: string, enum: ["1h", "1d"], default: "1d" }
        - $ref: "#/components/parameters/BookQuery"
      responses:
        "200":
          description: Emission series per side.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EmissionsSeries" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyRewarderSnapshots" }

  /v1/token/deep:
    get:
      tags: [protocol]
      operationId: getDeepToken
      summary: DEEP supply, float and the emission-to-revenue ratio.
      description: |
        Backed by the latest `prod.robinhood.deepstate_vault_snapshots` document
        (runbook P7-1) for supply/burn, and `prod.robinhood.robinhood_dex_swaps'`
        Uniswap v4 DEEP/USDG pool (runbook P3b) for price.

        The supply arithmetic is not the usual kind, so the fields spell it out:

            float = total_supply − rewarder_balance

        **Burned DEEP is deliberately not subtracted a second time** — `totalSupply`
        already excludes it, per `float_raw`'s own schema description below. (An
        earlier draft of this description said `− burned` here too; that was wrong,
        not merely stubbed, and is fixed as part of wiring this route for real.)
        `burned_raw` is the vault's `totalAssets`, which *is* cumulative DEEP burned
        (deposits are burned; the vault holds 8,014.13 DEEP against 104,044,463.60
        of accounted assets). `rewarder_balance_raw` is the undistributed remainder
        of the 1B prefund. What is left is the only DEEP that can actually move —
        so `float_market_cap_usd` is the honest market cap and a `total_supply ×
        price` figure would overstate it several times over.

        `price_source` is `uniswap_v4` while that pool has a priced swap indexed,
        falling back to `deepstate` — Deepstate's own DEEP/USDG book's most recent
        priced trade — only if it does not. DEEP/USDG's price-key scale is resolved
        (runbook P5-3/P6; `/v1/markets` now reports it `live`), so this fallback is
        no longer the placeholder it was when this route was written.

        `emission_to_revenue_ratio` composes DEEP distributed over the trailing
        24 h (`RewardsDistributed`, same book as `distributed_total_raw`) times
        this response's own `price_usdg`, divided by trailing-24h protocol fee
        revenue read through the same client method `GET /v1/stats/revenue`
        uses — reused rather than re-derived, so there is one fee formula, not
        two. That revenue figure sums BOTH configured books' USDG fee leg only
        (the same multi-book combination rule `GET /v1/stats/revenue` itself
        follows when called without `book`, because NVDA-denominated and
        DEEP-denominated fee legs cannot be added together), which slightly
        understates revenue and therefore slightly overstates the ratio.
        Omitted — not a fabricated 0 — whenever there is no priced DEEP figure
        yet or no fee revenue at all in the window. It is not in this schema's
        `required` list.
      responses:
        "200":
          description: DEEP token state.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeepToken" }
        "501": { $ref: "#/components/responses/NotReadyVault" }

  /v1/venues/share:
    get:
      tags: [analytics]
      operationId: getVenueShare
      summary: Volume by venue for a pair - Deepstate's share of its own market.
      description: |
        Backed by `prod.robinhood.robinhood_dex_swaps` (runbook P3b: Uniswap v3/v4
        NVDA/USDG and DEEP/USDG swaps; Ekubo and Arcus planned later) against
        Deepstate's own `prod.robinhood.deepstate_trades`.

        Deepstate is not the only venue for these pairs on this chain, and a
        dashboard that only counts our own book cannot say whether we are winning.
        `deepstate_share` on the response is the headline: our volume over the total.

        **Known data-quality finding (runbook P3b-4, measured 2026-09-04):** a
        launch-week check of this exact index measured Uniswap v3 NVDA/USDG volume
        at $73.68M against Dune's ~$105M for the same window — about 30% short. Log
        coverage for the window was verified complete; the cause is unresolved. Any
        `uniswap_v3` total this route reports for that window inherits the same gap.
      parameters:
        - name: pair
          in: query
          description: |
            Display pair. Defaults to the first configured market (NVDA/USDG) — the
            response is one pair's series, not a list, so an omitted value has to
            resolve to something singular rather than literally "every pair".
          schema: { type: string, example: "NVDA/USDG" }
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: res
          in: query
          schema: { type: string, enum: ["1h", "1d"], default: "1d" }
      responses:
        "200":
          description: Venue volume series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VenueShare" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyDexSwaps" }

  /v1/venues/basis:
    get:
      tags: [analytics]
      operationId: getVenueBasis
      summary: Price basis between Deepstate's mid and an AMM's price.
      description: |
        Backed by `prod.robinhood.robinhood_dex_swaps`' Uniswap v3 rows (runbook
        P3b) against Deepstate's own `prod.robinhood.deepstate_trades`, both
        volume-weighted per bucket. Always compares against v3 specifically — the
        field names (`uniswap_v3_price_raw`) are not "whichever venue is dominant
        for this pair" — and excludes `attribution: unpriced` trades, which carry no
        price to average in. A bucket missing either leg is omitted rather than
        reported half-filled.

        `basis_bps` is `(deepstate_mid − amm_price) / amm_price × 10000`. A
        persistent non-zero basis is either an arbitrage nobody is taking or a sign
        that one venue's price is stale — both worth seeing. Note the comparison is
        our **mid** against the AMM's executed price, so a wide Deepstate spread
        (≈4.00 USDG, ~183 bps, on 2026-09-02) shows up here as noise around the
        basis rather than as basis itself; the spread is on `/v1/book`.

        The same P3b-4 volume-coverage finding `GET /v1/venues/share` documents
        (Uniswap v3 NVDA/USDG measured ~30% short of Dune, cause unresolved) applies
        to every v3 price point this route computes, because both read the same
        underlying rows.
      parameters:
        - name: pair
          in: query
          description: Display pair. Defaults to the first configured market (NVDA/USDG).
          schema: { type: string, example: "NVDA/USDG" }
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: res
          in: query
          schema: { type: string, enum: ["1m", "1h"], default: "1h" }
      responses:
        "200":
          description: Basis series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VenueBasis" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyDexSwaps" }

  /v1/governance/proposals:
    get:
      tags: [protocol]
      operationId: listProposals
      summary: Governor proposals with vote tallies and quorum progress.
      description: |
        Backed by `prod.robinhood.deepstate_governance` (runbook P7-2), which
        re-reduces every proposal from the decoded Governor events on each pass and
        refreshes `state`/tallies/`quorum_raw` from the contract itself.

        `state` falls back to a Pending/Active guess from `vote_start`/`vote_end`
        for the few minutes between a proposal's first index and its first
        successful chain refresh; a terminal state (Defeated, Succeeded, Queued,
        Expired, Executed, Canceled) only ever comes from the contract. `parameters`
        carries only the Governor/token addresses and the clock mode — the moving
        fractions (`quorum_numerator`, `proposal_threshold_raw`, ...) float with
        STATE supply on no schedule and this route has no live contract read of its
        own, so they are omitted rather than cached stale.

        OpenZeppelin Governor v5 with a **timestamp clock** (`CLOCK_MODE =
        mode=timestamp`) and no timelock, so `vote_start`/`vote_end` are unix seconds,
        not block numbers. Two proposals exist (DGP-001, DGP-002).
      parameters:
        - name: state
          in: query
          description: Filter by Governor state.
          schema: { $ref: "#/components/schemas/ProposalState" }
      responses:
        "200":
          description: Proposals.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProposalPage" }
        "501": { $ref: "#/components/responses/NotReadyGovernance" }

  /v1/governance/delegates:
    get:
      tags: [protocol]
      operationId: listDelegates
      summary: Top delegates by current voting power.
      description: |
        Backed by `prod.robinhood.deepstate_events` (runbook P3/P7-3).
        `DelegateVotesChanged` reduced to the current votes per delegate — a terms
        aggregation on `delegate` with a size-1 `top_hits` sorted by
        (block_number, log_index) descending, i.e. each delegate's own latest event —
        expressed as a share of STATE `totalSupply`, read LIVE from the chain rather
        than cached, because it floats with every DEEP burned into STATE. Concentration
        is the point of the panel: the top delegate held 47.69% on the crawl day.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 20 }
      responses:
        "200":
          description: Delegates.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DelegatePage" }
        "501": { $ref: "#/components/responses/NotReadyEvents" }

  /v1/vault/history:
    get:
      tags: [protocol]
      operationId: getVaultHistory
      summary: STATE vault TVL, share price and cumulative DEEP burned.
      description: |
        Backed by `prod.robinhood.deepstate_vault_snapshots` (runbook P7-1), one
        document per hour since the 2026-08-15 deployment. `deep_balance_raw` is
        read back out of the document's `fee_assets` array by token address — the
        step writes USDG/NVDA/DEEP fee balances as one shared array, not as separate
        flat fields.

        STATE is a real ERC-4626 vault whose asset is DEEP, but deposits are *burned*:
        `total_assets_raw` is virtual accounting equal to cumulative DEEP burned
        (104,044,463.60 on 2026-09-02), and the vault holds only 8,014.13 DEEP.
        Share price is `convertToAssets(1e18)`.
      parameters:
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
      responses:
        "200":
          description: Vault snapshots, oldest first.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VaultHistory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyVault" }

  /v1/stream:
    get:
      tags: [stream]
      operationId: stream
      x-onesource-paid-surface: false
      summary: Server-sent events - head, book, tape and raw events.
      description: |
        `x-onesource-paid-surface: false` — a long-lived stream is not something
        the OneSource API gateway meters per call, so it is not sold on the paid
        host and stays direct-host only.

        A `text/event-stream` connection. One per browser tab (runbook P10-3).

        Wire format is standard SSE: each message is an `event:` line naming the
        event type, a `data:` line carrying one JSON object, and an `id:` line
        carrying a monotonic per-connection sequence number, terminated by a blank
        line. Reconnect with `Last-Event-ID` to resume; the server replies with a
        `book_reset` rather than replaying a gap it cannot prove is contiguous.

        Named events and their `data` schemas (`x-sse-events` carries the same map in
        machine-readable form):

        | `event:` | `data` schema | when |
        |---|---|---|
        | `head` | `HeadEvent` | every new block (~9.85/s) |
        | `book_delta` | `BookDeltaEvent` | every block that changes a subscribed book |
        | `book_reset` | `BookSnapshot` | after a reorg, a resubscribe, or a cold bootstrap (runbook §4.5) |
        | `trade` | `Trade` | every fill |
        | `event` | `RouterEvent` | every decoded Router / Rewarder / Governor log |
        | `heartbeat` | `HeartbeatEvent` | every 15 s, so idle proxies do not close the connection |

        **Reorg semantics.** Rows above `safe` arrive with `finality: "head"` and can
        be replaced. A reorg is delivered as a `book_reset` carrying the rebuilt book,
        not as a retraction of individual rows; the UI treatment is a pending row
        disappearing, the same as any explorer (runbook §4.5). `book_reset` also
        increments `deepstate_live_resets_total`.

        Only `head` and `heartbeat` are emitted today; the rest arrive with the book
        reducer's bootstrap (runbook P9-3) and the trade tape (P5).

        Two frames off the wire, verbatim:

        ```
        event: head
        id: 1
        data: {"block_number":52685677,"block_hash":"0x9f2b1c0d4e7a5b83c61f0d2a4e8b7c95d3a1f6e08b2c4d7a9e5f31b0c8d2a4e6","parent_hash":"0x3c7e91a5d2b04f68e1a9c3d5b7f20e846a1c9d3f5b7e02a4c6d8f1b3e5a7c9d0","timestamp":1788363287,"base_fee_per_gas_raw":"485000000","base_fee_gwei":0.485,"gas_used":1184213,"tx_count":37,"safe":52677134,"finalized":52673694,"safe_lag_blocks":8543,"finalized_lag_blocks":11983,"market_session":"regular","finality":"head"}

        event: heartbeat
        id: 2
        data: {"ts":"2026-09-02T15:35:02Z","sse_clients":41,"head":52685677,"safe":52677134,"finalized":52673694}
        ```
      parameters:
        - name: books
          in: query
          description: |
            Comma-separated book ids or slugs to subscribe to. Default: all
            configured books.
          schema: { type: string, example: "NVDA-USDG,DEEP-USDG" }
        - name: topics
          in: query
          description: |
            Comma-separated topics. `book` covers `book_delta` and `book_reset`.
            Default: all.
          schema:
            type: string
            example: "head,book,trades,events,gas"
      responses:
        "200":
          description: |
            An open event stream. Never closed by the server except on shutdown.
          headers:
            Content-Type:
              schema: { type: string, enum: ["text/event-stream"] }
            Cache-Control: { $ref: "#/components/headers/NoStore" }
            X-Accel-Buffering:
              description: |
                Always `no`. nginx in front of this service buffers by default, which
                would hold events until the buffer fills and destroy the point of the
                stream.
              schema: { type: string, enum: ["no"] }
          content:
            text/event-stream:
              schema:
                description: |
                  A stream of SSE frames. Each frame's `data:` payload is one of the
                  members below, selected by the frame's `event:` name.
                oneOf:
                  - $ref: "#/components/schemas/HeadEvent"
                  - $ref: "#/components/schemas/BookDeltaEvent"
                  - $ref: "#/components/schemas/BookSnapshot"
                  - $ref: "#/components/schemas/Trade"
                  - $ref: "#/components/schemas/RouterEvent"
                  - $ref: "#/components/schemas/HeartbeatEvent"
      x-sse-events:
        head: "#/components/schemas/HeadEvent"
        book_delta: "#/components/schemas/BookDeltaEvent"
        book_reset: "#/components/schemas/BookSnapshot"
        trade: "#/components/schemas/Trade"
        event: "#/components/schemas/RouterEvent"
        heartbeat: "#/components/schemas/HeartbeatEvent"

  /rpc:
    post:
      tags: [rpc]
      operationId: rpcProxy
      x-onesource-paid-surface: false
      summary: Allow-listed, read-only JSON-RPC 2.0 pass-through to robinhood-node.
      description: |
        `x-onesource-paid-surface: false` — a wallet's own RPC transport, not a
        priced data query; not sold on the paid host.

        Runbook D8 / P9-9. `wagmi` points its read transport at
        `fallback([http(this), http(https://rpc.mainnet.chain.robinhood.com)])`;
        **writes never come here** and go over the wallet's own connector.

        Accepts a single JSON-RPC request or a batch array. Every method in a batch
        is checked; one disallowed method fails only its own entry, not the batch.

        Allowed methods:

        `eth_chainId` · `eth_blockNumber` · `eth_call` · `eth_estimateGas` ·
        `eth_gasPrice` · `eth_feeHistory` · `eth_getBalance` ·
        `eth_getTransactionCount` · `eth_getTransactionReceipt` ·
        `eth_getTransactionByHash`

        Anything else — including `eth_sendRawTransaction`, `eth_getLogs`, `debug_*`
        and `trace_*` — returns JSON-RPC error **-32601 Method not found**, with HTTP
        `200`, because a JSON-RPC error is a successful HTTP response.

        Idempotent reads whose block argument is `latest` (or absent) are cached for
        1 s and served from that cache; the response carries `X-Cache: HIT|MISS`.
        Requests are rate-limited per client IP with a token bucket and capped in
        size; exceeding either returns HTTP `429` or `413` with a JSON-RPC error body.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/JsonRpcRequest"
                - type: array
                  minItems: 1
                  maxItems: 50
                  items: { $ref: "#/components/schemas/JsonRpcRequest" }
            example:
              jsonrpc: "2.0"
              id: 1
              method: "eth_blockNumber"
              params: []
      responses:
        "200":
          description: |
            A JSON-RPC response (or an array of them for a batch). A blocked method
            also lands here, as a JSON-RPC `error` object with code -32601.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
            X-Cache:
              description: Whether the 1 s idempotent-read cache served this response.
              schema: { type: string, enum: ["HIT", "MISS", "BYPASS"] }
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/JsonRpcResponse"
                  - type: array
                    items: { $ref: "#/components/schemas/JsonRpcResponse" }
              example:
                jsonrpc: "2.0"
                id: 1
                result: "0x323eb6d"
        "400":
          description: The body is not valid JSON-RPC 2.0.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JsonRpcResponse" }
              example:
                jsonrpc: "2.0"
                id: null
                error: { code: -32700, message: "Parse error" }
        "413":
          description: Request body exceeds the size cap.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JsonRpcResponse" }
              example:
                jsonrpc: "2.0"
                id: null
                error: { code: -32600, message: "Request too large" }
        "429":
          description: Per-IP token bucket exhausted.
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JsonRpcResponse" }
              example:
                jsonrpc: "2.0"
                id: null
                error: { code: -32005, message: "Rate limit exceeded" }
        "502":
          description: The node and the public fallback both failed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JsonRpcResponse" }
              example:
                jsonrpc: "2.0"
                id: null
                error: { code: -32603, message: "Upstream unavailable" }

  /metrics:
    get:
      tags: [health]
      operationId: getMetrics
      x-onesource-paid-surface: false
      summary: Prometheus text exposition.
      description: |
        `x-onesource-paid-surface: false` — operational telemetry, not sold on
        the paid host.

        Hand-rolled text exposition (format 0.0.4), no client library. Carries the
        live-layer series from runbook §4.6 — `deepstate_live_head_lag_blocks`,
        `deepstate_live_resets_total`, `deepstate_live_sse_clients` — which are the
        three the PrometheusRule alerts on. Scraped by a ServiceMonitor, not called by
        the site.
      responses:
        "200":
          description: Metrics.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            text/plain:
              schema: { type: string }
              example: |
                # HELP deepstate_live_head_lag_blocks Node head minus the highest block this process has applied.
                # TYPE deepstate_live_head_lag_blocks gauge
                deepstate_live_head_lag_blocks 0
                # HELP deepstate_live_resets_total Book resets performed after a reorg or a failed continuity check.
                # TYPE deepstate_live_resets_total counter
                deepstate_live_resets_total 0
                # HELP deepstate_live_sse_clients Currently connected SSE clients.
                # TYPE deepstate_live_sse_clients gauge
                deepstate_live_sse_clients 41

  /v1/analytics/quote-churn:
    get:
      tags: [analytics]
      operationId: getQuoteChurn
      summary: Rests, cancels and fill events per hour, cancel:fill ratio and median order lifetime (E2).
      description: |
        Runbook §0.3 E2 / P9-5's five-route gap. `prod.robinhood.deepstate_events`
        Router rows (`OrderRested`, `OrderCancelled`, `AskMatched`, `BidMatched`,
        `AsksMatched`, `BidsMatched`, `AskSubtreeMatched`, `BidSubtreeMatched`) bucketed
        by `book_id` and hour or day; `decode_status: window_watermark` cursor rows are
        excluded (they carry no `event_name` at all).

        `fill_events` counts **events**, not orders filled: `AsksMatched`/`BidsMatched`
        settle a list of resting orders in one event, and `AskSubtreeMatched`/
        `BidSubtreeMatched` settle a whole price subtree without enumerating the orders
        it consumed (spec §3.3) — so an order-level fill count is not recoverable from
        the event stream alone. `cancel_fill_ratio` is `cancels / fill_events`, omitted
        rather than reported as a divide-by-zero when a bucket has no fills.

        `median_order_lifetime_seconds` pairs each `OrderRested` with its terminal event
        (an `OrderCancelled`, or a match event that names the same order) BY THE ORDER'S
        NONCE — the packed order id's 32-bit sequence segment (runbook P6-1;
        `book.ParseOrderID(...).NonceRaw()`), not the whole word, because the word's size
        segment changes as an order fills. `OrderRested`/`OrderCancelled` carry the full
        word as `order_id`; `AskMatched`/`BidMatched` carry it as the resting-node word,
        also `order_id`; `AsksMatched`/`BidsMatched` carry a list, `order_ids`. All are
        decoded the same way (this route's own event read, no extra query).

        This pairing is **bounded to the request's own `[from, to)` window** — it uses
        only the events already fetched for the counts above, not a second read — so two
        classes of order are honestly excluded rather than mispaired:

        * An order already resting when `from` arrives has no `OrderRested` row in this
          window to pair from; its terminal event inside the window contributes nothing.
        * An order still resting at `to`, or one whose terminal event is a subtree match
          (`AskSubtreeMatched`/`BidSubtreeMatched` carry no per-order id at all — spec
          §3.3, one row per price subtree, not per order), never finds a terminal event
          and is excluded the same way.

        A bucket's median is over lifetimes of orders that **rested** in that bucket
        (regardless of which later bucket they terminated in) — the totals figure is one
        median across the whole window, not an aggregate of the per-bucket medians (the
        same reason `l1_share_pct`-style ratios elsewhere in this contract are never
        derived from bucket averages).
      parameters:
        - $ref: "#/components/parameters/AnalyticsBookRequired"
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - $ref: "#/components/parameters/AnalyticsRes"
      responses:
        "200":
          description: Quote-churn series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QuoteChurnSeries" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyEvents" }

  /v1/analytics/book-summary-history:
    get:
      tags: [analytics]
      operationId: getBookSummaryHistory
      summary: Mid, spread and depth-within-±1% over time (E3 history).
      description: |
        Runbook §0.3 E3 / P9-5's five-route gap. `GET /v1/book/{book}` already carries
        the *live* ±1% depth figures on `BookSummary`; this is the series behind them.
        `prod.robinhood.deepstate_book_summary` writes one document per change block
        per book, which is too dense to chart directly, so each bucket reports the
        **last** summary observed at or before the bucket's end — a close, not an
        average, because averaging a depth-in-quote figure across a changing book state
        does not mean anything the way an average price does.
      parameters:
        - $ref: "#/components/parameters/AnalyticsBookRequired"
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - $ref: "#/components/parameters/AnalyticsRes"
      responses:
        "200":
          description: Book summary history.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BookSummaryHistory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyBookSummary" }

  /v1/analytics/depth-history:
    get:
      tags: [analytics]
      operationId: getDepthHistory
      summary: Depth heatmap - resting size by price level over time (E4).
      description: |
        Runbook §0.3 E4 / P9-5's five-route gap. `prod.robinhood.deepstate_book_depth`
        (5-minute-stride history, every change block since 2026-09-03) has one document
        per resting order; each bucket reports the **last snapshot** observed at or
        before the bucket's end, grouped by side and price level, with `size_raw` summed
        across the orders resting at that level.

        **48.8% of depth rows carry no `size_raw`** (P8-3 finding 3; 74.6% of the
        NVDA/USDG ask side, plus 275 DEEP/USDG bids as the mirror case) — the reciprocal-
        key group runbook P6 documented, where the 20-byte segment reads as the
        RECIPROCAL of the order's own key rather than its size, and publishing it would
        overstate depth by many orders of magnitude. Those orders count in `order_count`
        (the resting set is what the site shows) but never in `size_raw` or
        `denominated_order_count`, and `size_raw` is **omitted, never zero**, at a price
        level where every resting order is in that class — `coverage` reports the split
        across the whole response so a caption can say how much of the heatmap is real.
      parameters:
        - $ref: "#/components/parameters/AnalyticsBookRequired"
        - name: side
          in: query
          description: Restrict to one side. Default both.
          schema: { $ref: "#/components/schemas/Side" }
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - $ref: "#/components/parameters/AnalyticsRes"
      responses:
        "200":
          description: Depth heatmap series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DepthHistory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyBookDepth" }

  /v1/analytics/makers:
    get:
      tags: [analytics]
      operationId: getMakerAnalytics
      summary: Per-maker time at top, notional rested, fills and DEEP earned (E6).
      description: |
        Runbook §0.3 E6 / P9-5's five-route gap. Four legs, merged by maker address:

        * **Time at top** — walks `prod.robinhood.deepstate_book_summary` in the window
          and attributes the gap between consecutive summaries to whichever address was
          `top_bid_maker` / `top_ask_maker` over that gap.
        * **Notional rested** — the **current** resting notional as of the latest
          `prod.robinhood.deepstate_book_depth` snapshot at or before `to`, summed by
          `owner_address`. This is a snapshot, not a sum across every historical replay
          in the window — replay rows re-list the same still-resting order at every
          stride, and summing them would count one order's size once per snapshot.
          `size_raw` follows the same non-denominated exclusion as
          `GET /v1/analytics/depth-history` and is omitted, never zero, when a maker's
          resting orders are all in that class.
        * **Fill count / fill rate** — `prod.robinhood.deepstate_trades`' `maker_address`
          field is **never populated** (same finding as `getActiveUsers`: the size
          segment changes as an order fills, so the tape cannot name the maker from the
          document alone). The maker is instead the OWNER of the resting order the fill
          matched: each trade's `order_id` decodes to a NONCE (the order's identity,
          `book/orderid.go`'s `Sequence` segment), and this route looks that nonce up in
          `prod.robinhood.deepstate_book_depth`'s OWN rows for the same window — the same
          rows the notional-rested leg already reads — taking the owner from whichever
          depth snapshot for that nonce is at or before the trade's block. **This is
          bounded to the request's own `[from, to)` window**: if a filled order's nonce
          never appears in a `deepstate_book_depth` snapshot inside that window (it rested
          entirely before `from` and its book_depth row predates the window too), that
          fill cannot be attributed and is silently excluded from every maker's
          `fill_count` — a documented undercount, not a wrong count. Subtree matches
          (`attribution: subtree_exact`) carry no per-order id at all (one row per price
          subtree, not per order — same limitation `getListTrades`' `attribution` enum
          documents) and are excluded from `fill_count` for the same reason.
          `fill_rate` is `fill_count / (fill_count + cancels)`, where `cancels` is this
          maker's own `OrderCancelled` count in the window — the fraction of this maker's
          terminated orders (in this window) that ended in a fill rather than a cancel.
        * **DEEP earned** — `RewardsDistributed.rewardee` amounts from
          `prod.robinhood.deepstate_events`, in the window.

        **A Rewarder is a per-pool hook, not protocol-wide** (runbook P10-4b):
        `poolHook` is the zero address on DEEP/USDG, so that book pays no emissions at
        all. `rewards_available` is `false` there and `deep_earned_raw` is **omitted
        from every row**, never rendered as a zero that would read as "earned nothing
        this window" rather than "this book has no reward hook".
      parameters:
        - $ref: "#/components/parameters/AnalyticsBookRequired"
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 50 }
      responses:
        "200":
          description: Per-maker analytics.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MakerAnalytics" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyEvents" }

  /v1/analytics/cost-to-quote:
    get:
      tags: [analytics]
      operationId: getCostToQuote
      summary: Gas spent resting and cancelling orders (E8).
      description: |
        Runbook §0.3 E8 / P9-5's five-route gap. `prod.robinhood.deepstate_events`
        stamps Router transactions with receipt gas fields (`effective_gas_price`,
        `gas_used`, `gas_used_for_l1`, one `eth_getTransactionReceipt` per tx) — this
        route sums `effective_gas_price × gas_used` over `OrderRested` /
        `OrderCancelled` transactions, bucketed by book and hour or day.

        **Gas is per transaction, not per event**: one multicall can carry several
        Router events, so every sum here dedupes by `transaction_hash` before adding —
        `tx_count` is the deduped count, `rests`/`cancels` are the raw event counts.
        `median_fee_raw` is one median across whichever of the two a bucket's
        transactions were; it is not split by rest vs. cancel; a single transaction can
        carry both actions (a multicall that cancels one order and rests another), so a
        per-type split would have to arbitrarily assign that transaction's one fee to
        one type or double-count it.

        `l1_fee_raw` is the `gas_used_for_l1` **portion of `total_fee_raw`**, not an
        addition to it — Arbitrum Nitro's `gasUsedForL1` is already included in
        `gasUsed`, priced at the same `effective_gas_price`, so `l1_fee_raw =
        effective_gas_price × gas_used_for_l1` and `l1_share_pct = l1_fee_raw /
        total_fee_raw × 100`.
      parameters:
        - $ref: "#/components/parameters/AnalyticsBookRequired"
        - $ref: "#/components/parameters/From"
        - $ref: "#/components/parameters/To"
        - $ref: "#/components/parameters/AnalyticsRes"
      responses:
        "200":
          description: Cost-to-quote series.
          headers:
            Cache-Control: { $ref: "#/components/headers/ShortCache" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CostToQuoteSeries" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "501": { $ref: "#/components/responses/NotReadyEvents" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        A OneSource API key, sent `Authorization: Bearer <key>` to the paid
        public host (`https://api.onesource.io/deepstate`, formerly its own
        host `https://markets.onesource.io/deepstate`, retired 2026-09-06 —
        see the API runbook's API-D17). The API
        gateway in front of that host validates the key and injects
        `X-Auth-Method: api_key`, `X-License-ID` and `X-License-Tier` on the
        request it forwards.

        Per-call payment (x402 / MPP, USDC on Base/Tempo) is also accepted on
        the same paid host as an alternative to a key — the gateway sets
        `X-Auth-Method: x402` or `X-Auth-Method: mpp` accordingly. Either way
        this is entirely a gateway concern: nothing below this scheme is
        checked by `deepstate-live` itself, and the direct host
        (`https://deepstate-live.onesource.io`) serves the same routes with no
        authentication at all.

        This scheme is not in the global `security` list — the direct host is
        unauthenticated by design, so no operation requires it — but it
        documents what the paid host expects.

  headers:
    NoStore:
      description: Always `no-store`; the payload is head-of-chain state.
      schema: { type: string, enum: ["no-store"] }
    ShortCache:
      description: Short shared cache, e.g. `public, max-age=5`.
      schema: { type: string, example: "public, max-age=5" }
    LongCache:
      description: Long shared cache for compiled-in configuration.
      schema: { type: string, example: "public, max-age=300" }

  parameters:
    BookPath:
      name: book
      in: path
      required: true
      description: |
        The 32-byte `book_id`, or its slug (`NVDA-USDG`, `DEEP-USDG`). Slugs are
        stable and are what the site's URLs use.
      schema: { type: string, example: "NVDA-USDG" }
    BookQuery:
      name: book
      in: query
      description: "Book id or slug. Default: every configured book."
      schema: { type: string, example: "NVDA-USDG" }
    From:
      name: from
      in: query
      description: Inclusive lower bound, RFC 3339 or unix seconds.
      schema: { type: string, example: "2026-09-02T00:00:00Z" }
    To:
      name: to
      in: query
      description: "Exclusive upper bound, RFC 3339 or unix seconds. Default: now."
      schema: { type: string, example: "2026-09-02T16:00:00Z" }
    AnalyticsBookRequired:
      name: book
      in: query
      required: true
      description: |
        Book id or slug. **Required** on the five §0.3 routes P9-5 added last (E2, E3,
        E4, E6, E8): unlike `open-orders-vs-gas`'s gas axis, every one of these is
        denominated in a book-specific unit (`token0`'s size, that book's own
        rewardee set) that does not mean anything summed across the two books — see
        `Market.size_token`.
      schema: { type: string, example: "NVDA-USDG" }
    AnalyticsRes:
      name: res
      in: query
      description: Bucket width, matching `GET /v1/stats/revenue`'s convention.
      schema: { type: string, enum: ["1h", "1d"], default: "1d" }

  responses:
    BadRequest:
      description: A query parameter is missing or malformed.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: "bad_request"
            message: "from must be RFC 3339 or unix seconds"
    NotFound:
      description: No such book.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: "not_found"
            message: "unknown book \"FOO-BAR\"; see GET /v1/markets"
    NotReadyBookDepth:
      description: |
        `prod.robinhood.deepstate_book_depth` does not exist yet (runbook P6).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.deepstate_book_depth"
            phase: "P6"
            message: "book replay has not shipped; see odap/DEEPSTATE_BUILD_RUNBOOK.md phase 6"
    NotReadyBookState:
      description: |
        No Router-state read has completed for this book yet — a cold start, or the
        node not answering. Unlike the index-backed stubs this one clears by itself:
        the next successful walk serves a `200`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "book-state-reader"
            phase: "P6"
            message: "no Router state read has completed for this book yet"
    NotReadyBookSummary:
      description: |
        `prod.robinhood.deepstate_book_summary` does not exist yet (runbook P6).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.deepstate_book_summary"
            phase: "P6"
            message: "book replay has not shipped; see odap/DEEPSTATE_BUILD_RUNBOOK.md phase 6"
    NotReadyTrades:
      description: |
        `prod.robinhood.deepstate_trades` does not exist yet (runbook P5).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.deepstate_trades"
            phase: "P5"
            message: "trade tape has not shipped; see odap/DEEPSTATE_BUILD_RUNBOOK.md phase 5"
    NotReadyEvents:
      description: |
        `prod.robinhood.deepstate_events` does not exist yet (runbook P3).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.deepstate_events"
            phase: "P3"
            message: "events stream has not shipped; see odap/DEEPSTATE_BUILD_RUNBOOK.md phase 3"
    NotReadyGovernance:
      description: |
        `prod.robinhood.deepstate_governance` does not exist yet (runbook P7).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.deepstate_governance"
            phase: "P7"
            message: "governance reduction has not shipped; see odap/DEEPSTATE_BUILD_RUNBOOK.md phase 7"
    NotReadyVault:
      description: |
        `prod.robinhood.deepstate_vault_snapshots` does not exist yet (runbook P7).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.deepstate_vault_snapshots"
            phase: "P7"
            message: "vault snapshots have not shipped; see odap/DEEPSTATE_BUILD_RUNBOOK.md phase 7"
    NotReadyRewarderSnapshots:
      description: |
        `prod.robinhood.deepstate_rewarder_snapshots` does not exist yet — a new
        hourly index planned in runbook P7.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.deepstate_rewarder_snapshots"
            phase: "P7"
            message: "rewarder emission snapshots have not shipped; a new hourly index planned in odap/DEEPSTATE_BUILD_RUNBOOK.md phase 7"
    NotReadyDexSwaps:
      description: |
        `prod.robinhood.dex_swaps` does not exist yet — a new dataset planned as
        phase 3b.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "prod.robinhood.dex_swaps"
            phase: "P3b"
            message: "off-Deepstate venue swaps are not indexed yet; a new dataset (Uniswap v3/v4 NVDA/USDG and DEEP/USDG pools, Ekubo and Arcus later) planned as phase 3b"
    NotReadyRewarderCache:
      description: |
        The Rewarder parameter cache has not shipped (runbook P9-4).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotReady" }
          example:
            not_ready: "rewarder_parameter_cache"
            phase: "P9-4"
            message: "rewarder view-call cache has not shipped; see odap/DEEPSTATE_BUILD_RUNBOOK.md phase 9"

  schemas:
    Uint256Raw:
      type: string
      pattern: "^[0-9]+$"
      description: |
        A `uint256` as a base-10 decimal string. Authoritative. Never parse with
        `Number()`; format with `viem`'s `formatUnits` and the field's token decimals.
      example: "9155668652"

    DecimalRaw:
      type: string
      pattern: "^[0-9]+(\\.[0-9]+)?$"
      description: |
        A fixed-point decimal string carrying more precision than a `double` holds.
        Authoritative.
      example: "216.47999901"

    LossyNumber:
      type: number
      description: Display only. Derived from the sibling `_raw` field and **lossy**.
      x-lossy: true

    Address:
      type: string
      pattern: "^0x[0-9a-fA-F]{40}$"
      example: "0x6cf19308C22FC82ea620Fa0B3E94948d20f27B96"

    Hash32:
      type: string
      pattern: "^0x[0-9a-fA-F]{64}$"
      example: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"

    Finality:
      type: string
      enum: [head, safe, finalized]
      description: |
        How settled the row's block is. `head` can still be reorged away; `safe` is
        anchored to L1; `finalized` is irreversible. The lag between them is variable
        — 8,543 and 11,983 blocks on 2026-09-02, 6,067 and 9,921 a day earlier —
        because it tracks L1 batch posting. Design for up to ~20 minutes.

    MarketSession:
      type: string
      enum: [regular, pre, after, overnight, closed_weekend, closed_holiday]
      description: |
        US equity session for the row's timestamp, in America/New_York with DST and
        the NYSE holiday calendar applied (runbook §5). The chain settles 24/7, so
        this is a property of the *reference* market, not of the book.

    Side:
      type: string
      enum: [bid, ask]
      description: |
        Display side: `bid` buys the market's base token, `ask` sells it. Derived, not
        raw — see `is_bid_token0`.

    Token:
      type: object
      required: [address, symbol, decimals]
      additionalProperties: false
      properties:
        address: { $ref: "#/components/schemas/Address" }
        symbol: { type: string, example: "USDG" }
        decimals: { type: integer, minimum: 0, maximum: 36, example: 6 }

    Market:
      type: object
      required:
        [
          book_id,
          book_slug,
          book_label,
          pool_id,
          epoch,
          token0,
          token1,
          base_token,
          quote_token,
          base_is_token0,
          size_token,
          router,
          has_rewarder,
          deployed_block,
          deployed_at,
          status,
        ]
      additionalProperties: false
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug:
          type: string
          description: URL-safe alias for `book_id`.
          example: "NVDA-USDG"
        book_label: { type: string, example: "NVDA/USDG" }
        pool_id: { $ref: "#/components/schemas/Hash32" }
        epoch: { type: integer, format: int64, minimum: 0 }
        token0:
          allOf: [{ $ref: "#/components/schemas/Token" }]
          description: The lower address of the sorted pair, as the protocol registers it.
        token1:
          allOf: [{ $ref: "#/components/schemas/Token" }]
          description: The higher address of the sorted pair.
        base_token:
          allOf: [{ $ref: "#/components/schemas/Token" }]
          description: The instrument. A display assignment made by this service.
        quote_token:
          allOf: [{ $ref: "#/components/schemas/Token" }]
          description: What the instrument is priced in. USDG on both books today.
        base_is_token0:
          type: boolean
          description: |
            `base_token == token0`. This single flag is what turns the raw
            `is_bid_token0` into a display `side`, and it differs between the two
            books — which is exactly where a sign error would hide.
        size_token:
          allOf: [{ $ref: "#/components/schemas/Token" }]
          description: |
            The token an order's `size_raw` is denominated in: **`token0`**, on both
            sides of both books — not the quote token and not the escrowed token.

            On NVDA/USDG token0 *is* USDG, so "token0" and "quote" happen to agree
            and either rule reads as correct. They diverge on DEEP/USDG, where
            token0 is DEEP: a resting order there with `size_raw`
            `11627906814619051965799` is 11,627.91 DEEP against 50.00 USDG, i.e.
            0.0043 USDG per DEEP — the price runbook P5-3 verified. Read the same
            number as USDG (6 decimals) and it becomes 1.16e16 USDG, which is the
            failure this field exists to prevent. **Always scale `size_raw` by
            `size_token.decimals`, never by `quote_token.decimals`.**
        router: { $ref: "#/components/schemas/Address" }
        rewarder:
          allOf: [{ $ref: "#/components/schemas/Address" }]
          description: |
            The market's reward hook, **omitted when the market has none**.
            The Rewarder IS the pool hook and it is per market, not
            protocol-wide: `poolHook(poolId)` read 2026-09-03 returns
            `0xe85adbc0…` for NVDA/USDG and the **zero address for DEEP/USDG**,
            so DEEP/USDG earns no emissions. Branch on `has_rewarder`.
        has_rewarder:
          type: boolean
          description: |
            Whether this market has a reward hook. deepstate.sh's own bundle
            carries the same per-market flag and hides the FARM / SUPER FARM
            presets when it is false — clients should do the same rather than
            offering reward-farming prices on a market that pays nothing.
        deployed_block: { type: integer, format: int64 }
        deployed_at:
          type: string
          format: date-time
          description: |
            `deployed_block`'s timestamp. Send it as `from` to ask any windowed
            route for this market's whole history: there is no data before it,
            and a fixed lookback stops meaning "everything" the day the market
            is older than the lookback.
        status:
          type: string
          enum: [live, price_decode_pending]
          description: |
            `price_decode_pending` means prices for this book are not published yet.
            DEEP/USDG was held there until its price-key scale was confirmed
            (runbook P5-3/P6: the size segment is token0, and the key is stored as
            the reciprocal on a book whose token0 is the base) — verified against 40
            clean fills and topOrder at five heights, agreeing to better than 1e-6.
            Both configured markets report `live` today.

    Health:
      type: object
      required: [status, uptime_seconds, chain, indexes]
      additionalProperties: false
      properties:
        status: { type: string, enum: [ok, degraded] }
        version: { type: string, example: "dev" }
        uptime_seconds: { type: number, example: 4213.7 }
        mock:
          type: boolean
          description: True when the process was started with `--mock`.
        chain:
          type: object
          required:
            [chain_id, head, safe, finalized, safe_lag_blocks, finalized_lag_blocks]
          additionalProperties: false
          properties:
            chain_id: { type: integer, example: 4663 }
            head: { type: integer, format: int64 }
            head_timestamp: { type: integer, format: int64 }
            safe: { type: integer, format: int64 }
            finalized: { type: integer, format: int64 }
            safe_lag_blocks: { type: integer, format: int64 }
            finalized_lag_blocks: { type: integer, format: int64 }
            base_fee_per_gas_raw: { $ref: "#/components/schemas/Uint256Raw" }
            base_fee_gwei: { $ref: "#/components/schemas/LossyNumber" }
            market_session: { $ref: "#/components/schemas/MarketSession" }
        indexes:
          type: array
          items:
            type: object
            required: [index, not_ready]
            additionalProperties: false
            properties:
              index: { type: string, example: "prod.robinhood.blocks" }
              not_ready:
                type: boolean
                description: The index does not exist yet, or is unreadable.
              max_block_number: { type: integer, format: int64 }
              head_lag_blocks:
                type: integer
                format: int64
                description: |
                  Node head minus this index's `max(block_number)`. The runbook alerts
                  above 600 for 5 minutes (~1 minute of chain at 9.85 blocks/s).
              phase:
                type: string
                description: Runbook phase that lands this index.
                example: "P3"
      example:
        status: "ok"
        version: "dev"
        uptime_seconds: 4213.7
        mock: false
        chain:
          chain_id: 4663
          head: 52685677
          head_timestamp: 1788363287
          safe: 52677134
          finalized: 52673694
          safe_lag_blocks: 8543
          finalized_lag_blocks: 11983
          base_fee_per_gas_raw: "485000000"
          base_fee_gwei: 0.485
          market_session: "regular"
        indexes:
          - index: "prod.robinhood.blocks"
            not_ready: false
            max_block_number: 52685612
            head_lag_blocks: 65
            phase: "P2"
          - index: "prod.robinhood.robinhood_gas_1m"
            not_ready: false
            phase: "P2"
          - index: "prod.robinhood.deepstate_events"
            not_ready: true
            phase: "P3"
          - index: "prod.robinhood.deepstate_trades"
            not_ready: true
            phase: "P5"
          - index: "prod.robinhood.deepstate_book_summary"
            not_ready: true
            phase: "P6"
          - index: "prod.robinhood.deepstate_book_depth"
            not_ready: true
            phase: "P6"
          - index: "prod.robinhood.deepstate_vault_snapshots"
            not_ready: true
            phase: "P7"
          - index: "prod.robinhood.deepstate_governance"
            not_ready: true
            phase: "P7"

    GasPoint:
      type: object
      required:
        [minute, base_fee_avg_raw, base_fee_min_raw, base_fee_max_raw, blocks, market_session, source]
      additionalProperties: false
      properties:
        minute: { type: string, format: date-time }
        block_number:
          type: integer
          format: int64
          description: Highest block in the minute. Present on tail points.
        finality: { $ref: "#/components/schemas/Finality" }
        base_fee_min_raw: { $ref: "#/components/schemas/Uint256Raw" }
        base_fee_avg_raw: { $ref: "#/components/schemas/Uint256Raw" }
        base_fee_max_raw: { $ref: "#/components/schemas/Uint256Raw" }
        base_fee_p50_raw: { $ref: "#/components/schemas/Uint256Raw" }
        base_fee_p95_raw: { $ref: "#/components/schemas/Uint256Raw" }
        base_fee_avg_gwei: { $ref: "#/components/schemas/LossyNumber" }
        blocks: { type: integer }
        tx_count: { type: integer }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        source:
          type: string
          enum: [rollup, tail]
          description: |
            `rollup` came from `robinhood_gas_1m` (blocks at or below `safe`, never
            rolled back). `tail` was computed live from `prod.robinhood.blocks` for
            minutes the rollup has not reached; those points can still move.

    GasSeries:
      type: object
      required: [resolution, from, to, points]
      additionalProperties: false
      properties:
        resolution: { type: string, enum: ["1m"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        points:
          type: array
          items: { $ref: "#/components/schemas/GasPoint" }
      example:
        resolution: "1m"
        from: "2026-09-02T15:30:00Z"
        to: "2026-09-02T15:35:00Z"
        points:
          - minute: "2026-09-02T15:30:00Z"
            finality: "safe"
            base_fee_min_raw: "458000000"
            base_fee_avg_raw: "491300000"
            base_fee_max_raw: "617000000"
            base_fee_p50_raw: "486000000"
            base_fee_p95_raw: "589000000"
            base_fee_avg_gwei: 0.4913
            blocks: 591
            tx_count: 21877
            market_session: "regular"
            source: "rollup"
          - minute: "2026-09-02T15:34:00Z"
            block_number: 52685677
            finality: "head"
            base_fee_min_raw: "471000000"
            base_fee_avg_raw: "485000000"
            base_fee_max_raw: "512000000"
            base_fee_avg_gwei: 0.485
            blocks: 588
            tx_count: 20941
            market_session: "regular"
            source: "tail"

    RestingOrder:
      type: object
      # `size_raw` is deliberately NOT required. P6 found 176 asks on NVDA/USDG
      # resting under the key convention the *other* book uses; their size segment
      # is not readable the way the rest of the tree's is, and reading it anyway
      # would publish ~8.8 trillion USDG of phantom ask depth. Those orders get a
      # row and a price and no size. Counts still include them, because the resting
      # set is what the site shows. Clients must expect a depth row without a size.
      required:
        [order_id, price_key_raw, price_raw, sequence_raw, side, is_bid_token0, queue_position]
      additionalProperties: false
      properties:
        order_id:
          allOf: [{ $ref: "#/components/schemas/Hash32" }]
          description: |
            The 32-byte radix-tree sort key, not an opaque hash: 4-byte price key,
            20-byte size, 8-byte sequence (spec §3.2). Unique only **within its
            book** — the sequence counter is per-book.
          example: "0x2ac581bd0000000000000000000000000000000221b86aac00000000fffbb08c"
        price_key_raw:
          type: string
          pattern: "^0x[0-9a-fA-F]{8}$"
          description: |
            The leading 4 bytes: a fixed-point base-2 logarithm of the pre-fee price
            spanning 192 binary octaves, `price = 2^(-192·key/2^32)` in raw units
            (spec §3.4). **Higher key means lower price.**
          example: "0x2ac581bd"
        price_raw:
          allOf: [{ $ref: "#/components/schemas/DecimalRaw" }]
          description: Decoded display price, quote per base, pre-fee.
        price: { $ref: "#/components/schemas/LossyNumber" }
        price_intent:
          allOf: [{ $ref: "#/components/schemas/DecimalRaw" }]
          description: |
            The round number the maker most likely typed, recovered by inverting the
            key (`key = ceil(-log2(price)·2^32/192)`; the contract rounds the price
            down). Reported alongside `price_raw`, never substituted for it.
          example: "220.48"
        size_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            The 20-byte segment: order size denominated in the book's **`token0`**
            (`Market.size_token`), not the quote token and not the escrowed amount.
            Scale it by `size_token.decimals`; on DEEP/USDG that is DEEP at 18
            decimals, not USDG at 6.

            Absent on the orders described in the schema-level note above.
        size: { $ref: "#/components/schemas/LossyNumber" }
        sequence_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            The 8-byte segment. It carries the book's `uint32` nonce, which runs
            **backwards** from `0xffffffff` — one decrement per rest — so a larger
            value is an *earlier* order. That inversion is what makes time priority
            fall out of the tree's key ordering.
          example: "4294684812"
        nonce_raw:
          type: integer
          format: int64
          description: |
            The nonce exactly as the Router's `nextNonce()` returns it. Decrements
            with time. Present for auditing against a contract read; do not render
            it, it reads as nonsense on a screen.
          example: 4294684812
        rest_index:
          type: integer
          format: int64
          description: |
            How many orders had rested in this book before this one — the nonce's
            complement, increasing with time. This is the one to render.
          example: 282483
        side: { $ref: "#/components/schemas/Side" }
        is_bid_token0:
          type: boolean
          description: |
            The raw `OrderRested` ABI flag: "this order buys token0 with token1".
            Token0-relative, so its display meaning flips between books (spec §2.4.1).
            Exposed so a client can audit the derivation, not so it can redo it.
        maker: { $ref: "#/components/schemas/Address" }
        rested_at_block: { type: integer, format: int64 }
        queue_position:
          type: integer
          minimum: 0
          description: |
            The order's rank in its side's priority order: the index of the tree's
            reverse in-order walk, so `0` is the order `topOrder()` names and `n` is
            the (n+1)th the engine would fill.

            **Not a rank within a price level.** It used to be described that way,
            and runbook P6 falsified the premise underneath it: the tree already
            interleaves price and time priority, and re-deriving a level from the
            decoded key reintroduces the price sort state.go had to stop trusting —
            on NVDA/USDG's ask side that sort puts 176 orders whose keys decode to
            ~7e-37 at the top of book, and `topOrder()` names none of them.
        denominated:
          type: boolean
          description: |
            False for an order whose size segment is not denominated the way the
            rest of the tree's is — the group described in the note above. `size_raw`
            and `size` are omitted on those rows, so this is the field that says
            whether a missing size means "unknown" rather than "zero".

    BookSnapshot:
      type: object
      required:
        [book_id, book_label, base_token, quote_token, block_number, finality, bids, asks, summary]
      additionalProperties: false
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        base_token: { $ref: "#/components/schemas/Token" }
        quote_token: { $ref: "#/components/schemas/Token" }
        block_number: { type: integer, format: int64 }
        block_hash: { $ref: "#/components/schemas/Hash32" }
        finality: { $ref: "#/components/schemas/Finality" }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        reset_reason:
          type: string
          enum: [bootstrap, reorg, resubscribe]
          description: Present only when delivered as an SSE `book_reset`.
        bids:
          type: array
          description: Descending by price. Best bid first.
          items: { $ref: "#/components/schemas/RestingOrder" }
        asks:
          type: array
          description: Ascending by price. Best ask first.
          items: { $ref: "#/components/schemas/RestingOrder" }
        summary: { $ref: "#/components/schemas/BookSummary" }
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        base_token:
          address: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
          symbol: "NVDA"
          decimals: 18
        quote_token:
          address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
          symbol: "USDG"
          decimals: 6
        block_number: 52685677
        block_hash: "0x9f2b1c0d4e7a5b83c61f0d2a4e8b7c95d3a1f6e08b2c4d7a9e5f31b0c8d2a4e6"
        finality: "head"
        market_session: "regular"
        bids:
          - order_id: "0x2ace848e00000000000000000000000000000000c2854f8800000000fffbb024"
            price_key_raw: "0x2ace848e"
            price_raw: "216.48219249"
            price: 216.48219249
            size_raw: "3263516552"
            size: 3263.516552
            sequence_raw: "4294684708"
            nonce_raw: 4294684708
            rest_index: 282587
            side: "bid"
            is_bid_token0: false
            maker: "0x884a1B2c3D4e5F60718293a4B5c6D7e8F90A1b2C"
            rested_at_block: 52685102
            queue_position: 0
          - order_id: "0x2ace85d500000000000000000000000000000000db5ac90000000000fffbb01b"
            price_key_raw: "0x2ace85d5"
            price_raw: "216.47999901"
            price: 216.47999901
            price_intent: "216.48"
            size_raw: "3680160000"
            size: 3680.16
            sequence_raw: "4294684699"
            nonce_raw: 4294684699
            rest_index: 282596
            side: "bid"
            is_bid_token0: false
            maker: "0x884a1B2c3D4e5F60718293a4B5c6D7e8F90A1b2C"
            rested_at_block: 52685140
            queue_position: 0
        asks:
          - order_id: "0x2ac5822b00000000000000000000000000000007abaf8d6000000000fffbb08b"
            price_key_raw: "0x2ac5822b"
            price_raw: "220.47924566"
            price: 220.47924566
            size_raw: "32945180000"
            size: 32945.18
            sequence_raw: "4294684811"
            nonce_raw: 4294684811
            rest_index: 282484
            side: "ask"
            is_bid_token0: true
            maker: "0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
            rested_at_block: 52685011
            queue_position: 0
          - order_id: "0x2ac581bd0000000000000000000000000000000221b86aac00000000fffbb08c"
            price_key_raw: "0x2ac581bd"
            price_raw: "220.47999716"
            price: 220.47999716
            price_intent: "220.48"
            size_raw: "9155668652"
            size: 9155.668652
            sequence_raw: "4294684812"
            nonce_raw: 4294684812
            rest_index: 282483
            side: "ask"
            is_bid_token0: true
            maker: "0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
            rested_at_block: 52685009
            queue_position: 0
        summary:
          book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
          book_label: "NVDA/USDG"
          block_number: 52685677
          finality: "head"
          market_session: "regular"
          best_bid_raw: "216.48219249"
          best_ask_raw: "220.47924566"
          mid_raw: "218.48071908"
          spread_raw: "3.99705317"
          spread_bps: 182.9476
          bid_count: 45
          ask_count: 208
          order_count: 253
          bid_depth_quote_raw: "1284770412"
          ask_depth_quote_raw: "6910233187"
          bid_depth_1pct_quote_raw: "1284770412"
          ask_depth_1pct_quote_raw: "6910233187"
          top_bid_order_id: "0x2ace848e00000000000000000000000000000000c2854f8800000000fffbb024"
          top_ask_order_id: "0x2ac5822b00000000000000000000000000000007abaf8d6000000000fffbb08b"
          top_bid_maker: "0x884a1B2c3D4e5F60718293a4B5c6D7e8F90A1b2C"
          top_ask_maker: "0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
          last_trade_price_raw: "216.48"
          base_fee_per_gas_raw: "485000000"

    BookSummary:
      type: object
      required:
        [book_id, book_label, block_number, finality, bid_count, ask_count, order_count]
      additionalProperties: false
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        block_number: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        best_bid_raw: { $ref: "#/components/schemas/DecimalRaw" }
        best_ask_raw: { $ref: "#/components/schemas/DecimalRaw" }
        mid_raw: { $ref: "#/components/schemas/DecimalRaw" }
        spread_raw: { $ref: "#/components/schemas/DecimalRaw" }
        spread_bps: { $ref: "#/components/schemas/LossyNumber" }
        bid_count: { type: integer, minimum: 0 }
        ask_count: { type: integer, minimum: 0 }
        order_count: { type: integer, minimum: 0 }
        bid_depth_quote_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: Sum of `size_raw` across resting bids, in quote token units.
        ask_depth_quote_raw: { $ref: "#/components/schemas/Uint256Raw" }
        bid_depth_1pct_quote_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: Bid depth within 1% of the best bid.
        ask_depth_1pct_quote_raw: { $ref: "#/components/schemas/Uint256Raw" }
        top_bid_order_id: { $ref: "#/components/schemas/Hash32" }
        top_ask_order_id: { $ref: "#/components/schemas/Hash32" }
        top_bid_maker: { $ref: "#/components/schemas/Address" }
        top_ask_maker: { $ref: "#/components/schemas/Address" }
        last_trade_price_raw: { $ref: "#/components/schemas/DecimalRaw" }
        base_fee_per_gas_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            The block header's base fee, carried here so the open-orders-vs-gas panel
            (E1) is one query rather than a correlation.

    BookDeltaEvent:
      type: object
      required: [book_id, block_number, finality, added, removed, summary]
      additionalProperties: false
      description: |
        **`added` and `removed` MAY intersect; when an id appears in both, `added`
        wins** (runbook P10-3). There is no in-place "this row's size changed"
        field — a resting order whose size segment changed (a partial fill) is
        expressed as removing its old id and adding a row under its new one, since
        an order's id is its price key + size + sequence (spec §3.2) and the size
        segment is part of that id. A client that filters `removed` and then
        concatenates `added` (as `book-reducer.ts` does, and as
        `internal/book.Book.DiffFrom`'s Go builder does on the server) implements
        this correctly without special-casing it: the surviving row is whichever
        one `added` names. Emitting the two lists disjoint, as
        `internal/mock/bookfeed.go`'s synthetic feed does by construction, is
        always valid under this rule — disjoint is the case where it makes no
        difference which list "wins".
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        block_number: { type: integer, format: int64 }
        block_hash: { $ref: "#/components/schemas/Hash32" }
        finality: { $ref: "#/components/schemas/Finality" }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        added:
          type: array
          items: { $ref: "#/components/schemas/RestingOrder" }
        removed:
          type: array
          description: |
            Order ids that left the book in this block. May include an id that
            also appears in `added` — see this schema's top-level description for
            what that means and which one wins.
          items: { $ref: "#/components/schemas/Hash32" }
        removed_by_subtree:
          type: array
          description: |
            Subtree roots settled in this block. A subtree match removes a whole price
            region in one event and does **not** enumerate the orders it consumed, so
            `removed` for those is the reducer's own inference — see the note on
            `RouterEvent.event_name`.
          items: { $ref: "#/components/schemas/Hash32" }
        summary: { $ref: "#/components/schemas/BookSummary" }

    HeadEvent:
      type: object
      required: [block_number, block_hash, timestamp, safe, finalized, finality]
      additionalProperties: false
      properties:
        block_number: { type: integer, format: int64 }
        block_hash: { $ref: "#/components/schemas/Hash32" }
        parent_hash: { $ref: "#/components/schemas/Hash32" }
        timestamp: { type: integer, format: int64 }
        base_fee_per_gas_raw: { $ref: "#/components/schemas/Uint256Raw" }
        base_fee_gwei: { $ref: "#/components/schemas/LossyNumber" }
        gas_used: { type: integer, format: int64 }
        tx_count: { type: integer }
        safe: { type: integer, format: int64 }
        finalized: { type: integer, format: int64 }
        safe_lag_blocks: { type: integer, format: int64 }
        finalized_lag_blocks: { type: integer, format: int64 }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        finality:
          allOf: [{ $ref: "#/components/schemas/Finality" }]
          description: Always `head` for this event; present for uniformity.
      example:
        block_number: 52685677
        block_hash: "0x9f2b1c0d4e7a5b83c61f0d2a4e8b7c95d3a1f6e08b2c4d7a9e5f31b0c8d2a4e6"
        parent_hash: "0x3c7e91a5d2b04f68e1a9c3d5b7f20e846a1c9d3f5b7e02a4c6d8f1b3e5a7c9d0"
        timestamp: 1788363287
        base_fee_per_gas_raw: "485000000"
        base_fee_gwei: 0.485
        gas_used: 1184213
        tx_count: 37
        safe: 52677134
        finalized: 52673694
        safe_lag_blocks: 8543
        finalized_lag_blocks: 11983
        market_session: "regular"
        finality: "head"

    HeartbeatEvent:
      type: object
      required: [ts]
      additionalProperties: false
      properties:
        ts: { type: string, format: date-time }
        sse_clients: { type: integer }
        head: { type: integer, format: int64 }
        safe: { type: integer, format: int64 }
        finalized: { type: integer, format: int64 }
      example:
        ts: "2026-09-02T15:35:02Z"
        sse_clients: 41
        head: 52685677
        safe: 52677134
        finalized: 52673694

    RouterEvent:
      type: object
      required:
        [block_number, transaction_hash, log_index, contract_label, event_name, finality]
      additionalProperties: false
      properties:
        block_number: { type: integer, format: int64 }
        block_hash: { $ref: "#/components/schemas/Hash32" }
        transaction_hash: { $ref: "#/components/schemas/Hash32" }
        log_index: { type: integer }
        timestamp: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        contract_label:
          type: string
          enum: [router, rewarder, governor, state_vault, deep, usdg, nvda, ccl, sgov]
        event_name:
          type: string
          description: |
            ABI event name. The Router's `Ask*`/`Bid*` names are **token0-relative**,
            like `isBid` — `AskSubtreeMatched` names the token0-ask side, which is the
            display bid side on NVDA/USDG and the display ask side on DEEP/USDG. Use
            the row's `side` field, never the event name, to place it on a chart.
          example: "OrderRested"
        event_topic0: { $ref: "#/components/schemas/Hash32" }
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        base_token: { $ref: "#/components/schemas/Address" }
        quote_token: { $ref: "#/components/schemas/Address" }
        order_id: { $ref: "#/components/schemas/Hash32" }
        order_ids:
          type: array
          items: { $ref: "#/components/schemas/Hash32" }
        subtree_root: { $ref: "#/components/schemas/Hash32" }
        price_raw: { $ref: "#/components/schemas/DecimalRaw" }
        size_raw: { $ref: "#/components/schemas/Uint256Raw" }
        quantity_raw: { $ref: "#/components/schemas/Uint256Raw" }
        quote_amount_raw: { $ref: "#/components/schemas/Uint256Raw" }
        amount_raw: { $ref: "#/components/schemas/Uint256Raw" }
        side: { $ref: "#/components/schemas/Side" }
        is_bid_token0: { type: boolean }
        maker: { $ref: "#/components/schemas/Address" }
        rewardee: { $ref: "#/components/schemas/Address" }
        side_token: { $ref: "#/components/schemas/Address" }
      example:
        block_number: 52685009
        block_hash: "0x1b7d3f5a9c2e4068b1d3f5a7c9e02b4d6f8a1c3e5b7d9f02a4c6e8b0d2f4a6c8"
        transaction_hash: "0x5e8a1c3f7b9d02e4a6c8f0b2d4e6a8c0f2b4d6e8a0c2f4b6d8e0a2c4f6b8d0e2"
        log_index: 4
        timestamp: 1788362219
        finality: "head"
        market_session: "regular"
        contract_label: "router"
        event_name: "OrderRested"
        event_topic0: "0x51377dce0000000000000000000000000000000000000000000000000000e799"
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_label: "NVDA/USDG"
        base_token: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
        quote_token: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
        order_id: "0x2ac581bd0000000000000000000000000000000221b86aac00000000fffbb08c"
        price_raw: "220.47999716"
        size_raw: "9155668652"
        side: "ask"
        is_bid_token0: true
        maker: "0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"

    Trade:
      type: object
      # `price_raw`, `base_amount_raw` and `quote_amount_raw` are deliberately NOT
      # required. A fill whose decoded price fails the sanity band is written with
      # `attribution: unpriced` and none of the three — a wrong price is worse than
      # a missing one, so the tape publishes the fill without inventing figures for
      # it. Same shape as `RestingOrder.size_raw`. Clients must handle their
      # absence; see the note on `attribution`.
      required:
        [
          block_number,
          transaction_hash,
          book_id,
          side,
          attribution,
          finality,
        ]
      additionalProperties: false
      properties:
        block_number: { type: integer, format: int64 }
        block_hash: { $ref: "#/components/schemas/Hash32" }
        transaction_hash: { $ref: "#/components/schemas/Hash32" }
        timestamp: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        base_token: { $ref: "#/components/schemas/Address" }
        quote_token: { $ref: "#/components/schemas/Address" }
        order_id: { $ref: "#/components/schemas/Hash32" }
        side:
          allOf: [{ $ref: "#/components/schemas/Side" }]
          description: |
            The side of the **resting** order that was consumed, in display terms. A
            trade against a resting ask is a taker buy.
        is_bid_token0: { type: boolean }
        match_event:
          type: string
          enum:
            [
              BidMatched,
              AskMatched,
              AsksMatched,
              BidsMatched,
              AskSubtreeMatched,
              BidSubtreeMatched,
            ]
        attribution:
          type: string
          enum: [exact, exact_unreconciled, subtree_exact, unpriced]
          description: |
            How this fill was priced, and how much the transaction's transfer legs
            corroborate it.

            Every match event carries its own price AND its own filled size in the
            node word it names, so a fill is priced from its own matched node and
            the transfers are only a cross-check (runbook P5-1). That is why there
            is no VWAP class: a per-transaction average would blur several
            individually-exact fills into one number that is true of none of them.

            * `exact` — priced from the node's key and size, and the Router legs
              reconcile against the filled sizes, so the fee split and
              `price_net_of_fee_raw` are *measured*.
            * `exact_unreconciled` — the same exact price and size from the node,
              but the transaction's legs could not be attributed to this fill alone
              (it also rests an order, or routes through other venues). No fee
              measurement.
            * `subtree_exact` — a subtree match, priced from the amounts the event
              itself carries. One row per **subtree**, not per order.
            * `unpriced` — the decoded price failed the sanity band. `price_raw`,
              `base_amount_raw` and `quote_amount_raw` are **absent**; the fill is
              still published because it happened.

            The tape's internal `window_watermark` marker (a no-fill window keeping
            the index cursor moving) is filtered before it reaches this API and is
            deliberately not part of this enum.
        price_raw:
          allOf: [{ $ref: "#/components/schemas/DecimalRaw" }]
          description: Pre-fee price, quote per base.
        price: { $ref: "#/components/schemas/LossyNumber" }
        price_net_of_fee_raw: { $ref: "#/components/schemas/DecimalRaw" }
        base_amount_raw: { $ref: "#/components/schemas/Uint256Raw" }
        quote_amount_raw: { $ref: "#/components/schemas/Uint256Raw" }
        fee_amount_raw: { $ref: "#/components/schemas/Uint256Raw" }
        fee_recipient: { $ref: "#/components/schemas/Address" }
        integrator_address: { $ref: "#/components/schemas/Address" }
        integrator_fee_bps: { type: integer }
        maker_address: { $ref: "#/components/schemas/Address" }
        taker_address: { $ref: "#/components/schemas/Address" }
        effective_gas_price_raw: { $ref: "#/components/schemas/Uint256Raw" }
        gas_used: { type: integer, format: int64 }
        gas_used_for_l1:
          type: integer
          format: int64
          description: |
            The L1 data-posting component of the fee (Arbitrum Nitro receipt extra).
            Part of what a market maker actually pays to run the quote treadmill (E8).

    TradePage:
      type: object
      required: [book_id, book_label, base_token, quote_token, trades]
      additionalProperties: false
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        base_token: { $ref: "#/components/schemas/Token" }
        quote_token: { $ref: "#/components/schemas/Token" }
        trades:
          type: array
          items: { $ref: "#/components/schemas/Trade" }
        next_before:
          type: integer
          nullable: true
          format: int64
          description: Pass as `before` to fetch the next page. `null` at the end.
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        base_token:
          address: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
          symbol: "NVDA"
          decimals: 18
        quote_token:
          address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
          symbol: "USDG"
          decimals: 6
        trades:
          - block_number: 52685331
            block_hash: "0x2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f"
            transaction_hash: "0xa1c3e5b7d9f02a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c"
            timestamp: 1788362840
            finality: "head"
            market_session: "regular"
            book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
            book_label: "NVDA/USDG"
            base_token: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
            quote_token: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
            order_id: "0x2ace85d500000000000000000000000000000000db5ac90000000000fffbb01b"
            side: "bid"
            is_bid_token0: false
            match_event: "BidMatched"
            attribution: "exact"
            price_raw: "216.47999901"
            price: 216.47999901
            price_net_of_fee_raw: "216.26352901"
            base_amount_raw: "7381992258086111446"
            quote_amount_raw: "1598069869"
            fee_amount_raw: "1598069"
            fee_recipient: "0xbfb7b3Ff3D498a559b946B836d26F0E168f273D5"
            maker_address: "0x884a1B2c3D4e5F60718293a4B5c6D7e8F90A1b2C"
            taker_address: "0x2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d"
            effective_gas_price_raw: "485000000"
            gas_used: 412903
            gas_used_for_l1: 287441
        next_before: 52685331

    Candle:
      type: object
      required: [t, open_raw, high_raw, low_raw, close_raw, volume_quote_raw, trades]
      additionalProperties: false
      properties:
        t: { type: string, format: date-time }
        open_raw: { $ref: "#/components/schemas/DecimalRaw" }
        high_raw: { $ref: "#/components/schemas/DecimalRaw" }
        low_raw: { $ref: "#/components/schemas/DecimalRaw" }
        close_raw: { $ref: "#/components/schemas/DecimalRaw" }
        volume_base_raw: { $ref: "#/components/schemas/Uint256Raw" }
        volume_quote_raw: { $ref: "#/components/schemas/Uint256Raw" }
        trades: { type: integer }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        open_bucket:
          type: boolean
          description: True for the still-forming bucket; it will change.

    CandleSeries:
      type: object
      required: [book_id, book_label, tf, candles]
      additionalProperties: false
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        base_token: { $ref: "#/components/schemas/Token" }
        quote_token: { $ref: "#/components/schemas/Token" }
        tf: { type: string, enum: ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"] }
        candles:
          type: array
          items: { $ref: "#/components/schemas/Candle" }
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        base_token:
          address: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
          symbol: "NVDA"
          decimals: 18
        quote_token:
          address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
          symbol: "USDG"
          decimals: 6
        tf: "1h"
        candles:
          - t: "2026-09-02T14:00:00Z"
            open_raw: "216.43128523"
            high_raw: "220.47999716"
            low_raw: "215.70112004"
            close_raw: "216.47999901"
            volume_base_raw: "1904773188220000000000"
            volume_quote_raw: "412330119842"
            trades: 14
            market_session: "regular"
            open_bucket: false
          - t: "2026-09-02T15:00:00Z"
            open_raw: "216.47999901"
            high_raw: "216.48219249"
            low_raw: "216.43128523"
            close_raw: "216.48219249"
            volume_base_raw: "402118004120000000000"
            volume_quote_raw: "87050224011"
            trades: 6
            market_session: "regular"
            open_bucket: true

    MarketStats:
      type: object
      required: [book_id, book_label, last_price_raw, windows]
      additionalProperties: false
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        base_token: { $ref: "#/components/schemas/Token" }
        quote_token: { $ref: "#/components/schemas/Token" }
        block_number: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        market_session: { $ref: "#/components/schemas/MarketSession" }
        last_price_raw: { $ref: "#/components/schemas/DecimalRaw" }
        last_price: { $ref: "#/components/schemas/LossyNumber" }
        windows:
          type: object
          required: ["24h", "7d", "30d"]
          additionalProperties: false
          properties:
            "24h": { $ref: "#/components/schemas/StatsWindow" }
            "7d": { $ref: "#/components/schemas/StatsWindow" }
            "30d": { $ref: "#/components/schemas/StatsWindow" }
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        base_token:
          address: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
          symbol: "NVDA"
          decimals: 18
        quote_token:
          address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
          symbol: "USDG"
          decimals: 6
        block_number: 52685677
        finality: "head"
        market_session: "regular"
        last_price_raw: "216.48219249"
        last_price: 216.48219249
        windows:
          "24h":
            volume_quote_raw: "500000000000"
            volume_quote: 500000
            trades: 24
            change_pct: -1.82
            high_raw: "220.47999716"
            low_raw: "215.70112004"
            vwap_raw: "217.90441002"
          "7d":
            volume_quote_raw: "19800000000000"
            volume_quote: 19800000
            trades: 171
            change_pct: 2.41
            high_raw: "229.11004221"
            low_raw: "210.44032118"
            vwap_raw: "219.03881204"
          "30d":
            volume_quote_raw: "141900000000000"
            volume_quote: 141900000
            trades: 402
            change_pct: 8.77
            high_raw: "241.72004112"
            low_raw: "198.03118022"
            vwap_raw: "221.44120038"

    StatsWindow:
      type: object
      required: [volume_quote_raw, trades]
      additionalProperties: false
      properties:
        volume_quote_raw: { $ref: "#/components/schemas/Uint256Raw" }
        volume_quote: { $ref: "#/components/schemas/LossyNumber" }
        trades: { type: integer }
        change_pct: { $ref: "#/components/schemas/LossyNumber" }
        high_raw: { $ref: "#/components/schemas/DecimalRaw" }
        low_raw: { $ref: "#/components/schemas/DecimalRaw" }
        vwap_raw: { $ref: "#/components/schemas/DecimalRaw" }

    OpenOrdersVsGas:
      type: object
      required: [book_id, book_label, from, to, points]
      additionalProperties: false
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        points:
          type: array
          items:
            type: object
            required: [t, block_number, order_count, base_fee_per_gas_raw, market_session]
            additionalProperties: false
            properties:
              t: { type: string, format: date-time }
              block_number: { type: integer, format: int64 }
              finality: { $ref: "#/components/schemas/Finality" }
              order_count: { type: integer }
              bid_count: { type: integer }
              ask_count: { type: integer }
              spread_bps: { $ref: "#/components/schemas/LossyNumber" }
              base_fee_per_gas_raw: { $ref: "#/components/schemas/Uint256Raw" }
              base_fee_gwei: { $ref: "#/components/schemas/LossyNumber" }
              market_session: { $ref: "#/components/schemas/MarketSession" }
        truncated:
          type: boolean
          description: |
            True when more change blocks exist in `[from, to)` than this response
            fetched. Points are oldest-first, so the ones missing are the most
            recent. Omitted (== false) otherwise.
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        from: "2026-09-02T14:00:00Z"
        to: "2026-09-02T16:00:00Z"
        points:
          - t: "2026-09-02T14:00:00Z"
            block_number: 52626100
            finality: "finalized"
            order_count: 241
            bid_count: 41
            ask_count: 200
            spread_bps: 184.1
            base_fee_per_gas_raw: "458000000"
            base_fee_gwei: 0.458
            market_session: "regular"
          - t: "2026-09-02T15:00:00Z"
            block_number: 52661600
            finality: "safe"
            order_count: 208
            bid_count: 33
            ask_count: 175
            spread_bps: 201.7
            base_fee_per_gas_raw: "1755000000"
            base_fee_gwei: 1.755
            market_session: "regular"

    RewardsLeaderboard:
      type: object
      required: [epoch, rows, total_distributed_raw]
      additionalProperties: false
      properties:
        epoch: { type: integer, format: int64 }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        reward_token: { $ref: "#/components/schemas/Token" }
        as_of_block: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        total_distributed_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            Distributed across **every** rewardee in the window, not just the rows
            returned — so `share_of_total` still means something under a `limit`.
        rewardee_count:
          type: integer
          description: Distinct rewardees in the window.
        top10_share:
          allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
          description: |
            The top ten rewardees' share of `total_distributed_raw`. The
            concentration number: on a book with 45 bids and 208 asks resting, a
            top-10 share near 1 says the reward is going to a handful of
            market-making addresses rather than to a crowd.
        rows:
          type: array
          items:
            type: object
            required: [rank, rewardee, amount_raw, payouts]
            additionalProperties: false
            properties:
              rank: { type: integer, minimum: 1 }
              rewardee: { $ref: "#/components/schemas/Address" }
              amount_raw: { $ref: "#/components/schemas/Uint256Raw" }
              amount: { $ref: "#/components/schemas/LossyNumber" }
              payouts:
                type: integer
                description: Number of `RewardsDistributed` events to this address.
              first_reward_at:
                type: integer
                format: int64
                description: Unix seconds of this address's first payout in the window.
              last_reward_at: { type: integer, format: int64 }
              share_of_total:
                allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
                description: "`amount_raw / total_distributed_raw`."
              side: { $ref: "#/components/schemas/Side" }
              side_token: { $ref: "#/components/schemas/Address" }
      example:
        epoch: 0
        from: "2026-08-15T07:43:00Z"
        to: "2026-09-02T15:35:00Z"
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_label: "NVDA/USDG"
        reward_token:
          address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
          symbol: "DEEP"
          decimals: 18
        as_of_block: 52677134
        finality: "safe"
        total_distributed_raw: "167234195360000000000000000"
        rewardee_count: 31
        top10_share: 0.9412
        rows:
          - rank: 1
            rewardee: "0x884a1B2c3D4e5F60718293a4B5c6D7e8F90A1b2C"
            amount_raw: "91204118220000000000000000"
            amount: 91204118.22
            payouts: 194
            first_reward_at: 1786779780
            last_reward_at: 1788362840
            share_of_total: 0.5454
            side: "bid"
            side_token: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
          - rank: 2
            rewardee: "0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
            amount_raw: "76033118004000000000000000"
            amount: 76033118
            payouts: 177
            first_reward_at: 1786780320
            last_reward_at: 1788362219
            share_of_total: 0.4546
            side: "ask"
            side_token: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"

    RevenueSeries:
      type: object
      required: [resolution, from, to, buckets, totals]
      additionalProperties: false
      properties:
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        fee_bps:
          type: integer
          description: |
            The Router's fee, read live from `feeConfig()` and cached
            in-process for ~60s (the same pattern `/v1/rewards/params` uses
            for the Rewarder). 10 today. Falls back to the compiled-in value
            10, with a logged warning, only if the live read fails — see
            `fee_source`.
        fee_recipient:
          allOf: [{ $ref: "#/components/schemas/Address" }]
          description: |
            The STATE vault, from `feeConfig().recipient`. Live-read alongside
            `fee_bps`; falls back to the compiled-in, confirmed-on-chain
            address on the same condition as `fee_bps` — see `fee_source`.
        fee_source:
          type: string
          enum: [router, fallback]
          description: |
            Where `fee_bps`/`fee_recipient` came from. `router` is the normal
            case — a live (cached ~60s) `feeConfig()` read. `fallback` means
            that read failed (or no RPC client is configured) and the
            compiled-in values were used instead; a `fallback` response is
            still safe to display; it just means a fee-schedule change would
            not show up until the node answers again.
        buckets:
          type: array
          items:
            type: object
            required: [bucket, fee_usdg_raw, trades]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              market_session:
                allOf: [{ $ref: "#/components/schemas/MarketSession" }]
                description: Present only at resolutions of 1h or finer.
              fee_usdg_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: Fee taken in USDG (6dp) — the taker sold the base.
              fee_base_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: |
                  Fee taken in the base token (NVDA or DEEP, 18dp) — the taker
                  bought the base. Kept separate from the USDG leg because they are
                  different assets, not different denominations. **Absent when
                  `book` is omitted and the two configured books are combined**:
                  NVDA and DEEP are different ERC-20s, so summing their raw fee
                  legs together would produce a number with no meaning — the
                  same reason nothing in this API sums two `_raw` fields of
                  different tokens. `fee_usdg_raw` (always USDG, both books)
                  and `trades` still combine safely and are always present.
              fee_usd: { $ref: "#/components/schemas/LossyNumber" }
              gross_notional_usdg_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: "1,000 × the fee at 10 bps — a check on the volume figure."
              trades: { type: integer }
              vwap_raw:
                allOf: [{ $ref: "#/components/schemas/DecimalRaw" }]
                description: Absent under the same multi-book condition as `fee_base_raw`.
        totals:
          type: object
          required: [fee_usdg_raw, trades]
          additionalProperties: false
          properties:
            fee_usdg_raw: { $ref: "#/components/schemas/Uint256Raw" }
            fee_base_raw:
              allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
              description: Same multi-book caveat as the per-bucket field above.
            fee_usd: { $ref: "#/components/schemas/LossyNumber" }
            gross_notional_usdg_raw: { $ref: "#/components/schemas/Uint256Raw" }
            trades: { type: integer }
      example:
        resolution: "1d"
        from: "2026-08-31T00:00:00Z"
        to: "2026-09-02T00:00:00Z"
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_label: "NVDA/USDG"
        fee_bps: 10
        fee_recipient: "0xbfb7b3Ff3D498a559b946B836d26F0E168f273D5"
        fee_source: "router"
        buckets:
          # 2,400,000 USDG of volume at 10 bps = 2,400 USDG of fee, split between
          # the two output tokens: 1,200 USDG plus 1,200/219.04 = 5.478 NVDA.
          - bucket: "2026-08-31T00:00:00Z"
            fee_usdg_raw: "1200000000"
            fee_base_raw: "5478329000000000000"
            fee_usd: 2400
            gross_notional_usdg_raw: "2400000000000"
            trades: 19
            vwap_raw: "219.03881204"
          # 500,000 USDG of volume (the site's 24H headline) = 500 USDG of fee:
          # 250 USDG plus 250/217.90 = 1.1474 NVDA.
          - bucket: "2026-09-01T00:00:00Z"
            fee_usdg_raw: "250000000"
            fee_base_raw: "1147352000000000000"
            fee_usd: 500
            gross_notional_usdg_raw: "500000000000"
            trades: 24
            vwap_raw: "217.90441002"
        totals:
          fee_usdg_raw: "1450000000"
          fee_base_raw: "6625681000000000000"
          fee_usd: 2900
          gross_notional_usdg_raw: "2900000000000"
          trades: 43

    ActiveUsersSeries:
      type: object
      required: [resolution, from, to, buckets, totals]
      additionalProperties: false
      properties:
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        buckets:
          type: array
          items:
            type: object
            required: [bucket, takers, makers, farmers, total_unique]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              market_session:
                allOf: [{ $ref: "#/components/schemas/MarketSession" }]
                description: Present only at resolutions of 1h or finer.
              takers:
                type: integer
                description: Distinct addresses that crossed the spread.
              makers:
                type: integer
                description: Distinct addresses that rested an order.
              farmers:
                type: integer
                description: Distinct `RewardsDistributed` recipients.
              total_unique:
                type: integer
                description: |
                  The **union**, not the sum. One address is often all three, and
                  those are the addresses worth looking at.
        totals:
          type: object
          required: [takers, makers, farmers, total_unique]
          additionalProperties: false
          properties:
            takers: { type: integer }
            makers: { type: integer }
            farmers: { type: integer }
            total_unique: { type: integer }
      example:
        resolution: "1d"
        from: "2026-08-31T00:00:00Z"
        to: "2026-09-02T00:00:00Z"
        buckets:
          - bucket: "2026-08-31T00:00:00Z"
            takers: 11
            makers: 4
            farmers: 6
            total_unique: 17
          - bucket: "2026-09-01T00:00:00Z"
            takers: 14
            makers: 4
            farmers: 7
            total_unique: 21
        totals:
          takers: 22
          makers: 5
          farmers: 9
          total_unique: 31

    FlowSources:
      type: object
      required: [resolution, from, to, buckets, totals]
      additionalProperties: false
      properties:
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        buckets:
          type: array
          items:
            type: object
            required: [bucket, rows]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              market_session:
                allOf: [{ $ref: "#/components/schemas/MarketSession" }]
                description: Present only at resolutions of 1h or finer.
              rows:
                type: array
                items: { $ref: "#/components/schemas/FlowSourceRow" }
        totals:
          type: array
          items: { $ref: "#/components/schemas/FlowSourceRow" }
      example:
        resolution: "1d"
        from: "2026-09-01T00:00:00Z"
        to: "2026-09-02T00:00:00Z"
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_label: "NVDA/USDG"
        buckets:
          - bucket: "2026-09-01T00:00:00Z"
            rows:
              - source: "direct"
                volume_usdg_raw: "402118004"
                trades: 15
                share: 0.609
              - source: "integrator:0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
                volume_usdg_raw: "184220118"
                trades: 6
                share: 0.279
              - source: "contract:0x2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d"
                volume_usdg_raw: "73773878"
                trades: 3
                share: 0.112
        totals:
          - source: "direct"
            volume_usdg_raw: "402118004"
            trades: 15
            share: 0.609
          - source: "integrator:0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
            volume_usdg_raw: "184220118"
            trades: 6
            share: 0.279
          - source: "contract:0x2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d"
            volume_usdg_raw: "73773878"
            trades: 3
            share: 0.112

    FlowSourceRow:
      type: object
      required: [source, volume_usdg_raw, trades, share]
      additionalProperties: false
      properties:
        source:
          type: string
          description: |
            `direct`, `integrator:<address>` (authoritative — the interface
            identified itself and took a fee) or `contract:<tx_to>` (an aggregator
            or bot sat in between; the address is reported as found).
          example: "integrator:0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
        volume_usdg_raw: { $ref: "#/components/schemas/Uint256Raw" }
        trades: { type: integer }
        share: { $ref: "#/components/schemas/LossyNumber" }

    EmissionsSeries:
      type: object
      required: [resolution, from, to, reward_token, sides]
      additionalProperties: false
      properties:
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_label: { type: string }
        reward_token: { $ref: "#/components/schemas/Token" }
        sides:
          type: array
          description: One entry per side, addressed by the side's token.
          items:
            type: object
            required: [side_token, side, buckets]
            additionalProperties: false
            properties:
              side_token: { $ref: "#/components/schemas/Address" }
              side: { $ref: "#/components/schemas/Side" }
              is_token0:
                type: boolean
                description: |
                  Whether this side's token is the pair's token0. Exposed so a
                  client can line these rows up against the raw `is_bid_token0`
                  flag on book and event rows.
              buckets:
                type: array
                items:
                  type: object
                  required: [bucket, distributed_raw, ceiling_raw]
                  additionalProperties: false
                  properties:
                    bucket: { type: string, format: date-time }
                    market_session:
                      allOf: [{ $ref: "#/components/schemas/MarketSession" }]
                      description: Present only at resolutions of 1h or finer.
                    distributed_raw:
                      allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                      description: "Sum of `RewardsDistributed.amount` in the bucket."
                    ceiling_raw:
                      allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                      description: "Delta of `cumulativeEmissionsAt(token, ts)` across the bucket."
                    accrued_raw:
                      allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                      description: "`totalAccrued(token)` at the bucket's snapshot."
                    capture_ratio:
                      allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
                      description: |
                        `distributed / ceiling`. Below 1 means makers left rewards
                        unclaimed — no eligible order at the top of book — which is
                        the number that says whether the incentive is working.
                    payouts: { type: integer }
        truncated:
          type: boolean
          description: |
            True when either backing read was capped: more snapshot rows exist in
            the window than were fetched (the newest buckets are then missing), or
            more `RewardsDistributed` events than were summed (`distributed_raw`
            is then a lower bound). Omitted (== false) otherwise.
      example:
        resolution: "1d"
        from: "2026-08-31T00:00:00Z"
        to: "2026-09-02T00:00:00Z"
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_label: "NVDA/USDG"
        reward_token:
          address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
          symbol: "DEEP"
          decimals: 18
        sides:
          - side_token: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
            side: "bid"
            is_token0: true
            buckets:
              - bucket: "2026-08-31T00:00:00Z"
                distributed_raw: "4112004120000000000000000"
                ceiling_raw: "4880220118000000000000000"
                accrued_raw: "87092114100000000000000000"
                capture_ratio: 0.8426
                payouts: 188
              - bucket: "2026-09-01T00:00:00Z"
                distributed_raw: "4112004120000000000000000"
                ceiling_raw: "4602118004000000000000000"
                accrued_raw: "91204118220000000000000000"
                capture_ratio: 0.8935
                payouts: 194
          - side_token: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
            side: "ask"
            is_token0: false
            buckets:
              - bucket: "2026-08-31T00:00:00Z"
                distributed_raw: "3402118004000000000000000"
                ceiling_raw: "4880220118000000000000000"
                accrued_raw: "72631000000000000000000000"
                capture_ratio: 0.6971
                payouts: 171
              - bucket: "2026-09-01T00:00:00Z"
                distributed_raw: "3402118004000000000000000"
                ceiling_raw: "4602118004000000000000000"
                accrued_raw: "76033118004000000000000000"
                capture_ratio: 0.7393
                payouts: 177

    DeepToken:
      type: object
      required:
        [token, price_usdg_raw, price_source, total_supply_raw, rewarder_balance_raw, burned_raw, float_raw]
      additionalProperties: false
      properties:
        token: { $ref: "#/components/schemas/Token" }
        as_of_block: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        price_usdg_raw:
          allOf: [{ $ref: "#/components/schemas/DecimalRaw" }]
          description: |
            DEEP in USDG. The example value is **illustrative**: the DEEP/USDG book
            carried 6 logs in 600,000 blocks, and its price-key scale is not
            verified yet (runbook P5-3), which is why the market is marked
            `price_decode_pending` in `/v1/markets`. Do not build a valuation on
            this field until that status changes.
        price_usdg: { $ref: "#/components/schemas/LossyNumber" }
        price_source:
          type: string
          enum: [deepstate, uniswap_v4]
          description: |
            Which venue the price came from. `deepstate` while only our own book is
            indexed; it can become `uniswap_v4` once the off-venue dataset lands,
            because DEEP's deeper market may not be ours.
        total_supply_raw: { $ref: "#/components/schemas/Uint256Raw" }
        rewarder_balance_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: The undistributed remainder of the 1B prefund.
        burned_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            Cumulative DEEP burned into the STATE vault — the vault's `totalAssets`.
        float_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            `total_supply − rewarder_balance`. The only DEEP that can actually move.

            **Burned DEEP is deliberately not subtracted again.** DEEP is burned
            into the vault, which means `totalSupply` has already lost it —
            895,955,537.40 *is* 1,000,000,000 − 104,044,463.60. Subtracting
            `burned_raw` a second time gives −40,854,730.84, a negative float, which
            is how you can tell the mistake has been made. `burned_raw` is reported
            alongside as the STATE-economics figure it is, not as a term in this
            subtraction.

            The vault's own small DEEP balance (8,014.13) sits inside the float; it
            is not worth a separate term at that size.
        float_market_cap_usd:
          allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
          description: |
            Float × price. A `total_supply × price` figure would overstate this
            several times over, which is why it is not reported.
        distributed_total_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: All-time sum of `RewardsDistributed`.
        emission_to_revenue_ratio:
          allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
          description: |
            DEEP emitted (at `price_usdg`) per dollar of protocol fee revenue,
            over the trailing 24 h — what the protocol pays for the liquidity
            it earns on. Independent of `distributed_total_raw`, which is
            all-time. Absent (not 0) when there is no priced DEEP figure yet or
            no fee revenue at all in the trailing 24 h.
      example:
        token:
          address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
          symbol: "DEEP"
          decimals: 18
        as_of_block: 52685677
        finality: "head"
        price_usdg_raw: "0.00210000"
        price_usdg: 0.0021
        price_source: "deepstate"
        total_supply_raw: "895955537400000000000000000"
        rewarder_balance_raw: "832765804640000000000000000"
        burned_raw: "104044463600000000000000000"
        float_raw: "63189732760000000000000000"
        float_market_cap_usd: 132698.44
        distributed_total_raw: "167234195360000000000000000"
        # Trailing-24h figure, independent of distributed_total_raw above
        # (all-time): illustrative 24h DEEP distributed × price_usdg ÷ 24h fee
        # revenue.
        emission_to_revenue_ratio: 2.55

    VenueShare:
      type: object
      required: [pair, resolution, from, to, buckets, totals, deepstate_share]
      additionalProperties: false
      properties:
        pair: { type: string, example: "NVDA/USDG" }
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        buckets:
          type: array
          items:
            type: object
            required: [bucket, rows]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              market_session:
                allOf: [{ $ref: "#/components/schemas/MarketSession" }]
                description: Present only at resolutions of 1h or finer.
              rows:
                type: array
                items: { $ref: "#/components/schemas/VenueVolumeRow" }
        totals:
          type: array
          items: { $ref: "#/components/schemas/VenueVolumeRow" }
        deepstate_share:
          allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
          description: Deepstate volume over total volume across the window.
      example:
        pair: "NVDA/USDG"
        resolution: "1d"
        from: "2026-09-01T00:00:00Z"
        to: "2026-09-02T00:00:00Z"
        buckets:
          - bucket: "2026-09-01T00:00:00Z"
            rows:
              - venue: "deepstate"
                volume_usdg_raw: "500000000000"
                volume_usd: 500000
                trades: 24
              - venue: "uniswap_v4"
                volume_usdg_raw: "1240118004000"
                volume_usd: 1240118
                trades: 412
              - venue: "uniswap_v3"
                volume_usdg_raw: "184220118000"
                volume_usd: 184220.12
                trades: 88
        totals:
          - venue: "deepstate"
            volume_usdg_raw: "500000000000"
            volume_usd: 500000
            trades: 24
          - venue: "uniswap_v4"
            volume_usdg_raw: "1240118004000"
            volume_usd: 1240118
            trades: 412
          - venue: "uniswap_v3"
            volume_usdg_raw: "184220118000"
            volume_usd: 184220.12
            trades: 88
        deepstate_share: 0.2597

    VenueVolumeRow:
      type: object
      required: [venue, volume_usdg_raw, trades]
      additionalProperties: false
      properties:
        venue:
          type: string
          enum: [deepstate, uniswap_v3, uniswap_v4, ekubo, arcus, other]
        volume_usdg_raw: { $ref: "#/components/schemas/Uint256Raw" }
        volume_usd: { $ref: "#/components/schemas/LossyNumber" }
        trades: { type: integer }

    VenueBasis:
      type: object
      required: [pair, resolution, from, to, points]
      additionalProperties: false
      properties:
        pair: { type: string, example: "NVDA/USDG" }
        resolution: { type: string, enum: ["1m", "1h"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        points:
          type: array
          items:
            type: object
            required: [bucket, basis_bps]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              market_session: { $ref: "#/components/schemas/MarketSession" }
              deepstate_mid_raw: { $ref: "#/components/schemas/DecimalRaw" }
              deepstate_mid: { $ref: "#/components/schemas/LossyNumber" }
              uniswap_v3_price_raw: { $ref: "#/components/schemas/DecimalRaw" }
              uniswap_v3_price: { $ref: "#/components/schemas/LossyNumber" }
              basis_bps:
                allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
                description: |
                  `(deepstate_mid − amm_price) / amm_price × 10000`. Our mid against
                  the AMM's executed price, so a wide Deepstate spread shows up as
                  noise around the basis rather than as basis itself.
      example:
        pair: "NVDA/USDG"
        resolution: "1h"
        from: "2026-09-02T14:00:00Z"
        to: "2026-09-02T16:00:00Z"
        points:
          - bucket: "2026-09-02T14:00:00Z"
            market_session: "regular"
            deepstate_mid_raw: "218.48071908"
            deepstate_mid: 218.48071908
            uniswap_v3_price_raw: "218.11004221"
            uniswap_v3_price: 218.11004221
            basis_bps: 16.9932
          - bucket: "2026-09-02T15:00:00Z"
            market_session: "regular"
            deepstate_mid_raw: "218.46001102"
            deepstate_mid: 218.46001102
            uniswap_v3_price_raw: "218.70112004"
            uniswap_v3_price: 218.70112004
            basis_bps: -11.0245

    RewardsParams:
      type: object
      required: [rewarder, reward_token, as_of_block, finality, sides]
      additionalProperties: false
      properties:
        rewarder: { $ref: "#/components/schemas/Address" }
        reward_token: { $ref: "#/components/schemas/Token" }
        as_of_block: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        emission_duration_seconds: { type: integer, format: int64 }
        emission_time_constant: { $ref: "#/components/schemas/Uint256Raw" }
        quantity_ramp_period_seconds: { type: integer, format: int64 }
        side_emission_cap_raw: { $ref: "#/components/schemas/Uint256Raw" }
        deep_total_supply_raw: { $ref: "#/components/schemas/Uint256Raw" }
        rewarder_deep_balance_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            What is left of the 1B DEEP prefund. 832,765,804.64 on 2026-09-02 —
            167.2M distributed in the protocol's first 17 days.
        sides:
          type: array
          description: |
            One entry per side, addressed by the side's **token** — the Rewarder has
            no side bool (runbook §1.2).
          items:
            type: object
            required: [side_token, side]
            additionalProperties: false
            properties:
              side_token: { $ref: "#/components/schemas/Address" }
              side: { $ref: "#/components/schemas/Side" }
              book_id: { $ref: "#/components/schemas/Hash32" }
              book_label: { type: string }
              emission_start: { type: integer, format: int64 }
              total_accrued_raw: { $ref: "#/components/schemas/Uint256Raw" }
              full_reward_quantity_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: |
                  `fullRewardQuantityAt(token, now)` — the size at or above which an
                  order earns the full rate. The site's `SUPER FARM` preset.
              streaming_rate_per_second_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: "`emissionsBetween(token, now-60, now) / 60`."
              apr_pct: { $ref: "#/components/schemas/LossyNumber" }
      example:
        rewarder: "0xE85ADBC03a6b52a2c9894c1BB525eC883ea156D7"
        reward_token:
          address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
          symbol: "DEEP"
          decimals: 18
        as_of_block: 52685677
        finality: "head"
        emission_duration_seconds: 31536000
        emission_time_constant: "7776000"
        quantity_ramp_period_seconds: 604800
        side_emission_cap_raw: "500000000000000000000000000"
        deep_total_supply_raw: "895955537400000000000000000"
        rewarder_deep_balance_raw: "832765804640000000000000000"
        sides:
          - side_token: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
            side: "bid"
            book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
            book_label: "NVDA/USDG"
            emission_start: 1786779780
            total_accrued_raw: "91204118220000000000000000"
            full_reward_quantity_raw: "5213000000"
            streaming_rate_per_second_raw: "1140377000000000000"
            apr_pct: 127.3
          - side_token: "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC"
            side: "ask"
            book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
            book_label: "NVDA/USDG"
            emission_start: 1786779780
            total_accrued_raw: "76033118004000000000000000"
            full_reward_quantity_raw: "54638900000000000000"
            streaming_rate_per_second_raw: "1140377000000000000"
            apr_pct: 118.4

    ProposalState:
      type: string
      enum: [Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed]

    Proposal:
      type: object
      required:
        [proposal_id, proposer, title, vote_start, vote_end, state, for_raw, against_raw, abstain_raw]
      additionalProperties: false
      properties:
        proposal_id: { type: string, example: "1" }
        label: { type: string, example: "DGP-001" }
        proposer: { $ref: "#/components/schemas/Address" }
        title: { type: string }
        description: { type: string }
        targets:
          type: array
          items: { $ref: "#/components/schemas/Address" }
        vote_start:
          type: integer
          format: int64
          description: |
            Unix seconds, not a block number — the Governor uses a timestamp clock
            (`CLOCK_MODE = mode=timestamp`).
        vote_end: { type: integer, format: int64 }
        created_block: { type: integer, format: int64 }
        created_tx: { $ref: "#/components/schemas/Hash32" }
        finality: { $ref: "#/components/schemas/Finality" }
        state:
          allOf: [{ $ref: "#/components/schemas/ProposalState" }]
          description: |
            Must agree with `vote_start` / `vote_end`, which is a real
            constraint and easy to violate in a fixture: `Active` requires
            `vote_start` in the past and `vote_end` in the future, `Pending`
            requires `vote_start` in the future, and everything terminal
            requires `vote_end` in the past.

            A consumer renders the badge and the schedule line from different
            fields, so an inconsistent triple shows up on screen as "Active ·
            voting opens in 1.0 d" — which is how this example was caught
            after its `state` was edited without its window.
          x-invariant: state must agree with vote_start/vote_end
        for_raw: { $ref: "#/components/schemas/Uint256Raw" }
        against_raw: { $ref: "#/components/schemas/Uint256Raw" }
        abstain_raw: { $ref: "#/components/schemas/Uint256Raw" }
        quorum_raw:
          allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
          description: |
            OMITTED while the proposal is `Pending`, and that is a property of
            the contract rather than of this service: `quorum(timepoint)` reads
            `getPastTotalSupply` at the proposal's snapshot, and OpenZeppelin's
            `Votes` REVERTS for a timepoint at or after the current clock. A
            Pending proposal's snapshot is in the future, so there is no quorum
            to state until voting opens. Verified against the live Governor on
            2026-09-03 with both DGP-001 and DGP-002 pending.

            Consumers must render the absence as "opens in ..." rather than as a
            zero-progress bar, and a producer must not call `quorum(snapshot)`
            eagerly for every proposal.
        quorum_progress_pct:
          allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
          description: |
            `(for_raw + abstain_raw) / quorum_raw`, as a percentage.

            FOR plus ABSTAIN, not FOR alone and not all three: this Governor
            reports `COUNTING_MODE = support=bravo&quorum=for,abstain`, so an
            abstention counts toward quorum and an AGAINST vote does not. Absent
            whenever `quorum_raw` is.
        voter_count: { type: integer }

    ProposalPage:
      type: object
      required: [proposals, parameters]
      additionalProperties: false
      properties:
        proposals:
          type: array
          items: { $ref: "#/components/schemas/Proposal" }
        parameters:
          type: object
          additionalProperties: false
          properties:
            governor: { $ref: "#/components/schemas/Address" }
            token: { $ref: "#/components/schemas/Address" }
            clock_mode: { type: string, example: "mode=timestamp" }
            voting_delay_seconds: { type: integer, format: int64 }
            voting_period_seconds: { type: integer, format: int64 }
            quorum_numerator: { type: integer }
            quorum_denominator: { type: integer }
            proposal_threshold_raw:
              allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
              description: |
                A FRACTION OF SUPPLY, not a constant. `proposalThreshold()` is
                `proposalThresholdNumerator / PROPOSAL_THRESHOLD_DENOMINATOR` of
                `total_supply_raw` — 1% when read on 2026-09-03, i.e. 811,177.07
                STATE against a supply of 81,117,706.76 — and STATE supply grows
                every time someone burns DEEP for it. So this moves on its own,
                with no governance action and no deploy. Read it; never cache it
                as a literal. The same applies to `quorum_numerator` /
                `quorum_denominator`, which express a fraction rather than an
                amount for the same reason.
            late_quorum_extension_seconds: { type: integer, format: int64 }
            total_supply_raw: { $ref: "#/components/schemas/Uint256Raw" }
            has_timelock: { type: boolean }
      example:
        proposals:
          - proposal_id: "43127004112893001220148820011230081774112004120038114002711220042"
            label: "DGP-001"
            proposer: "0x884a1B2c3D4e5F60718293a4B5c6D7e8F90A1b2C"
            title: "Adjust maker reward ramp"
            description: "Adjust maker reward ramp\n\nReduce QUANTITY_RAMP_PERIOD..."
            targets: ["0xE85ADBC03a6b52a2c9894c1BB525eC883ea156D7"]
            vote_start: 1788554087
            vote_end: 1789158887
            created_block: 52026625
            created_tx: "0x0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e"
            finality: "finalized"
            state: "Pending"
            for_raw: "0"
            against_raw: "0"
            abstain_raw: "0"
            voter_count: 0
          - proposal_id: "88112004120038114002711220042431270041128930012201488200112300817"
            label: "DGP-002"
            proposer: "0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
            title: "Fee split to the STATE vault"
            description: "Fee split to the STATE vault\n\nRoute 100% of protocol fees..."
            targets: ["0x6cf19308C22FC82ea620Fa0B3E94948d20f27B96"]
            vote_start: 1788300000
            vote_end: 1788904800
            created_block: 52054076
            created_tx: "0x4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c"
            finality: "finalized"
            state: "Active"
            for_raw: "4210000000000000000000000"
            against_raw: "180000000000000000000000"
            abstain_raw: "95000000000000000000000"
            quorum_raw: "8111770676238201567682765"
            quorum_progress_pct: 53.07
            voter_count: 7
        parameters:
          governor: "0x3DC3b787EBDC78bf916f4e30195C61c764C111Ff"
          token: "0xbfb7b3Ff3D498a559b946B836d26F0E168f273D5"
          clock_mode: "mode=timestamp"
          voting_delay_seconds: 259200
          voting_period_seconds: 604800
          quorum_numerator: 10
          quorum_denominator: 100
          proposal_threshold_raw: "811177067623820156768277"
          late_quorum_extension_seconds: 86400
          total_supply_raw: "81117706762382015676827655"
          has_timelock: false

    DelegatePage:
      type: object
      required: [total_supply_raw, delegates]
      additionalProperties: false
      properties:
        total_supply_raw: { $ref: "#/components/schemas/Uint256Raw" }
        as_of_block: { type: integer, format: int64 }
        finality: { $ref: "#/components/schemas/Finality" }
        delegates:
          type: array
          items:
            type: object
            required: [rank, delegate, votes_raw, share_pct]
            additionalProperties: false
            properties:
              rank: { type: integer, minimum: 1 }
              delegate: { $ref: "#/components/schemas/Address" }
              votes_raw: { $ref: "#/components/schemas/Uint256Raw" }
              votes: { $ref: "#/components/schemas/LossyNumber" }
              share_pct: { $ref: "#/components/schemas/LossyNumber" }
      example:
        total_supply_raw: "79590238460000000000000000"
        as_of_block: 52677134
        finality: "safe"
        delegates:
          - rank: 1
            delegate: "0x884a1B2c3D4e5F60718293a4B5c6D7e8F90A1b2C"
            votes_raw: "37957585127374000000000000"
            votes: 37957585.13
            share_pct: 47.69
          - rank: 2
            delegate: "0x7C3e9F1a5B2d4E68A091c3D5b7F20e846A1c9D3f"
            votes_raw: "9152377423280000000000000"
            votes: 9152377.42
            share_pct: 11.5

    VaultHistory:
      type: object
      required: [vault, asset, share, snapshots]
      additionalProperties: false
      properties:
        vault: { $ref: "#/components/schemas/Address" }
        asset: { $ref: "#/components/schemas/Token" }
        share: { $ref: "#/components/schemas/Token" }
        snapshots:
          type: array
          items:
            type: object
            required: [block_number, timestamp, total_supply_raw, total_assets_raw, share_price_raw]
            additionalProperties: false
            properties:
              block_number: { type: integer, format: int64 }
              timestamp: { type: integer, format: int64 }
              finality: { $ref: "#/components/schemas/Finality" }
              market_session: { $ref: "#/components/schemas/MarketSession" }
              total_supply_raw: { $ref: "#/components/schemas/Uint256Raw" }
              total_assets_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: |
                  Cumulative DEEP burned. Virtual accounting — deposits are burned, so
                  the vault's real DEEP balance is a rounding error next to this.
              share_price_raw:
                allOf: [{ $ref: "#/components/schemas/DecimalRaw" }]
                description: "`convertToAssets(1e18)`, DEEP per STATE."
              usdg_balance_raw: { $ref: "#/components/schemas/Uint256Raw" }
              deep_balance_raw: { $ref: "#/components/schemas/Uint256Raw" }
              router_usdg_raw: { $ref: "#/components/schemas/Uint256Raw" }
              router_nvda_raw: { $ref: "#/components/schemas/Uint256Raw" }
              router_deep_raw: { $ref: "#/components/schemas/Uint256Raw" }
              nvda_ui_multiplier_raw: { $ref: "#/components/schemas/Uint256Raw" }
      example:
        vault: "0xbfb7b3Ff3D498a559b946B836d26F0E168f273D5"
        asset:
          address: "0x1DA24f6Bb623b9d1aFEae3F3146659A2662D6d27"
          symbol: "DEEP"
          decimals: 18
        share:
          address: "0xbfb7b3Ff3D498a559b946B836d26F0E168f273D5"
          symbol: "STATE"
          decimals: 18
        snapshots:
          - block_number: 52642000
            timestamp: 1788357600
            finality: "finalized"
            market_session: "regular"
            total_supply_raw: "79590238460000000000000000"
            total_assets_raw: "104044463600000000000000000"
            share_price_raw: "1.30724410"
            usdg_balance_raw: "128742700000"
            deep_balance_raw: "8014130000000000000000"
            router_usdg_raw: "35093030000"
            router_nvda_raw: "136680000000000000000"
            router_deep_raw: "0"
            nvda_ui_multiplier_raw: "1000000000000000000"

    JsonRpcRequest:
      type: object
      required: [jsonrpc, method]
      additionalProperties: false
      properties:
        jsonrpc: { type: string, enum: ["2.0"] }
        id:
          nullable: true
        method:
          type: string
          enum:
            - eth_chainId
            - eth_blockNumber
            - eth_call
            - eth_estimateGas
            - eth_gasPrice
            - eth_feeHistory
            - eth_getBalance
            - eth_getTransactionCount
            - eth_getTransactionReceipt
            - eth_getTransactionByHash
          description: |
            The allow-list. Any other method is accepted by the JSON parser and
            rejected with -32601 — including `eth_sendRawTransaction`, which this
            service will never carry (runbook D8).
        params:
          type: array
          items: {}
      example:
        jsonrpc: "2.0"
        id: 1
        method: "eth_call"
        params:
          - to: "0x6cf19308C22FC82ea620Fa0B3E94948d20f27B96"
            data: "0x40b18be20000000000000000000000005fc5360d0400a0fd4f2af552add042d716f1d168000000000000000000000000d0601ce157db5bdc3162bbac2a2c8af5320d9eec"
          - "latest"

    JsonRpcResponse:
      type: object
      required: [jsonrpc, id]
      additionalProperties: false
      properties:
        jsonrpc: { type: string, enum: ["2.0"] }
        id:
          nullable: true
        result: {}
        error:
          type: object
          required: [code, message]
          additionalProperties: false
          properties:
            code: { type: integer }
            message: { type: string }
            data: {}

    NotReady:
      type: object
      required: [not_ready]
      additionalProperties: false
      description: |
        The body every stubbed route returns with HTTP `501`. `not_ready` names the
        OpenSearch index (or in-process cache) the route waits on; `phase` names the
        runbook phase that lands it. The route's `200` schema above is already the
        contract — only the data is missing.
      properties:
        not_ready:
          type: string
          example: "prod.robinhood.deepstate_trades"
        phase: { type: string, example: "P5" }
        message: { type: string }

    Error:
      type: object
      required: [error]
      additionalProperties: false
      properties:
        error: { type: string, example: "bad_request" }
        message: { type: string }

    QuoteChurnSeries:
      type: object
      required: [book_id, book_label, resolution, from, to, buckets, totals]
      additionalProperties: false
      description: |
        E2. See the route description for exactly how `median_order_lifetime_seconds` is
        paired and bounded.
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        buckets:
          type: array
          items:
            type: object
            required: [bucket, rests, cancels, fill_events]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              market_session: { $ref: "#/components/schemas/MarketSession" }
              rests: { type: integer, minimum: 0 }
              cancels: { type: integer, minimum: 0 }
              fill_events:
                type: integer
                minimum: 0
                description: |
                  Match-type events (`AskMatched`, `BidMatched`, `AsksMatched`,
                  `BidsMatched`, `AskSubtreeMatched`, `BidSubtreeMatched`), counted per
                  **event**. `AsksMatched`/`BidsMatched` can each settle several
                  resting orders, and a subtree match settles a whole price region
                  without enumerating them — so this is not the number of orders
                  filled.
              cancel_fill_ratio:
                allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
                description: "`cancels / fill_events`. Omitted when `fill_events` is 0."
              median_order_lifetime_seconds:
                type: integer
                format: int64
                description: |
                  Median lifetime of orders that RESTED in this bucket (regardless of
                  which bucket they terminated in). Omitted — not 0 — when no order that
                  rested in this bucket has a terminal event inside the request's own
                  `[from, to)` window; see the route description for the two excluded
                  classes.
        totals:
          type: object
          required: [rests, cancels, fill_events]
          additionalProperties: false
          properties:
            rests: { type: integer }
            cancels: { type: integer }
            fill_events: { type: integer }
            cancel_fill_ratio: { $ref: "#/components/schemas/LossyNumber" }
            median_order_lifetime_seconds:
              type: integer
              format: int64
              description: |
                One median across every paired order in the whole window — not an
                aggregate of the per-bucket medians above.
        truncated:
          type: boolean
          description: |
            True when more matching events exist in `[from, to)` than this response
            fetched (`hits.total.value` exceeded the read size) — the counts above are
            then a lower bound, not a full window. Omitted (== false) otherwise.
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        resolution: "1h"
        from: "2026-09-02T10:00:00Z"
        to: "2026-09-02T16:00:00Z"
        buckets:
          - bucket: "2026-09-02T10:00:00Z"
            market_session: "regular"
            rests: 428
            cancels: 402
            fill_events: 5
            cancel_fill_ratio: 80.4
            median_order_lifetime_seconds: 96
          - bucket: "2026-09-02T11:00:00Z"
            market_session: "regular"
            rests: 411
            cancels: 389
            fill_events: 3
            cancel_fill_ratio: 129.67
            median_order_lifetime_seconds: 104
        totals:
          rests: 839
          cancels: 791
          fill_events: 8
          cancel_fill_ratio: 98.88
          median_order_lifetime_seconds: 99

    BookSummaryHistory:
      type: object
      required: [book_id, book_label, resolution, from, to, points]
      additionalProperties: false
      description: E3's history — the live figure is `GET /v1/book/{book}`'s `summary`.
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        points:
          type: array
          items:
            type: object
            required: [bucket, block_number, finality]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              block_number:
                allOf: [{ type: integer, format: int64 }]
                description: The `deepstate_book_summary` row this bucket's close came from.
              finality: { $ref: "#/components/schemas/Finality" }
              market_session: { $ref: "#/components/schemas/MarketSession" }
              mid_raw: { $ref: "#/components/schemas/DecimalRaw" }
              spread_bps: { $ref: "#/components/schemas/LossyNumber" }
              bid_depth_1pct_quote_raw: { $ref: "#/components/schemas/Uint256Raw" }
              ask_depth_1pct_quote_raw: { $ref: "#/components/schemas/Uint256Raw" }
        truncated:
          type: boolean
          description: True when more summary rows exist in the window than were read.
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        resolution: "1h"
        from: "2026-09-02T14:00:00Z"
        to: "2026-09-02T16:00:00Z"
        points:
          - bucket: "2026-09-02T14:00:00Z"
            block_number: 52626100
            finality: "finalized"
            market_session: "regular"
            mid_raw: "218.460000000000000000"
            spread_bps: 184.1
            bid_depth_1pct_quote_raw: "82441000000"
            ask_depth_1pct_quote_raw: "51239000000"
          - bucket: "2026-09-02T15:00:00Z"
            block_number: 52661600
            finality: "safe"
            market_session: "regular"
            mid_raw: "218.480000000000000000"
            spread_bps: 201.7
            bid_depth_1pct_quote_raw: "79802000000"
            ask_depth_1pct_quote_raw: "53108000000"

    DepthHistory:
      type: object
      required: [book_id, book_label, resolution, from, to, size_token, points, coverage]
      additionalProperties: false
      description: E4. See the route description for the non-denominated exclusion.
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        size_token:
          allOf: [{ $ref: "#/components/schemas/Token" }]
          description: What `size_raw` is denominated in — `token0`, per `Market.size_token`.
        points:
          type: array
          items:
            type: object
            required: [bucket, block_number, finality, side, price_raw, order_count, denominated_order_count]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              block_number: { type: integer, format: int64 }
              finality: { $ref: "#/components/schemas/Finality" }
              market_session: { $ref: "#/components/schemas/MarketSession" }
              side: { $ref: "#/components/schemas/Side" }
              price_raw: { $ref: "#/components/schemas/DecimalRaw" }
              size_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: |
                  Sum of `size_raw` across the denominated resting orders at this
                  price level. Omitted — never `0` — when every order at this level is
                  in the non-denominated reciprocal-key class.
              order_count:
                type: integer
                description: Every resting order at this level, denominated or not.
              denominated_order_count:
                type: integer
                description: The subset of `order_count` that `size_raw` was summed from.
        coverage:
          type: object
          required: [order_count, denominated_order_count]
          additionalProperties: false
          description: Totals across every point, for a one-line coverage caption.
          properties:
            order_count: { type: integer }
            denominated_order_count: { type: integer }
        truncated:
          type: boolean
          description: True when more depth rows exist in the window than were read.
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        resolution: "1h"
        from: "2026-09-02T15:00:00Z"
        to: "2026-09-02T16:00:00Z"
        size_token: { address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", symbol: "USDG", decimals: 6 }
        points:
          - bucket: "2026-09-02T15:00:00Z"
            block_number: 52661600
            finality: "safe"
            market_session: "regular"
            side: "bid"
            price_raw: "216.480000000000000000"
            size_raw: "5213000000"
            order_count: 4
            denominated_order_count: 4
          - bucket: "2026-09-02T15:00:00Z"
            block_number: 52661600
            finality: "safe"
            market_session: "regular"
            side: "ask"
            price_raw: "220.480000000000000000"
            order_count: 3
            denominated_order_count: 0
        coverage:
          order_count: 253
          denominated_order_count: 129

    MakerAnalytics:
      type: object
      required: [book_id, book_label, from, to, size_token, rewards_available, rows]
      additionalProperties: false
      description: E6. See the route description for how the four legs are sourced.
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        size_token: { $ref: "#/components/schemas/Token" }
        rewards_available:
          type: boolean
          description: |
            False on a book with no Rewarder (DEEP/USDG, runbook P10-4b) — every row's
            `deep_earned_raw` is then omitted, not zero.
        as_of_block:
          type: integer
          format: int64
          description: The `deepstate_book_depth` snapshot `notional_rested_raw` is read from.
        finality: { $ref: "#/components/schemas/Finality" }
        rows:
          type: array
          items:
            type: object
            required: [maker, time_at_top_bid_seconds, time_at_top_ask_seconds, order_count, denominated_order_count]
            additionalProperties: false
            properties:
              maker: { $ref: "#/components/schemas/Address" }
              time_at_top_bid_seconds: { type: integer, format: int64 }
              time_at_top_ask_seconds: { type: integer, format: int64 }
              notional_rested_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: |
                  Current resting notional as of `as_of_block`, in `size_token` units.
                  Omitted — never `0` — when this maker's resting orders are all in the
                  non-denominated reciprocal-key class (see `getDepthHistory`).
              order_count: { type: integer }
              denominated_order_count: { type: integer }
              fill_count:
                type: integer
                description: |
                  Fills attributed to this maker in the window via the nonce join — see
                  the route description. A documented lower bound, not an exact count:
                  a fill whose resting order's nonce has no `deepstate_book_depth` row
                  inside this same window (or was a subtree match) cannot be attributed
                  and is excluded here rather than guessed.
              fill_rate:
                allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
                description: |
                  `fill_count / (fill_count + cancels)`, this maker's own `OrderCancelled`
                  count in the window. Omitted when the maker has neither a fill nor a
                  cancel attributed in the window (a rate needs a real denominator).
              deep_earned_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: |
                  Sum of `RewardsDistributed.amount_raw` for this maker in the window.
                  Present only when `rewards_available` is true.
        truncated:
          type: boolean
          description: True when any of the four underlying reads was capped.
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        from: "2026-09-01T16:00:00Z"
        to: "2026-09-02T16:00:00Z"
        size_token: { address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", symbol: "USDG", decimals: 6 }
        rewards_available: true
        as_of_block: 52685009
        finality: "safe"
        rows:
          - maker: "0x6cf19308C22FC82ea620Fa0B3E94948d20f27B96"
            time_at_top_bid_seconds: 41820
            time_at_top_ask_seconds: 6120
            notional_rested_raw: "82441000000"
            order_count: 12
            denominated_order_count: 12
            fill_count: 194
            fill_rate: 0.86
            deep_earned_raw: "1811294000000000000000"
          - maker: "0xbfb7b3Ff3D498a559b946B836d26F0E168f273D5"
            time_at_top_bid_seconds: 3600
            time_at_top_ask_seconds: 0
            order_count: 176
            denominated_order_count: 0
            fill_count: 0
            fill_rate: 0

    CostToQuoteSeries:
      type: object
      required: [book_id, book_label, resolution, from, to, buckets, totals]
      additionalProperties: false
      description: E8.
      properties:
        book_id: { $ref: "#/components/schemas/Hash32" }
        book_slug: { type: string }
        book_label: { type: string }
        resolution: { type: string, enum: ["1h", "1d"] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        buckets:
          type: array
          items:
            type: object
            required: [bucket, tx_count, rests, cancels, total_fee_raw]
            additionalProperties: false
            properties:
              bucket: { type: string, format: date-time }
              market_session: { $ref: "#/components/schemas/MarketSession" }
              tx_count:
                type: integer
                description: Distinct rest/cancel transaction hashes (deduped).
              rests: { type: integer, description: "`OrderRested` event count." }
              cancels: { type: integer, description: "`OrderCancelled` event count." }
              total_fee_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: "Σ `effective_gas_price × gas_used` over `tx_count` distinct transactions, wei."
              median_fee_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: Median per-transaction fee. Omitted when `tx_count` is 0.
              l1_fee_raw:
                allOf: [{ $ref: "#/components/schemas/Uint256Raw" }]
                description: "The `gas_used_for_l1` portion of `total_fee_raw` — not an addition to it."
              l1_share_pct:
                allOf: [{ $ref: "#/components/schemas/LossyNumber" }]
                description: "`l1_fee_raw / total_fee_raw × 100`. Omitted when `total_fee_raw` is 0."
        totals:
          type: object
          required: [tx_count, rests, cancels, total_fee_raw]
          additionalProperties: false
          properties:
            tx_count: { type: integer }
            rests: { type: integer }
            cancels: { type: integer }
            total_fee_raw: { $ref: "#/components/schemas/Uint256Raw" }
            median_fee_raw: { $ref: "#/components/schemas/Uint256Raw" }
            l1_fee_raw: { $ref: "#/components/schemas/Uint256Raw" }
            l1_share_pct: { $ref: "#/components/schemas/LossyNumber" }
        truncated:
          type: boolean
          description: True when more matching events exist in the window than were read.
      example:
        book_id: "0xdf941c235503a5d2e67aee5dea00f2965f99421c0d034bd77f924c05c66bf399"
        book_slug: "NVDA-USDG"
        book_label: "NVDA/USDG"
        resolution: "1h"
        from: "2026-09-02T14:00:00Z"
        to: "2026-09-02T16:00:00Z"
        buckets:
          - bucket: "2026-09-02T14:00:00Z"
            market_session: "regular"
            tx_count: 402
            rests: 219
            cancels: 198
            total_fee_raw: "48973600000000000"
            median_fee_raw: "118000000000000"
            l1_fee_raw: "9186100000000000"
            l1_share_pct: 18.75
          - bucket: "2026-09-02T15:00:00Z"
            market_session: "regular"
            tx_count: 388
            rests: 208
            cancels: 187
            total_fee_raw: "51204800000000000"
            median_fee_raw: "121500000000000"
            l1_fee_raw: "9832100000000000"
            l1_share_pct: 19.20
        totals:
          tx_count: 790
          rests: 427
          cancels: 385
          total_fee_raw: "100178400000000000"
          median_fee_raw: "119700000000000"
          l1_fee_raw: "19018200000000000"
          l1_share_pct: 18.99
