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

# Collect an audience into a dataset

> Collect the repliers, quote posters or reposters of a post, the
members of a public X list, or the account's own posts or replies,
into a stored dataset you can then read, export, or copy into a
contact list.

`source: "research"` is the exception: instead of walking an audience
it reads people's recent posts and writes a short outreach brief for
each one (what they do, what they are building, recent topics,
personalization hooks that QUOTE their real posts, and an opener
angle). Give it exactly one source - `handles`, `list_id`, `agent_id`
or `dataset_id` - plus optional `focus`, `title` and `max_rows` (1-25,
default 10). The briefs land as an ordinary dataset, so
`GET /v1/datasets/{id}/rows`, `/export` and `/contacts` read them
unchanged. More than 5 profiles research in the background and answer
`202`. If none of them could be researched, the answer is `200` with
`data: null`, the reasons in `meta.skipped`, and every credit
returned.

**This endpoint can answer before the work is finished.** A small
collection runs inside the request and answers `200` with a `ready`
dataset. A big one, **or one whose size cannot be established up
front**, answers `202` with a `collecting` dataset: poll
`GET /v1/datasets/{id}` until `status` is `ready` (or `failed`) before
reading its rows. Only one background collection runs per account at a
time; a second one gets `409 collection_in_progress`.

If nothing matched the filters, the response is `200` with
`data: null` and a `note`: no dataset is created for an empty result,
and the daily collection slot is given back.

`my_posts` and `my_replies` read the account's own synced posts: they
take no `target`, always finish inside the request, and ignore the
profile filters (the response `note` says so rather than dropping them
silently).

**Cost.** Every collection counts against a limit of 10 per day for
the account, shared with the collections Ask SuperX runs in the app
(`429 collection_quota_exceeded`). Collections that read X also spend
the enrichment allowance: one unit for the size estimate, plus the
pages the walk is expected to need, all charged before the walk
starts so `X-RateLimit-Remaining` stays honest about work that
outlives the response. Own-content collections spend nothing.

**Research is charged in AI credits, not collections.**
`source: "research"` costs a flat 1 credit per profile ACTUALLY
researched: profiles that could not be researched come back in
`meta.skipped` and their credits are returned. It spends one of the
plan's daily profile-research runs (`429 ai_action_limited` with
`scope: "account"`) and a slice of a platform-wide fair-use ceiling on
live-data actions shared by every SuperX account
(`429 ai_action_limited` with `scope: "platform"`), and it does NOT
count against the 10 collections a day. An empty credit pool is
`429 ai_credits_exhausted`. What was charged comes back in
`meta.credits_charged` and in the `X-Credits-*` headers on the `200`;
a background research run settles when it finishes, so its `202`
carries neither, and the figure lands on `GET /v1/me` and your ledger.

Datasets created here appear alongside the ones Ask SuperX built, and
are kept for 30 days.

Send an `Idempotency-Key` header to make retries safe: the same key
with the same body replays the first response with
`Idempotency-Replayed: true`, and the same key with a different body
is rejected with `409 idempotency_key_reuse`.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/datasets
openapi: 3.1.0
info:
  title: SuperX API
  version: 1.0.0
  description: >
    The SuperX public API: your Twitter/X content, analytics, audience and

    scheduling data over REST.


    All endpoints require an API key (`Authorization: Bearer sxk_...`) except

    `GET /v1/docs`. Keys are created in the SuperX app under Account > API / MCP
    / CLI

    and are server-side secrets.


    Timestamps are UTC ISO-8601 in both directions; inputs must carry an
    explicit

    `Z` or numeric offset. Every authenticated response carries

    `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`
    headers.
  contact:
    name: SuperX
    url: https://superx.so
servers:
  - url: https://api.superx.so
security:
  - apiKey: []
