> ## 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.

# Sort recent posts on a topic into Read, Pass or Not sure

> Searches recent public posts for a topic and sorts them into three
lanes: `read`, `pass` and `unsure`. Up to 40 posts a run.

Every post comes back with its lane, a `pct`, a `kind` (`insight`,
`story`, `data`, `progress`, `news`, `question`, `intro`, `launch`,
`joke` or `opinion`) and the `answers` behind the call: is it
specific and informative, does it say something a regular reader of
the topic has not already heard, is it engagement bait, is it a plug,
is it a platitude, and is there room to add something in a reply.
Each answer is a probability between 0 and 1.

**`pct` is how clear the call was, not how good the post is.** On
`read` and `pass` it runs 50 to 99, so a post that is confidently not
worth reading is a `pass` at 99; on `unsure` it is the raw
worth-reading score. Always read it together with `lane`, and never
rank posts by it across lanes.

The searches ask for original posts, English only, with a floor of 30
likes. Replies and reposts are never returned. None of those three is
settable by the caller: the like floor is the one the lane thresholds
were tuned against, so a very new or very small topic can come back
empty. Treat the floor as the search's request rather than a
guarantee about `counts.likes`, since upstream applies it against its
own snapshot and a few posts do come back under it.

**What is judged is the TEXT of a post.** No author, handle, avatar
or follower count is in what the model reads, so a result is a call
on the writing, never a rating of a person or an account.

`query` is searched as typed. A plain multi-word query with no quotes
or operators is ALSO searched as an exact phrase, so a topic catches
both the broad matches and the posts that name it outright; a query
that already carries quotes, a field lookup, `OR` / `AND` or an
exclusion is searched exactly once, as written.

`max_age_days` (1 to 7, default 3) is how far back to look.
`posts_searched` counts the distinct posts left after duplicates and
posts marked sensitive upstream are dropped, and before the 40-post
cap; `receipt` reports what the run cost in posts judged, questions
answered, milliseconds and US dollars of model spend.

A post the safety question flags is dropped before it is returned,
and so is one whose judgement failed, so `posts` can be shorter than
`receipt.posts`.

Two fields come back as an empty string rather than being omitted
when upstream gave nothing usable: `kind` (no readable label) and
`created_at` (no readable timestamp). Treat an empty string as
"unknown" rather than assuming the field is always populated.

Nothing is posted, saved or sent.

A flat 2 credits per run, and each plan has a daily cap on how many
runs it gets. The searches spend the key owner's own live X
allowance, the one shared with Ask SuperX in the app: 1 lookup for a
single-word query, 2 for a multi-word one. So a 429 here can also
carry `lookup_quota_exceeded`, and on THIS endpoint that error
carries a `code` and a `message` only, with no `retry_after` field
and no `Retry-After` header: it resets at midnight UTC, except for
the short-lived "could not be verified" case, which is worth one
retry a minute later. A run that finds nothing to judge returns 200
with an empty `posts` array and charges no credits, but it still
counts against the daily cap. Needs a key with the `write` scope.

