openapi: 3.1.0
info:
  title: Token Kiosk — LLM Gateway API
  version: 1.0.0
  description: >
    Credit-based LLM inference gateway with SIWE (Sign-In with Ethereum)
    authentication and x402 stablecoin top-ups. Top up a USDC/USDT balance,
    create an API key, then call OpenAI-compatible inference endpoints.


    **Authentication**

    - **API key** — `Authorization: Bearer sk-<64 hex>` for inference and usage.
    - **SIWE** — a signed message + signature (in the body for `POST`/`DELETE`,
      or as query parameters for `GET`) for API-key management and wallet-scoped reads.
    - **x402** — a payment signature in the `X-Payment` header for top-ups.
  license:
    name: Proprietary
servers:
  - url: https://agent-router.gaib.ai
    description: Production (Base mainnet)
  - url: https://agent-router.gaib.cloud
    description: Staging (Base Sepolia testnet)

tags:
  - name: Inference
  - name: Models
  - name: Top-up
  - name: API Keys
  - name: Account
  - name: System

paths:
  /health:
    get:
      tags: [System]
      summary: Health check
      security: []
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }

  /v1/models:
    get:
      tags: [Models]
      summary: List models with pricing
      description: >
        Returns all available models with downstream prices (USD per 1M tokens,
        the rate you're billed at). No authentication required.
      security: []
      responses:
        '200':
          description: List of models
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModelList'

  /v1/chat/completions:
    post:
      tags: [Inference]
      summary: Create chat completion (OpenAI-compatible)
      description: >
        OpenAI-compatible inference. Balance is reserved before the upstream call
        based on `max_tokens`, then reconciled to actual usage. Set `stream: true`
        for server-sent events. Requests containing `thinking` or `reasoning_effort`
        are rejected with HTTP 400.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatCompletionRequest'
      responses:
        '200':
          description: >
            Chat completion. When `stream` is false, a JSON completion object.
            When `stream` is true, a `text/event-stream` of OpenAI SSE chunks
            terminated by `data: [DONE]`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatCompletionResponse'
            text/event-stream:
              schema:
                type: string
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/InsufficientBalance' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }

  /v1/topup:
    post:
      tags: [Top-up]
      summary: Fund wallet balance (x402)
      description: >
        Two-step x402 flow. The first call (no `X-Payment` header) returns HTTP 402
        with payment requirements. Re-send the same request with a signed
        `X-Payment` header to settle and credit the balance. USDC (ERC-3009) and
        USDT (Permit2) are accepted on the deployment's networks.
      security:
        - x402Payment: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TopupRequest'
      responses:
        '200':
          description: Payment settled, balance credited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TopupResponse'
        '402':
          description: Payment required — payment requirements returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequirements'
        '400': { $ref: '#/components/responses/ValidationError' }

  /v1/topups/{wallet}:
    get:
      tags: [Top-up]
      summary: List top-up history
      description: >
        List a wallet's top-ups, newest first (used for receipts). Wallet-scoped:
        authenticate with an API key (header or `api_key` query param) or SIWE
        (query params).
      parameters:
        - $ref: '#/components/parameters/WalletPath'
        - $ref: '#/components/parameters/SiweMessageQuery'
        - $ref: '#/components/parameters/SiweSignatureQuery'
        - name: api_key
          in: query
          required: false
          schema: { type: string }
          description: API key, as an alternative to the `Authorization` header.
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: Top-up history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TopupsResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/auth/keys:
    post:
      tags: [API Keys]
      summary: Create API key
      description: Create a new API key. Authenticate with SIWE in the request body.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreateRequest'
      responses:
        '201':
          description: >
            API key created. The plaintext `key` is returned **only once**.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreateResponse'
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [API Keys]
      summary: List API keys
      description: List the wallet's API keys. Pass SIWE auth as query parameters.
      security: []
      parameters:
        - $ref: '#/components/parameters/SiweMessageQuery'
        - $ref: '#/components/parameters/SiweSignatureQuery'
      responses:
        '200':
          description: API keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKeySummary'
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/auth/keys/{key_id}:
    delete:
      tags: [API Keys]
      summary: Revoke API key
      description: Revoke an API key. Authenticate with SIWE in the request body.
      security: []
      parameters:
        - name: key_id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiweAuth'
      responses:
        '200':
          description: Key revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked: { type: boolean, example: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/usage/{wallet}:
    get:
      tags: [Account]
      summary: Wallet balance and per-key usage
      description: >
        Returns balance plus a per-key breakdown of requests, tokens, and charges.
        Authenticate with an API key (header) or SIWE (query params).
      parameters:
        - $ref: '#/components/parameters/WalletPath'
        - $ref: '#/components/parameters/SiweMessageQuery'
        - $ref: '#/components/parameters/SiweSignatureQuery'
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: Usage breakdown
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/balance/{wallet}:
    get:
      tags: [Account]
      summary: Available balance
      description: >
        Return a wallet's available credit (balance minus locked-for-in-flight
        amount). Public — no authentication. Unknown wallets return 0.
      security: []
      parameters:
        - $ref: '#/components/parameters/WalletPath'
      responses:
        '200':
          description: Available balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk-<64 hex>
      description: API key issued by `POST /v1/auth/keys`.
    x402Payment:
      type: apiKey
      in: header
      name: X-Payment
      description: Base64-encoded x402 payment signature.

  parameters:
    WalletPath:
      name: wallet
      in: path
      required: true
      description: Ethereum wallet address (case-insensitive).
      schema:
        type: string
        pattern: '^0x[a-fA-F0-9]{40}$'
        example: '0xabc0000000000000000000000000000000000000'
    SiweMessageQuery:
      name: message
      in: query
      required: false
      description: Prepared SIWE message (for SIWE auth on GET endpoints).
      schema: { type: string }
    SiweSignatureQuery:
      name: signature
      in: query
      required: false
      description: Hex-encoded wallet signature of the SIWE message.
      schema: { type: string }

  responses:
    ValidationError:
      description: Invalid or missing parameters
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: VALIDATION_ERROR, message: Thinking/reasoning models are not supported }
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: UNAUTHORIZED, message: Invalid API key }
    InsufficientBalance:
      description: Not enough credit for the reserved cost
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: INSUFFICIENT_BALANCE, message: 'Insufficient balance. Top up at POST /v1/topup' }
    RateLimited:
      description: Per-wallet rate limit exceeded
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: RATE_LIMITED, message: Rate limit exceeded }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: NOT_FOUND, message: Not found }
    UpstreamError:
      description: LLM provider failed or timed out (balance lock released, no charge)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: UPSTREAM_ERROR, message: Provider returned an error }

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          description: Machine-readable error code.
          enum:
            - VALIDATION_ERROR
            - UNAUTHORIZED
            - INSUFFICIENT_BALANCE
            - NOT_FOUND
            - RATE_LIMITED
            - INTERNAL_ERROR
            - UPSTREAM_ERROR
        message:
          type: string
          description: Human-readable description.

    Model:
      type: object
      properties:
        id: { type: string, example: gemini/gemini-2.5-flash }
        name: { type: string, example: gemini-2.5-flash }
        provider: { type: string, example: gemini }
        contextLength: { type: integer, example: 1048576 }
        promptPricePer1MTokens: { type: number, description: USD per 1M prompt tokens, example: 0.15 }
        completionPricePer1M: { type: number, description: USD per 1M completion tokens, example: 0.60 }

    ModelList:
      type: object
      properties:
        object: { type: string, example: list }
        data:
          type: array
          items: { $ref: '#/components/schemas/Model' }

    ChatMessage:
      type: object
      required: [role, content]
      properties:
        role:
          type: string
          enum: [system, user, assistant, tool]
        content:
          type: string

    ChatCompletionRequest:
      type: object
      required: [model, messages]
      additionalProperties: true
      description: >
        Standard OpenAI fields not listed here (`stop`, `presence_penalty`,
        `frequency_penalty`, `response_format`, `seed`, `n`, `logprobs`) are
        accepted and forwarded to the provider. `thinking` and `reasoning_effort`
        are rejected with HTTP 400.
      properties:
        model:
          type: string
          example: gemini/gemini-2.5-flash
        messages:
          type: array
          items: { $ref: '#/components/schemas/ChatMessage' }
        max_tokens:
          type: integer
          default: 1024
        temperature: { type: number }
        top_p: { type: number }
        stream: { type: boolean, default: false }
        tools:
          type: array
          items: { type: object }
        tool_choice:
          oneOf:
            - { type: string }
            - { type: object }

    ChatCompletionResponse:
      type: object
      additionalProperties: true
      properties:
        id: { type: string, example: chatcmpl-abc123 }
        object: { type: string, example: chat.completion }
        choices:
          type: array
          items:
            type: object
            properties:
              index: { type: integer }
              message: { $ref: '#/components/schemas/ChatMessage' }
              finish_reason: { type: string, example: stop }
        usage:
          type: object
          properties:
            prompt_tokens: { type: integer }
            completion_tokens: { type: integer }
            total_tokens: { type: integer }

    TopupRequest:
      type: object
      required: [amount]
      properties:
        amount:
          type: number
          minimum: 1
          description: USD amount to credit. Minimum $1.
          example: 5

    PaymentRequirements:
      type: object
      properties:
        accepts:
          type: array
          items:
            type: object
            properties:
              scheme: { type: string, example: exact }
              network: { type: string, example: 'eip155:8453' }
              amount: { type: string, example: '5000000' }
              maxAmountRequired: { type: string, example: '5000000' }
              asset: { type: string, example: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }
              payTo: { type: string }
              extra: { type: object }

    TopupResponse:
      type: object
      properties:
        balance_usdc:
          type: integer
          description: New balance in micro-USDC (/ 1_000_000 = USD).
          example: 5000000
        credited_usdc:
          type: integer
          description: Amount credited in micro-USDC.
          example: 5000000

    TopupRecord:
      type: object
      properties:
        id: { type: integer, example: 12 }
        amount_usdc: { type: integer, description: micro-USDC, example: 5000000 }
        tx_hash: { type: string, description: On-chain settlement tx hash }
        created_at: { type: string, format: date-time }

    TopupsResponse:
      type: object
      properties:
        wallet: { type: string }
        network:
          type: string
          description: Human-readable settlement network.
          example: Base (Mainnet)
        topups:
          type: array
          items: { $ref: '#/components/schemas/TopupRecord' }

    SiweAuth:
      type: object
      required: [message, signature]
      properties:
        message: { type: string, description: Prepared SIWE message }
        signature: { type: string, description: Hex-encoded wallet signature }

    ApiKeyCreateRequest:
      allOf:
        - $ref: '#/components/schemas/SiweAuth'
        - type: object
          properties:
            label:
              type: string
              description: Human-readable label.
              example: my-app

    ApiKeyCreateResponse:
      type: object
      properties:
        id: { type: integer, example: 1 }
        key:
          type: string
          description: Plaintext API key — returned only once.
          example: sk-a3f9e2b1c4d5...
        label: { type: string, example: my-app }
        created_at: { type: string, format: date-time }

    ApiKeySummary:
      type: object
      properties:
        id: { type: integer, example: 1 }
        label: { type: string, example: my-app }
        created_at: { type: string, format: date-time }
        revoked_at:
          type: [string, 'null']
          format: date-time

    UsageKey:
      type: object
      properties:
        api_key_id: { type: integer, example: 1 }
        label: { type: string, example: my-app }
        request_count: { type: integer, example: 3 }
        total_prompt_tokens: { type: integer, example: 66 }
        total_completion_tokens: { type: integer, example: 174 }
        total_charged_usdc: { type: integer, description: micro-USDC, example: 50000 }
        total_platform_revenue_usd: { type: number, example: 0.008 }

    UsageResponse:
      type: object
      properties:
        wallet: { type: string }
        balance_usdc: { type: integer, description: micro-USDC, example: 4950000 }
        locked_usdc: { type: integer, description: micro-USDC, example: 0 }
        available_usdc: { type: integer, description: 'balance_usdc − locked_usdc', example: 4950000 }
        keys:
          type: array
          items: { $ref: '#/components/schemas/UsageKey' }

    BalanceResponse:
      type: object
      properties:
        wallet: { type: string }
        available_usdc:
          type: integer
          description: Available credit in micro-USDC (balance_usdc − locked_usdc).
          example: 4950000