tags:
  - name: Identity
  - name: Content
  - name: Analytics
  - name: Inspiration
  - name: Audience
  - name: Contact Lists
  - name: Signals
  - name: Datasets
    description: |
      The audience collections Ask SuperX builds in the app: the repliers,
      quoters or reposters of a post, the members of an X list, your own
      posts or replies, or a research brief.

      Collect a new one with `POST /v1/datasets` (a big collection answers
      202 and finishes in the background), read it, export it, or copy the
      people in it into a contact list you created. Datasets are kept for 30
      days, and the ones Ask SuperX builds in the app show up here too.
  - name: Engage
    description: |
      The account's saved Engage feeds and the candidate posts in them, for
      an agent to score and a person to act on.

      Read-only by design. These endpoints return feed candidates for a
      person to review; replies are written and sent by a person in the
      SuperX app, which is why there is no reply endpoint here. Sending
      spammy, automated, or AI-generated replies that read as inauthentic
      may get your X account suspended under X's inauthentic-behavior
      rules and your SuperX account terminated. AI suggestions must be
      reviewed and meaningfully edited before they are sent, and you are
      solely responsible for what you post. Reply activity is logged and
      may be audited.
  - name: X Lookups
    description: |
      Read any public post, its top replies, any public profile, or an
      account's latest posts LIVE from X, rather than from SuperX's stored
      data.

      These are owner-scoped, so none of them take an `account_id`: nothing
      about a public lookup is per-X-account. They cost the tighter
      live-enrichment allowance and share a daily allowance of 300 live
      lookups with Ask SuperX inside the app, so use them for what you
      actually need rather than to sweep. Repeat lookups within 15 minutes
      may be served from a short-lived server-side cache.
  - name: Tools
    description: |
      The composer helpers from the SuperX app, over the API: rewrite a post,
      edit one selected piece of it, apply one preset rewrite, check a claim,
      or compare two versions.

      Every one of them returns TEXT and writes nothing. Nothing here is
      posted, scheduled or sent; save what you keep with
      `POST /v1/posts/draft` or `POST /v1/scheduled-posts`. They cost AI
      credits, measured from the models' real cost, and spend no live-data
      allowance.
  - name: Media
  - name: Workers
    description: |
      The posts the account's Workers have written, and the three things a
      person does with one: save it as a draft, schedule it, or dismiss it.

      A Worker is a scheduled AI writer set up in the SuperX app. Creating,
      editing and running one stays in the app, so there is no create or run
      endpoint here. What a run produces lands in the app's "To review" queue
      and is read with `GET /v1/workers/suggestions`.

      NOTHING HERE POSTS TO X. Drafting or scheduling a suggestion writes a
      post the user can see in the app and on `GET /v1/scheduled-posts`,
      exactly like the app's own buttons. AI output must be reviewed and
      meaningfully edited by a person before it goes out, so show the text to
      the user and confirm the time before you schedule anything.
  - name: Scheduling
  - name: Tags
  - name: Context
  - name: Queue
  - name: DMs
    description: |
      Queue direct messages into the account's own DM pipeline, and read or
      cancel what is waiting.

      NOTHING HERE SENDS A MESSAGE. `POST /v1/dm/campaigns` enqueues; the
      SuperX app's scheduler is what sends, within the account's daily and
      monthly DM limits, so the create response is counts, not deliveries.
      Unsent messages can be cancelled until the scheduler picks them up.

      You are responsible for the messages you queue. Unsolicited or
      automated bulk DMs may get an X account suspended under X's rules and
      a SuperX account terminated, so confirm the recipient list and the
      exact wording with the person on whose behalf you are queueing.
  - name: Articles
  - name: Meta
