openapi: 3.0.3
info:
  title: SolaceScan API
  description: |
    SolaceScan public explorer API for Chain 138 with tiered access control.
    
    ## Authentication
    
    Track 1 endpoints are public and require no authentication.
    Track 2-4 endpoints require JWT authentication via wallet signature.
    
    ## Rate Limiting
    
    - Track 1: 100 requests/minute per IP
    - Track 2-4: Based on user tier and subscription
    
  version: 1.0.0
  contact:
    name: API Support
    email: support@d-bis.org
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://api.d-bis.org
    description: Production server
  - url: http://localhost:8080
    description: Development server

tags:
  - name: Health
    description: Health check endpoints
  - name: Auth
    description: Wallet and user-session authentication endpoints
  - name: Access
    description: RPC product catalog, subscriptions, and API key lifecycle
  - name: Blocks
    description: Block-related endpoints
  - name: Transactions
    description: Transaction-related endpoints
  - name: Addresses
    description: Address-related endpoints
  - name: Search
    description: Unified search endpoints
  - name: Track1
    description: Public RPC gateway endpoints (no auth required)
  - name: MissionControl
    description: Public mission-control health, bridge trace, and cached liquidity helpers
  - name: Track2
    description: Indexed explorer endpoints (auth required)
  - name: Track3
    description: Analytics endpoints (Track 3+ required)
  - name: Track4
    description: Operator endpoints (Track 4 + IP whitelist)

