openapi: 3.1.0
info:
  title: BlockVectra Console API
  version: 0.2.0
  description: Console account management APIs for wallet login, identities, API
    keys, plans, balances, charges, top-ups, usage and reset chances. Product
    APIs (JSON-RPC, Data API and WebSocket) do not depend on these account
    management APIs.
servers:
  - url: https://console-api.blockvectra.com/v1
tags:
  - name: Auth
    description: Login, sign-up, and sessions
  - name: Me
    description: Current user, username, and identity linking
  - name: Keys
    description: API key creation, query, renaming, rotation, and revocation
  - name: Billing
    description: Balances, charges, top-ups, and usage
paths:
  /auth/siwe/challenge:
    post:
      tags:
        - Auth
      operationId: siweChallenge
      summary: Get SIWE message
      description: Generate an EIP-4361 message and single-use nonce valid for 5
        minutes. Programmatic clients omit Origin; browsers send their console
        Origin. Sign the returned message verbatim with an Ethereum mainnet EOA
        using EIP-191 personal_sign. Contract wallets and smart accounts are
        unsupported. purpose=login requires no session; purpose=link requires
        the current session.
      security:
        - {}
        - SessionToken: []
      requestBody:
        $ref: "#/components/requestBodies/SiweChallengeBody"
      responses:
        "200":
          description: Message and nonce.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiweChallengeResponse"
              examples:
                siweChallengeLogin:
                  $ref: "#/components/examples/siweChallengeLogin"
                siweChallengeLink:
                  $ref: "#/components/examples/siweChallengeLink"
        "400":
          description: Invalid `address` or `purpose` (including invalid EIP-55 checksum),
            present but unconfigured `Origin` header (including empty, `null`;
            omitting `Origin` is programmatic mode, not an error), unknown
            fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                siweChallengeBadChecksum:
                  $ref: "#/components/examples/siweChallengeBadChecksum"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "405":
          $ref: "#/components/responses/MethodNotAllowed"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/siwe/login:
    post:
      tags:
        - Auth
      operationId: siweLogin
      summary: SIWE login (first login creates account)
      description: >
        Submit raw challenge message and signature. Request `Origin` mode must
        match challenge: browser challenge must send same `Origin`
        (`https://<stored domain>`), programmatic challenge must omit `Origin`,
        otherwise returns 400 `siwe_invalid` (reason `domain_mismatch`).
        Verification failure returns 400 `siwe_invalid` with `reason` indicating
        failure type (expired, chain_mismatch, domain_mismatch, signature; nonce
        reuse classified as expired). Message must be submitted verbatim:
        modifying any content (including Chain ID) results in reason
        `signature`; `chain_mismatch` occurs only if configured Chain ID changes
        after challenge issuance. If wallet identity does not exist, an account
        is created (`account_created: true`, new account balance 0).


        Scope of support:

        - Supported: EOA wallets on Ethereum mainnet (Chain ID 1).

        - Not supported: Any other chains (Chain ID is embedded by server;
        submitting modified Chain ID fails byte-for-byte check returning 400
        `siwe_invalid`, reason `signature`), contract wallets (EIP-1271), and
        smart accounts (eth_call verification not supported; mismatch returns
        siwe_invalid).
      security: []
      requestBody:
        $ref: "#/components/requestBodies/SiweLoginBody"
      responses:
        "200":
          description: "Login successful. Session token appears only this once; response includes `Cache-Control: no-store`."
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
              examples:
                sessionNewAccount:
                  $ref: "#/components/examples/sessionNewAccount"
                sessionExistingAccount:
                  $ref: "#/components/examples/sessionExistingAccount"
        "400":
          description: Signature or message verification failed, unknown fields, invalid
            `ref`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                siweLoginInvalid:
                  $ref: "#/components/examples/siweLoginInvalid"
                siweLoginUnknownField:
                  $ref: "#/components/examples/siweLoginUnknownField"
                siweLoginInvalidRef:
                  $ref: "#/components/examples/siweLoginInvalidRef"
        "403":
          $ref: "#/components/responses/UserDisabled"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/SignupRateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/SignupPaused"
  /auth/logout:
    post:
      tags:
        - Auth
      operationId: logout
      summary: Log out current session
      security:
        - SessionToken: []
      responses:
        "204":
          description: Current session revoked; subsequent requests with this token return
            401.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /me:
    get:
      tags:
        - Me
      operationId: getMe
      summary: Current username, identity list, and current session
      security:
        - SessionToken: []
      responses:
        "200":
          description: Current user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
              examples:
                meOk:
                  $ref: "#/components/examples/meOk"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - Me
      operationId: updateUsername
      summary: Update username
      description: >
        `{"username": string | null}`: leading/trailing whitespace stripped,
        empty string treated as null (clears username), up to 64 Unicode
        characters, without control characters (Unicode category `Cc`),
        otherwise 400 `invalid_request` (`reason: invalid_username`). Returns
        same body as `GET /me` on success. Not unique, no other format
        constraints.
      security:
        - SessionToken: []
      requestBody:
        $ref: "#/components/requestBodies/UsernameBody"
      responses:
        "200":
          description: Updated current user (matching `GET /me` shape).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
              examples:
                meUsernameUpdated:
                  $ref: "#/components/examples/meUsernameUpdated"
        "400":
          description: Username exceeds length limit, contains control characters, or
            unknown fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                errInvalidUsername:
                  $ref: "#/components/examples/errInvalidUsername"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/identities/siwe:
    post:
      tags:
        - Me
      operationId: linkSiwe
      summary: Link wallet
      description: >
        Submit `purpose=link` challenge message (requested in same session) and
        signature. Request `Origin` mode must match challenge: browser challenge
        must send same `Origin` (`https://<stored domain>`), programmatic
        challenge must omit `Origin`, otherwise returns 400 `siwe_invalid`
        (reason `domain_mismatch`). New link returns 201; returns 200
        idempotently if wallet already belongs to this user.


        Scope of support:

        - Supported: EOA wallets on Ethereum mainnet (Chain ID 1).

        - Not supported: Any other chains (Chain ID is embedded by server;
        submitting modified Chain ID fails byte-for-byte check returning 400
        `siwe_invalid`, reason `signature`), contract wallets (EIP-1271), and
        smart accounts.
      security:
        - SessionToken: []
      requestBody:
        $ref: "#/components/requestBodies/SiweVerifyBody"
      responses:
        "200":
          description: Wallet already linked to this user (idempotent).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentityView"
              examples:
                linkSiweAlreadyOwn:
                  $ref: "#/components/examples/linkSiweAlreadyOwn"
        "201":
          description: Newly linked identity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentityView"
              examples:
                linkSiweCreated:
                  $ref: "#/components/examples/linkSiweCreated"
        "400":
          description: Verification failed (including challenge purpose not link, or not
            requested in this session).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkSiweWrongPurpose:
                  $ref: "#/components/examples/linkSiweWrongPurpose"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Wallet already linked to another user, or user already has 5 linked
            identities.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkSiweInUse:
                  $ref: "#/components/examples/linkSiweInUse"
                errIdentityLimitReached:
                  $ref: "#/components/examples/errIdentityLimitReached"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/identities/oauth:
    post:
      tags:
        - Me
      operationId: linkOauth
      summary: Link GitHub or Google identity using login code
      description: Exchange an OAuth login code and its PKCE verifier using the
        current session. Returns 201 for a new identity and 200 when already
        linked to this user.
      security:
        - SessionToken: []
      requestBody:
        $ref: "#/components/requestBodies/OAuthTokenBody"
      responses:
        "200":
          description: Identity already linked to this user (idempotent).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentityView"
              examples:
                linkOauthAlreadyOwn:
                  $ref: "#/components/examples/linkOauthAlreadyOwn"
        "201":
          description: Newly linked identity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentityView"
              examples:
                linkOauthCreated:
                  $ref: "#/components/examples/linkOauthCreated"
        "400":
          description: Invalid, reused login code, or verifier mismatch.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkOauthCodeReused:
                  $ref: "#/components/examples/linkOauthCodeReused"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Identity already linked to another user, or user already has 5
            linked identities.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkOauthInUse:
                  $ref: "#/components/examples/linkOauthInUse"
                errIdentityLimitReached:
                  $ref: "#/components/examples/errIdentityLimitReached"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/identities/{identity_id}:
    delete:
      tags:
        - Me
      operationId: unlinkIdentity
      summary: Unlink identity
      description: Identity must belong to user and cannot be the sole identity;
        sessions authenticated with this identity are revoked.
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/IdentityIdPath"
      responses:
        "204":
          description: Unlinked successfully.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Identity does not exist or does not belong to user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                unlinkNotOwn:
                  $ref: "#/components/examples/unlinkNotOwn"
        "409":
          description: Cannot unlink sole identity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                unlinkLastIdentity:
                  $ref: "#/components/examples/unlinkLastIdentity"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /keys:
    get:
      tags:
        - Keys
      operationId: listKeys
      summary: List API keys
      description: Returns account keys in descending order of `created_at`,
        unpaginated. Revoked keys excluded by default; included when
        `include_revoked=true`.
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/IncludeRevokedQuery"
      responses:
        "200":
          description: API key list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyListResponse"
              examples:
                keysList:
                  $ref: "#/components/examples/keysList"
                keysListIncludeRevoked:
                  $ref: "#/components/examples/keysListIncludeRevoked"
        "400":
          description: "`include_revoked` is not `true`/`false`."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keysListBadFlag:
                  $ref: "#/components/examples/keysListBadFlag"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags:
        - Keys
      operationId: createKey
      summary: Create key (secret displayed only once)
      description: Create an active API key; default rate limits are returned by GET
        /plans in key_defaults. The new key is usable within about 5 seconds.
        Optional cu_cap sets a lifetime CU cap; expires_in_secs and expires_at
        are mutually exclusive expiration options. An expired key returns 403
        key_expired; an exhausted cap returns 403 key_cap_exhausted. Rotation
        preserves the cap and expiration.
      security:
        - SessionToken: []
      requestBody:
        $ref: "#/components/requestBodies/KeyCreateBody"
      responses:
        "201":
          description: New key and its secret (`api_key`, displayed only this once).
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyCreateResponse"
              examples:
                keyCreated:
                  $ref: "#/components/examples/keyCreated"
        "400":
          description: "Invalid label (empty, exceeding 64 chars, control characters), unknown fields; `cu_cap` not an integer in 1–9007199254740991; `expires_in_secs` and `expires_at` both provided or malformed; expiration not in future or exceeding maximum validity (`reason: expires_at`), cap out of bounds (`reason: cu_cap`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyCreateBadLabel:
                  $ref: "#/components/examples/keyCreateBadLabel"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Unrevoked key limit reached.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyCreateLimitUnfunded:
                  $ref: "#/components/examples/keyCreateLimitUnfunded"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          description: "Rate limit exceeded, or daily key creation limit reached for account (with reason: daily_creations)."
          headers:
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyRateLimitedDailyCreations:
                  $ref: "#/components/examples/keyRateLimitedDailyCreations"
                errRateLimited:
                  $ref: "#/components/examples/errRateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /keys/{key_id}:
    get:
      tags:
        - Keys
      operationId: getKey
      summary: Single key
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/KeyIdPath"
      responses:
        "200":
          description: API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyView"
              examples:
                keyGet:
                  $ref: "#/components/examples/keyGet"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Key does not exist or does not belong to this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyGetOtherAccount:
                  $ref: "#/components/examples/keyGetOtherAccount"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - Keys
      operationId: renameKey
      summary: Rename key
      description: Only `label` can be modified; revoked keys can also be renamed.
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/KeyIdPath"
      requestBody:
        $ref: "#/components/requestBodies/KeyRenameBody"
      responses:
        "200":
          description: Renamed key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyView"
              examples:
                keyRenamed:
                  $ref: "#/components/examples/keyRenamed"
        "400":
          description: Invalid label, unknown fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyRenameEmptyLabel:
                  $ref: "#/components/examples/keyRenameEmptyLabel"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Key does not exist or does not belong to this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyRenameOtherAccount:
                  $ref: "#/components/examples/keyRenameOtherAccount"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /keys/{key_id}/rotate:
    post:
      tags:
        - Keys
      operationId: rotateKey
      summary: Rotate key (new secret displayed only once)
      description: >
        Permitted for `active`, unexpired keys only. Creates a new key
        (inheriting label, limits, `cu_cap`, `expires_at`, and settled
        `cu_spent`; quota not reset by rotation) and revokes old key with
        immediate effect; `replaced` in response is revoked old key whose
        `replaced_by_key_id` matches new key `key_id`.
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/KeyIdPath"
      responses:
        "201":
          description: New key, its secret (displayed only this once), and revoked old key.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyRotateResponse"
              examples:
                keyRotated:
                  $ref: "#/components/examples/keyRotated"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Key does not exist or does not belong to this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyRotateOtherAccount:
                  $ref: "#/components/examples/keyRotateOtherAccount"
        "409":
          description: Key is not `active`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyRotateNotActive:
                  $ref: "#/components/examples/keyRotateNotActive"
        "429":
          description: "Rate limit exceeded, or daily key creation limit reached for account (rotations count as new keys, with reason: daily_creations)."
          headers:
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyRateLimitedDailyCreations:
                  $ref: "#/components/examples/keyRateLimitedDailyCreations"
                errRateLimited:
                  $ref: "#/components/examples/errRateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /keys/{key_id}/revoke:
    post:
      tags:
        - Keys
      operationId: revokeKey
      summary: Revoke key (idempotent)
      description: Both `active` and `disabled` keys can be revoked; returns 200
        verbatim if already revoked. Data plane returns 401 `invalid_api_key`
        for this key within ~5 s.
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/KeyIdPath"
      responses:
        "200":
          description: Revoked key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyView"
              examples:
                keyRevoked:
                  $ref: "#/components/examples/keyRevoked"
                keyRevokedAgain:
                  $ref: "#/components/examples/keyRevokedAgain"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Key does not exist or does not belong to this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                keyRevokeOtherAccount:
                  $ref: "#/components/examples/keyRevokeOtherAccount"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /plans:
    get:
      tags:
        - Billing
      operationId: getPlans
      summary: Plan policies and pricing parameters
      description: Public plan policies, pricing, method CU weights, reset promotions,
        and default API key rate limits. Requires no authentication. GET /resets
        returns account-specific reset chances.
      security: []
      responses:
        "200":
          description: Currently active free plan, reset promotions, default key rate
            limits, and pricing policies.
          headers:
            Cache-Control:
              description: Client and CDN cache duration in seconds.
              schema:
                type: string
                example: max-age=60
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlansResponse"
              examples:
                plansOk:
                  $ref: "#/components/examples/plansOk"
                plansPromo:
                  $ref: "#/components/examples/plansPromo"
                plansPromoMonthly:
                  $ref: "#/components/examples/plansPromoMonthly"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /balance:
    get:
      tags:
        - Billing
      operationId: getBalance
      summary: Balance and settlement cutoff timestamp
      security:
        - SessionToken: []
      responses:
        "200":
          description: Balance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BalanceResponse"
              examples:
                balanceOk:
                  $ref: "#/components/examples/balanceOk"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /charges:
    get:
      tags:
        - Billing
      operationId: listCharges
      summary: Charge records (paginated)
      description: One record per settlement period, sorted in descending order of
        `charge_id`.
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/BeforeQuery"
      responses:
        "200":
          description: Page of charge records.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChargeListResponse"
              examples:
                chargesPage:
                  $ref: "#/components/examples/chargesPage"
        "400":
          description: Invalid `limit` or `before`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                chargesLimitTooLarge:
                  $ref: "#/components/examples/chargesLimitTooLarge"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /topups:
    get:
      tags:
        - Billing
      operationId: listTopups
      summary: Top-up records (paginated)
      description: Sorted in descending order of `topup_id`.
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/BeforeQuery"
      responses:
        "200":
          description: Page of top-up records.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TopupListResponse"
              examples:
                topupsPage:
                  $ref: "#/components/examples/topupsPage"
        "400":
          description: Invalid `limit` or `before`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                topupsLimitZero:
                  $ref: "#/components/examples/topupsLimitZero"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /usage:
    get:
      tags:
        - Billing
      operationId: getUsage
      summary: Calls and CU usage grouped by UTC date, API key, chain, and method
      description: "Closed UTC date range of at most 31 days; to cannot be later than today. key_id must belong to this account, including revoked keys. Rows are grouped by date, key_id, chain, and method. Includes unsettled usage. Limited to 10 requests per minute per user. Push methods are push.token_transfer, push.native_transfer, push.log, and push.address_day: calls counts billed events or address-days, cu is their metered cost, and chain is null. Push usage belongs to its charge date; delivery retries do not add usage."
      security:
        - SessionToken: []
      parameters:
        - $ref: "#/components/parameters/UsageFromQuery"
        - $ref: "#/components/parameters/UsageToQuery"
        - $ref: "#/components/parameters/UsageKeyIdQuery"
      responses:
        "200":
          description: Summary rows in ascending order of `(date, key_id, chain, method)`,
            with nulls first; groups without usage are omitted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
              examples:
                usageOk:
                  $ref: "#/components/examples/usageOk"
        "400":
          description: Invalid date format, `from > to`, range exceeding 31 days, or `to`
            later than today.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                usageRangeTooLong:
                  $ref: "#/components/examples/usageRangeTooLong"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: "`key_id` does not belong to this account."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                usageOtherKey:
                  $ref: "#/components/examples/usageOtherKey"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: Usage service error or timeout (10 s), affecting this endpoint only.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                errUsageUnavailable:
                  $ref: "#/components/examples/errUsageUnavailable"
  /resets:
    get:
      tags:
        - Billing
      operationId: getResets
      summary: Reset chances
      description: List active reset chances ordered by expiration, including fully
        used but unexpired chances. items is empty when no chances apply.
        available is the sum of remaining chances; next_expires_at is null when
        none remain. reset_to_units is the target balance for the next use.
      security:
        - SessionToken: []
      responses:
        "200":
          description: Reset chances.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResetsResponse"
              examples:
                resetsOk:
                  $ref: "#/components/examples/resetsOk"
                resetsMonthly:
                  $ref: "#/components/examples/resetsMonthly"
                resetsMonthlyNewAccount:
                  $ref: "#/components/examples/resetsMonthlyNewAccount"
                resetsNone:
                  $ref: "#/components/examples/resetsNone"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
  /resets/use:
    post:
      tags:
        - Billing
      operationId: useReset
      summary: Use one reset chance
      description: Use the earliest expiring chance to replenish balance up to
        reset_to_units. No request body. Returns 409 no_reset_available when no
        chances remain, or reason nothing_to_reset when balance is already at or
        above the target; in either case no chance is consumed. A disabled
        account returns 403 user_disabled.
      security:
        - SessionToken: []
      responses:
        "200":
          description: Reset completed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResetUseResponse"
              examples:
                resetUsed:
                  $ref: "#/components/examples/resetUsed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Account disabled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                resetUseDisabled:
                  $ref: "#/components/examples/resetUseDisabled"
        "409":
          description: No reset chance available, or balance not below target.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                errNoResetAvailable:
                  $ref: "#/components/examples/errNoResetAvailable"
                errNothingToReset:
                  $ref: "#/components/examples/errNothingToReset"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"