A 503 here is `upstream_unavailable` when live X data is throttling
SuperX, or `accounts_unavailable` when linked-account verification is
briefly unreadable. Both are safe to retry.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/posts/triage
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/posts/triage:
    post:
      tags:
        - Tools
      summary: Sort recent posts on a topic into Read, Pass or Not sure
      description: |
        Searches recent public posts for a topic and sorts them into three
        lanes: `read`, `pass` and `unsure`. Up to 40 posts a run.

        Every post comes back with its lane, a `pct`, a `kind` (`insight`,
        `story`, `data`, `progress`, `news`, `question`, `intro`, `launch`,
        `joke` or `opinion`) and the `answers` behind the call: is it
        specific and informative, does it say something a regular reader of
        the topic has not already heard, is it engagement bait, is it a plug,
        is it a platitude, and is there room to add something in a reply.
        Each answer is a probability between 0 and 1.

        **`pct` is how clear the call was, not how good the post is.** On
        `read` and `pass` it runs 50 to 99, so a post that is confidently not
        worth reading is a `pass` at 99; on `unsure` it is the raw
        worth-reading score. Always read it together with `lane`, and never
        rank posts by it across lanes.

        The searches ask for original posts, English only, with a floor of 30
        likes. Replies and reposts are never returned. None of those three is
        settable by the caller: the like floor is the one the lane thresholds
        were tuned against, so a very new or very small topic can come back
        empty. Treat the floor as the search's request rather than a
        guarantee about `counts.likes`, since upstream applies it against its
        own snapshot and a few posts do come back under it.

        **What is judged is the TEXT of a post.** No author, handle, avatar
        or follower count is in what the model reads, so a result is a call
        on the writing, never a rating of a person or an account.

        `query` is searched as typed. A plain multi-word query with no quotes
        or operators is ALSO searched as an exact phrase, so a topic catches
        both the broad matches and the posts that name it outright; a query
        that already carries quotes, a field lookup, `OR` / `AND` or an
        exclusion is searched exactly once, as written.

        `max_age_days` (1 to 7, default 3) is how far back to look.
        `posts_searched` counts the distinct posts left after duplicates and
        posts marked sensitive upstream are dropped, and before the 40-post
        cap; `receipt` reports what the run cost in posts judged, questions
        answered, milliseconds and US dollars of model spend.

        A post the safety question flags is dropped before it is returned,
        and so is one whose judgement failed, so `posts` can be shorter than
        `receipt.posts`.

        Two fields come back as an empty string rather than being omitted
        when upstream gave nothing usable: `kind` (no readable label) and
        `created_at` (no readable timestamp). Treat an empty string as
        "unknown" rather than assuming the field is always populated.

        Nothing is posted, saved or sent.

        A flat 2 credits per run, and each plan has a daily cap on how many
        runs it gets. The searches spend the key owner's own live X
        allowance, the one shared with Ask SuperX in the app: 1 lookup for a
        single-word query, 2 for a multi-word one. So a 429 here can also
        carry `lookup_quota_exceeded`, and on THIS endpoint that error
        carries a `code` and a `message` only, with no `retry_after` field
        and no `Retry-After` header: it resets at midnight UTC, except for
        the short-lived "could not be verified" case, which is worth one
        retry a minute later. A run that finds nothing to judge returns 200
        with an empty `posts` array and charges no credits, but it still
        counts against the daily cap. Needs a key with the `write` scope.

        A 503 here is `upstream_unavailable` when live X data is throttling
        SuperX, or `accounts_unavailable` when linked-account verification is
        briefly unreadable. Both are safe to retry.
      operationId: postTriage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TriageRequest'
            example:
              query: coding agents
              max_age_days: 3
      responses:
        '200':
          description: The sorted posts and the run's receipt.
          headers:
            X-Credits-Charged:
              $ref: '#/components/headers/X-Credits-Charged'
            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:
                    $ref: '#/components/schemas/TriageData'
                  meta:
                    $ref: '#/components/schemas/ToolMeta'
              example:
                data:
                  query: coding agents
                  max_age_days: 3
                  posts_searched: 34
                  posts:
                    - id: '2100458357006205177'
                      url: https://x.com/example/status/2100458357006205177
                      text: >-
                        We moved our test suite onto a coding agent for a week.
                        It fixed 41 of 60 flaky tests and broke 3, and the 3 it
                        broke were all in the same file.
                      created_at: '2026-09-17T05:34:31.000Z'
                      author:
                        name: Example Person
                        handle: example
                        avatar: >-
                          https://pbs.twimg.com/profile_images/1466482275361562626/jP_Sgq2A_400x400.jpg
                        verified: true
                      media: []
                      counts:
                        likes: 1652
                        replies: 114
                        reposts: 382
                        views: 350188
                      lane: read
                      pct: 79
                      kind: data
                      answers:
                        r_specific: 0.88
                        r_new: 0.74
                        r_bait: 0.03
                        r_promo: 0.02
                        r_platitude: 0.02
                        r_reply_room: 0.66
                        farm: 0
                  receipt:
                    posts: 34
                    questions_answered: 272
                    ms: 2637
                    usd: 0.0029
                meta:
                  credits_charged: 2
                  account_id: '1178367350552305665'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/WriteForbidden'
        '429':
          $ref: '#/components/responses/AiActionLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '503':
          description: |
            `upstream_unavailable` when live X data is throttling SuperX, or
            `accounts_unavailable` when linked-account verification is briefly
            unreadable. Both are safe to retry; nothing was charged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                error:
                  code: upstream_unavailable
                  message: The post search is temporarily unavailable. Retry shortly.