paths:
  /v1/datasets:
    post:
      tags:
        - Datasets
      summary: Collect an audience into a dataset
      description: |
        Collect the repliers, quote posters or reposters of a post, the
        members of a public X list, or the account's own posts or replies,
        into a stored dataset you can then read, export, or copy into a
        contact list.

        `source: "research"` is the exception: instead of walking an audience
        it reads people's recent posts and writes a short outreach brief for
        each one (what they do, what they are building, recent topics,
        personalization hooks that QUOTE their real posts, and an opener
        angle). Give it exactly one source - `handles`, `list_id`, `agent_id`
        or `dataset_id` - plus optional `focus`, `title` and `max_rows` (1-25,
        default 10). The briefs land as an ordinary dataset, so
        `GET /v1/datasets/{id}/rows`, `/export` and `/contacts` read them
        unchanged. More than 5 profiles research in the background and answer
        `202`. If none of them could be researched, the answer is `200` with
        `data: null`, the reasons in `meta.skipped`, and every credit
        returned.

        **This endpoint can answer before the work is finished.** A small
        collection runs inside the request and answers `200` with a `ready`
        dataset. A big one, **or one whose size cannot be established up
        front**, answers `202` with a `collecting` dataset: poll
        `GET /v1/datasets/{id}` until `status` is `ready` (or `failed`) before
        reading its rows. Only one background collection runs per account at a
        time; a second one gets `409 collection_in_progress`.

        If nothing matched the filters, the response is `200` with
        `data: null` and a `note`: no dataset is created for an empty result,
        and the daily collection slot is given back.

        `my_posts` and `my_replies` read the account's own synced posts: they
        take no `target`, always finish inside the request, and ignore the
        profile filters (the response `note` says so rather than dropping them
        silently).

        **Cost.** Every collection counts against a limit of 10 per day for
        the account, shared with the collections Ask SuperX runs in the app
        (`429 collection_quota_exceeded`). Collections that read X also spend
        the enrichment allowance: one unit for the size estimate, plus the
        pages the walk is expected to need, all charged before the walk
        starts so `X-RateLimit-Remaining` stays honest about work that
        outlives the response. Own-content collections spend nothing.

        **Research is charged in AI credits, not collections.**
        `source: "research"` costs a flat 1 credit per profile ACTUALLY
        researched: profiles that could not be researched come back in
        `meta.skipped` and their credits are returned. It spends one of the
        plan's daily profile-research runs (`429 ai_action_limited` with
        `scope: "account"`) and a slice of a platform-wide fair-use ceiling on
        live-data actions shared by every SuperX account
        (`429 ai_action_limited` with `scope: "platform"`), and it does NOT
        count against the 10 collections a day. An empty credit pool is
        `429 ai_credits_exhausted`. What was charged comes back in
        `meta.credits_charged` and in the `X-Credits-*` headers on the `200`;
        a background research run settles when it finishes, so its `202`
        carries neither, and the figure lands on `GET /v1/me` and your ledger.

        Datasets created here appear alongside the ones Ask SuperX built, and
        are kept for 30 days.

        Send an `Idempotency-Key` header to make retries safe: the same key
        with the same body replays the first response with
        `Idempotency-Replayed: true`, and the same key with a different body
        is rejected with `409 idempotency_key_reuse`.
      operationId: createDataset
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDatasetRequest'
            example:
              source: repliers
              target: https://x.com/robj3d3/status/1234567890123456789
              title: Repliers to my launch post
              max_rows: 500
              filters:
                min_followers: 500
                require_can_dm: true
      responses:
        '200':
          description: |
            The collection finished inside the request. `data` is null when
            nothing matched, with a `note` explaining why. A research run
            adds `meta` (what was requested, what was researched, what it
            cost) and the `X-Credits-*` headers.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credits-Charged:
              description: Credits kept for this call. Research runs only.
              schema:
                type: integer
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
            X-Credits-Reset:
              $ref: '#/components/headers/X-Credits-Reset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/DatasetDetail'
                    nullable: true
                  meta:
                    type: object
                    description: Research runs only. Absent on audience collections.
                    properties:
                      requested:
                        type: integer
                        description: Profiles the source resolved to.
                      researched:
                        type: integer
                        description: Profiles a brief was actually written for.
                      credits_charged:
                        type: integer
                        description: Credits kept, one per researched profile.
                      skipped:
                        type: array
                        description: Profiles that produced no brief, with the reason.
                        items:
                          type: object
                          properties:
                            handle:
                              type: string
                              nullable: true
                            reason:
                              type: string
                  note:
                    type: string
                    description: >-
                      Present when something needs saying, such as an empty
                      result or ignored filters.
              examples:
                collection:
                  value:
                    data:
                      id: VKcPAVU1FSkx4kzPuehGK
                      title: Repliers to my launch post
                      source: repliers
                      status: ready
                      target_ref: '1234567890123456789'
                      x_account_id: '1178367350552305665'
                      ask_chat_id: null
                      columns:
                        - handle
                        - name
                        - bio
                        - website
                        - followers
                        - following
                        - location
                        - can_dm
                        - verified
                      row_count: 64
                      scanned_count: 71
                      total_estimate: 71
                      coverage_complete: true
                      has_people: true
                      created_at: '2026-09-10T10:12:00.000Z'
                      expires_at: '2026-10-10T10:12:00.000Z'
                      coverage: Includes all 71 direct replies to the post.
                      filters:
                        minFollowers: 500
                        requireCanDm: true
                research:
                  summary: source "research", 3 profiles, finished in the request
                  value:
                    data:
                      id: Rk2PAVU1FSkx4kzPuehGK
                      title: Outreach briefs (3 profiles)
                      source: research
                      status: ready
                      target_ref: handles
                      x_account_id: '1178367350552305665'
                      ask_chat_id: null
                      columns:
                        - handle
                        - name
                        - followers
                        - can_dm
                        - bio
                        - does
                        - building
                        - recent_topics
                        - hooks
                        - opener_angle
                      row_count: 3
                      scanned_count: 3
                      total_estimate: 3
                      coverage_complete: true
                      has_people: true
                      created_at: '2026-09-10T10:12:00.000Z'
                      expires_at: '2026-10-10T10:12:00.000Z'
                      coverage: Researched all 3 requested profiles.
                    meta:
                      requested: 3
                      researched: 3
                      credits_charged: 3
                    note: >-
                      Every hook quotes one of the person's real posts; proposed
                      quotes that failed the verbatim check were dropped. Briefs
                      are text for a person to act on: read the full rows with
                      GET /v1/datasets/{id}/rows.
        '202':
          description: |
            The collection, or the research run, started in the background.
            Poll `GET /v1/datasets/{id}` until `status` is `ready`. Research
            settles its credits when the run finishes, so no `X-Credits-*`
            header is sent here and `meta` carries no charge.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DatasetDetail'
                  meta:
                    type: object
                    description: Research runs only. Absent on audience collections.
                    properties:
                      requested:
                        type: integer
                        description: Profiles the source resolved to.
                      skipped:
                        type: array
                        description: >-
                          Profiles rejected before the run started, with the
                          reason.
                        items:
                          type: object
                          properties:
                            handle:
                              type: string
                              nullable: true
                            reason:
                              type: string
                  note:
                    type: string
              example:
                data:
                  id: Jc9PxVU1FSkx4kzPuehGK
                  title: Repliers to my launch post
                  source: repliers
                  status: collecting
                  target_ref: '1234567890123456789'
                  x_account_id: '1178367350552305665'
                  ask_chat_id: null
                  columns:
                    - handle
                    - name
                    - bio
                    - website
                    - followers
                    - following
                    - location
                    - can_dm
                    - verified
                  row_count: 0
                  scanned_count: 0
                  total_estimate: 4200
                  coverage_complete: false
                  has_people: true
                  created_at: '2026-09-10T10:12:00.000Z'
                  expires_at: '2026-10-10T10:12:00.000Z'
                  progress:
                    page: 0
                    scanned: 0
                    matched: 0
                    total_estimate: 4200
                    started_at: '2026-09-10T10:12:00.000Z'
                note: >-
                  Collecting in the background; poll GET /v1/datasets/{id} until
                  status is ready.
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/WriteForbidden'
        '404':
          description: |
            `account_not_found` when `account_id` is not one of your accounts.
            On `source: "research"` the source id is named too:
            `list_not_found`, `agent_not_found`, or `not_found` for a dataset
            id that is unknown or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                account_not_found:
                  value:
                    error:
                      code: account_not_found
                      message: No account with that id belongs to this key
                list_not_found:
                  value:
                    error:
                      code: list_not_found
                      message: No contact list with that id belongs to this account.
                agent_not_found:
                  value:
                    error:
                      code: agent_not_found
                      message: No signal agent with that id belongs to this account.
                not_found:
                  value:
                    error:
                      code: not_found
                      message: >-
                        No dataset with that id. Use the dataset_id from a
                        collect_audience or refine_dataset result.
        '409':
          description: |
            Another background collection is still running for this account
            (`collection_in_progress`), the `Idempotency-Key` was reused with a
            different request body (`idempotency_key_reuse`), or the dataset a
            research run was pointed at is not ready yet
            (`dataset_not_ready`, carrying `dataset_status`). None of them
            started anything.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                dataset_not_ready:
                  value:
                    error:
                      code: dataset_not_ready
                      message: >-
                        The source dataset is not ready yet. Poll GET
                        /v1/datasets/{id} until status is ready, then research
                        it.
                      dataset_status: collecting
                collection_in_progress:
                  value:
                    error:
                      code: collection_in_progress
                      message: >-
                        Another audience collection is still running for this
                        account. Poll it with GET /v1/datasets/{id} and start
                        this one when it finishes.
                idempotency_key_reuse:
                  value:
                    error:
                      code: idempotency_key_reuse
                      message: >-
                        This Idempotency-Key was already used with a different
                        request body.
        '429':
          description: |
            Read `error.code`. A collection is refused when today's 10
            collections are used up (`collection_quota_exceeded`, shared with
            Ask SuperX in the app) or the enrichment allowance the walk needs
            is exhausted (`rate_limited`). A research run is refused when the
            plan's daily research runs are used up (`ai_action_limited` with
            `scope: "account"`), when the platform-wide fair-use ceiling on
            live-data actions is reached (`ai_action_limited` with
            `scope: "platform"`, shared by every account and not about your
            own quota), or when the AI credit pool is empty
            (`ai_credits_exhausted`). Every flavour carries `Retry-After`, and
            the two `ai_action_limited` ones carry `reset_at` (unix seconds)
            as well. Nothing was started or charged.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
            X-Credits-Remaining:
              description: Credits left in the pool. Sent only on `ai_credits_exhausted`.
              schema:
                type: integer
            X-Credits-Reset:
              description: >-
                Unix seconds when the pool refills. Sent only on
                `ai_credits_exhausted`.
              schema:
                type: integer
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorEnvelope'
                  - $ref: '#/components/schemas/ActionLimitedError'
                  - $ref: '#/components/schemas/CreditExhaustedError'
              examples:
                collection_quota_exceeded:
                  value:
                    error:
                      code: collection_quota_exceeded
                      message: >-
                        You have used today's audience collections. Existing
                        datasets still read and export; the limit resets at
                        midnight UTC.
                      retry_after: 20400
                      limit: 10
                      reset_at: 1789430400
                rate_limited:
                  value:
                    error:
                      code: rate_limited
                      message: >-
                        Rate limit exceeded for the Pro plan (5 enrichment
                        requests/min). This collection needs 4 lookups.
                      retry_after: 42
                ai_action_limited_account:
                  summary: >-
                    source "research", the plan's daily research runs are used
                    up
                  value:
                    error:
                      code: ai_action_limited
                      message: >-
                        Daily limit reached for this action on your plan (10
                        profile research runs per day). It resets at midnight
                        UTC.
                      scope: account
                      retry_after: 20400
                      reset_at: 1789430400
                ai_action_limited_platform:
                  summary: source "research", the shared fair-use ceiling is reached
                  value:
                    error:
                      code: ai_action_limited
                      message: >-
                        The platform-wide fair-use ceiling for live-data actions
                        has been reached for today. It resets at midnight UTC.
                      scope: platform
                      retry_after: 20400
                      reset_at: 1789430400
                ai_credits_exhausted:
                  summary: source "research", not enough credits for the batch
                  value:
                    error:
                      code: ai_credits_exhausted
                      message: Not enough AI credits. Resets in 5 hours.
                      credits_required: 10
                      credits_remaining: 4
                      reset_at: 1790812800
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '503':
          $ref: '#/components/responses/AccountsUnavailable'
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Unique key (max 64 characters) for safe retries. Replays carry the
        "Idempotency-Replayed" response header set to "true". Keys are retained
        for 24 hours.
      schema:
        type: string
        maxLength: 64
  schemas:
    CreateDatasetRequest:
      type: object
      required:
        - source
      properties:
        source:
          type: string
          enum:
            - repliers
            - quoters
            - reposters
            - list_members
            - my_replies
            - my_posts
            - research
          description: |
            Who to collect. repliers, quoters and reposters need a post
            target; list_members needs a public X list; my_posts and
            my_replies read the account's own synced posts and take no
            target.

            `research` is different: it reads people's recent posts and writes
            an outreach brief per person instead of walking an audience. It
            takes exactly ONE of `handles`, `list_id`, `agent_id` or
            `dataset_id`, plus `focus` and a `max_rows` of 1-25 (default 10),
            and ignores `target` and `filters`.
        target:
          type: string
          description: |
            An x.com post URL or numeric post id, or for list_members an
            x.com list URL (/i/lists/<id>) or numeric list id. Omit for
            my_posts and my_replies.
        title:
          type: string
          maxLength: 120
          description: Title for the dataset. A sensible one is generated when omitted.
        max_rows:
          type: integer
          minimum: 1
          maximum: 1000
          default: 500
          description: |
            Rows to collect at most. For `source: "research"` the range is
            1-25 and the default is 10 (one profile per row).
        account_id:
          type: string
          description: Which of your accounts to collect as. Omit for the main account.
        handles:
          type: array
          maxItems: 25
          items:
            type: string
          description: >-
            `research` only: research these X handles (with or without the @).
            Exactly one source field.
        list_id:
          type: string
          description: >-
            `research` only: research the members of this contact list. Exactly
            one source field.
        agent_id:
          type: integer
          minimum: 1
          description: >-
            `research` only: research this signal agent's leads. Exactly one
            source field.
        dataset_id:
          type: string
          description: >-
            `research` only: research the people in this dataset. Exactly one
            source field.
        focus:
          type: string
          maxLength: 300
          description: >-
            `research` only: an optional steer, e.g. "founders who might need
            audience-growth tooling".
        filters:
          type: object
          properties:
            keywords:
              type: array
              items:
                type: string
              description: |
                Keep only rows whose reply, quote or (own content) post text
                contains one of these. Not applied to reposters or list
                members, whose rows carry no text.
            bio_keywords:
              type: array
              items:
                type: string
              description: >-
                Keep only people whose X bio contains one of these. Ignored for
                own content.
            min_followers:
              type: integer
              minimum: 0
              description: >-
                Keep only people with at least this many followers. Ignored for
                own content.
            require_website:
              type: boolean
              description: >-
                Keep only people with a website link in their profile. Ignored
                for own content.
            require_can_dm:
              type: boolean
              description: Keep only people whose DMs look open. Ignored for own content.
            since_days:
              type: integer
              minimum: 1
              description: 'Own content only: keep posts from the last N days.'
            sort:
              type: string
              enum:
                - recent
                - likes
                - impressions
              description: >-
                Own content only: which posts to keep when max_rows cuts the
                list.
    DatasetDetail:
      allOf:
        - $ref: '#/components/schemas/Dataset'
        - type: object
          properties:
            coverage:
              type: string
              description: An honest one-line coverage statement. Ready datasets only.
            filters:
              type: object
              additionalProperties: true
              description: The filters the collection applied. Ready datasets only.
            progress:
              $ref: '#/components/schemas/DatasetProgress'
            failure_note:
              type: string
              description: Why the collection stopped. Failed datasets only.
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
            message:
              type: string
            retry_after:
              type: integer
              description: Seconds to wait before retrying (rate-limit errors only).
    ActionLimitedError:
      type: object
      required:
        - error
      description: |
        The `429 ai_action_limited` body: a per-day cap on how many times an
        expensive action can run, counted separately from the credit pool and
        from the request rate limits. Sent with a `Retry-After` header. Caps
        reset at midnight UTC.

        Read `scope` before telling a user anything: `account` means the
        plan's own daily cap for that action, `platform` means the
        platform-wide fair-use ceiling on live-data actions, which is shared
        with every SuperX account. A `platform` refusal is not about this
        account's quota, so wait and retry rather than reporting the account
        as out.
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - ai_action_limited
            message:
              type: string
            scope:
              type: string
              enum:
                - account
                - platform
              description: |
                Whose ceiling refused the call: this account's own per-day cap
                for the action, or the platform-wide fair-use ceiling on
                live-data actions.
            retry_after:
              type: integer
              description: Seconds to wait before the cap resets.
            reset_at:
              type: integer
              description: Unix seconds when the cap resets (the next UTC midnight).
    CreditExhaustedError:
      type: object
      required:
        - error
      description: |
        The `429 ai_credits_exhausted` body: the standard envelope plus the
        credit counters. Sent with a `Retry-After` header and the
        `X-Credits-Remaining` / `X-Credits-Reset` headers. `code` is stable;
        the counters are additive fields, safe to ignore.
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - ai_credits_exhausted
            message:
              type: string
            credits_required:
              type: integer
              description: Credits the call needed.
            credits_remaining:
              type: integer
              nullable: true
              description: Credits available when it was refused.
            reset_at:
              type: integer
              nullable: true
              description: Unix timestamp (seconds) when the pool refills.
    Dataset:
      type: object
      properties:
        id:
          type: string
          description: Pass this to the other dataset endpoints.
        title:
          type: string
        source:
          type: string
          enum:
            - repliers
            - quoters
            - reposters
            - list_members
            - my_replies
            - my_posts
            - research
          description: >-
            What the collection walked. `my_posts` / `my_replies` hold posts,
            everything else holds people.
        status:
          type: string
          enum:
            - ready
            - collecting
            - failed
        target_ref:
          type: string
          description: >-
            What the collection was pointed at - a post id, an X list id, or
            free text for a research brief.
        x_account_id:
          type: string
          description: >-
            The X account the collection ran as. Not a filter - datasets belong
            to the key owner, not to one account.
        ask_chat_id:
          type: string
          nullable: true
          description: The Ask SuperX chat the dataset was built in, when known.
        columns:
          type: array
          items:
            type: string
          description: The row keys this dataset carries, in export order.
        row_count:
          type: integer
        scanned_count:
          type: integer
          description: >-
            What the walk went through (people datasets), or the rows kept
            (own-content datasets).
        total_estimate:
          type: integer
          nullable: true
        coverage_complete:
          type: boolean
          description: False when the collection hit its row cap before the end.
        has_people:
          type: boolean
          description: >-
            False for own-content datasets - there is nobody in them to add to a
            contact list.
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: Datasets are kept for 30 days.
    DatasetProgress:
      type: object
      description: Live progress of a collection that is still running.
      properties:
        page:
          type: integer
        scanned:
          type: integer
        matched:
          type: integer
        total_estimate:
          type: integer
          nullable: true
        started_at:
          type: string
          format: date-time
    LegacyErrorEnvelope:
      type: object
      required:
        - error
      description: |
        Legacy shape used ONLY by the shared subscription middleware: a plain
        string `error` field. Seen on `403` when the subscription has lapsed
        (string starts with "subscription_required:") and on `500` when
        subscription verification fails (string starts with "internal_error:").
      properties:
        error:
          type: string
          example: >-
            subscription_required: The SuperX API requires an active
            subscription
  headers:
    X-RateLimit-Limit:
      description: The limit of the rate window closest to exhaustion.
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests remaining in that window.
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix timestamp (seconds) when that window resets.
      schema:
        type: integer
    X-Credits-Remaining:
      description: AI credits left in the account's pool, bonus credits included.
      schema:
        type: integer
    X-Credits-Reset:
      description: Unix timestamp (seconds) when the AI credit pool refills.
      schema:
        type: integer
  responses:
    InvalidParameter:
      description: A parameter is missing, malformed, or out of range.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: invalid_parameter
              message: since must be a UTC ISO-8601 timestamp
    Unauthorized:
      description: >-
        Missing/malformed Authorization header (`unauthorized`) or an
        unknown/revoked key (`invalid_api_key`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: invalid_api_key
              message: Unknown or revoked API key
    WriteForbidden:
      description: >-
        Read-only key (`insufficient_scope`), an account shared with you
        (`writes_main_account_only`; shared accounts are read-only through the
        API, your own linked accounts are not), or a lapsed subscription (legacy
        string envelope).
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/ErrorEnvelope'
              - $ref: '#/components/schemas/LegacyErrorEnvelope'
          example:
            error:
              code: insufficient_scope
              message: >-
                This API key is read-only. Create a key with the write scope to
                use this endpoint.
    InternalError:
      description: >-
        Unexpected server error. May also use the legacy string envelope when
        subscription verification fails.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/ErrorEnvelope'
              - $ref: '#/components/schemas/LegacyErrorEnvelope'
          example:
            error:
              code: internal_error
              message: Failed to fetch posts
    UpstreamError:
      description: A dependent SuperX service returned an unexpected response.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: upstream_error
              message: Failed to fetch scheduled posts. Try again shortly.
    AccountsUnavailable:
      description: >-
        Linked-account verification is temporarily unavailable (fail closed).
        The main account keeps working.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: accounts_unavailable
              message: >-
                Account information is temporarily unavailable. Try again
                shortly.
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        A SuperX API key ("sxk_..."), created in the SuperX app under Account >
        API / MCP / CLI. Keys are server-side secrets.

````