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

# Nav sidebar summary — verticals → competitions → event and market counts

> TWO kinds of number here, obeying TWO different rules. `count` / `live` and every chip bucket count EVENTS and equal the LENGTH of the correspondingly filtered list (see the parity paragraph below). `openMarketCount` / `totalMarketCount` count MARKETS and equal the SUM of the same-named field over that same list — never its length, and never derivable from `count`. ⚠️ That SUM rule holds at the ROW level (totals, vertical, competition) and NOT on the crypto chips, which carry the same two field names one level down. A chip counts the BUCKET'S OWN markets, so summing `openMarketCount` over `/api/events?asset=btc` comes out LARGER — that list returns events, and a mixed event's ETH markets ride along. Chips reconcile upward against the crypto row instead; see the description on the chip fields themselves before writing any reconciliation against them. The SUB-TOPIC chips (`subcategories`) carry the same two field names and are the OTHER case: a rubric owns its events outright — one rubric per event — so the SUM rule holds there exactly as it does at the row level. What does NOT hold for them is the residual: never render "others" as vertical minus Σ chips, because retired rubrics and non-emittable slugs sit inside the vertical and under no chip. Vertical slugs are the platform-canonical vocabulary, so a new vertical appears here the moment it has data. Counts use exactly the `/api/events` default open-view visibility (≥1 tradeable market + listable-now floor), so every FILTERABLE bucket (vertical, competition, scope, and the crypto block's asset/cadence/frequency chips) equals the length of the correspondingly filtered `/api/events` response, up to that endpoint's pagination window (the count reflects the full matching set; a single `/api/events` page may return fewer rows) — this parity holds for the DEFAULT OPEN VIEW only (no `?closed=true`), the same open-view scope the crypto block itself is computed over; a `?closed=true` list has no matching sidebar count to compare against. The crypto block's `preMarket` bucket is checked against `/api/events?vertical=crypto&preMarket=true`. The vertical is REQUIRED in that link and is not decoration: `?preMarket=` is platform-wide by design (PRE_MARKET means the same thing in every vertical, so gating it would make `?vertical=football&preMarket=true` silently empty and stop `false` from being a complement), so the bare form returns every vertical's pre-market events and matches no sidebar number at all. By DEFAULT the response is render-ready: zero-count competitions and zero-count verticals are trimmed. Pass `?includeEmpty=1` for the full catalog view (every active competition, catalog-first zero rows included — a vertical appears once it has an active competition or a countable event). Counts are identical in both views. `live` uses the same in-play predicate as `/api/events?sort=live`. Cached server-side for 3 minutes (one shared cache for both views). Scope chips are data-driven per level (totals, vertical, competition); `?scope=` on `/api/events` filters by them. Sub-topic `subcategories[]` chips are data-driven per VERTICAL and filter via `?subcategory=`, which needs no companion `?vertical=` because a rubric slug is globally unique and names its own vertical — so they obey the parity rule above unaided: a chip reading N lists N.



## OpenAPI

````yaml /api-reference/openapi.json get /api/verticals
openapi: 3.1.0
info:
  title: PredictStreet API
  description: >-
    Client-facing HTTP API for the PredictStreet prediction-market platform.
    **Partner kinds.** Every authenticated endpoint resolves a request's
    *effective wallet* from the partner row. `single_wallet` partners bind to
    one `associatedWallet` set at creation. `multi_wallet` partners declare the
    actor on every request via an `X-User-Wallet: 0x<40-hex>` header. See the
    [Partner kinds](/auth/api-keys#partner-kinds) doc for the full contract.
  version: '2026-06-16'
  contact:
    name: PredictStreet partners
    email: partners@predictstreet.com
servers:
  - url: https://core.api.dev.predictstreet.sde.adifoundation.ai
    description: Testnet (partner integrator API — final domain TBD)
security: []
tags:
  - name: Deposits
    description: >-
      Gasless USDC deposit relay - submit a signed EIP-2612 permit and the
      platform broadcasts the on-chain deposit for you (no gas).
  - name: Events
    description: >-
      Polymarket-style event grouping with football metadata (group, stage,
      teams, tags).
  - name: Tags
    description: Curated tag taxonomy used to filter events.
  - name: Markets
    description: 'Public market data: list, detail, orderbook, trades, OHLC.'
  - name: Crypto
    description: >-
      Crypto price markets: the underlying-asset price curve and Up/Down series
      navigation. The markets themselves are ordinary binary markets — list them
      through /api/markets and /api/events.
  - name: Orders
    description: >-
      Signed-order place / cancel / read. Requires `X-Api-Key` with
      `orders:read` / `orders:write` scope; every write additionally requires an
      EIP-712 signature over the order.
  - name: Portfolio
    description: >-
      Balances, positions, trades, fees, vault info for the key's
      `associatedWallet`. Requires `X-Api-Key` with `portfolio:read` scope.
  - name: Matches
    description: >-
      A match groups several events into one fixture/card (1X2 + first-scorer +
      over-under under one matchup).
  - name: Vault
    description: >-
      Backend co-signatures for ERC-1155 split / merge, and recovery for
      off-chain locks when the corresponding chain tx never confirmed. Requires
      `X-Api-Key` with `vault:write` scope plus an EIP-712 signature over the
      operation.
  - name: Leaderboard
    description: Public ranked leaderboard across PnL / volume buckets.
  - name: Search
    description: Global search across users, events, and matches.
  - name: Withdrawal Security
    description: >-
      Self-service withdrawal 2FA (TOTP) and withdrawal-address whitelist. Same
      endpoints serve the web app (user sessions) and API-key integrators.
      Mutations need `vault:write`, reads `portfolio:read`. Both are opt-in per
      wallet and only gate withdrawals once the platform enables
      withdrawal-security enforcement.
  - name: Platform
    description: >-
      Public platform state: the contract registry, the platform fee
      configuration, the platform freeze flag and active notices. No
      authentication.
paths:
  /api/verticals:
    get:
      tags:
        - Verticals
      summary: Nav sidebar summary — verticals → competitions → event and market counts
      description: >-
        TWO kinds of number here, obeying TWO different rules. `count` / `live`
        and every chip bucket count EVENTS and equal the LENGTH of the
        correspondingly filtered list (see the parity paragraph below).
        `openMarketCount` / `totalMarketCount` count MARKETS and equal the SUM
        of the same-named field over that same list — never its length, and
        never derivable from `count`. ⚠️ That SUM rule holds at the ROW level
        (totals, vertical, competition) and NOT on the crypto chips, which carry
        the same two field names one level down. A chip counts the BUCKET'S OWN
        markets, so summing `openMarketCount` over `/api/events?asset=btc` comes
        out LARGER — that list returns events, and a mixed event's ETH markets
        ride along. Chips reconcile upward against the crypto row instead; see
        the description on the chip fields themselves before writing any
        reconciliation against them. The SUB-TOPIC chips (`subcategories`) carry
        the same two field names and are the OTHER case: a rubric owns its
        events outright — one rubric per event — so the SUM rule holds there
        exactly as it does at the row level. What does NOT hold for them is the
        residual: never render "others" as vertical minus Σ chips, because
        retired rubrics and non-emittable slugs sit inside the vertical and
        under no chip. Vertical slugs are the platform-canonical vocabulary, so
        a new vertical appears here the moment it has data. Counts use exactly
        the `/api/events` default open-view visibility (≥1 tradeable market +
        listable-now floor), so every FILTERABLE bucket (vertical, competition,
        scope, and the crypto block's asset/cadence/frequency chips) equals the
        length of the correspondingly filtered `/api/events` response, up to
        that endpoint's pagination window (the count reflects the full matching
        set; a single `/api/events` page may return fewer rows) — this parity
        holds for the DEFAULT OPEN VIEW only (no `?closed=true`), the same
        open-view scope the crypto block itself is computed over; a
        `?closed=true` list has no matching sidebar count to compare against.
        The crypto block's `preMarket` bucket is checked against
        `/api/events?vertical=crypto&preMarket=true`. The vertical is REQUIRED
        in that link and is not decoration: `?preMarket=` is platform-wide by
        design (PRE_MARKET means the same thing in every vertical, so gating it
        would make `?vertical=football&preMarket=true` silently empty and stop
        `false` from being a complement), so the bare form returns every
        vertical's pre-market events and matches no sidebar number at all. By
        DEFAULT the response is render-ready: zero-count competitions and
        zero-count verticals are trimmed. Pass `?includeEmpty=1` for the full
        catalog view (every active competition, catalog-first zero rows included
        — a vertical appears once it has an active competition or a countable
        event). Counts are identical in both views. `live` uses the same in-play
        predicate as `/api/events?sort=live`. Cached server-side for 3 minutes
        (one shared cache for both views). Scope chips are data-driven per level
        (totals, vertical, competition); `?scope=` on `/api/events` filters by
        them. Sub-topic `subcategories[]` chips are data-driven per VERTICAL and
        filter via `?subcategory=`, which needs no companion `?vertical=`
        because a rubric slug is globally unique and names its own vertical — so
        they obey the parity rule above unaided: a chip reading N lists N.
      operationId: VerticalsController_summary
      parameters:
        - name: includeEmpty
          required: false
          in: query
          description: >-
            true/1 → include zero-count competitions and zero-count verticals
            (the full active catalog). Absent or empty → the default trimmed
            view. Any other unparseable value → 400 invalid_includeEmpty (same
            contract as the `closed` flag on the list endpoints).
          schema:
            type: string
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerticalsSummaryRespDto'
components:
  schemas:
    VerticalsSummaryRespDto:
      type: object
      properties:
        totals:
          $ref: '#/components/schemas/VerticalsTotalsDto'
        verticals:
          type: array
          items:
            $ref: '#/components/schemas/VerticalSummaryDto'
      required:
        - totals
        - verticals
    VerticalsTotalsDto:
      type: object
      properties:
        events:
          type: number
          description: All listable events, every vertical included.
        live:
          type: number
          description: Subset currently in play.
        openMarketCount:
          type: number
          description: >-
            MARKETS, not events. Each is the exact SUM of the same-named field
            on every event this row lists, so a card badge and this number
            reconcile by addition. `openMarketCount` = markets on the OPEN VIEW
            (OPEN / PAUSED / launched PRE_MARKET) — NOT the same as tradeable: a
            PAUSED market is counted here and rejects orders, so do not label
            this "open to trade". `totalMarketCount` = every market of those
            events the public may know exists (the open view plus
            closed/resolved/voided), which is MORE than a card renders — a card
            shows no closed markets. `openMarketCount <= totalMarketCount`
            always. A row may differ from the sum over its competitions in
            EITHER direction, so never compute an "other" bucket as the
            difference: events with no competition link count at the vertical
            level only, and events whose derived category is non-canonical count
            at the competition level only. NEVER derive either from `count`: an
            event carries any number of markets, including none countable.
        totalMarketCount:
          type: number
          description: >-
            MARKETS, not events — every market of this row's listable events
            that the public may know exists: visible, in an open-view or closed
            status, and past its pre-market launch time if it has one. Larger
            than what a card renders, which shows no closed markets. The SUM of
            the same-named field over the correspondingly filtered `/api/events`
            list. See `openMarketCount` for the full contract.
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/ScopeCountDto'
      required:
        - events
        - live
        - openMarketCount
        - totalMarketCount
        - scopes
    VerticalSummaryDto:
      type: object
      properties:
        slug:
          type: string
          description: >-
            Vertical slug — the DB category-axis value VERBATIM ('football',
            'tennis', 'politics', …) — the value `/api/events?vertical=` accepts
            and the value each event card's `vertical` carries. Display
            names/icons are a frontend concern.
        group:
          type: string
          description: >-
            Top-nav group ('sports' | 'crypto' | 'politics' | 'economics' | …).
            The frontend renders the Sports sidebar from verticals whose group
            is 'sports' and routes the rest to their own top-nav tabs, so a
            non-sport vertical (crypto/politics/economics) never shows under
            Sports. A not-yet-classified vertical returns 'other'; treat any
            group you don't recognize as unclassified (ignore / ping backend).
          example: sports
        count:
          type: number
          description: >-
            Listable events bucketed into this vertical. May differ from the sum
            of the competition counts in EITHER direction: events with no
            competition link count here and appear under no competition, while
            events whose derived category is non-canonical count in their
            competition and in no vertical bucket. Never compute an "other"
            bucket as the difference.
        live:
          type: number
          description: Subset currently in play.
        openMarketCount:
          type: number
          description: >-
            MARKETS, not events. Each is the exact SUM of the same-named field
            on every event this row lists, so a card badge and this number
            reconcile by addition. `openMarketCount` = markets on the OPEN VIEW
            (OPEN / PAUSED / launched PRE_MARKET) — NOT the same as tradeable: a
            PAUSED market is counted here and rejects orders, so do not label
            this "open to trade". `totalMarketCount` = every market of those
            events the public may know exists (the open view plus
            closed/resolved/voided), which is MORE than a card renders — a card
            shows no closed markets. `openMarketCount <= totalMarketCount`
            always. A row may differ from the sum over its competitions in
            EITHER direction, so never compute an "other" bucket as the
            difference: events with no competition link count at the vertical
            level only, and events whose derived category is non-canonical count
            at the competition level only. NEVER derive either from `count`: an
            event carries any number of markets, including none countable.
        totalMarketCount:
          type: number
          description: >-
            MARKETS, not events — every market of this row's listable events
            that the public may know exists: visible, in an open-view or closed
            status, and past its pre-market launch time if it has one. Larger
            than what a card renders, which shows no closed markets. The SUM of
            the same-named field over the correspondingly filtered `/api/events`
            list. See `openMarketCount` for the full contract.
        competitions:
          description: >-
            Active competitions of this vertical. By default only rows with
            count > 0; with `?includeEmpty=1` ALL active competitions,
            catalog-first zero-count rows included.
          type: array
          items:
            $ref: '#/components/schemas/VerticalCompetitionSummaryDto'
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/ScopeCountDto'
        subcategories:
          description: >-
            Sub-topic chips — the sidebar level BELOW the vertical for topics
            that have no leagues.
            Politics/economics/climate/culture/financials/science carry no
            competitions and fold into just two scopes (futures/specials), so
            this is what gives them a navigable sidebar. Ordered by the
            catalogue's own editorial `sort_order`, NOT by count — unlike every
            other array here, so render it in the order received. Data-driven
            and count > 0 only: a rubric appears once a listable event of this
            vertical carries it, and `?includeEmpty=1` does NOT add zero-count
            rubrics. The vocabulary is operator-owned, not a fixed enum, so
            chips appear and disappear without a deploy and the frontend must
            render what arrives rather than a known list. OPTIONAL AND
            MEANINGFULLY SO: the field is OMITTED when the chip query was
            unavailable and is `[]` when it ran and found nothing classified.
            Absence is "not known" — render nothing, do not render "no topics".
          type: array
          items:
            $ref: '#/components/schemas/VerticalSubcategoryCountDto'
        crypto:
          description: >-
            Crypto-only sidebar sub-groupings: the asset axis (data-driven — one
            chip per coin with data) + `others`, the cadence axis (recurring
            windowed cadences), and the Pre-Market status bucket. Present ONLY
            on the crypto vertical (slug 'crypto'); absent elsewhere — and NOT
            guaranteed present even there: if the chip query is unavailable the
            field is omitted rather than filled with zeros, so the crypto row
            still carries its own counts while the chips are simply not known.
            Render that as 'chips unavailable' (or nothing), never as a platform
            with no crypto assets. Each bucket is an independent event-overlap
            count — never derive one from another, or from count.
          allOf:
            - $ref: '#/components/schemas/VerticalCryptoBlockDto'
      required:
        - slug
        - group
        - count
        - live
        - openMarketCount
        - totalMarketCount
        - competitions
        - scopes
    ScopeCountDto:
      type: object
      properties:
        slug:
          type: string
          enum:
            - games
            - futures
            - totals
            - handicap
            - correct-score
            - player-props
            - team-props
            - awards
            - specials
          description: >-
            Chip slug — a closed backend vocabulary (a coarse fold of
            market_type). The value `/api/events?scope=` accepts. Labels are a
            frontend concern.
        count:
          type: number
          description: >-
            Listable events carrying ≥1 bettable market of this scope. Only
            count > 0 entries are emitted. Σ over scopes may EXCEED the parent
            count (an event can sit in several scopes) — never derive one from
            the other.
      required:
        - slug
        - count
    VerticalCompetitionSummaryDto:
      type: object
      properties:
        slug:
          type: string
          description: Competition slug — the value `/api/events?competition=` accepts.
        name:
          type: string
        season:
          type: object
          nullable: true
          description: Season label ("2025-26"); null for evergreen.
        logo:
          type: object
          nullable: true
          description: Competition crest URL, or null.
        count:
          type: number
          description: >-
            Listable events (≥1 tradeable market, same visibility as
            `/api/events`) whose effective competition is this one.
        live:
          type: number
          description: Subset currently in play.
        openMarketCount:
          type: number
          description: >-
            MARKETS, not events. Each is the exact SUM of the same-named field
            on every event this row lists, so a card badge and this number
            reconcile by addition. `openMarketCount` = markets on the OPEN VIEW
            (OPEN / PAUSED / launched PRE_MARKET) — NOT the same as tradeable: a
            PAUSED market is counted here and rejects orders, so do not label
            this "open to trade". `totalMarketCount` = every market of those
            events the public may know exists (the open view plus
            closed/resolved/voided), which is MORE than a card renders — a card
            shows no closed markets. `openMarketCount <= totalMarketCount`
            always. A row may differ from the sum over its competitions in
            EITHER direction, so never compute an "other" bucket as the
            difference: events with no competition link count at the vertical
            level only, and events whose derived category is non-canonical count
            at the competition level only. NEVER derive either from `count`: an
            event carries any number of markets, including none countable.
        totalMarketCount:
          type: number
          description: >-
            MARKETS, not events — every market of this row's listable events
            that the public may know exists: visible, in an open-view or closed
            status, and past its pre-market launch time if it has one. Larger
            than what a card renders, which shows no closed markets. The SUM of
            the same-named field over the correspondingly filtered `/api/events`
            list. See `openMarketCount` for the full contract.
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/ScopeCountDto'
      required:
        - slug
        - name
        - season
        - logo
        - count
        - live
        - openMarketCount
        - totalMarketCount
        - scopes
    VerticalSubcategoryCountDto:
      type: object
      properties:
        slug:
          type: string
          description: >-
            Rubric slug — the value `/api/events?subcategory=` accepts. Globally
            unique and namespaced by hand ('econ-inflation'), so it is
            SELF-SCOPING: the filter needs no companion `?vertical=` and a chip
            reading N lists exactly N. Render `name`, filter by this.
        name:
          type: string
          description: Display label ("Inflation"). Icons are a frontend concern.
        count:
          type: number
          description: >-
            Listable events of this vertical carrying this rubric. Only count >
            0 entries are emitted. UNLIKE `scopes` and `competitions` these
            buckets are DISJOINT — an event has exactly one rubric — so Σ over
            the array is <= the vertical `count`, reaching equality once every
            listable event is classified. The shortfall is events with no rubric
            yet; it is the one residual in this payload that IS meaningful,
            though the catalogue ships a per-vertical catch-all so it should
            trend to zero.
        openMarketCount:
          type: number
          description: >-
            MARKETS of this rubric's listable events — the same two measures as
            the vertical row, computed by the same SQL fragment:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — a PAUSED market is counted and rejects orders, so do
            not label it "open to trade"), `totalMarketCount` = every market the
            public may know exists, closed ones included. Unlike the crypto
            chips these ARE the sum over the correspondingly filtered list: Σ
            `openMarketCount` across `/api/events?subcategory=<slug>` equals
            this number, because a rubric owns its events outright. A chip may
            read `count > 0` with `openMarketCount` 0: an in-play event is
            listable even with no publicly visible market, exactly as at the row
            level. Two caveats inherited from the row-level rule: the
            `/api/events` window is capped, so the sum is only checkable for
            buckets under that cap, and parity is against the DEFAULT open view
            — `?closed=true` changes the set on the list side but not here. DO
            NOT RENDER THE RESIDUAL (vertical minus Σ chips) AS AN "OTHERS"
            BUCKET, for two independent reasons. (1) It is not only unclassified
            events. A RETIRED rubric keeps its events and markets in the
            vertical total while showing under no chip — though
            `?subcategory=<slug>` still LISTS them, deliberately, so shared
            links survive a retirement: unadvertised, not unlistable. A rubric
            whose slug the public validator would reject is the stronger case —
            no chip AND a 400 on the filter. Either way the residual includes
            markets that DO have a rubric. (2) The chips and the vertical total
            are read by two different queries on two different connections,
            therefore two MVCC snapshots; a market published between them can
            put a chip momentarily ABOVE its vertical, and the envelope is
            cached that way for the full TTL. `Σ chips <= vertical` is the
            intent, not an arithmetic guarantee.
        totalMarketCount:
          type: number
          description: >-
            MARKETS of this rubric's listable events — the same two measures as
            the vertical row, computed by the same SQL fragment:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — a PAUSED market is counted and rejects orders, so do
            not label it "open to trade"), `totalMarketCount` = every market the
            public may know exists, closed ones included. Unlike the crypto
            chips these ARE the sum over the correspondingly filtered list: Σ
            `openMarketCount` across `/api/events?subcategory=<slug>` equals
            this number, because a rubric owns its events outright. A chip may
            read `count > 0` with `openMarketCount` 0: an in-play event is
            listable even with no publicly visible market, exactly as at the row
            level. Two caveats inherited from the row-level rule: the
            `/api/events` window is capped, so the sum is only checkable for
            buckets under that cap, and parity is against the DEFAULT open view
            — `?closed=true` changes the set on the list side but not here. DO
            NOT RENDER THE RESIDUAL (vertical minus Σ chips) AS AN "OTHERS"
            BUCKET, for two independent reasons. (1) It is not only unclassified
            events. A RETIRED rubric keeps its events and markets in the
            vertical total while showing under no chip — though
            `?subcategory=<slug>` still LISTS them, deliberately, so shared
            links survive a retirement: unadvertised, not unlistable. A rubric
            whose slug the public validator would reject is the stronger case —
            no chip AND a 400 on the filter. Either way the residual includes
            markets that DO have a rubric. (2) The chips and the vertical total
            are read by two different queries on two different connections,
            therefore two MVCC snapshots; a market published between them can
            put a chip momentarily ABOVE its vertical, and the envelope is
            cached that way for the full TTL. `Σ chips <= vertical` is the
            intent, not an arithmetic guarantee.
      required:
        - slug
        - name
        - count
        - openMarketCount
        - totalMarketCount
    VerticalCryptoBlockDto:
      type: object
      properties:
        frequencies:
          description: >-
            The user-facing recurrence axis. Folds TWO mechanisms into one list:
            the series grid step (5-minute…weekly) AND the product's own
            declared window (`monthly` = a calendar month, midnight to midnight
            US Eastern time; `one-time` = a single deadline). Those last two
            carry no cadence at all, which is why this is a separate array from
            `cadences` and not a rename of it.
          type: array
          items:
            $ref: '#/components/schemas/CryptoFrequencyCountDto'
        cadences:
          type: array
          items:
            $ref: '#/components/schemas/CryptoCadenceCountDto'
        assets:
          type: array
          items:
            $ref: '#/components/schemas/CryptoAssetCountDto'
        preMarket:
          description: >-
            Events with ≥1 market in PRE_MARKET (past the launch gate) — a
            TRADEABLE state (orders accepted, matching deferred), not a
            read-only one. Overlaps the asset/cadence/frequency axes. Checkable
            against `/api/events?vertical=crypto&preMarket=true` — the vertical
            is REQUIRED, because `?preMarket=` is platform-wide by design
            (PRE_MARKET means the same thing in every vertical); the bare
            `?preMarket=true` returns every vertical's pre-market events and
            mirrors no sidebar number. NB its two market counts are ALWAYS EQUAL
            and that is structural, not a coincidence: PRE_MARKET is not a
            closed status, so a pre-market market can only ever be counted on
            the open view. Render one number, not two.
          allOf:
            - $ref: '#/components/schemas/CryptoStatusBucketDto'
        others:
          description: >-
            Crypto events with ≥1 visible market that has no identifiable asset
            (neither a windowed-series asset nor a threshold instrument id, or
            the source is blank) — market-level, not event-level: a mixed-asset
            event (one market with an identifiable coin, another with none) can
            count here AND under a coin chip at once.
          allOf:
            - $ref: '#/components/schemas/CryptoStatusBucketDto'
      required:
        - frequencies
        - cadences
        - assets
        - preMarket
        - others
    CryptoFrequencyCountDto:
      type: object
      properties:
        slug:
          type: string
          enum:
            - 5-minute
            - 15-minute
            - 30-minute
            - hourly
            - 6-hour
            - daily
            - weekly
            - monthly
            - annual
            - one-time
          description: >-
            Frequency slug — the user-facing "how often" axis, WIDER than the
            cadence vocabulary: it folds the series grid step together with each
            product's own declared window, which is why `monthly` and `one-time`
            appear here and not under `cadences`. `/api/events?frequency=`
            accepts it.
        count:
          type: number
          description: >-
            Listable crypto events of this frequency. count>0 only — a slug with
            no data is simply ABSENT from the array rather than present with 0.
            Notably `annual`: it is a valid filter value with no producer today
            (no yearly product exists), so it never appears here until one does.
        openMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL).
        totalMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL).
      required:
        - slug
        - count
        - openMarketCount
        - totalMarketCount
    CryptoCadenceCountDto:
      type: object
      properties:
        slug:
          type: string
          enum:
            - 5-minute
            - 15-minute
            - 30-minute
            - hourly
            - 6-hour
            - daily
            - weekly
          description: >-
            Cadence slug — the 7 the DB CHECK allows (5-minute | 15-minute |
            30-minute | hourly | 6-hour | daily | weekly).
            `/api/events?cadence=` accepts exactly these; `monthly` / `annual`
            are NOT cadences (the grid is epoch-anchored, so neither is
            creatable) and 400. LOW-LEVEL: this is the series GRID STEP only —
            for the user-facing "how often does it recur" axis use `frequencies`
            below, which folds in the calendar-month and by-deadline products
            that carry no cadence at all.
        count:
          type: number
          description: >-
            Listable crypto events of this recurring cadence. count>0 only.
            EMPTY until the windowed product ships.
        openMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL).
        totalMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL).
      required:
        - slug
        - count
        - openMarketCount
        - totalMarketCount
    CryptoAssetCountDto:
      type: object
      properties:
        slug:
          type: string
          description: >-
            Coin slug — data-driven (e.g. btc, eth, …), one chip per coin that
            has data. `/api/events?asset=` accepts it.
        name:
          type: string
          description: Display name (Bitcoin, …). Icons are a frontend concern.
        count:
          type: number
          description: Listable crypto events with ≥1 market of this coin. count>0 only.
        openMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL).
        totalMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL).
      required:
        - slug
        - name
        - count
        - openMarketCount
        - totalMarketCount
    CryptoStatusBucketDto:
      type: object
      properties:
        count:
          type: number
          description: >-
            Listable crypto events in this bucket. May be 0 — see the market
            counts below.
        openMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL). This bucket is a fixed
            field and is always present. When `count` is 0 both market counts
            are 0 too — a bucket with no events is not advertised, and that is
            how a field which cannot be omitted says so. Render-gate on `count`,
            exactly as you would skip a chip that is absent from an array.
        totalMarketCount:
          type: number
          description: >-
            MARKETS of THIS bucket, not of its events. An event carrying 3 open
            BTC markets and 2 open ETH markets contributes 3 to btc and 2 to eth
            — never 5 to either. Same two measures as the row level:
            `openMarketCount` = the open view (OPEN / PAUSED / launched
            PRE_MARKET — PAUSED is counted and rejects orders, so do not label
            it "open to trade"), `totalMarketCount` = every market the public
            may know exists, closed ones included. Unlike the row-level counts
            these are NOT the sum over `/api/events?<axis>=` — that list returns
            events, whose markets include the other axes'. They reconcile
            UPWARDS instead: Σ `assets[]` + `others` == the crypto row's
            `openMarketCount` exactly, EXCEPT for markets whose asset key is
            non-NULL but not emittable as a slug (illegal characters, or the
            reserved literal `others`) — those are on no axis at all, counted in
            neither a chip nor `others`, while the row above still counts them.
            Rare, but it makes the sum a `<=` in general: NEVER compute a
            residual (row minus Σ chips) and render it as a bucket, because that
            residual is real markets belonging to no bucket. The same sum of
            `totalMarketCount` is further below the row, because a bucket whose
            markets are ALL CLOSED has no events and is not advertised (see
            `count`), so its closed markets leave the sum with it. `cadences` /
            `frequencies` / `preMarket` stay <= the row on both fields (a market
            with no series carries no cadence). Never compute a residual from
            either sum, and do not add chips ACROSS axes — they are OVERLAPPING
            views of the same markets, not disjoint ones. (`assets` + `others`
            do partition them; `cadences` and `frequencies` do NOT — a threshold
            market carries neither — which is why those two are only <= the
            row.) A cross-axis sum therefore double-counts AND misses at the
            same time. Both numbers are computed on a different database read
            from the row-level ones they reconcile against, so the equality is
            exact per read, not across a market published in between (that skew
            can be served for the 3-minute cache TTL). This bucket is a fixed
            field and is always present. When `count` is 0 both market counts
            are 0 too — a bucket with no events is not advertised, and that is
            how a field which cannot be omitted says so. Render-gate on `count`,
            exactly as you would skip a chip that is absent from an array.
      required:
        - count
        - openMarketCount
        - totalMarketCount

````

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