components:
  schemas:
    TriageRequest:
      type: object
      required:
        - query
      properties:
        query:
          type: string
          minLength: 2
          maxLength: 80
          description: |
            What to search for, on one line: a topic, a phrase, a hashtag, or
            a search expression with quotes or operators. A plain multi-word
            query is also searched as an exact phrase.
        max_age_days:
          type: integer
          minimum: 1
          maximum: 7
          default: 3
          description: How far back to search, in days.
        account_id:
          type: string
    TriageData:
      type: object
      properties:
        query:
          type: string
          description: The query as searched, trimmed.
        max_age_days:
          type: integer
        posts_searched:
          type: integer
          description: Distinct posts the searches returned, before the 40-post cap.
        posts:
          type: array
          description: >-
            The judged posts, most liked first. Can be shorter than
            `receipt.posts`.
          items:
            $ref: '#/components/schemas/TriagedPost'
        receipt:
          $ref: '#/components/schemas/TriageReceipt'
    ToolMeta:
      type: object
      description: What the call cost and which account it ran as.
      properties:
        credits_charged:
          type: integer
          description: AI credits kept for this call, settled from the models' real cost.
        account_id:
          type: string
          description: The X account the call ran as.
    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).
    TriagedPost:
      type: object
      description: |
        One judged post. The lane, the percentage and the answers all come
        from the TEXT of the post: nothing about its author is in what the
        model reads.
      properties:
        id:
          type: string
        url:
          type: string
          description: Link to the post on X.
        text:
          type: string
        created_at:
          type: string
          description: |
            When the post went out, UTC ISO-8601. An EMPTY STRING when
            upstream gave no usable timestamp, so parse defensively rather
            than assuming a date is always there.
        author:
          type: object
          description: Who posted it, for display only. The model never saw any of it.
          properties:
            name:
              type: string
            handle:
              type: string
            avatar:
              type: string
            verified:
              type: boolean
        media:
          type: array
          items:
            $ref: '#/components/schemas/TriagedPostMedia'
        counts:
          type: object
          properties:
            likes:
              type: integer
            replies:
              type: integer
            reposts:
              type: integer
              description: Reposts and quotes together.
            views:
              type: integer
        lane:
          type: string
          enum:
            - read
            - pass
            - unsure
          description: |
            `read` is worth a reader's time, `pass` is not, and `unsure` is
            the middle the rule refuses to call either way.
        pct:
          type: integer
          description: |
            **How CLEAR the call was, not how good the post is. Always read it
            together with `lane`, never on its own.**

            On `read` and on `pass` it runs 50 to 99, measuring the distance
            from the threshold in whichever direction the lane went: a post
            that is confidently NOT worth reading is a `pass` at 99, exactly
            as a clearly worthwhile post is a `read` at 99. An anti-signal
            strong enough to fail a post on its own is always 99.

            On `unsure` it is the raw worth-reading score instead, which is
            what makes that lane interesting.

            So never sort or compare posts by `pct` across lanes, and never
            render it as a quality score.
        kind:
          type: string
          enum:
            - ''
            - insight
            - story
            - data
            - progress
            - news
            - question
            - intro
            - launch
            - joke
            - opinion
          description: |
            What sort of post it is. An EMPTY STRING when no readable label
            came back, so treat that as "unknown" rather than as a category.
        answers:
          type: object
          description: |
            The yes/no questions behind the call, each a probability from 0 to
            1. Quote these when explaining a lane rather than inventing a
            reason.
          properties:
            r_specific:
              type: number
              description: >-
                Names a particular that carries information (a result, a
                mechanism, a mistake and its cost).
            r_new:
              type: number
              description: >-
                Says something a regular reader of this topic has not already
                heard many times.
            r_bait:
              type: number
              description: >-
                Engagement bait: reply, like, follow or connect asks, or a
                cliffhanger with no content.
            r_promo:
              type: number
              description: Primarily sells, launches or plugs something.
            r_platitude:
              type: number
              description: Motivational or aphoristic filler with no information.
            r_reply_room:
              type: number
              description: >-
                A knowledgeable reader could add something worthwhile in a
                reply.
            farm:
              type: number
              description: |
                How much the post's own counts look like engagement farming
                (replies or reposts far out of step with likes). Read off the
                counts rather than the words, so it is the one figure here
                that is not a question about the text.
    TriageReceipt:
      type: object
      description: What the run actually cost.
      properties:
        posts:
          type: integer
          description: Posts judged, including any dropped before they were returned.
        questions_answered:
          type: integer
        ms:
          type: integer
          description: Wall-clock milliseconds spent judging the batch.
        usd:
          type: number
          description: Measured model spend for the run, in US dollars.
    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
    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.
    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).
    TriagedPostMedia:
      type: object
      description: |
        One image, video or gif attached to the post. Video and gif carry a
        still frame in `poster`; the playable file is not returned.
      properties:
        kind:
          type: string
          enum:
            - image
            - video
            - gif
        url:
          type: string
          description: The image, or the still frame for a video or a gif.
        poster:
          type: string
          description: Still frame. Present on video and gif only.
        w:
          type: integer
          description: Original width in pixels, omitted when upstream did not report it.
        h:
          type: integer
          description: Original height in pixels, omitted when upstream did not report it.
  headers:
    X-Credits-Charged:
      description: >-
        AI credits kept for this call. Sent on every endpoint that settles its
        charge before answering, which is all of them except the ones that
        answer 202 and finish in the background.
      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.
    AiActionLimited:
      description: |
        Rate limited (`rate_limited`, honor `Retry-After`), the AI credit pool
        is used up (`ai_credits_exhausted`, with the credit counters), or a
        daily cap on the action was hit (`ai_action_limited`).

        On `ai_action_limited`, `error.scope` says which ceiling. `account`
        is one of your own: the plan's daily cap for a metered action, the
        once-an-hour limit on style guide regeneration (where `reset_at` is an
        hour after the last run rather than midnight), the 3 live style-guide
        attempts a day, or the 20 page reads a day shared with the SuperX app.
        `platform` is the platform-wide fair-use ceiling on live-data actions,
        shared with every SuperX account: a `platform` refusal is not about
        this account's quota, so wait for `reset_at` and retry.

        The free endpoints send `X-Credits-Remaining` and `X-Credits-Reset` on
        this 429 as they do on a 200, since nothing was charged either way.
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Sent on every flavour of this 429.
          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/CreditExhaustedError'
              - $ref: '#/components/schemas/ActionLimitedError'
              - $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            ai_action_limited_account:
              value:
                error:
                  code: ai_action_limited
                  message: >-
                    Daily limit reached for this action on your plan (10 lead
                    searches per day). It resets at midnight UTC.
                  scope: account
                  retry_after: 20400
                  reset_at: 1789430400
            ai_action_limited_platform:
              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:
              value:
                error:
                  code: ai_credits_exhausted
                  message: Not enough AI credits. Resets in 5 hours.
                  credits_required: 2
                  credits_remaining: 0
                  reset_at: 1790812800
    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.
  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.

````