openapi: 3.0.3
info:
  title: x402
  version: 0.7.0
  description: |
    Agent/machine payments via HTTP 402 on Base mainnet (USDC).

    ## API Versioning
    Every response includes an `X-API-Version` header with the current API
    version (semver). Deprecated endpoints additionally return `Deprecation: true`,
    an optional `Sunset` date, and a `Link` header pointing to the successor.

    ## Authentication
    Most endpoints require an `X-API-Key` header. Obtain one by signing up via
    `POST /dashboard/signup`.

    ## x402 Payment Flow
    1. Client calls a paywalled resource.
    2. Server returns **402** with a challenge in `WWW-Authenticate: X402`.
    3. Client submits payment on-chain and re-requests with an `X-PAYMENT` header
       containing `nonce`, `txHash`, and `signature`.
    4. Server verifies the tx (2-block confirmation for micropayments) and grants access.

    ## Rate Limiting
    Requests are rate-limited per tenant based on subscription plan.
    Every rate-limited response (both **429** and successful) includes standard
    `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers.

    | Plan | Tokens / min | Refill rate |
    |------|-------------|-------------|
    | free | 10 | ~0.17/s |
    | starter | 100 | ~1.67/s |
    | growth | 500 | ~8.33/s |
    | scale | 2000 | ~33.33/s |

    Plan capacities are configurable via `RATE_LIMIT_<PLAN>_CAPACITY` env vars.
    When the bucket is exhausted the server responds with **429** and a
    `Retry-After` header.

    ## Security Headers
    Every response includes the following security headers:
    - `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload`
    - `X-Frame-Options: DENY`
    - `X-Content-Type-Options: nosniff`
    - `Referrer-Policy: strict-origin-when-cross-origin`
    - `Permissions-Policy: camera=(), microphone=(), geolocation=()`
    - `Content-Security-Policy: default-src 'none'` (JSON responses only)

    ## Body Size Limit
    All endpoints enforce a **100 KB** request body limit. Requests exceeding
    this limit receive a **413 Payload Too Large** response.

servers:
  - url: https://api.example.com/{stage}
    variables:
      stage:
        default: dev
        enum: [dev, staging, prod]

tags:
  - name: public
    description: Unauthenticated landing + docs + demo signup
  - name: core
    description: Health and paywalled resource endpoints
  - name: quotes
    description: Fiat-to-crypto quote aggregation
  - name: dashboard
    description: Tenant self-service dashboard (HTML)
  - name: webhooks
    description: Inbound webhook sinks (exchange + Stripe)
  - name: admin
    description: Operational admin endpoints (require X-ADMIN-KEY header)

paths:
  /:
    get:
      tags: [public]
      summary: Landing page (HTML)
      operationId: getLanding
      description: |
        Marketing landing page with product pitch, demo-signup form, and
        pricing tiers. Rendered server-side from a static template; no
        auth, no cookies.
      responses:
        '200':
          description: Landing HTML
          content:
            text/html:
              schema: { type: string }

  /docs:
    get:
      tags: [public]
      summary: Interactive API docs (Swagger UI)
      operationId: getDocs
      description: |
        Swagger UI loaded from unpkg.com, pointed at `/openapi.yaml`.
        Supports "Try it out" for authenticated routes if the user pastes
        an API key.
      responses:
        '200':
          description: Swagger UI HTML
          content:
            text/html:
              schema: { type: string }

  /openapi.yaml:
    get:
      tags: [public]
      summary: Raw OpenAPI 3.0 spec
      operationId: getOpenapiYaml
      description: |
        Returns this document as text/yaml. Cached for 5 minutes. Served
        with `Access-Control-Allow-Origin: *` so third-party tools can
        consume it cross-origin.
      responses:
        '200':
          description: The openapi.yaml document
          content:
            application/yaml:
              schema: { type: string }

  /demo/signup:
    post:
      tags: [public]
      summary: Claim a free demo API key
      operationId: postDemoSignup
      description: |
        Creates a free-tier tenant and returns a fresh API key. IP-rate-
        limited to 5 signups per hour. Email is captured for outreach but
        not verified. This is the landing-page funnel for new users.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                  example: you@company.com
      responses:
        '200':
          description: Demo key created
          headers:
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required: [accountId, apiKey, plan, docsUrl, dashboardUrl, message]
                properties:
                  accountId: { type: string, format: uuid }
                  apiKey: { type: string, example: 'x402_...' }
                  plan: { type: string, example: free }
                  docsUrl: { type: string, example: /docs }
                  dashboardUrl: { type: string, example: /dashboard?accountId=... }
                  message: { type: string }
        '400':
          $ref: '#/components/responses/ValidationError'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/health:
    get:
      tags: [core]
      summary: Health check
      operationId: getHealth
      responses:
        '200':
          description: Service is healthy
          headers:
            X-API-Version:
              $ref: '#/components/headers/X-API-Version'
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  stage:
                    type: string
                    example: dev
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/health/ready:
    get:
      tags: [core]
      summary: Deep readiness check
      operationId: getHealthReady
      description: |
        Verifies DynamoDB connectivity, Secrets Manager access, and chain RPC
        reachability. Returns 200 when all subsystems are healthy, 503 otherwise.
      responses:
        '200':
          description: All subsystems healthy
          headers:
            X-API-Version:
              $ref: '#/components/headers/X-API-Version'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthReadyResponse'
        '503':
          description: One or more subsystems unhealthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthReadyResponse'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/quote:
    post:
      tags: [quotes]
      summary: Best fiat-to-crypto quote across exchanges
      operationId: postQuote
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
      responses:
        '200':
          description: Best available quote
          headers:
            X-API-Version:
              $ref: '#/components/headers/X-API-Version'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '502':
          $ref: '#/components/responses/UpstreamError'

  /v1/resource:
    post:
      tags: [core]
      summary: Access an x402-paywalled resource
      operationId: postResource
      description: |
        If no valid payment header is present the server returns **402** with an
        x402 challenge. Re-request with the `X-PAYMENT` header after settling on-chain.
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/XPayment'
      responses:
        '200':
          description: Paid access granted
          headers:
            X-API-Version:
              $ref: '#/components/headers/X-API-Version'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Payment required — x402 challenge issued
          headers:
            WWW-Authenticate:
              schema:
                type: string
                example: X402
              description: Always `X402` for x402 challenges
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentChallenge'
        '403':
          $ref: '#/components/responses/FraudDetected'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/payments:
    get:
      tags: [core]
      summary: List payment history for the authenticated tenant
      operationId: getPayments
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Number of payments to return per page
        - in: query
          name: cursor
          required: false
          schema:
            type: string
          description: Opaque pagination cursor from a previous response's nextCursor
      responses:
        '200':
          description: Paginated list of payments
          headers:
            X-API-Version:
              $ref: '#/components/headers/X-API-Version'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentsHistoryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/webhooks/{provider}:
    post:
      tags: [webhooks]
      summary: Exchange webhook sink
      operationId: postExchangeWebhook
      description: |
        Receives callbacks from exchange/onramp providers. The handler verifies
        the provider-specific HMAC/signature before processing.
      parameters:
        - in: path
          name: provider
          required: true
          schema:
            type: string
            enum: [moonpay, coinbase, kraken, binance, uphold]
      responses:
        '200':
          description: Webhook accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/webhooks/stripe:
    post:
      tags: [webhooks]
      summary: Stripe subscription webhook
      operationId: postStripeWebhook
      description: |
        Receives Stripe subscription lifecycle events
        (`customer.subscription.created`, `updated`, `deleted`). Signature is
        verified via the `Stripe-Signature` header before any processing.
      parameters:
        - in: header
          name: Stripe-Signature
          required: true
          schema:
            type: string
          description: Stripe webhook signature for HMAC verification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StripeSubscriptionEvent'
      responses:
        '200':
          description: Event processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  action:
                    type: string
                    example: plan_updated
                  plan:
                    type: string
                    enum: [starter, growth, scale]
        '401':
          $ref: '#/components/responses/Unauthorized'

  /admin/tenants:
    get:
      tags: [admin]
      summary: List all tenants (admin)
      operationId: getAdminTenants
      description: |
        Returns a paginated list of all tenants. Optionally filter by subscription
        plan. Requires the `X-ADMIN-KEY` header with a valid admin API key.
      security: [{ AdminKeyAuth: [] }]
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Number of tenants to return per page
        - in: query
          name: cursor
          required: false
          schema:
            type: string
            minLength: 1
          description: Opaque pagination cursor from a previous response's nextCursor
        - in: query
          name: plan
          required: false
          schema:
            type: string
            enum: [free, starter, growth, scale]
          description: Filter tenants by subscription plan
      responses:
        '200':
          description: Paginated list of tenants
          headers:
            X-API-Version:
              $ref: '#/components/headers/X-API-Version'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTenantsResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /dashboard:
    get:
      tags: [dashboard]
      summary: Dashboard page
      operationId: getDashboard
      description: |
        Serves the HTML dashboard UI. Optionally pass `accountId` to display
        recent payments for that tenant.
      parameters:
        - in: query
          name: accountId
          required: false
          schema:
            type: string
            format: uuid
          description: Tenant account ID to look up payments
      responses:
        '200':
          description: HTML dashboard page
          content:
            text/html:
              schema:
                type: string

  /dashboard/routes:
    get:
      tags: [dashboard]
      summary: List routes for the authenticated tenant
      operationId: getRoutes
      security: [{ ApiKeyAuth: [] }]
      responses:
        '200':
          description: List of tenant routes
          content:
            application/json:
              schema:
                type: object
                properties:
                  routes:
                    type: array
                    items:
                      $ref: '#/components/schemas/RouteItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      tags: [dashboard]
      summary: Create or update a route
      operationId: putRoute
      description: |
        Creates a new route or updates an existing one for the authenticated tenant.
        The route `path` is the unique key — if a route with the same path already
        exists it will be updated, otherwise a new route is created.
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRouteInput'
      responses:
        '200':
          description: Route created or updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouteItem'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
    delete:
      tags: [dashboard]
      summary: Delete a route
      operationId: deleteRoute
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [path]
              properties:
                path:
                  type: string
                  pattern: ^\/
                  description: Route path to delete
      responses:
        '200':
          description: Route deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Route not found

  /dashboard/rotate-key:
    post:
      tags: [dashboard]
      summary: Rotate tenant API key
      operationId: postRotateKey
      description: |
        Generates a new API key for the authenticated tenant, invalidating the
        previous key immediately. The new key is returned **once** in the response
        body — store it securely. Subsequent requests must use the new key.
      security: [{ ApiKeyAuth: [] }]
      responses:
        '200':
          description: New API key issued
          headers:
            X-API-Version:
              $ref: '#/components/headers/X-API-Version'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotateKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /dashboard/signup:
    post:
      tags: [dashboard]
      summary: Create a new tenant account
      operationId: postSignup
      description: |
        Creates a new free-tier tenant and returns the account ID and API key.
        The API key is shown only once — store it immediately.
      responses:
        '200':
          description: HTML page with account credentials
          content:
            text/html:
              schema:
                type: string
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: Signup failed

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    AdminKeyAuth:
      type: apiKey
      in: header
      name: X-ADMIN-KEY
      description: Admin API key for operational endpoints

  headers:
    X-API-Version:
      description: Current API version (semver). Present on every response.
      schema:
        type: string
        example: '1.0.0'
    Deprecation:
      description: Set to `true` when an endpoint is deprecated.
      schema:
        type: string
        example: 'true'
    Sunset:
      description: ISO 8601 date when a deprecated endpoint will be removed.
      schema:
        type: string
        example: '2026-12-31'
    RateLimit-Limit:
      description: Maximum tokens in the tenant's bucket (per plan tier).
      schema:
        type: integer
        example: 100
    RateLimit-Remaining:
      description: Tokens remaining in the current window.
      schema:
        type: integer
        example: 87
    RateLimit-Reset:
      description: Seconds until the next token is added.
      schema:
        type: integer
        example: 6

  parameters:
    XPayment:
      in: header
      name: X-PAYMENT
      required: false
      description: |
        JSON-encoded payment proof: `{"nonce":"...","txHash":"0x...","signature":"..."}`.
        Required on the second request after receiving a 402 challenge.
      schema:
        type: string

  responses:
    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: VALIDATION_ERROR
              message: Invalid request
              details:
                - path: [fiatAmount]
                  message: Expected number, received string
            correlationId: 550e8400-e29b-41d4-a716-446655440000

    Unauthorized:
      description: Missing or invalid API key / webhook signature
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: UNAUTHORIZED
              message: Unauthorized
            correlationId: 550e8400-e29b-41d4-a716-446655440000

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until the next token is available
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimit-Limit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimit-Remaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: RATE_LIMITED
              message: Too many requests
              details:
                retryAfter: 5
            correlationId: 550e8400-e29b-41d4-a716-446655440000

    FraudDetected:
      description: Request blocked by fraud detection
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: FRAUD_DETECTED
              message: Fraudulent activity detected
              details:
                eventType: high_velocity
            correlationId: 550e8400-e29b-41d4-a716-446655440000

    PayloadTooLarge:
      description: Request body exceeds the 100 KB size limit
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: VALIDATION_ERROR
              message: Request body too large
              details:
                maxBytes: 102400
            correlationId: 550e8400-e29b-41d4-a716-446655440000

    UpstreamError:
      description: Upstream exchange or chain provider failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: UPSTREAM_ERROR
              message: Upstream coinbase failed
            correlationId: 550e8400-e29b-41d4-a716-446655440000

  schemas:
    HealthCheckResult:
      type: object
      required: [name, ok, latencyMs]
      properties:
        name:
          type: string
          example: dynamodb
        ok:
          type: boolean
          example: true
        latencyMs:
          type: number
          example: 12
        error:
          type: string
          example: DDB unreachable

    HealthReadyResponse:
      type: object
      required: [ok, stage, checks]
      properties:
        ok:
          type: boolean
          example: true
        stage:
          type: string
          example: dev
        checks:
          type: array
          items:
            $ref: '#/components/schemas/HealthCheckResult'

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - VALIDATION_ERROR
                - UNAUTHORIZED
                - PAYMENT_REQUIRED
                - NOT_FOUND
                - CONFLICT
                - RATE_LIMITED
                - FRAUD_DETECTED
                - UPSTREAM_ERROR
                - INTERNAL_ERROR
            message:
              type: string
            details:
              description: Additional context (Zod issues, retry-after, etc.)
        correlationId:
          type: string
          format: uuid

    AdminTenantsResponse:
      type: object
      properties:
        tenants:
          type: array
          items:
            $ref: '#/components/schemas/TenantItem'
        nextCursor:
          type: string
          nullable: true
          description: Opaque cursor for the next page, or null if no more results

    PaymentsHistoryResponse:
      type: object
      properties:
        payments:
          type: array
          items:
            $ref: '#/components/schemas/PaymentItem'
        nextCursor:
          type: string
          nullable: true
          description: Opaque cursor for the next page, or null if no more results

    PaymentItem:
      type: object
      properties:
        idempotencyKey:
          type: string
        accountId:
          type: string
          format: uuid
        amountWei:
          type: string
          pattern: ^\d+$
        assetSymbol:
          type: string
        txHash:
          type: string
          pattern: ^0x[a-fA-F0-9]{64}$
        blockNumber:
          type: integer
        status:
          type: string
          enum: [confirmed]
        createdAt:
          type: string
          format: date-time
        confirmedAt:
          type: string
          format: date-time

    QuoteRequest:
      type: object
      required: [fiatCurrency, fiatAmount, cryptoAsset]
      properties:
        fiatCurrency:
          type: string
          enum: [USD, EUR, GBP]
        fiatAmount:
          type: number
          minimum: 0
          exclusiveMinimum: true
          maximum: 50000
        cryptoAsset:
          type: string
          enum: [USDC, XRP, ETH]
        exchange:
          type: string
          enum: [moonpay, coinbase, kraken, binance, uphold]
          description: Pin to a specific exchange (optional)

    QuoteResponse:
      type: object
      properties:
        exchange:
          type: string
        fiatCurrency:
          type: string
        fiatAmount:
          type: number
        cryptoAsset:
          type: string
        cryptoAmount:
          type: string
        rate:
          type: number

    ResourceResponse:
      type: object
      properties:
        ok:
          type: boolean
          example: true
        txHash:
          type: string
          pattern: ^0x[a-fA-F0-9]{64}$
          example: '0xabc123...'
        resource:
          type: string
          example: /v1/resource
        accountId:
          type: string
          format: uuid

    PaymentChallenge:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: PAYMENT_REQUIRED
            message:
              type: string
              example: Payment required
        challenge:
          type: object
          description: x402 challenge details (amount, asset, recipient, nonce)
          properties:
            resource:
              type: string
            amountWei:
              type: string
              pattern: ^\d+$
            assetSymbol:
              type: string
              example: USDC
        correlationId:
          type: string
          format: uuid

    PaymentHeader:
      type: object
      required: [nonce, txHash, signature]
      properties:
        nonce:
          type: string
          minLength: 16
          maxLength: 64
        txHash:
          type: string
          pattern: ^0x[a-fA-F0-9]{64}$
        signature:
          type: string
          minLength: 1

    TenantItem:
      type: object
      properties:
        accountId:
          type: string
          format: uuid
        apiKeyHash:
          type: string
          pattern: ^[a-f0-9]{64}$
        stripeCustomerId:
          type: string
        plan:
          type: string
          enum: [free, starter, growth, scale]
        createdAt:
          type: string
          format: date-time

    RouteItem:
      type: object
      properties:
        tenantId:
          type: string
          format: uuid
        path:
          type: string
          pattern: ^\/
        priceWei:
          type: string
          pattern: ^\d+$
        asset:
          type: string
          enum: [USDC]
        fraudRules:
          $ref: '#/components/schemas/FraudRules'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    FraudRules:
      type: object
      properties:
        maxAmountWei:
          type: string
          pattern: ^\d+$
        velocityPerMinute:
          type: integer
          minimum: 1

    UpdateRouteInput:
      type: object
      required: [path, priceWei]
      properties:
        path:
          type: string
          pattern: ^\/
          minLength: 1
          maxLength: 512
          description: Route path (must start with /)
        priceWei:
          type: string
          pattern: ^\d+$
          description: Price in wei as a string
        asset:
          type: string
          enum: [USDC]
          default: USDC
        fraudRules:
          $ref: '#/components/schemas/FraudRules'

    RotateKeyResponse:
      type: object
      required: [accountId, apiKey, message]
      properties:
        accountId:
          type: string
          format: uuid
        apiKey:
          type: string
          description: >
            New API key prefixed with `x402_`. Shown only once — store it
            immediately. The previous key is invalidated.
          example: x402_abc123...
        message:
          type: string
          example: API key rotated. Save this key now — it will not be shown again.

    StripeSubscriptionEvent:
      type: object
      required: [id, type, data]
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - customer.subscription.created
            - customer.subscription.updated
            - customer.subscription.deleted
        data:
          type: object
          required: [object]
          properties:
            object:
              type: object
              required: [id, customer, status, items]
              properties:
                id:
                  type: string
                customer:
                  type: string
                status:
                  type: string
                items:
                  type: object
                  properties:
                    data:
                      type: array
                      minItems: 1
                      items:
                        type: object
                        properties:
                          price:
                            type: object
                            properties:
                              lookup_key:
                                type: string
                                enum:
                                  - price_starter_monthly
                                  - price_growth_monthly
                                  - price_scale_monthly