components:
  securitySchemes:
    SessionToken:
      type: http
      scheme: bearer
      bearerFormat: rgs_<64-character lowercase hex>
      description: >
        `Authorization: Bearer rgs_...`, issued by `POST /auth/siwe/login` or
        `POST /auth/oauth/token`. Absolute TTL 7 days,

        idle 24 hours. Invalid returns 401 `unauthenticated` with
        `WWW-Authenticate: Bearer`.
  requestBodies:
    SiweChallengeBody:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SiweChallengeRequest"
          examples:
            reqChallengeLogin:
              $ref: "#/components/examples/reqChallengeLogin"
            reqChallengeLink:
              $ref: "#/components/examples/reqChallengeLink"
            reqChallengeBadChecksum:
              $ref: "#/components/examples/reqChallengeBadChecksum"
            reqChallengeUnknownPurpose:
              $ref: "#/components/examples/reqChallengeUnknownPurpose"
    SiweLoginBody:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SiweLoginRequest"
          examples:
            reqSiweVerify:
              $ref: "#/components/examples/reqSiweVerify"
            reqSiweLoginAttributed:
              $ref: "#/components/examples/reqSiweLoginAttributed"
            reqSiweLoginBadRef:
              $ref: "#/components/examples/reqSiweLoginBadRef"
            reqSiweVerifyUnknownField:
              $ref: "#/components/examples/reqSiweVerifyUnknownField"
    UsernameBody:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/UsernameRequest"
          examples:
            reqUsernameSet:
              $ref: "#/components/examples/reqUsernameSet"
            reqUsernameClear:
              $ref: "#/components/examples/reqUsernameClear"
            reqUsernameTooLong:
              $ref: "#/components/examples/reqUsernameTooLong"
    SiweVerifyBody:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SiweVerifyRequest"
          examples:
            reqSiweVerify:
              $ref: "#/components/examples/reqSiweVerify"
            reqSiweVerifyUnknownField:
              $ref: "#/components/examples/reqSiweVerifyUnknownField"
    OAuthTokenBody:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/OAuthTokenRequest"
          examples:
            reqOauthToken:
              $ref: "#/components/examples/reqOauthToken"
            reqOauthTokenUnknownField:
              $ref: "#/components/examples/reqOauthTokenUnknownField"
    KeyCreateBody:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/KeyCreateRequest"
          examples:
            reqKeyCreate:
              $ref: "#/components/examples/reqKeyCreate"
            reqKeyControlChar:
              $ref: "#/components/examples/reqKeyControlChar"
    KeyRenameBody:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/KeyLabelRequest"
          examples:
            reqKeyRename:
              $ref: "#/components/examples/reqKeyRename"
            reqKeyEmptyLabel:
              $ref: "#/components/examples/reqKeyEmptyLabel"
  schemas:
    SiweChallengeRequest:
      type: object
      additionalProperties: false
      required:
        - address
        - purpose
      properties:
        address:
          type: string
          pattern: ^0x[0-9a-fA-F]{40}$
          description: EOA address. Must be a valid EIP-55 checksum if mixed case.
        purpose:
          type: string
          enum:
            - login
            - link
          description: "`login` for login / sign-up; `link` to link wallet to current session user (requires session)."
    SiweChallengeResponse:
      type: object
      additionalProperties: false
      required:
        - nonce
        - message
        - expires_at
      properties:
        nonce:
          type: string
          pattern: ^[0-9a-f]{32}$
        message:
          $ref: "#/components/schemas/SiweMessage"
        expires_at:
          $ref: "#/components/schemas/Timestamp"
          description: Matches Expiration Time in message (Issued At + 300 s).
    SiweMessage:
      type: string
      description: EIP-4361 message (LF line endings, no trailing newline), up to 2048
        bytes.
      maxLength: 2048
      pattern: "^[a-z0-9.-]+(:[0-9]+)? wants you to sign in with your Ethereum account:\\n0x[0-9a-fA-F]{40}\\n\\n[ -~]+\\n\\nURI: [!-~]+\\nVersion: 1\\nChain ID: [0-9]+\\nNonce: [0-9a-f]{32}\\nIssued At: [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\\nExpiration Time: [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
    Timestamp:
      type: string
      format: date-time
      pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$
      description: RFC 3339 UTC, second precision, ending in `Z`
        (`2026-09-26T08:00:00Z`); no fractional seconds, without `+00:00`
        timezone offsets.
      example: 2026-09-26T08:00:00Z
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - siwe_invalid
                - oauth_invalid
                - login_code_invalid
                - unauthenticated
                - user_disabled
                - not_found
                - provider_disabled
                - method_not_allowed
                - identity_in_use
                - identity_limit_reached
                - last_identity
                - key_not_active
                - key_limit_reached
                - no_reset_available
                - payload_too_large
                - rate_limited
                - signup_rate_limited
                - signup_paused
                - usage_unavailable
                - service_unavailable
                - internal
            message:
              type: string
              description: Human-readable explanation, subject to wording adjustments.
            reason:
              type: string
              enum:
                - expired
                - chain_mismatch
                - domain_mismatch
                - signature
                - active_keys
                - daily_creations
                - invalid_username
                - nothing_to_reset
                - per_ip
                - global
                - cu_cap
                - expires_at
                - audience
              description: "Machine-readable reason (siwe_invalid: expired, chain_mismatch, domain_mismatch, signature; key_limit_reached: active_keys; rate_limited: daily_creations for 24h key creation limit; invalid_request: invalid_username, cu_cap, expires_at; no_reset_available: nothing_to_reset when balance is already at or above target; signup_rate_limited: per_ip or global)."
            limit:
              type: integer
              description: Limit threshold value (e.g. active_keys limit for
                key_limit_reached).
    SiweLoginRequest:
      type: object
      additionalProperties: false
      required:
        - message
        - signature
      properties:
        message:
          $ref: "#/components/schemas/SiweMessage"
        signature:
          type: string
          pattern: ^0x[0-9a-fA-F]{130}$
          description: 65-byte ECDSA signature (r||s||v).
        ref:
          $ref: "#/components/schemas/SignupRef"
        referrer:
          $ref: "#/components/schemas/SignupReferrer"
    SignupRef:
      type:
        - string
        - "null"
      pattern: ^[a-z0-9._-]{1,64}$
      description: >
        Optional sign-up channel attribution tag, saved once only when creating
        a new account. Lowercase letters, digits, `.`, `_`, `-`, 1–64
        characters; any other value (including uppercase, empty string) returns
        400 `invalid_request` without case folding.
      example: hn.launch-2026
    SignupReferrer:
      type:
        - string
        - "null"
      description: >
        Optional referrer URL or hostname, saved once only when creating a new
        account, storing hostname only (lowercase, IDNA ASCII, without protocol,
        port, path, query). Treated as omitted (stored as null) if no usable
        hostname can be extracted, with account creation succeeding normally;
        only non-string types return 400.
      example: https://news.ycombinator.com/item?id=1
    SessionResponse:
      type: object
      additionalProperties: false
      required:
        - session
        - account_created
      properties:
        session:
          type: object
          additionalProperties: false
          required:
            - token
            - expires_at
            - idle_timeout_secs
          properties:
            token:
              type: string
              pattern: ^rgs_[0-9a-f]{64}$
              description: Session token, displayed only this once.
            expires_at:
              $ref: "#/components/schemas/Timestamp"
              description: Absolute expiration timestamp (issued at + 7 days).
            idle_timeout_secs:
              type: integer
              description: Idle timeout (seconds).
        account_created:
          type: boolean
          description: Whether this login created a new account.
    MeResponse:
      type: object
      additionalProperties: false
      required:
        - username
        - created_at
        - identities
        - session
      properties:
        username:
          $ref: "#/components/schemas/Username"
        created_at:
          $ref: "#/components/schemas/Timestamp"
        identities:
          type: array
          items:
            $ref: "#/components/schemas/IdentityView"
        session:
          type: object
          additionalProperties: false
          required:
            - expires_at
            - idle_timeout_secs
          properties:
            expires_at:
              $ref: "#/components/schemas/Timestamp"
            idle_timeout_secs:
              type: integer
    Username:
      type:
        - string
        - "null"
      maxLength: 64
      description: >
        Username: populated with default value on first login (GitHub login for
        GitHub, null for SIWE and Google; SIWE accounts start with no username
        and clients derive the display from the address); modified via `PATCH
        /me` only; linking identities does not alter it. Not unique, not
        queryable. Stripped of leading/trailing whitespace on `PATCH`, empty
        string treated as null, up to 64 Unicode characters, without control
        characters (Unicode category `Cc`), otherwise 400 `invalid_request`
        (`reason: invalid_username`).
    IdentityView:
      type: object
      additionalProperties: false
      required:
        - identity_id
        - provider
        - subject
        - display
        - created_at
        - last_login_at
      properties:
        identity_id:
          type: string
          pattern: ^id_[a-z2-7]{26}$
        provider:
          type: string
          enum:
            - siwe
            - github
            - google
        subject:
          type: string
          description: "SIWE: `eip155:1:<lowercase address>`; GitHub: numeric id; Google: `sub`."
        display:
          type:
            - string
            - "null"
          description: EIP-55 address for SIWE, login for GitHub (display only), null for
            Google.
        created_at:
          $ref: "#/components/schemas/Timestamp"
        last_login_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    UsernameRequest:
      type: object
      additionalProperties: false
      required:
        - username
      properties:
        username:
          $ref: "#/components/schemas/Username"
    SiweVerifyRequest:
      type: object
      additionalProperties: false
      required:
        - message
        - signature
      properties:
        message:
          $ref: "#/components/schemas/SiweMessage"
        signature:
          type: string
          pattern: ^0x[0-9a-fA-F]{130}$
          description: 65-byte ECDSA signature (r||s||v).
    OAuthTokenRequest:
      type: object
      additionalProperties: false
      required:
        - code
        - code_verifier
      properties:
        code:
          type: string
          pattern: ^rgc_[0-9a-f]{64}$
          description: Single-use login code delivered via callback redirect.
        code_verifier:
          type: string
          pattern: ^[A-Za-z0-9._~-]{43,128}$
          description: PKCE verifier corresponding to `code_challenge` at start (RFC 7636).
    KeyListResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/KeyView"
    KeyView:
      type: object
      additionalProperties: false
      required:
        - key_id
        - label
        - status
        - cu_per_sec
        - burst_cu
        - created_at
        - created_by
        - revoked_at
        - replaced_by_key_id
        - cu_cap
        - cu_spent
        - expires_at
      properties:
        key_id:
          type: string
          pattern: ^k_[0-9a-f]{12}$
        label:
          description: Null for keys created by support.
          oneOf:
            - $ref: "#/components/schemas/KeyLabel"
            - type: "null"
        status:
          type: string
          enum:
            - active
            - disabled
            - revoked
        cu_per_sec:
          type: integer
          description: Token bucket refill rate (CU/s), default 100. Effective aggregate
            across all chains and Data API for this key.
        burst_cu:
          type: integer
          description: Token bucket capacity (CU), default 400. Effective aggregate across
            all chains and Data API for this key.
        created_at:
          $ref: "#/components/schemas/Timestamp"
        created_by:
          type: string
          enum:
            - console
            - operator
          description: "`console` for self-created by client, `operator` for created by support."
        revoked_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        replaced_by_key_id:
          type:
            - string
            - "null"
          pattern: ^k_[0-9a-f]{12}$
          description: New key created from rotation; null if never rotated.
        cu_cap:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 9007199254740991
          description: Lifetime CU cap for this key; null if unlimited. Inherited by
            rotated new key.
        cu_spent:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Settled CU counted toward key cap (settled hourly; excludes
            unsettled usage in latest period); inherited by rotated new key.
        expires_at:
          description: Expiration timestamp; null if no expiration. Data plane rejects
            expired key (403 `key_expired`), status remains `active`. Inherited
            by rotated new key.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    KeyLabel:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[^\u0000-\u001F\u007F]{1,64}$
      description: 1–64 characters, without control characters.
    KeyCreateRequest:
      type: object
      additionalProperties: false
      required:
        - label
      properties:
        label:
          $ref: "#/components/schemas/KeyLabel"
        cu_cap:
          type: integer
          minimum: 1
          maximum: 9007199254740991
          description: Lifetime CU cap for this key; unlimited if omitted.
        expires_in_secs:
          type: integer
          minimum: 1
          maximum: 2147483647
          description: Seconds until expiration from current server time (converted to
            absolute time); mutually exclusive with `expires_at`.
        expires_at:
          type: string
          format: date-time
          description: Expiration timestamp, RFC 3339 string with timezone (`Z` or
            offset); must be in the future within maximum validity; mutually
            exclusive with `expires_in_secs`.
    KeyCreateResponse:
      type: object
      additionalProperties: false
      required:
        - key
        - api_key
      properties:
        key:
          $ref: "#/components/schemas/KeyView"
        api_key:
          $ref: "#/components/schemas/ApiKeySecret"
    ApiKeySecret:
      type: string
      pattern: ^rgw_[0-9a-f]{64}$
      description: Key secret, displayed only this once.
    KeyLabelRequest:
      type: object
      additionalProperties: false
      required:
        - label
      properties:
        label:
          $ref: "#/components/schemas/KeyLabel"
    KeyRotateResponse:
      type: object
      additionalProperties: false
      required:
        - key
        - api_key
        - replaced
      properties:
        key:
          $ref: "#/components/schemas/KeyView"
        api_key:
          $ref: "#/components/schemas/ApiKeySecret"
        replaced:
          $ref: "#/components/schemas/KeyView"
    PlansResponse:
      type: object
      additionalProperties: false
      required:
        - free
        - promo
        - key_defaults
        - pricing
        - method_weights
      properties:
        free:
          $ref: "#/components/schemas/FreePlanView"
        promo:
          description: Active reset promotion granted to new accounts (latest active
            round); null if none. Account reset chances can be queried via `GET
            /resets`.
          oneOf:
            - $ref: "#/components/schemas/ResetPromoView"
            - type: "null"
        key_defaults:
          $ref: "#/components/schemas/KeyDefaultsView"
        pricing:
          $ref: "#/components/schemas/PricingPlanView"
        method_weights:
          type: array
          description: >
            CU weight per call, by method across all chains; includes JSON-RPC
            methods and Data API `data.<op>`; method availability across chains
            can be queried via data plane `GET /v1/chains` and `/v1/status`.
            Sorted by method, including `*` row for unlisted methods.
          items:
            $ref: "#/components/schemas/MethodWeightView"
    FreePlanView:
      type: object
      additionalProperties: false
      required:
        - signup_units
        - monthly_units
        - window_days
        - max_calls_per_sec
      properties:
        signup_units:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Sign-up grant quota for new users (units).
        monthly_units:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Balance replenishment target (units) at the end of a usage-started
            window; not a UTC calendar-month grant. Remaining credit is not
            accumulated or taken away.
        window_days:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Usage-started window length in days (currently 30); starts at the
            first settled usage period, not account creation or UTC month start.
        max_calls_per_sec:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Per-second call limit for free plan. Token bucket capacity and
            refill rate equal this value, shared across all keys, chains, and
            Data API for the account.
    ResetPromoView:
      type: object
      additionalProperties: false
      required:
        - type
        - chances_per_account
        - valid_days
        - reset_to_units
        - issued_at
        - ends_at
      properties:
        type:
          type: string
          enum:
            - reset
          description: Promotion type; currently reset only.
        period:
          type: string
          enum:
            - month
          description: >
            Present only for monthly rounds (omitted otherwise, not null):
            chances issued per UTC calendar month, valid from `max(first day of
            month 00:00 UTC, account_creation_time)` until that timestamp plus
            one calendar month; `valid_days` is `null`, `issued_at` marks start
            of first month.
        chances_per_account:
          type: integer
          minimum: 1
          maximum: 1000
          description: Number of reset chances granted to each eligible account in this
            round.
        valid_days:
          description: >
            Validity in days from `max(issued_at, account_creation_time)`;
            `null` does not imply a monthly round (which has `period` without
            fixed days, issued per calendar month) — a regular round omitting
            `valid_days` (lacking `period` key) is also `null`, defaulting to
            one calendar month validity. Check `period` presence to distinguish.
          oneOf:
            - type: integer
              minimum: 1
            - type: "null"
        reset_to_units:
          type: integer
          format: int64
          minimum: 1
          maximum: 9007199254740991
          description: Target balance to replenish to upon reset (units).
        issued_at:
          $ref: "#/components/schemas/Timestamp"
        ends_at:
          description: Only accounts created prior to this timestamp receive chances in
            this round; null if no end time.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        next_issue_at:
          allOf:
            - $ref: "#/components/schemas/Timestamp"
          description: >
            Earliest upcoming auto-grant timestamp among promotion rounds
            strictly after current time: for monthly rounds, start of next
            calendar month; for regular rounds, its `issued_at` (if in the
            future). Present whenever such a timestamp exists; omitted (not
            null) when no future grants remain.
        next_period:
          type: string
          enum:
            - month
          description: Present only when `next_issue_at` originates from a monthly round
            (omitted otherwise, not null).
    KeyDefaultsView:
      type: object
      additionalProperties: false
      required:
        - cu_per_sec
        - burst_cu
      properties:
        cu_per_sec:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Default CU per-second rate limit for new API keys.
        burst_cu:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Default burst CU capacity for new API keys, also capping sum of
            weights in a single request.
    PricingPlanView:
      type: object
      additionalProperties: false
      required:
        - units_per_usd
        - cu_per_unit
        - min_topup_usd
        - push_free_addresses
      properties:
        units_per_usd:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Number of units per 1 USD.
        cu_per_unit:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Number of Compute Units (CU) per 1 unit.
        min_topup_usd:
          type: number
          description: Minimum top-up amount (USD).
        push_free_addresses:
          type: integer
          minimum: 0
          maximum: 2147483647
          description: >
            Free address allowance per account per UTC day, shared by all
            subscription groups regardless of plan. For each group, count its
            maximum address count while online during that day; allocate the
            allowance in ascending group ID order. The same address in two
            groups counts twice; the number of chains in a group does not
            multiply its address count. A group offline or deleted for the
            entire day contributes nothing. For each group, the remaining count
            after its share of the allowance is multiplied by the
            `push.address_day` CU weight in `method_weights`. The current
            configured allowance comes from the same pricing policy used for the
            address-day charge; it is not an account capacity limit or a
            separate allowance per group.
    MethodWeightView:
      type: object
      additionalProperties: false
      required:
        - method
        - cu_weight
      properties:
        method:
          type: string
          description: Exact JSON-RPC method name, prefix rule ending with `*`, or `*`
            (weight for all unlisted methods).
        cu_weight:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Compute Units (CU) metered per call for this method.
    BalanceResponse:
      type: object
      additionalProperties: false
      required:
        - balance_units
        - balance_updated_at
        - settled_through
        - plan
        - free_monthly_units
        - max_calls_per_sec
        - free_window_ends_at
      properties:
        balance_units:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Settled balance (amount_units), can be negative. On free plan,
            remaining free grant constitutes this balance (free accounts have no
            paid top-ups; balance derived from grants).
        balance_updated_at:
          $ref: "#/components/schemas/Timestamp"
        settled_through:
          description: Cutoff timestamp of latest settlement; null if never settled.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        plan:
          type: string
          enum:
            - free
            - paid
          description: "Account plan: `free` (free tier, no paid top-ups) or `paid` (paid tier, after first paid top-up)."
        free_monthly_units:
          description: Free balance replenishment target (amount_units); null for paid
            tier. The historical field name does not imply a calendar month. At
            the end of a usage-started window (currently 30 days), eligible free
            accounts below the target are topped up to it; no accumulation or
            reduction of remaining credit.
          type:
            - integer
            - "null"
          format: int64
          maximum: 9007199254740991
        max_calls_per_sec:
          description: Per-second call limit per account for free tier; null for paid
            tier. Token bucket capacity and refill rate equal this value, shared
            across all keys, chains, and Data API for the account. Each call in
            batch counted individually.
          type:
            - integer
            - "null"
          format: int64
          maximum: 9007199254740991
        free_window_ends_at:
          description: Expiration timestamp of the current open free usage window (ISO
            8601 UTC); null if account has no open window or is on the paid
            plan.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    ChargeListResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_before
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ChargeView"
        next_before:
          type:
            - string
            - "null"
          pattern: ^ch_[a-z2-7]{26}$
    ChargeView:
      type: object
      additionalProperties: false
      required:
        - charge_id
        - period_start
        - period_end
        - cu
        - amount_units
        - created_at
      properties:
        charge_id:
          type: string
          pattern: ^ch_[a-z2-7]{26}$
        period_start:
          $ref: "#/components/schemas/Timestamp"
        period_end:
          $ref: "#/components/schemas/Timestamp"
        cu:
          type: integer
          format: int64
          minimum: 0
        amount_units:
          type: integer
          format: int64
          minimum: 0
          maximum: 9007199254740991
          description: "floor((carry_in + cu * 1) / 1000): carry_in is fractional remainder below 1 unit from previous period (0 for initial period), settled 15 minutes after period close; remainder carries forward (cumulative charges equal floor(cumulative_cu / 1000)). Example: 508 CU carry-in + 2557 CU period = 3 units charged, 65 CU remainder carried forward; charges do not expose remainder fields."
        created_at:
          $ref: "#/components/schemas/Timestamp"
    TopupListResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_before
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/TopupView"
        next_before:
          type:
            - string
            - "null"
          pattern: ^tu_[a-z2-7]{26}$
    TopupView:
      type: object
      additionalProperties: false
      required:
        - topup_id
        - amount_units
        - created_at
        - kind
      properties:
        topup_id:
          type: string
          pattern: ^tu_[a-z2-7]{26}$
        amount_units:
          type: integer
          format: int64
          maximum: 9007199254740991
        created_at:
          $ref: "#/components/schemas/Timestamp"
        kind:
          type: string
          enum:
            - signup_grant
            - free_refill
            - promo_grant
            - paid
          description: Top-up type (signup_grant for sign-up grant, free_refill for
            periodic refill, promo_grant for reset chance credit, paid for paid
            top-up).
        amount_usd:
          type: string
          pattern: ^[0-9]+(\.[0-9]{1,6})?$
          description: Fiat amount of paid top-up (present only for paid top-ups with fiat
            records).
        currency:
          type: string
          pattern: ^[A-Z0-9]{2,12}$
          description: Settlement currency (present only for paid top-ups with fiat
            records).
    UsageResponse:
      type: object
      additionalProperties: false
      required:
        - from
        - to
        - rows
      properties:
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        rows:
          type: array
          items:
            $ref: "#/components/schemas/UsageRow"
    UsageRow:
      type: object
      additionalProperties: false
      required:
        - date
        - key_id
        - chain
        - method
        - calls
        - cu
      properties:
        date:
          type: string
          format: date
        key_id:
          type: string
          pattern: ^k_[0-9a-f]{12}$
        chain:
          type:
            - string
            - "null"
          description: Chain slug; null for chain-agnostic calls (e.g. Data API
            data.chains) and push charges.
        method:
          type: string
          description: Metered method name, including JSON-RPC, Data API, WebSocket, and
            push methods.
        calls:
          type: integer
          format: int64
          minimum: 0
          description: Call count; for push, billed event count or address-day count.
        cu:
          type: integer
          format: int64
          minimum: 0
    ResetsResponse:
      type: object
      additionalProperties: false
      required:
        - available
        - next_expires_at
        - reset_to_units
        - items
      properties:
        available:
          type: integer
          minimum: 0
          description: Sum of remaining chances across all active rounds.
        next_expires_at:
          description: Expiration timestamp of the chance consumed by next use; null if no
            remaining chances.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        reset_to_units:
          type: integer
          format: int64
          minimum: 1
          maximum: 9007199254740991
          description: Target balance replenished to by next use (units).
        items:
          type: array
          items:
            $ref: "#/components/schemas/ResetChanceView"
    ResetChanceView:
      type: object
      additionalProperties: false
      required:
        - round
        - available_from
        - expires_at
        - total
        - used
        - remaining
      properties:
        round:
          type: string
          pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
          description: Round identifier; monthly rounds formatted as `<id>-YYYY-MM` (e.g.
            `monthly-2026-10`).
        available_from:
          $ref: "#/components/schemas/Timestamp"
        expires_at:
          $ref: "#/components/schemas/Timestamp"
        total:
          type: integer
          minimum: 1
          description: Number of chances granted to this account in this round.
        used:
          type: integer
          minimum: 0
          description: Number of chances used.
        remaining:
          type: integer
          minimum: 0
          description: "`max(0, total - used)`."
    ResetUseResponse:
      type: object
      additionalProperties: false
      required:
        - refill_units
        - balance_units
        - remaining
      properties:
        refill_units:
          type: integer
          format: int64
          minimum: 1
          maximum: 9007199254740991
          description: Amount credited in this reset (difference between target and
            pre-reset balance).
        balance_units:
          type: integer
          format: int64
          maximum: 9007199254740991
          description: Balance after credit (settled balance, matching `GET /balance`).
        remaining:
          type: integer
          minimum: 0
          description: Sum of remaining chances across all rounds after use.
  examples:
    reqChallengeLogin:
      summary: Challenge for login
      value:
        address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
        purpose: login
    reqChallengeLink:
      summary: Challenge for linking (requires session)
      value:
        address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
        purpose: link
    reqChallengeBadChecksum:
      summary: Mixed case but invalid EIP-55 checksum (valid format, server returns 400)
      value:
        address: "0xF39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
        purpose: login
    reqChallengeUnknownPurpose:
      summary: purpose is not login/link
      value:
        address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
        purpose: register
    siweChallengeLogin:
      summary: Challenge message for login
      value:
        nonce: 3f7c0a9e5b1d4c2a8e6f0b3d9a7c5e1f
        message: >-
          console.blockvectra.com wants you to sign in with your Ethereum
          account:

          0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266


          Sign in to the BlockVectra RPC console.


          URI: https://console.blockvectra.com

          Version: 1

          Chain ID: 1

          Nonce: 3f7c0a9e5b1d4c2a8e6f0b3d9a7c5e1f

          Issued At: 2026-09-26T08:00:00Z

          Expiration Time: 2026-09-26T08:05:00Z
        expires_at: 2026-09-26T08:05:00Z
    siweChallengeLink:
      summary: Challenge message for linking (statement specifies account ID)
      value:
        nonce: 8e1f4c7a0b3d6e9f2a5c8b1d4e7f0a3c
        message: >-
          console.blockvectra.com wants you to sign in with your Ethereum
          account:

          0x70997970C51812dc3A010C7d01b50e0d17dc79C8


          Link this wallet to BlockVectra RPC console account
          acct_ejb7hrlrentyincbnwemctnhde.


          URI: https://console.blockvectra.com

          Version: 1

          Chain ID: 1

          Nonce: 8e1f4c7a0b3d6e9f2a5c8b1d4e7f0a3c

          Issued At: 2026-09-26T08:10:00Z

          Expiration Time: 2026-09-26T08:15:00Z
        expires_at: 2026-09-26T08:15:00Z
    siweChallengeBadChecksum:
      summary: Invalid EIP-55 checksum
      value:
        error:
          code: invalid_request
          message: address is not a valid EIP-55 checksum address
    errUnauthenticated:
      summary: Invalid session (missing, malformed, unknown, expired, or revoked)
      value:
        error:
          code: unauthenticated
          message: missing or invalid session token
    errUnauthenticatedLinkChallenge:
      summary: purpose=link without session
      value:
        error:
          code: unauthenticated
          message: missing or invalid session token
    errMethodNotAllowed:
      summary: Method not allowed
      value:
        error:
          code: method_not_allowed
          message: method not allowed
    payloadTooLarge:
      summary: Payload too large
      value:
        error:
          code: payload_too_large
          message: payload too large
    errRateLimited:
      summary: Rate limit exceeded
      value:
        error:
          code: rate_limited
          message: rate limit exceeded
    errInternal:
      summary: Internal error
      value:
        error:
          code: internal
          message: internal error
    reqSiweVerify:
      summary: Submit raw message and signature
      value:
        message: >-
          console.blockvectra.com wants you to sign in with your Ethereum
          account:

          0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266


          Sign in to the BlockVectra RPC console.


          URI: https://console.blockvectra.com

          Version: 1

          Chain ID: 1

          Nonce: 3f7c0a9e5b1d4c2a8e6f0b3d9a7c5e1f

          Issued At: 2026-09-26T08:00:00Z

          Expiration Time: 2026-09-26T08:05:00Z
        signature: "0x5f2a8c3e1d9b7f604a2e8c1b3d5f7a9e0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c7e9b0d2f4a6c8e1b3d5f7a9c0e2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c1c"
    reqSiweLoginAttributed:
      summary: With sign-up attribution (saved only on new account creation)
      value:
        message: >-
          console.blockvectra.com wants you to sign in with your Ethereum
          account:

          0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266


          Sign in to the BlockVectra RPC console.


          URI: https://console.blockvectra.com

          Version: 1

          Chain ID: 1

          Nonce: 3f7c0a9e5b1d4c2a8e6f0b3d9a7c5e1f

          Issued At: 2026-09-26T08:00:00Z

          Expiration Time: 2026-09-26T08:05:00Z
        signature: "0x5f2a8c3e1d9b7f604a2e8c1b3d5f7a9e0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c7e9b0d2f4a6c8e1b3d5f7a9c0e2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c1c"
        ref: hn.launch-2026
        referrer: https://news.ycombinator.com/item?id=1
    reqSiweLoginBadRef:
      summary: ref contains uppercase
      value:
        message: >-
          console.blockvectra.com wants you to sign in with your Ethereum
          account:

          0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266


          Sign in to the BlockVectra RPC console.


          URI: https://console.blockvectra.com

          Version: 1

          Chain ID: 1

          Nonce: 3f7c0a9e5b1d4c2a8e6f0b3d9a7c5e1f

          Issued At: 2026-09-26T08:00:00Z

          Expiration Time: 2026-09-26T08:05:00Z
        signature: "0x5f2a8c3e1d9b7f604a2e8c1b3d5f7a9e0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c7e9b0d2f4a6c8e1b3d5f7a9c0e2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c1c"
        ref: Twitter
    reqSiweVerifyUnknownField:
      summary: Contains undefined fields
      value:
        message: >-
          console.blockvectra.com wants you to sign in with your Ethereum
          account:

          0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266


          Sign in to the BlockVectra RPC console.


          URI: https://console.blockvectra.com

          Version: 1

          Chain ID: 1

          Nonce: 3f7c0a9e5b1d4c2a8e6f0b3d9a7c5e1f

          Issued At: 2026-09-26T08:00:00Z

          Expiration Time: 2026-09-26T08:05:00Z
        signature: "0x5f2a8c3e1d9b7f604a2e8c1b3d5f7a9e0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c7e9b0d2f4a6c8e1b3d5f7a9c0e2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c1c"
        chain_id: 1
    sessionNewAccount:
      summary: First login with new wallet, account created
      value:
        session:
          token: rgs_4f2d0c3b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928170605f4
          expires_at: 2026-10-03T08:00:00Z
          idle_timeout_secs: 86400
        account_created: true
    sessionExistingAccount:
      summary: Subsequent login with existing wallet identity
      value:
        session:
          token: rgs_9a8b7c6d5e4f30211203f4e5d6c7b8a99a8b7c6d5e4f30211203f4e5d6c7b8a9
          expires_at: 2026-10-03T09:00:00Z
          idle_timeout_secs: 86400
        account_created: false
    siweLoginInvalid:
      summary: "Invalid signature (with reason: signature)"
      value:
        error:
          code: siwe_invalid
          message: invalid SIWE message or signature
          reason: signature
    siweLoginUnknownField:
      summary: Request body contains undefined fields
      value:
        error:
          code: invalid_request
          message: unknown field `chain_id`
    siweLoginInvalidRef:
      summary: Invalid ref (contains uppercase), account not created
      value:
        error:
          code: invalid_request
          message: invalid request
    errUserDisabled:
      summary: Account disabled
      value:
        error:
          code: user_disabled
          message: this user has been disabled
    errSignupRateLimited:
      summary: New sign-up budget exhausted for same network (existing identities can
        log in)
      value:
        error:
          code: signup_rate_limited
          message: too many new accounts were created from this network recently; retry in
            3600 seconds
          reason: per_ip
    errSignupPaused:
      summary: New account creation temporarily unavailable
      value:
        error:
          code: signup_paused
          message: new signups are temporarily paused
    meOk:
      summary: Current user
      value:
        username: null
        created_at: 2026-09-26T08:00:00Z
        identities:
          - identity_id: id_plen7kcp3t3rcyhzq6z3go64v4
            provider: siwe
            subject: eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
            display: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
            created_at: 2026-09-26T08:00:00Z
            last_login_at: 2026-09-26T08:00:00Z
          - identity_id: id_xupnufatg6to3qmkjqvahsyhcu
            provider: github
            subject: "583231"
            display: octocat
            created_at: 2026-09-26T08:05:00Z
            last_login_at: null
        session:
          expires_at: 2026-10-03T08:00:00Z
          idle_timeout_secs: 86400
    reqUsernameSet:
      summary: Set username
      value:
        username: alice
    reqUsernameClear:
      summary: Clear username
      value:
        username: null
    reqUsernameTooLong:
      summary: 65 characters
      value:
        username: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    meUsernameUpdated:
      summary: After updating username
      value:
        username: alice
        created_at: 2026-09-26T08:00:00Z
        identities:
          - identity_id: id_plen7kcp3t3rcyhzq6z3go64v4
            provider: siwe
            subject: eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
            display: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
            created_at: 2026-09-26T08:00:00Z
            last_login_at: 2026-09-26T08:00:00Z
          - identity_id: id_xupnufatg6to3qmkjqvahsyhcu
            provider: github
            subject: "583231"
            display: octocat
            created_at: 2026-09-26T08:05:00Z
            last_login_at: null
        session:
          expires_at: 2026-10-03T08:00:00Z
          idle_timeout_secs: 86400
    errInvalidUsername:
      summary: Username exceeds length limit
      value:
        error:
          code: invalid_request
          message: username must be at most 64 characters and contain no control
            characters
          reason: invalid_username
    linkSiweAlreadyOwn:
      summary: Wallet already linked to this user (idempotent)
      value:
        identity_id: id_plen7kcp3t3rcyhzq6z3go64v4
        provider: siwe
        subject: eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
        display: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
        created_at: 2026-09-26T08:00:00Z
        last_login_at: 2026-09-26T08:00:00Z
    linkSiweCreated:
      summary: Link new wallet
      value:
        identity_id: id_5r4lvvqyinfzgphe5dn2hfd5fq
        provider: siwe
        subject: eip155:1:0x70997970c51812dc3a010c7d01b50e0d17dc79c8
        display: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
        created_at: 2026-09-26T08:10:30Z
        last_login_at: null
    linkSiweWrongPurpose:
      summary: Link attempted with purpose=login challenge
      value:
        error:
          code: siwe_invalid
          message: invalid SIWE message or signature
          reason: signature
    linkSiweInUse:
      summary: Wallet already linked to another user
      value:
        error:
          code: identity_in_use
          message: this identity is linked to another user
    errIdentityLimitReached:
      summary: User already has 5 linked identities
      value:
        error:
          code: identity_limit_reached
          message: a user can have at most 5 identities
    reqOauthToken:
      summary: Exchange login code and verifier
      value:
        code: rgc_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
        code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    reqOauthTokenUnknownField:
      summary: Contains undefined fields
      value:
        code: rgc_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
        code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
        redirect_uri: http://127.0.0.1:53682/cb
    linkOauthAlreadyOwn:
      summary: GitHub identity already linked to this user (idempotent)
      value:
        identity_id: id_xupnufatg6to3qmkjqvahsyhcu
        provider: github
        subject: "583231"
        display: octocat
        created_at: 2026-09-26T08:05:00Z
        last_login_at: null
    linkOauthCreated:
      summary: Link new GitHub identity
      value:
        identity_id: id_xupnufatg6to3qmkjqvahsyhcu
        provider: github
        subject: "583231"
        display: octocat
        created_at: 2026-09-26T08:05:00Z
        last_login_at: null
    linkOauthCodeReused:
      summary: Login code already used
      value:
        error:
          code: login_code_invalid
          message: login code is invalid, expired or already used
    linkOauthInUse:
      summary: GitHub identity already linked to another user
      value:
        error:
          code: identity_in_use
          message: this identity is linked to another user
    unlinkNotOwn:
      summary: Identity belongs to another user
      value:
        error:
          code: not_found
          message: the requested resource was not found
    unlinkLastIdentity:
      summary: Cannot unlink sole identity
      value:
        error:
          code: last_identity
          message: the last identity cannot be removed
    keysList:
      summary: Active keys
      value:
        items:
          - key_id: k_0123456789ab
            label: prod-backend
            status: active
            cu_per_sec: 100
            burst_cu: 400
            created_at: 2026-09-26T08:01:00Z
            created_by: console
            revoked_at: null
            replaced_by_key_id: null
            cu_cap: null
            cu_spent: 0
            expires_at: null
          - key_id: k_cdef01234567
            label: null
            status: disabled
            cu_per_sec: 1000
            burst_cu: 4000
            created_at: 2026-09-20T10:00:00Z
            created_by: operator
            revoked_at: null
            replaced_by_key_id: null
            cu_cap: null
            cu_spent: 0
            expires_at: null
    keysListIncludeRevoked:
      summary: include_revoked=true, including revoked and replaced keys
      value:
        items:
          - key_id: k_89abcdef0123
            label: prod-backend
            status: active
            cu_per_sec: 100
            burst_cu: 400
            created_at: 2026-09-26T09:00:00Z
            created_by: console
            revoked_at: null
            replaced_by_key_id: null
            cu_cap: null
            cu_spent: 0
            expires_at: null
          - key_id: k_0123456789ab
            label: prod-backend
            status: revoked
            cu_per_sec: 100
            burst_cu: 400
            created_at: 2026-09-26T08:01:00Z
            created_by: console
            revoked_at: 2026-09-26T09:00:00Z
            replaced_by_key_id: k_89abcdef0123
            cu_cap: null
            cu_spent: 0
            expires_at: null
    keysListBadFlag:
      summary: include_revoked is not a boolean
      value:
        error:
          code: invalid_request
          message: include_revoked must be true or false
    errServiceUnavailable:
      summary: Service unavailable
      value:
        error:
          code: service_unavailable
          message: the service is temporarily unavailable
    reqKeyCreate:
      summary: Create new key
      value:
        label: prod-backend
    reqKeyControlChar:
      summary: label contains control characters
      value:
        label: "prod\tbackend"
    keyCreated:
      summary: New key (secret displayed only once)
      value:
        key:
          key_id: k_0123456789ab
          label: prod-backend
          status: active
          cu_per_sec: 100
          burst_cu: 400
          created_at: 2026-09-26T08:01:00Z
          created_by: console
          revoked_at: null
          replaced_by_key_id: null
          cu_cap: null
          cu_spent: 0
          expires_at: null
        api_key: rgw_4f2d0c3b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928170605f4
    keyCreateBadLabel:
      summary: label contains control characters
      value:
        error:
          code: invalid_request
          message: label must be 1-64 characters without control characters
    keyCreateLimitUnfunded:
      summary: Active key limit reached (20 keys)
      value:
        error:
          code: key_limit_reached
          message: account already has the maximum number of unrevoked keys; revoke one
            first
          reason: active_keys
          limit: 20
    keyRateLimitedDailyCreations:
      summary: Daily key creation limit reached for account
      value:
        error:
          code: rate_limited
          message: rate limit exceeded
          reason: daily_creations
    keyGet:
      summary: Single key
      value:
        key_id: k_0123456789ab
        label: prod-backend
        status: active
        cu_per_sec: 100
        burst_cu: 400
        created_at: 2026-09-26T08:01:00Z
        created_by: console
        revoked_at: null
        replaced_by_key_id: null
        cu_cap: null
        cu_spent: 0
        expires_at: null
    keyGetOtherAccount:
      summary: Key belonging to another account (indistinguishable from nonexistent)
      value:
        error:
          code: not_found
          message: key not found
    reqKeyRename:
      summary: Rename key
      value:
        label: staging-backend
    reqKeyEmptyLabel:
      summary: Empty label
      value:
        label: ""
    keyRenamed:
      summary: Renamed key
      value:
        key_id: k_0123456789ab
        label: staging-backend
        status: active
        cu_per_sec: 100
        burst_cu: 400
        created_at: 2026-09-26T08:01:00Z
        created_by: console
        revoked_at: null
        replaced_by_key_id: null
        cu_cap: null
        cu_spent: 0
        expires_at: null
    keyRenameEmptyLabel:
      summary: Empty label
      value:
        error:
          code: invalid_request
          message: label must be 1-64 characters without control characters
    keyRenameOtherAccount:
      summary: Key belonging to another account
      value:
        error:
          code: not_found
          message: key not found
    keyRotated:
      summary: New key, its secret, and revoked old key
      value:
        key:
          key_id: k_89abcdef0123
          label: prod-backend
          status: active
          cu_per_sec: 100
          burst_cu: 400
          created_at: 2026-09-26T09:00:00Z
          created_by: console
          revoked_at: null
          replaced_by_key_id: null
          cu_cap: null
          cu_spent: 0
          expires_at: null
        api_key: rgw_7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b
        replaced:
          key_id: k_0123456789ab
          label: prod-backend
          status: revoked
          cu_per_sec: 100
          burst_cu: 400
          created_at: 2026-09-26T08:01:00Z
          created_by: console
          revoked_at: 2026-09-26T09:00:00Z
          replaced_by_key_id: k_89abcdef0123
          cu_cap: null
          cu_spent: 0
          expires_at: null
    keyRotateOtherAccount:
      summary: Key belonging to another account
      value:
        error:
          code: not_found
          message: key not found
    keyRotateNotActive:
      summary: Rotating revoked key
      value:
        error:
          code: key_not_active
          message: only an active key can be rotated
    keyRevoked:
      summary: Revoked key
      value:
        key_id: k_0123456789ab
        label: prod-backend
        status: revoked
        cu_per_sec: 100
        burst_cu: 400
        created_at: 2026-09-26T08:01:00Z
        created_by: console
        revoked_at: 2026-09-26T10:00:00Z
        replaced_by_key_id: null
        cu_cap: null
        cu_spent: 0
        expires_at: null
    keyRevokedAgain:
      summary: Revoke again (idempotent, returned verbatim)
      value:
        key_id: k_0123456789ab
        label: prod-backend
        status: revoked
        cu_per_sec: 100
        burst_cu: 400
        created_at: 2026-09-26T08:01:00Z
        created_by: console
        revoked_at: 2026-09-26T10:00:00Z
        replaced_by_key_id: null
        cu_cap: null
        cu_spent: 0
        expires_at: null
    keyRevokeOtherAccount:
      summary: Key belonging to another account
      value:
        error:
          code: not_found
          message: key not found
    plansOk:
      summary: Plan policies and pricing parameters (no active promo)
      value:
        free:
          signup_units: 30000
          monthly_units: 100
          window_days: 30
          max_calls_per_sec: 5
        promo: null
        key_defaults:
          cu_per_sec: 400
          burst_cu: 1600
        pricing:
          units_per_usd: 10000
          cu_per_unit: 1000
          min_topup_usd: 1
          push_free_addresses: 1000
        method_weights:
          - method: "*"
            cu_weight: 10
          - method: debug_trace*
            cu_weight: 100
          - method: trace_*
            cu_weight: 100
          - method: eth_blockNumber
            cu_weight: 1
          - method: eth_call
            cu_weight: 15
          - method: eth_chainId
            cu_weight: 1
          - method: eth_createAccessList
            cu_weight: 20
          - method: eth_estimateGas
            cu_weight: 20
          - method: eth_getBlockByNumber
            cu_weight: 5
          - method: eth_getBlockReceipts
            cu_weight: 10
          - method: eth_getLogs
            cu_weight: 30
          - method: eth_getProof
            cu_weight: 10
          - method: eth_sendRawTransaction
            cu_weight: 30
          - method: eth_simulateV1
            cu_weight: 20
    plansPromo:
      summary: Plan policies and pricing parameters (active reset promo)
      value:
        free:
          signup_units: 30000
          monthly_units: 100
          window_days: 30
          max_calls_per_sec: 5
        promo:
          type: reset
          chances_per_account: 3
          valid_days: 30
          reset_to_units: 30000
          issued_at: 2026-10-01T00:00:00Z
          ends_at: null
        key_defaults:
          cu_per_sec: 400
          burst_cu: 1600
        pricing:
          units_per_usd: 10000
          cu_per_unit: 1000
          min_topup_usd: 1
          push_free_addresses: 1000
        method_weights:
          - method: "*"
            cu_weight: 10
          - method: eth_blockNumber
            cu_weight: 1
          - method: eth_call
            cu_weight: 15
    plansPromoMonthly:
      summary: Plan policies and pricing parameters (active monthly reset promo)
      value:
        free:
          signup_units: 30000
          monthly_units: 100
          window_days: 30
          max_calls_per_sec: 5
        promo:
          type: reset
          chances_per_account: 1
          valid_days: null
          period: month
          reset_to_units: 30000
          issued_at: 2026-10-01T00:00:00Z
          ends_at: null
          next_issue_at: 2026-11-01T00:00:00Z
          next_period: month
        key_defaults:
          cu_per_sec: 400
          burst_cu: 1600
        pricing:
          units_per_usd: 10000
          cu_per_unit: 1000
          min_topup_usd: 1
          push_free_addresses: 1000
        method_weights:
          - method: "*"
            cu_weight: 10
          - method: eth_blockNumber
            cu_weight: 1
          - method: eth_call
            cu_weight: 15
    balanceOk:
      summary: Balance
      value:
        balance_units: 100000
        balance_updated_at: 2026-09-26T07:15:02Z
        settled_through: 2026-09-26T07:00:00Z
        plan: free
        free_monthly_units: 100
        max_calls_per_sec: 5
        free_window_ends_at: null
    chargesPage:
      summary: Page of charge records
      value:
        items:
          - charge_id: ch_63i5wlgssx4oizytig7775nsse
            period_start: 2026-09-26T06:00:00Z
            period_end: 2026-09-26T07:00:00Z
            cu: 123456
            amount_units: 123
            created_at: 2026-09-26T07:15:02Z
        next_before: ch_63i5wlgssx4oizytig7775nsse
    chargesLimitTooLarge:
      summary: limit exceeds 100
      value:
        error:
          code: invalid_request
          message: limit must be between 1 and 100
    topupsPage:
      summary: Page of top-up records (no more data)
      value:
        items:
          - topup_id: tu_aeka32itbwcnnmczuivkqa7jly
            amount_units: 100000
            created_at: 2026-09-26T06:00:00Z
            kind: paid
            amount_usd: "10.000000"
            currency: USD
        next_before: null
    topupsLimitZero:
      summary: limit is 0
      value:
        error:
          code: invalid_request
          message: limit must be between 1 and 100
    usageOk:
      summary: JSON-RPC and push usage grouped by date, key, chain, and method
      value:
        from: 2026-09-01
        to: 2026-09-26
        rows:
          - date: 2026-09-25
            key_id: k_0123456789ab
            chain: null
            method: push.address_day
            calls: 2
            cu: 66
          - date: 2026-09-25
            key_id: k_0123456789ab
            chain: null
            method: push.log
            calls: 1
            cu: 150
          - date: 2026-09-25
            key_id: k_0123456789ab
            chain: null
            method: push.native_transfer
            calls: 1
            cu: 150
          - date: 2026-09-25
            key_id: k_0123456789ab
            chain: null
            method: push.token_transfer
            calls: 3
            cu: 450
          - date: 2026-09-25
            key_id: k_0123456789ab
            chain: robinhood_mainnet
            method: eth_blockNumber
            calls: 1520
            cu: 30400
    usageRangeTooLong:
      summary: Range span 32 days, exceeding 31 days
      value:
        error:
          code: invalid_request
          message: date range cannot exceed 31 days
    usageOtherKey:
      summary: key_id does not belong to this account
      value:
        error:
          code: not_found
          message: key not found
    errUsageUnavailable:
      summary: Usage service unavailable
      value:
        error:
          code: usage_unavailable
          message: usage service unavailable
    resetsOk:
      summary: Active promo round, 2 chances remaining
      value:
        available: 2
        next_expires_at: 2026-10-31T00:00:00Z
        reset_to_units: 30000
        items:
          - round: launch
            available_from: 2026-10-01T00:00:00Z
            expires_at: 2026-10-31T00:00:00Z
            total: 3
            used: 1
            remaining: 2
    resetsMonthly:
      summary: Monthly round chances stacked with manual grant (earliest expiring used
        first)
      value:
        available: 2
        next_expires_at: 2026-10-30T04:38:35Z
        reset_to_units: 30000
        items:
          - round: reset-20260930
            available_from: 2026-09-30T04:38:35Z
            expires_at: 2026-10-30T04:38:35Z
            total: 1
            used: 0
            remaining: 1
          - round: monthly-2026-10
            available_from: 2026-10-01T00:00:00Z
            expires_at: 2026-11-01T00:00:00Z
            total: 1
            used: 0
            remaining: 1
    resetsMonthlyNewAccount:
      summary: "Mid-month account: November chance valid until creation time next month, December chance from Dec 1 (two active)"
      value:
        available: 2
        next_expires_at: 2026-12-20T10:00:00Z
        reset_to_units: 30000
        items:
          - round: monthly-2026-11
            available_from: 2026-11-20T10:00:00Z
            expires_at: 2026-12-20T10:00:00Z
            total: 1
            used: 0
            remaining: 1
          - round: monthly-2026-12
            available_from: 2026-12-01T00:00:00Z
            expires_at: 2027-01-01T00:00:00Z
            total: 1
            used: 0
            remaining: 1
    resetsNone:
      summary: No applicable promo or chances expired
      value:
        available: 0
        next_expires_at: null
        reset_to_units: 30000
        items: []
    resetUsed:
      summary: Use one chance, balance replenished to target
      value:
        refill_units: 29850
        balance_units: 30000
        remaining: 1
    resetUseDisabled:
      summary: Account disabled after session check
      value:
        error:
          code: user_disabled
          message: this user has been disabled
    errNoResetAvailable:
      summary: No reset chance available
      value:
        error:
          code: no_reset_available
          message: no reset chance is available
    errNothingToReset:
      summary: Balance not below reset target
      value:
        error:
          code: no_reset_available
          message: the balance already reaches the reset amount
          reason: nothing_to_reset
  responses:
    Unauthorized:
      description: Missing or invalid session (without distinguishing cause).
      headers:
        WWW-Authenticate:
          $ref: "#/components/headers/WwwAuthenticate"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errUnauthenticated:
              $ref: "#/components/examples/errUnauthenticated"
            errUnauthenticatedLinkChallenge:
              $ref: "#/components/examples/errUnauthenticatedLinkChallenge"
    MethodNotAllowed:
      description: Path exists but HTTP method does not match.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errMethodNotAllowed:
              $ref: "#/components/examples/errMethodNotAllowed"
    PayloadTooLarge:
      description: Request body exceeds 64 KiB limit. Exceeding this limit returns 413
        (`payload_too_large`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            payloadTooLarge:
              $ref: "#/components/examples/payloadTooLarge"
    TooManyRequests:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errRateLimited:
              $ref: "#/components/examples/errRateLimited"
    InternalError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errInternal:
              $ref: "#/components/examples/errInternal"
    UserDisabled:
      description: Account has been disabled; login not permitted.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errUserDisabled:
              $ref: "#/components/examples/errUserDisabled"
    SignupRateLimited:
      description: "Rate limit exceeded during login: request rate limit is `rate_limited`; sign-up budget exhaustion is `signup_rate_limited`, with `reason` `per_ip` (same network prefix) or `global` (all sources combined)."
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errRateLimited:
              $ref: "#/components/examples/errRateLimited"
            errSignupRateLimited:
              $ref: "#/components/examples/errSignupRateLimited"
    SignupPaused:
      description: New account creation temporarily unavailable; affects new
        registrations only; existing users log in normally.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errSignupPaused:
              $ref: "#/components/examples/errSignupPaused"
    ServiceUnavailable:
      description: Service unavailable; includes Retry-After header.
      headers:
        Retry-After:
          description: Wait time before retrying, in seconds.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            errServiceUnavailable:
              $ref: "#/components/examples/errServiceUnavailable"
  headers:
    WwwAuthenticate:
      description: "401 always includes `WWW-Authenticate: Bearer`."
      required: true
      schema:
        type: string
        const: Bearer
    RetryAfter:
      description: Suggested wait time in seconds.
      required: true
      schema:
        type: integer
        minimum: 1
      example: 6
    CacheControl:
      description: No store (included in all responses).
      required: true
      schema:
        type: string
        const: no-store
  parameters:
    IdentityIdPath:
      name: identity_id
      in: path
      required: true
      schema:
        type: string
        pattern: ^id_[a-z2-7]{26}$
      example: id_xupnufatg6to3qmkjqvahsyhcu
    IncludeRevokedQuery:
      name: include_revoked
      in: query
      required: false
      description: "`true` to include revoked keys."
      schema:
        type: boolean
        default: false
      example: true
    KeyIdPath:
      name: key_id
      in: path
      required: true
      schema:
        type: string
        pattern: ^k_[0-9a-f]{12}$
      example: k_0123456789ab
    LimitQuery:
      name: limit
      in: query
      required: false
      description: Number of items per page.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
      example: 50
    BeforeQuery:
      name: before
      in: query
      required: false
      description: Opaque cursor from previous page (`next_before` from previous
        response); returns records strictly before this cursor.
      schema:
        type: string
        pattern: ^(ch|tu)_[a-z2-7]{26}$
      example: ch_63i5wlgssx4oizytig7775nsse
    UsageFromQuery:
      name: from
      in: query
      required: true
      description: Start UTC date (inclusive).
      schema:
        type: string
        format: date
      example: 2026-09-01
    UsageToQuery:
      name: to
      in: query
      required: true
      description: End UTC date (inclusive), not later than today; inclusive range of
        days ≤ 31.
      schema:
        type: string
        format: date
      example: 2026-09-26
    UsageKeyIdQuery:
      name: key_id
      in: query
      required: false
      description: Filter by this API key (must belong to this account, including
        revoked keys).
      schema:
        type: string
        pattern: ^k_[0-9a-f]{12}$
      example: k_0123456789ab