paths:
  /health:
    get:
      tags:
        - Health
      summary: Health check
      description: Returns the health status of the API
      operationId: getHealth
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  timestamp:
                    type: string
                    format: date-time
                  database:
                    type: string
                    example: connected

  /api/v1/auth/nonce:
    post:
      tags:
        - Auth
      summary: Generate wallet auth nonce
      description: Creates a nonce challenge for wallet-signature authentication.
      operationId: createWalletAuthNonce
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WalletNonceRequest'
      responses:
        '200':
          description: Nonce generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletNonceResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          description: Wallet auth storage or database not available

  /api/v1/auth/wallet:
    post:
      tags:
        - Auth
      summary: Authenticate with wallet signature
      description: Exchanges an address, signature, and nonce for a JWT used by wallet-authenticated track endpoints.
      operationId: authenticateWallet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WalletAuthRequest'
      responses:
        '200':
          description: Wallet authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletAuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Wallet auth storage or database not available

  /api/v1/auth/refresh:
    post:
      tags:
        - Auth
      summary: Refresh a wallet JWT
      description: |
        Accepts a still-valid wallet JWT via `Authorization: Bearer <token>`,
        revokes its `jti` server-side, and returns a freshly issued token with
        a new `jti` and a per-track TTL (Track 4 is capped at 60 minutes).
        Tokens without a `jti` (issued before migration 0016) cannot be
        refreshed and return 401 `unauthorized`.
      operationId: refreshWalletJWT
      security:
        - bearerAuth: []
      responses:
        '200':
          description: New token issued; old token revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletAuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Wallet auth storage or jwt_revocations table missing

  /api/v1/auth/logout:
    post:
      tags:
        - Auth
      summary: Revoke the current wallet JWT
      description: |
        Inserts the bearer token's `jti` into the `jwt_revocations` table
        (migration 0016). Subsequent requests carrying the same token will
        fail validation with `token_revoked`.
      operationId: logoutWallet
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Token revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: jwt_revocations table missing; run migration 0016_jwt_revocations

  /api/v1/auth/register:
    post:
      tags:
        - Auth
      summary: Register an explorer access user
      description: "Creates an email/password account for the `/access` console and returns a user session token."
      operationId: registerAccessUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserRegisterRequest'
      responses:
        '200':
          description: User created and session issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          description: Database not available

  /api/v1/auth/login:
    post:
      tags:
        - Auth
      summary: Log in to the explorer access console
      description: "Authenticates an email/password user and returns a user session token for `/api/v1/access/*` endpoints."
      operationId: loginAccessUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserLoginRequest'
      responses:
        '200':
          description: Session issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSessionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/access/me:
    get:
      tags:
        - Access
      summary: Get current access-console user
      description: Returns the signed-in user profile and any known product subscriptions.
      operationId: getAccessMe
      security:
        - userSessionAuth: []
      responses:
        '200':
          description: Current user and subscriptions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessMeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/access/products:
    get:
      tags:
        - Access
      summary: List available RPC access products
      description: Returns the commercial and operational RPC products currently modeled by the explorer access layer.
      operationId: listAccessProducts
      responses:
        '200':
          description: Product catalog
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessProductsResponse'

  /api/v1/access/subscriptions:
    get:
      tags:
        - Access
      summary: List subscriptions for the signed-in user
      operationId: listAccessSubscriptions
      security:
        - userSessionAuth: []
      responses:
        '200':
          description: Subscription list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessSubscriptionsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/access/admin/subscriptions:
    get:
      tags:
        - Access
      summary: List subscriptions for admin review
      description: Returns pending or filtered subscriptions for users whose email is allowlisted in `ACCESS_ADMIN_EMAILS`.
      operationId: listAccessAdminSubscriptions
      security:
        - userSessionAuth: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [pending, active, suspended, revoked]
      responses:
        '200':
          description: Subscription list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessSubscriptionsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Admin privileges required
        '503':
          description: Database not available
    post:
      tags:
        - Access
      summary: Approve, suspend, or revoke a subscription
      operationId: updateAccessAdminSubscription
      security:
        - userSessionAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminSubscriptionActionRequest'
      responses:
        '200':
          description: Subscription updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessSubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Admin privileges required
        '503':
          description: Database not available
    post:
      tags:
        - Access
      summary: Request or activate product access
      description: |
        Creates or updates a product subscription. Self-service products become `active` immediately.
        Approval-gated products such as Core RPC are created in `pending` state.
      operationId: createAccessSubscription
      security:
        - userSessionAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
      responses:
        '200':
          description: Subscription saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessSubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/access/api-keys:
    get:
      tags:
        - Access
      summary: List API keys for the signed-in user
      operationId: listAccessApiKeys
      security:
        - userSessionAuth: []
      responses:
        '200':
          description: API key records
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessAPIKeysResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available
    post:
      tags:
        - Access
      summary: Create an API key
      description: |
        Issues an API key for the chosen tier and product. If the product is approval-gated and not already active
        for the user, this endpoint returns `subscription_required`.
      operationId: createAccessApiKey
      security:
        - userSessionAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAPIKeyRequest'
      responses:
        '200':
          description: API key created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateAPIKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Product access is pending approval or inactive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: subscription_required
                  message: Product access is pending approval or inactive
        '503':
          description: Database not available

  /api/v1/access/api-keys/{id}:
    post:
      tags:
        - Access
      summary: Revoke an API key
      description: "Revokes the identified API key. `DELETE` is also accepted by the handler, but the current frontend uses `POST`."
      operationId: revokeAccessApiKey
      security:
        - userSessionAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: API key revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RevokeAPIKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available
    delete:
      tags:
        - Access
      summary: Revoke an API key
      description: Alternate HTTP verb for API key revocation.
      operationId: revokeAccessApiKeyDelete
      security:
        - userSessionAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: API key revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RevokeAPIKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/access/usage:
    get:
      tags:
        - Access
      summary: Get usage summary for the signed-in user
      description: Returns aggregated per-product usage derived from issued API keys.
      operationId: getAccessUsage
      security:
        - userSessionAuth: []
      responses:
        '200':
          description: Usage summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessUsageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/access/audit:
    get:
      tags:
        - Access
      summary: Get recent API activity for the signed-in user
      description: Returns recent validated API-key usage log rows for the current user.
      operationId: getAccessAudit
      security:
        - userSessionAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 20
      responses:
        '200':
          description: Audit entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessAuditResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/access/admin/audit:
    get:
      tags:
        - Access
      summary: Get recent API activity across users for admin review
      description: Returns recent validated API-key usage log rows for access admins, optionally filtered by product.
      operationId: getAccessAdminAudit
      security:
        - userSessionAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: product
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Audit entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessAuditResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '503':
          description: Database not available

  /api/v1/access/internal/validate-key:
    get:
      tags:
        - Access
      summary: Validate an API key for nginx auth_request or similar edge subrequests
      description: >-
        Requires `X-Access-Internal-Secret` and accepts the presented API key in
        `X-API-Key` or `Authorization: Bearer ...`. Returns `200` or `401` and
        emits validation metadata in response headers.
      operationId: validateAccessApiKeyInternalGet
      parameters:
        - name: X-Access-Internal-Secret
          in: header
          required: true
          schema:
            type: string
        - name: X-API-Key
          in: header
          required: false
          schema:
            type: string
        - name: Authorization
          in: header
          required: false
          schema:
            type: string
        - name: X-Access-Method
          in: header
          required: false
          schema:
            type: string
        - name: X-Access-Request-Count
          in: header
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: Key validated
          headers:
            X-Validated-Product:
              schema:
                type: string
            X-Validated-Tier:
              schema:
                type: string
            X-Validated-Scopes:
              schema:
                type: string
            X-Quota-Remaining:
              schema:
                type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available
    post:
      tags:
        - Access
      summary: Validate an API key for internal edge enforcement
      description: Requires `X-Access-Internal-Secret` and returns validated key metadata while incrementing usage counters.
      operationId: validateAccessApiKeyInternal
      parameters:
        - name: X-Access-Internal-Secret
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InternalValidateAPIKeyRequest'
      responses:
        '200':
          description: Key validated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InternalValidateAPIKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Database not available

  /api/v1/blocks:
    get:
      tags:
        - Blocks
      summary: List blocks
      description: Returns a paginated list of blocks
      operationId: listBlocks
      parameters:
        - name: limit
          in: query
          description: Number of blocks to return
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: page
          in: query
          description: Page number
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: chain_id
          in: query
          description: Chain ID filter
          required: false
          schema:
            type: integer
            default: 138
      responses:
        '200':
          description: List of blocks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlockListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/v1/blocks/{chain_id}/{number}:
    get:
      tags:
        - Blocks
      summary: Get block by number
      description: Returns block details by chain ID and block number
      operationId: getBlockByNumber
      parameters:
        - name: chain_id
          in: path
          required: true
          description: Chain ID
          schema:
            type: integer
            example: 138
        - name: number
          in: path
          required: true
          description: Block number
          schema:
            type: integer
            example: 1000
      responses:
        '200':
          description: Block details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Block'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/v1/transactions:
    get:
      tags:
        - Transactions
      summary: List transactions
      description: Returns a paginated list of transactions
      operationId: listTransactions
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: chain_id
          in: query
          schema:
            type: integer
            default: 138
      responses:
        '200':
          description: List of transactions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionListResponse'

  /api/v1/search:
    get:
      tags:
        - Search
      summary: Unified search
      description: |
        Searches for blocks, transactions, or addresses.
        Automatically detects the type based on the query format.
      operationId: search
      parameters:
        - name: q
          in: query
          required: true
          description: Search query (block number, address, or transaction hash)
          schema:
            type: string
            example: "0x1234567890abcdef"
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/track1/blocks/latest:
    get:
      tags:
        - Track1
      summary: Get latest blocks (Public)
      description: Returns the latest blocks via RPC gateway. No authentication required.
      operationId: getLatestBlocks
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
            maximum: 50
      responses:
        '200':
          description: Latest blocks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlockListResponse'

  /api/v1/mission-control/stream:
    get:
      tags:
        - MissionControl
      summary: Mission-control SSE stream
      description: |
        Server-Sent Events stream with the same inner `data` payload as `GET /api/v1/track1/bridge/status`.
        Emits one event immediately, then refreshes every 20 seconds. Configure nginx with `proxy_buffering off`.
      operationId: getMissionControlStream
      responses:
        '200':
          description: SSE stream
          content:
            text/event-stream:
              schema:
                type: string

  /api/v1/mission-control/liquidity/token/{address}/pools:
    get:
      tags:
        - MissionControl
      summary: Cached liquidity proxy
      description: |
        30-second in-memory cached proxy to the token-aggregation pools endpoint for the configured `CHAIN_ID`.
      operationId: getMissionControlLiquidityPools
      parameters:
        - name: address
          in: path
          required: true
          schema:
            type: string
            pattern: '^0x[a-fA-F0-9]{40}$'
      responses:
        '200':
          description: Upstream JSON response
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          description: "`TOKEN_AGGREGATION_BASE_URL` not configured"

  /api/v1/mission-control/bridge/trace:
    get:
      tags:
        - MissionControl
      summary: Resolve a transaction through Blockscout and label 138-side contracts
      description: |
        Queries Blockscout using `BLOCKSCOUT_INTERNAL_URL` and labels the `from` and `to` addresses using Chain 138 entries from `SMART_CONTRACTS_MASTER_JSON`.
      operationId: getMissionControlBridgeTrace
      parameters:
        - name: tx
          in: query
          required: true
          schema:
            type: string
            pattern: '^0x[a-fA-F0-9]{64}$'
      responses:
        '200':
          description: Labeled bridge trace
        '400':
          $ref: '#/components/responses/BadRequest'
        '502':
          description: Blockscout lookup failed

  /api/v1/track4/operator/run-script:
    post:
      tags:
        - Track4
      summary: Run an allowlisted operator script
      description: |
        Track 4 endpoint. Requires authenticated wallet, IP allowlisting, `OPERATOR_SCRIPTS_ROOT`, and `OPERATOR_SCRIPT_ALLOWLIST`.
      operationId: runOperatorScript
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [script]
              properties:
                script:
                  type: string
                  description: "Path relative to `OPERATOR_SCRIPTS_ROOT`"
                args:
                  type: array
                  items:
                    type: string
                  maxItems: 24
      responses:
        '200':
          description: Script execution result
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '503':
          description: Script root or allowlist not configured

  /api/v1/track2/search:
    get:
      tags:
        - Track2
      summary: Advanced search (Auth Required)
      description: Advanced search with indexed data. Requires Track 2+ authentication.
      operationId: track2Search
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Search results
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token obtained from /api/v1/auth/wallet
    userSessionAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: User session token obtained from /api/v1/auth/register or /api/v1/auth/login

  schemas:
    WalletNonceRequest:
      type: object
      required: [address]
      properties:
        address:
          type: string
          pattern: '^0x[a-fA-F0-9]{40}$'

    WalletNonceResponse:
      type: object
      properties:
        address:
          type: string
        nonce:
          type: string
        message:
          type: string

    WalletAuthRequest:
      type: object
      required: [address, signature, nonce]
      properties:
        address:
          type: string
          pattern: '^0x[a-fA-F0-9]{40}$'
        signature:
          type: string
        nonce:
          type: string

    WalletAuthResponse:
      type: object
      properties:
        token:
          type: string
        expires_at:
          type: string
          format: date-time
        user:
          type: object
          additionalProperties: true

    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        username:
          type: string
        is_admin:
          type: boolean

    UserRegisterRequest:
      type: object
      required: [email, username, password]
      properties:
        email:
          type: string
          format: email
        username:
          type: string
        password:
          type: string
          minLength: 8

    UserLoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string

    UserSessionResponse:
      type: object
      properties:
        user:
          $ref: '#/components/schemas/User'
        token:
          type: string
        expires_at:
          type: string
          format: date-time

    AccessProduct:
      type: object
      properties:
        slug:
          type: string
        name:
          type: string
        provider:
          type: string
        vmid:
          type: integer
        http_url:
          type: string
        ws_url:
          type: string
        default_tier:
          type: string
        requires_approval:
          type: boolean
        billing_model:
          type: string
        description:
          type: string
        use_cases:
          type: array
          items:
            type: string
        management_features:
          type: array
          items:
            type: string

    AccessProductsResponse:
      type: object
      properties:
        products:
          type: array
          items:
            $ref: '#/components/schemas/AccessProduct'
        note:
          type: string

    AccessAPIKeyRecord:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        tier:
          type: string
        productSlug:
          type: string
        scopes:
          type: array
          items:
            type: string
        monthlyQuota:
          type: integer
        requestsUsed:
          type: integer
        approved:
          type: boolean
        approvedAt:
          type: string
          format: date-time
          nullable: true
        rateLimitPerSecond:
          type: integer
        rateLimitPerMinute:
          type: integer
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        revoked:
          type: boolean
        createdAt:
          type: string
          format: date-time

    AccessSubscription:
      type: object
      properties:
        id:
          type: string
        productSlug:
          type: string
        tier:
          type: string
        status:
          type: string
          enum: [active, pending, suspended, revoked]
        monthlyQuota:
          type: integer
        requestsUsed:
          type: integer
        requiresApproval:
          type: boolean
        approvedAt:
          type: string
          format: date-time
          nullable: true
        approvedBy:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time

    AccessUsageSummary:
      type: object
      properties:
        product_slug:
          type: string
        active_keys:
          type: integer
        requests_used:
          type: integer
        monthly_quota:
          type: integer

    AccessMeResponse:
      type: object
      properties:
        user:
          $ref: '#/components/schemas/User'
        subscriptions:
          type: array
          items:
            $ref: '#/components/schemas/AccessSubscription'

    AccessSubscriptionsResponse:
      type: object
      properties:
        subscriptions:
          type: array
          items:
            $ref: '#/components/schemas/AccessSubscription'

    AccessSubscriptionResponse:
      type: object
      properties:
        subscription:
          $ref: '#/components/schemas/AccessSubscription'

    AccessAPIKeysResponse:
      type: object
      properties:
        api_keys:
          type: array
          items:
            $ref: '#/components/schemas/AccessAPIKeyRecord'

    CreateSubscriptionRequest:
      type: object
      required: [product_slug]
      properties:
        product_slug:
          type: string
        tier:
          type: string

    CreateAPIKeyRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        tier:
          type: string
        product_slug:
          type: string
        expires_days:
          type: integer
        monthly_quota:
          type: integer
        scopes:
          type: array
          items:
            type: string

    AdminSubscriptionActionRequest:
      type: object
      required: [subscription_id, status]
      properties:
        subscription_id:
          type: string
        status:
          type: string
          enum: [active, suspended, revoked]
        notes:
          type: string

    CreateAPIKeyResponse:
      type: object
      properties:
        api_key:
          type: string
          description: Plaintext key is only returned at creation time.
        record:
          $ref: '#/components/schemas/AccessAPIKeyRecord'

    RevokeAPIKeyResponse:
      type: object
      properties:
        revoked:
          type: boolean
        api_key_id:
          type: string

    AccessUsageResponse:
      type: object
      properties:
        usage:
          type: array
          items:
            $ref: '#/components/schemas/AccessUsageSummary'

    AccessAuditEntry:
      type: object
      properties:
        id:
          type: integer
        apiKeyId:
          type: string
        keyName:
          type: string
        productSlug:
          type: string
        methodName:
          type: string
        requestCount:
          type: integer
        lastIp:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time

    AccessAuditResponse:
      type: object
      properties:
        entries:
          type: array
          items:
            $ref: '#/components/schemas/AccessAuditEntry'

    InternalValidatedAPIKey:
      type: object
      properties:
        apiKeyId:
          type: string
        userId:
          type: string
        name:
          type: string
        tier:
          type: string
        productSlug:
          type: string
        scopes:
          type: array
          items:
            type: string
        monthlyQuota:
          type: integer
        requestsUsed:
          type: integer
        rateLimitPerSecond:
          type: integer
        rateLimitPerMinute:
          type: integer
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true

    InternalValidateAPIKeyRequest:
      type: object
      required: [api_key]
      properties:
        api_key:
          type: string
        method_name:
          type: string
        request_count:
          type: integer
        last_ip:
          type: string

    InternalValidateAPIKeyResponse:
      type: object
      properties:
        valid:
          type: boolean
        key:
          $ref: '#/components/schemas/InternalValidatedAPIKey'

    Block:
      type: object
      properties:
        chain_id:
          type: integer
          example: 138
        number:
          type: integer
          example: 1000
        hash:
          type: string
          example: "0x1234567890abcdef"
        parent_hash:
          type: string
        timestamp:
          type: string
          format: date-time
        miner:
          type: string
        transaction_count:
          type: integer
        gas_used:
          type: integer
        gas_limit:
          type: integer

    Transaction:
      type: object
      properties:
        chain_id:
          type: integer
        hash:
          type: string
        block_number:
          type: integer
        from_address:
          type: string
        to_address:
          type: string
        value:
          type: string
        gas:
          type: integer
        gas_price:
          type: string
        status:
          type: string
          enum: [success, failed]

    BlockListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Block'
        pagination:
          $ref: '#/components/schemas/Pagination'

    TransactionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Transaction'
        pagination:
          $ref: '#/components/schemas/Pagination'

    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        total_pages:
          type: integer

    SearchResponse:
      type: object
      properties:
        query:
          type: string
        results:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
                enum: [block, transaction, address]
              data:
                type: object

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string

  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "bad_request"
              message: "Invalid request parameters"

    Unauthorized:
      description: Unauthorized - Authentication required
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "unauthorized"
              message: "Authentication required"

    Forbidden:
      description: Forbidden - Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "forbidden"
              message: "Insufficient permissions. Track 2+ required."

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "not_found"
              message: "Resource not found"

    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "internal_error"
              message: "An internal error occurred"
