# Cloven Mind API — OpenAPI 3.1.0
# Generated by /api/openapi.yaml
# Source of truth: lib/openapi/spec.ts

openapi: "3.1.0"
info:
  title: Cloven Mind API
  version: "0.3.0"
  description: |
      The continuous context layer for AI agents. Always fresh. Always cited. MCP-native, x402-ready.
      
      **v0.3.0 — Sprint 6:** Stripe subscription rail removed. Monetisation is now fully crypto-native:
      1. **Prepaid USDC credits** — buy credit packs at `/console/credits`. Each `/v1/*` call debits 1 credit. Balance verified via on-chain Base Transfer events.
      2. **x402 per-call micropayment** — autonomous agents pay per-call in USDC on Base without a pre-provisioned key.
      
      **Auth paths:**
      1. **Bearer token** — `Authorization: Bearer cv_<env>_<token>`. Issue keys at https://cloven.cloud/console.
      2. **x402 micropayment** — first call returns 402 with `PaymentRequirements`; agent signs USDC transfer on Base and retries with `X-Payment: <base64-proof>`.
      
      **Credit quotas (prepaid):**
      - Starter: 1,000 credits / $1 USDC.
      - Hobby: 10,000 credits / $8 USDC (20% discount).
      - Pro: 100,000 credits / $70 USDC (30% discount).
      - Team: 1,000,000 credits / $600 USDC (40% discount).
      
      **Free-tier fallback:** Keys with zero credits fall through to the 100 calls/day sliding-window free tier (crypto pack only).
      
      All 4xx/5xx responses use the `ErrorEnvelope` schema with a machine-readable `code` field.
  contact:
    email: ops@cloven.cloud
    url: "https://cloven.cloud"
  license:
    name: Commercial
    url: "https://cloven.cloud/terms"
  x-logo:
    url: "https://cloven.cloud/brand/cleft-drop-3d-1024.png"
    altText: Cloven Cleft Drop logo
servers:
  - url: "https://cloven.cloud"
    description: Production
  - url: "http://localhost:3000"
    description: Local dev
security:
  - bearerAuth: []

tags:
  - name: v1
    description: Core Mind API — fresh, brief, search, snapshot, subscribe, cite. All routes gate on API key (Bearer) or x402 micropayment.
  - name: mcp
    description: MCP HTTP transport — JSON-RPC 2.0 envelope. Primary surface for Claude Desktop, Cursor, and server-side agents.
  - name: keys
    description: API key management — issue, list, revoke. Auth via Supabase cookie session (not Bearer key).
  - name: credits
    description: Prepaid USDC credit deposits on Base — buy packs, poll deposit status, fast-path verify by tx hash. Auth via Supabase cookie session.
  - name: internal
    description: Internal cron and diagnostic endpoints. QStash-signed. Not for public consumption.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: cv_<env>_<token>
      description: "Cloven API key issued at https://cloven.cloud/console. Format: `cv_<env>_<32-base62-chars>` where `<env>` is `live` or `test`."
    x402Auth:
      type: apiKey
      in: header
      name: X-Payment
      description: "x402 micropayment proof for autonomous agent pay-per-call. Value is base64-encoded JSON: `{ scheme, network, txHash, payer }`. Send a USDC transfer on Base to the Cloven treasury matching the `PaymentRequirements` from the 402 response, then retry the original request with this header set."
  schemas:
    Tier:
      type: string
      enum:
        - free
        - hobby
        - pro
        - team
        - enterprise
      description: Subscription tier controlling quota limits and pack access. `free` = 100 calls/day (crypto only). `hobby` = 10k calls/month (all packs). `pro` = 100k calls/month + custom briefer prompts. `team` = 1M calls/month + SLA. `enterprise` = custom.
    Op:
      type: string
      enum:
        - fresh
        - brief
        - search
        - snapshot
        - subscribe
        - cite
      description: Operation name — used in quota accounting, x402 pricing, and trace capture.
    TraceConsent:
      type: string
      enum:
        - full
        - aggregated
        - none
      description: "`full` = raw trace events stored and eligible for Phase 3 Commons sale (with opt-in). `aggregated` = counters only. `none` = no trace persistence."
    AttestationType:
      type: string
      enum:
        - erc8004
        - eas
      description: Phase 3 — onchain provenance attestation type. `erc8004` = ERC-8004 registry on Base. `eas` = Ethereum Attestation Service schema.
    Freshness:
      type: object
      required:
        - generatedAt
        - ageSeconds
      properties:
        generatedAt:
          type: string
          format: date-time
          description: ISO8601 timestamp when this state blob was last compacted.
        ageSeconds:
          type: integer
          minimum: 0
          description: Seconds since `generatedAt`. Values > 300 indicate the pack cron is lagging.
      description: Freshness envelope present on every Mind response.
    Citation:
      type: object
      required:
        - ref
        - sourceId
        - url
        - fetchedAt
      properties:
        ref:
          type: integer
          minimum: 1
          description: "[N] anchor index as it appears inline in the Brief prose."
        sourceId:
          type: string
          description: Source connector identifier — matches `SourceConnector.id`.
        url:
          type: string
          format: uri
          description: Public URL to the data source.
        fetchedAt:
          type: string
          format: date-time
          description: When the cited datum was fetched by the ingestion pipeline.
        excerpt:
          type: string
          description: Optional short excerpt for hover preview in UIs.
      description: A resolved citation mapping a Brief [N] anchor to the underlying data source.
    Attestation:
      type: object
      required:
        - type
        - ref
      properties:
        type:
          $ref: "#/components/schemas/AttestationType"
        ref:
          type: string
          description: Onchain reference — tx hash (erc8004) or attestation UID (eas).
      description: Phase 3 onchain provenance attestation. Present only when `CLOVEN_ATTESTATION=1` and a trace is persisted to the ERC-8004 registry or EAS.
    MindResponse:
      type: object
      required:
        - state
        - brief
        - citations
        - freshness
      properties:
        state:
          type: object
          additionalProperties: true
          description: "Pack-specific structured state. Shape is defined by the pack's Zod schema. Crypto pack: `top_movers`, `protocol_tvl`, `trending_pools`, `sentiment`, `narratives`, `flags`, `source_status`."
        brief:
          type: string
          description: Narrated prose brief with inline [N] citation anchors. LLM-generated by Groq llama-3.3-70b from the compacted state. Empty string for historical snapshots from `/v1/snapshot`.
        citations:
          type: array
          items:
            $ref: "#/components/schemas/Citation"
          description: Ordered list of citations. Index 0 = [1] in brief prose.
        freshness:
          $ref: "#/components/schemas/Freshness"
        attestation:
          $ref: "#/components/schemas/Attestation"
          description: Phase 3 onchain attestation. Absent in Phase 1 unless explicitly enabled.
      description: Full Mind response — state + brief + citations + freshness. Returned by `/v1/fresh` and `/v1/snapshot`.
    BriefResponse:
      type: object
      required:
        - brief
        - citations
        - freshness
      properties:
        brief:
          type: string
          description: Narrated prose brief with inline [N] citation anchors.
        citations:
          type: array
          items:
            $ref: "#/components/schemas/Citation"
        freshness:
          $ref: "#/components/schemas/Freshness"
      description: Brief-only response — cheaper than `/v1/fresh` when the caller maintains its own state cache.
    SearchMatch:
      type: object
      required:
        - path
        - value
        - score
      properties:
        path:
          type: string
          description: JSONPath-style path into the state tree, e.g. `top_movers[3].symbol`.
        value:
          description: The leaf value at this path (string, number, boolean, or null).
        score:
          type: number
          minimum: 0
          maximum: 1
          description: "Relevance score: 1.0 = word-boundary match, 0.5 = substring match."
      description: A single full-text search result within the compacted state.
    SearchResponse:
      type: object
      required:
        - matches
        - total
        - freshness
      properties:
        matches:
          type: array
          items:
            $ref: "#/components/schemas/SearchMatch"
          description: Top-k matches, sorted by score descending.
        total:
          type: integer
          minimum: 0
          description: Total matches before k truncation.
        freshness:
          $ref: "#/components/schemas/Freshness"
      description: Search results over the compacted state JSON tree.
    CiteResponse:
      type: object
      required:
        - citation
        - source
        - freshness
      properties:
        citation:
          $ref: "#/components/schemas/Citation"
        source:
          type: object
          required:
            - id
            - name
            - publicUrl
          properties:
            id:
              type: string
            name:
              type: string
            publicUrl:
              type: string
              format: uri
          description: Source connector metadata.
        freshness:
          $ref: "#/components/schemas/Freshness"
      description: Citation resolution — maps a [N] anchor to the source connector metadata.
    ApiKeyRow:
      type: object
      required:
        - keyId
        - name
        - tier
        - createdAt
        - traceConsent
      properties:
        keyId:
          type: string
          description: Public key prefix — first 16 characters of the full `cv_<env>_<token>`. Safe to log; never the full plaintext.
        name:
          type: string
          description: Human-readable label set at key creation.
        tier:
          $ref: "#/components/schemas/Tier"
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
          description: Last time this key was used in an API call. Null if never used.
        revokedAt:
          type: string
          format: date-time
          nullable: true
          description: When the key was soft-revoked. Null if active.
        traceConsent:
          $ref: "#/components/schemas/TraceConsent"
      description: A row from the `api_keys` table as returned by `GET /api/keys`. The full plaintext key is never returned after the initial `POST /api/keys` response.
    IssuedKey:
      type: object
      required:
        - key
        - keyId
        - name
        - tier
        - createdAt
      properties:
        key:
          type: string
          description: Full plaintext API key — format `cv_<env>_<32-base62-chars>`. Store this immediately; it is returned ONCE and never retrievable again.
          example: cv_live_aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2u
        keyId:
          type: string
          description: Public key prefix (first 16 chars). Use this as the key's stable id.
        name:
          type: string
        tier:
          $ref: "#/components/schemas/Tier"
        createdAt:
          type: string
          format: date-time
      description: Issued key response from `POST /api/keys`. The `key` field contains the single-show plaintext.
    KeyListResponse:
      type: object
      required:
        - keys
      properties:
        keys:
          type: array
          items:
            $ref: "#/components/schemas/ApiKeyRow"
          description: All non-deleted keys owned by the authenticated user, newest first.
    RevokeResponse:
      type: object
      required:
        - ok
        - revokedAt
      properties:
        ok:
          type: boolean
          enum:
            - true
        revokedAt:
          type: string
          format: date-time
      description: Successful revocation response.
    ApiKeyContext:
      type: object
      required:
        - keyId
        - tier
        - packIds
        - createdAt
      properties:
        keyId:
          type: string
        userId:
          type: string
          format: uuid
          nullable: true
          description: Supabase user id. Null when the caller is x402-paid and anonymous.
        tier:
          $ref: "#/components/schemas/Tier"
        packIds:
          type: array
          items:
            type: string
          description: Pack ids this key has access to. Free tier = `["crypto"]`.
        createdAt:
          type: string
          format: date-time
        traceConsent:
          $ref: "#/components/schemas/TraceConsent"
      description: Resolved API key context — internal shape used in route handlers and traces.
    PaymentRequirements:
      type: object
      required:
        - scheme
        - network
        - asset
        - amount
        - recipient
        - validUntil
        - nonce
      properties:
        scheme:
          type: string
          enum:
            - x402
        network:
          type: string
          enum:
            - base
        asset:
          type: string
          enum:
            - USDC
        amount:
          type: string
          description: Decimal USDC amount string, e.g. `"0.001"`. The client transfers exactly this amount to `recipient`.
          example: "0.001"
        recipient:
          type: string
          pattern: ^0x[0-9a-fA-F]{40}$
          description: Cloven treasury wallet address on Base.
        validUntil:
          type: integer
          description: Unix timestamp (seconds). The USDC transfer transaction must confirm before this time.
        nonce:
          type: string
          description: Server-generated nonce. The client must include this in the transfer calldata to prevent tx reuse.
      description: "x402 payment requirements returned in a 402 response. Client signs a USDC transfer on Base meeting these requirements, then retries the request with `X-Payment: <base64-proof>` header."
    PaymentRequiredBody:
      type: object
      required:
        - payment
      properties:
        payment:
          $ref: "#/components/schemas/PaymentRequirements"
      description: Body of a 402 Payment Required response.
    CreditPack:
      type: string
      enum:
        - starter
        - hobby
        - pro
        - team
      description: "Credit pack tier. Starter: 1k credits / $1 USDC. Hobby: 10k / $8 (20% discount). Pro: 100k / $70 (30%). Team: 1M / $600 (40%). Credits are prepaid and never expire."
    Deposit:
      type: object
      required:
        - id
        - userId
        - pack
        - credits
        - amountUsdc
        - status
        - nonce
        - createdAt
        - expiresAt
      properties:
        id:
          type: string
          format: uuid
          description: Deposit row id.
        userId:
          type: string
          format: uuid
          description: Supabase user id of the buyer.
        keyId:
          type: string
          nullable: true
          description: API key id the credits will be applied to. Null applies to the user's default key.
        pack:
          $ref: "#/components/schemas/CreditPack"
        credits:
          type: integer
          minimum: 0
          description: Number of credits this deposit grants on verification.
        amountUsdc:
          type: string
          description: Exact USDC amount required as a 6-decimal string, e.g. `"1.000000"`.
        status:
          type: string
          enum:
            - pending
            - verified
            - expired
          description: "`pending` = awaiting on-chain confirmation. `verified` = credits granted. `expired` = 30-minute window elapsed without matching Transfer."
        txHash:
          type: string
          nullable: true
          description: Base transaction hash of the matching USDC Transfer. Set on verification.
        payerAddress:
          type: string
          nullable: true
          description: Sender address of the matching USDC Transfer.
        nonce:
          type: string
          description: Server-generated nonce for uniqueness when two users buy the same amount simultaneously.
        createdAt:
          type: string
          format: date-time
        verifiedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
      description: A credit deposit record. Pending rows are matched to on-chain USDC Transfer events by the credit-verify cron.
    DepositIntent:
      type: object
      required:
        - id
        - recipient
        - amountUsdc
        - nonce
        - expiresAt
      properties:
        id:
          type: string
          format: uuid
          description: Deposit row id — use as `{depositId}` in the status-poll and fast-path verify endpoints.
        recipient:
          type: string
          pattern: ^0x[0-9a-fA-F]{40}$
          description: Cloven treasury wallet that must receive the USDC transfer. Same as `X402_RECIPIENT_ADDRESS`.
        amountUsdc:
          type: string
          description: USDC amount as a 6-decimal string. Paste directly into a wallet input.
          example: "1.000000"
        nonce:
          type: string
          description: Server nonce — surfaces in the deposit row and the EIP-681 URI for uniqueness matching.
        expiresAt:
          type: integer
          description: Unix timestamp (seconds) at which the deposit intent expires. The cron stops scanning for matching Transfers after this.
      description: Returned by `POST /api/console/credits`. Render in the BuyCreditsModal for the user to complete the USDC transfer.
    CreditBalanceResponse:
      type: object
      required:
        - balance
        - keyCount
        - recentDeposits
      properties:
        balance:
          type: integer
          minimum: 0
          description: Current `credit_balance` across the caller's API keys (aggregate or per-key depending on query).
        keyCount:
          type: integer
          minimum: 0
          description: Number of API keys owned by the caller.
        recentDeposits:
          type: array
          items:
            $ref: "#/components/schemas/Deposit"
          description: Most recent 20 deposit records for this user, newest first.
      description: Credit balance overview returned by `GET /api/console/credits`.
    McpRequest:
      type: object
      required:
        - jsonrpc
        - method
      properties:
        jsonrpc:
          type: string
          enum:
            - "2.0"
        id:
          description: JSON-RPC request id — string, number, or null. Omit for notifications.
          oneOf:
            - type: string

            - type: number

            - type: "null"

        method:
          type: string
          enum:
            - initialize
            - tools/list
            - tools/call
            - resources/list
            - resources/read
          description: MCP JSON-RPC method. Most agent integrations use `tools/call` with `cloven.fresh`, `cloven.brief`, etc.
        params:
          type: object
          additionalProperties: true
          description: "Method-specific params. For `tools/call`: `{ name, arguments }`."
      description: MCP HTTP transport request — JSON-RPC 2.0 envelope.
    McpResponse:
      type: object
      required:
        - jsonrpc
        - id
      properties:
        jsonrpc:
          type: string
          enum:
            - "2.0"
        id:
          description: Echo of the request id.
          oneOf:
            - type: string

            - type: number

            - type: "null"

        result:
          description: Present on success — tool-specific response body.
        error:
          type: object
          properties:
            code:
              type: integer
            message:
              type: string
            data: {}
          description: Present on error — JSON-RPC error object.
      description: MCP HTTP transport response — JSON-RPC 2.0 envelope.
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - requestId
          properties:
            code:
              type: string
              enum:
                - AUTH_MISSING
                - AUTH_INVALID
                - QUOTA_EXHAUSTED
                - PACK_NOT_FOUND
                - PACK_NOT_SUBSCRIBED
                - SNAPSHOT_NOT_FOUND
                - CITATION_NOT_FOUND
                - PAYMENT_REQUIRED
                - PAYMENT_INVALID
                - UPSTREAM_TIMEOUT
                - UPSTREAM_ERROR
                - VALIDATION_ERROR
                - RATE_LIMITED
                - INTERNAL_ERROR
              description: Machine-readable error code — SCREAMING_SNAKE_CASE, stable across API versions.
            message:
              type: string
              description: Human-readable error detail.
            requestId:
              type: string
              format: uuid
              description: Unique id for this request. Pass to support when reporting issues.
          additionalProperties: true
      description: Cloven canonical error envelope. Present on all 4xx / 5xx responses from /v1/* and /api/* routes.
paths:
  /v1/fresh:
    get:
      operationId: freshGet
      tags:
        - v1
      summary: Get current Mind State
      description: |
          Returns the latest compacted Mind State for a pack, the narrated prose Brief (LLM-generated by Groq llama-3.3-70b), ordered Citations, and a Freshness envelope. The state blob shape is pack-specific (crypto schema documented at `/docs/packs/crypto`). State is refreshed every 5 minutes by the QStash cron pipeline.
          
          Auth: `Authorization: Bearer cv_*` OR x402 USDC payment. Free tier is restricted to the `crypto` pack.
      parameters:
        - name: pack
          in: query
          required: true
          description: "Pack identifier. Phase 1 live: `crypto`. Phase 1.5: `ai`, `markets`. Defaults to `crypto` if omitted."
          schema:
            type: string
            enum:
              - crypto
              - ai
              - markets
            default: crypto
        - name: query
          in: query
          required: false
          description: Optional free-text refinement. Phase 1.5 narrows the brief to the matching slice. Ignored in Phase 1 (full state always returned).
          schema:
            type: string
      responses:
        "200":
          description: Current Mind State returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MindResponse"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: "Payment Required — no valid API key; x402 payment quote attached. Client should read `payment` field and submit a USDC transfer on Base, then retry with `X-Payment: <base64-proof>` header."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "403":
          description: Forbidden — key is valid but does not have access to the requested pack. Upgrade tier to unlock additional packs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Not Found — pack, snapshot, or citation does not exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Too Many Requests — per-key quota exhausted. `Retry-After` header set to seconds until quota window resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Service Unavailable — pack state not yet available (first-pulse race). Retry after a few seconds.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-402-payment:
        scheme: x402
        network: base
        asset: USDC
        amount: "0.001"
        description: Clients without a valid Bearer key receive a 402 with PaymentRequirements. Submit a USDC transfer for 0.001 to the Cloven treasury on Base, then retry with X-Payment header.
        op: fresh
  /v1/brief:
    get:
      operationId: briefGet
      tags:
        - v1
      summary: Get prose Brief only
      description: |
          Returns the narrated prose Brief and Citations without the full structured state. Use this when your agent already maintains a state cache and only needs the latest LLM-narrated summary. Briefer runs daily (or on-demand for Pro+ custom briefer prompts).
          
          x402 price: $0.005 USDC — higher than `fresh` because the briefer LLM runs on demand.
      parameters:
        - name: pack
          in: query
          required: true
          description: "Pack identifier. Phase 1 live: `crypto`. Phase 1.5: `ai`, `markets`. Defaults to `crypto` if omitted."
          schema:
            type: string
            enum:
              - crypto
              - ai
              - markets
            default: crypto
      responses:
        "200":
          description: Brief returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BriefResponse"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: "Payment Required — no valid API key; x402 payment quote attached. Client should read `payment` field and submit a USDC transfer on Base, then retry with `X-Payment: <base64-proof>` header."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "403":
          description: Forbidden — key is valid but does not have access to the requested pack. Upgrade tier to unlock additional packs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Not Found — pack, snapshot, or citation does not exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Too Many Requests — per-key quota exhausted. `Retry-After` header set to seconds until quota window resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Brief not yet available for this pack.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-402-payment:
        scheme: x402
        network: base
        asset: USDC
        amount: "0.005"
        description: Clients without a valid Bearer key receive a 402 with PaymentRequirements. Submit a USDC transfer for 0.005 to the Cloven treasury on Base, then retry with X-Payment header.
        op: brief
  /v1/search:
    get:
      operationId: searchGet
      tags:
        - v1
      summary: Full-text search over compacted state
      description: |
          Case-insensitive full-text walk over the cached state JSON tree. Returns top-k leaf paths with their values, scored by relevance: 1.0 = word-boundary match, 0.5 = substring. No external search index required — runs in-memory over the Redis-cached blob.
          
          Phase 1.5 will replace this with vector-based semantic search. Current implementation is deterministic and fast for the crypto state size (~40 KB JSON).
      parameters:
        - name: pack
          in: query
          required: true
          description: "Pack identifier. Phase 1 live: `crypto`. Phase 1.5: `ai`, `markets`. Defaults to `crypto` if omitted."
          schema:
            type: string
            enum:
              - crypto
              - ai
              - markets
            default: crypto
        - name: q
          in: query
          required: true
          description: Search query — must be non-empty.
          schema:
            type: string
            minLength: 1
        - name: k
          in: query
          required: false
          description: Max results to return. Default 10, max 50.
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        "200":
          description: Search results returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: "Payment Required — no valid API key; x402 payment quote attached. Client should read `payment` field and submit a USDC transfer on Base, then retry with `X-Payment: <base64-proof>` header."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "403":
          description: Forbidden — key is valid but does not have access to the requested pack. Upgrade tier to unlock additional packs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Not Found — pack, snapshot, or citation does not exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Too Many Requests — per-key quota exhausted. `Retry-After` header set to seconds until quota window resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Service Unavailable — pack state not yet available (first-pulse race). Retry after a few seconds.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-402-payment:
        scheme: x402
        network: base
        asset: USDC
        amount: "0.002"
        description: Clients without a valid Bearer key receive a 402 with PaymentRequirements. Submit a USDC transfer for 0.002 to the Cloven treasury on Base, then retry with X-Payment header.
        op: search
  /v1/snapshot:
    get:
      operationId: snapshotGet
      tags:
        - v1
      summary: Historical Mind State at timestamp
      description: |
          Time-travel: retrieve a historical Mind State at the given ISO timestamp. The timestamp is truncated to minute precision. Free/Hobby tiers can access the last 24 hours of hot Redis history. Pro+ tiers can access 30 days (Postgres `mind_history` table). Team+ tiers 365 days.
          
          The `brief` field is empty for historical snapshots — the briefer runs daily, not per-compaction. Use `/v1/brief` for the current narrated brief.
          
          Historical snapshots are immutable: `cache-control: public, max-age=86400, immutable`.
      parameters:
        - name: pack
          in: query
          required: true
          description: "Pack identifier. Phase 1 live: `crypto`. Phase 1.5: `ai`, `markets`. Defaults to `crypto` if omitted."
          schema:
            type: string
            enum:
              - crypto
              - ai
              - markets
            default: crypto
        - name: at
          in: query
          required: true
          description: "ISO8601 timestamp for the requested historical snapshot. Truncated to minute precision internally, e.g. `2026-05-24T12:05:00Z` → key `2026-05-24T12:05`."
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Historical snapshot returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MindResponse"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: "Payment Required — no valid API key; x402 payment quote attached. Client should read `payment` field and submit a USDC transfer on Base, then retry with `X-Payment: <base64-proof>` header."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "403":
          description: Forbidden — key is valid but does not have access to the requested pack. Upgrade tier to unlock additional packs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Snapshot not found — the requested minute is outside the hot Redis window for this tier, or before the pack existed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Too Many Requests — per-key quota exhausted. `Retry-After` header set to seconds until quota window resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-402-payment:
        scheme: x402
        network: base
        asset: USDC
        amount: "0.01"
        description: Clients without a valid Bearer key receive a 402 with PaymentRequirements. Submit a USDC transfer for 0.01 to the Cloven treasury on Base, then retry with X-Payment header.
        op: snapshot
  /v1/subscribe:
    get:
      operationId: subscribeGet
      tags:
        - v1
      summary: SSE stream of pack pulses
      description: |
          Server-Sent Events stream of live pack pulses. The connection stays open and the server pushes frames as the pack state evolves.
          
          **Auth note:** Browser `EventSource` cannot set custom request headers, so the API key is passed as the `token` query parameter instead of `Authorization: Bearer`. x402 is not supported for SSE (no per-call payment model fits a long-lived stream).
          
          **Frame types:**
          - `event: pulse` — emitted every 15 seconds with the current cached state.
          - `event: delta` — emitted when `generatedAt` advances (a new compaction landed upstream).
          - `event: error` — non-fatal error string; stream stays open.
          - `: keepalive <epoch_ms>` — SSE comment frame emitted every 25 seconds to prevent proxy connection timeouts.
          
          The stream closes when the client disconnects or the key is revoked.
      security: []
      parameters:
        - name: pack
          in: query
          required: true
          description: "Pack identifier. Phase 1 live: `crypto`. Phase 1.5: `ai`, `markets`. Defaults to `crypto` if omitted."
          schema:
            type: string
            enum:
              - crypto
              - ai
              - markets
            default: crypto
        - name: token
          in: query
          required: true
          description: "API key — same value you would pass as `Authorization: Bearer cv_*`. Required because `EventSource` cannot set request headers."
          schema:
            type: string
            pattern: ^cv_
      responses:
        "200":
          description: "SSE stream opened. Content-Type is `text/event-stream`. Frames are `event: pulse | delta | error` with JSON `data`."
          content:
            text/event-stream:
              schema:
                type: string
                description: "SSE wire format. Each frame: `event: <type>\\ndata: <json>\\n\\n`. Keepalive comment: `: keepalive <ts>\\n\\n`."
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: Forbidden — key is valid but does not have access to the requested pack. Upgrade tier to unlock additional packs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Not Found — pack, snapshot, or citation does not exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Too Many Requests — per-key quota exhausted. `Retry-After` header set to seconds until quota window resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-sse-events:
        pulse:
          description: Current pack state (emitted every 15s).
          schema:
            type: object
            required:
              - pack
              - state
              - ts
            properties:
              pack:
                type: string
              state:
                type: object
                additionalProperties: true
              ts:
                type: string
                format: date-time
        delta:
          description: State advanced — new compaction landed (emitted when generatedAt changes).
          schema:
            type: object
            required:
              - pack
              - state
              - ts
            properties:
              pack:
                type: string
              state:
                type: object
                additionalProperties: true
              ts:
                type: string
                format: date-time
        error:
          description: Non-fatal stream error. Stream stays open.
          schema:
            type: object
            required:
              - message
            properties:
              message:
                type: string
  /v1/cite:
    get:
      operationId: citeGet
      tags:
        - v1
      summary: Resolve a citation anchor
      description: |
          Resolves a [N] citation anchor (1-indexed) from a Brief to the underlying source connector metadata. The anchor index matches the stable `pack.sources` array order — consistent across the lifetime of a pack version.
          
          Useful for building citation hover-cards or verifying claims in a Brief before passing them downstream.
      parameters:
        - name: pack
          in: query
          required: true
          description: "Pack identifier. Phase 1 live: `crypto`. Phase 1.5: `ai`, `markets`. Defaults to `crypto` if omitted."
          schema:
            type: string
            enum:
              - crypto
              - ai
              - markets
            default: crypto
        - name: ref
          in: query
          required: true
          description: Citation anchor number — 1-indexed integer. Range 1..N where N = number of sources in the pack.
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Citation resolved successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CiteResponse"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: "Payment Required — no valid API key; x402 payment quote attached. Client should read `payment` field and submit a USDC transfer on Base, then retry with `X-Payment: <base64-proof>` header."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "403":
          description: Forbidden — key is valid but does not have access to the requested pack. Upgrade tier to unlock additional packs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Citation not found — `ref` is out of range for this pack's source list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Too Many Requests — per-key quota exhausted. `Retry-After` header set to seconds until quota window resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-402-payment:
        scheme: x402
        network: base
        asset: USDC
        amount: "0.001"
        description: Clients without a valid Bearer key receive a 402 with PaymentRequirements. Submit a USDC transfer for 0.001 to the Cloven treasury on Base, then retry with X-Payment header.
        op: cite
  /api/mcp:
    post:
      operationId: mcpPost
      tags:
        - mcp
      summary: MCP HTTP transport — tool call
      description: |
          Cloven MCP server over HTTP using the JSON-RPC 2.0 envelope format. Supports `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`.
          
          Available tools: `cloven.fresh`, `cloven.brief`, `cloven.search`, `cloven.snapshot`, `cloven.subscribe`, `cloven.cite`.
          
          Auth: `Authorization: Bearer cv_*` (prepaid credit key) or `X-Payment: <base64>` (x402 per-call USDC path). Unauthenticated first call returns 402 with `PaymentRequirements`.
          
          The HTTP transport is compliant with the Anthropic MCP spec's WebStandardStreamableHTTPServerTransport. SSE server-initiated messages are served via `GET /api/mcp`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/McpRequest"
      responses:
        "200":
          description: JSON-RPC 2.0 response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/McpResponse"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: "Payment Required — no valid API key; x402 payment quote attached. Client should read `payment` field and submit a USDC transfer on Base, then retry with `X-Payment: <base64-proof>` header."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "429":
          description: Too Many Requests — per-key quota exhausted. `Retry-After` header set to seconds until quota window resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    get:
      operationId: mcpSse
      tags:
        - mcp
      summary: MCP HTTP transport — SSE (server-initiated messages)
      description: SSE endpoint for server-initiated MCP notifications. Required by the Streamable HTTP transport spec. Auth same as POST.
      security:
        - bearerAuth: []

      responses:
        "200":
          description: SSE stream of MCP server-initiated messages.
          content:
            text/event-stream:
              schema:
                type: string
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: "Payment Required — no valid API key; x402 payment quote attached. Client should read `payment` field and submit a USDC transfer on Base, then retry with `X-Payment: <base64-proof>` header."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/keys:
    post:
      operationId: keysIssue
      tags:
        - keys
      summary: Issue a new API key
      description: |
          Creates a new API key scoped to the authenticated Supabase session. The full plaintext key is returned **once** in this response and never again. Store it immediately in your secrets manager.
          
          Auth: Supabase cookie session (anonymous or claimed via magic link). No API key is required for key management — that would be circular. The cookie session is the bootstrap surface.
          
          Tier defaults to `free`. Credit balance starts at 0 — buy a credit pack at `/api/console/credits` to pre-fund the key.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 80
                  description: Human-readable label for this key.
                tier:
                  $ref: "#/components/schemas/Tier"
                  default: free
                  description: Tier for the new key. Defaults to `free`. Credit balance is funded via USDC deposits at `/api/console/credits`.
      responses:
        "200":
          description: Key issued. **Save `key` immediately — it cannot be retrieved again.**
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IssuedKey"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    get:
      operationId: keysList
      tags:
        - keys
      summary: List API keys
      description: Returns all non-deleted API keys owned by the authenticated user. The `key_hash` is never returned — only the `keyId` (key prefix) and metadata.
      security: []
      responses:
        "200":
          description: Keys listed successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyListResponse"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    delete:
      operationId: keysRevoke
      tags:
        - keys
      summary: Revoke an API key
      description: |
          Soft-revokes a key by setting `revoked_at = now()`. Revoked keys are rejected on the next auth attempt (cached for up to 60 seconds in Redis). The key record is retained for audit purposes.
          
          The `id` parameter is the `keyId` (key prefix — first 16 chars), not the internal UUID.
      security: []
      parameters:
        - name: id
          in: query
          required: true
          description: Key prefix (`keyId`) returned by `POST /api/keys` or `GET /api/keys`.
          schema:
            type: string
      responses:
        "200":
          description: Key revoked successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RevokeResponse"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Key not found or already revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/console/credits:
    post:
      operationId: creditsCreateIntent
      tags:
        - credits
      summary: Buy a credit pack — create deposit intent
      description: |
          Creates a pending deposit row and returns a `DepositIntent` for the caller to complete the USDC transfer on Base. The BuyCreditsModal renders the intent as a QR code + clipboard address.
          
          The credit-verify cron (`POST /api/cron/credit-verify`) scans Base every 60 seconds for matching USDC Transfer events. When matched, the deposit flips to `verified` and `api_keys.credit_balance` is incremented atomically.
          
          Alternatively the caller can fast-path verify by POSTing the `txHash` to `/api/console/credits/{depositId}`.
          
          Auth: Supabase cookie session required.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - pack
              properties:
                pack:
                  $ref: "#/components/schemas/CreditPack"
                keyId:
                  type: string
                  description: Target API key id. If omitted, credits apply to the caller's default key.
      responses:
        "200":
          description: Deposit intent created. Complete the USDC transfer to `recipient` within 30 minutes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DepositIntent"
        "400":
          description: Bad Request — invalid or missing query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    get:
      operationId: creditsGetBalance
      tags:
        - credits
      summary: Get credit balance + recent deposits
      description: |
          Returns the caller's current credit balance and the 20 most recent deposit records. Use this to populate the credits dashboard and poll for deposit status changes.
          
          Auth: Supabase cookie session required.
      security: []
      responses:
        "200":
          description: Credit balance and deposit history returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditBalanceResponse"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/console/credits/{depositId}:
    get:
      operationId: creditsGetDeposit
      tags:
        - credits
      summary: Poll a single deposit status
      description: |
          Returns the current status of a single deposit row. Poll every 5–10 seconds from the BuyCreditsModal while status is `pending`. Stop polling when status flips to `verified` or `expired`.
          
          Auth: Supabase cookie session — deposit must belong to the caller.
      security: []
      parameters:
        - name: depositId
          in: path
          required: true
          description: Deposit row UUID returned by `POST /api/console/credits`.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Deposit row returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Deposit"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Deposit not found or does not belong to caller.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      operationId: creditsVerifyByTxHash
      tags:
        - credits
      summary: Fast-path verify deposit by pasting tx hash
      description: |
          Allows the caller to submit the Base transaction hash immediately after transfer, triggering an on-demand on-chain verification without waiting for the 60-second cron cycle.
          
          The route reads the receipt from Base, checks the Transfer event matches (recipient + amount), and flips the deposit to `verified` + increments `credit_balance` atomically.
          
          Auth: Supabase cookie session — deposit must belong to the caller.
      security: []
      parameters:
        - name: depositId
          in: path
          required: true
          description: Deposit row UUID.
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - txHash
              properties:
                txHash:
                  type: string
                  pattern: ^0x[0-9a-fA-F]{64}$
                  description: Base transaction hash of the USDC transfer.
      responses:
        "200":
          description: Deposit verified — credits granted.
          content:
            application/json:
              schema:
                type: object
                required:
                  - verified
                  - newBalance
                properties:
                  verified:
                    type: boolean
                    enum:
                      - true
                  newBalance:
                    type: integer
                    minimum: 0
        "400":
          description: Invalid txHash format or Transfer event does not match deposit amount.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthorized — missing or invalid Bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Deposit not found, already verified, or expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/cron/credit-verify:
    post:
      operationId: cronCreditVerify
      tags:
        - internal
      summary: Credit-verify cron — scan Base for pending USDC deposits
      description: |
          QStash-signed internal cron endpoint. Runs every 60 seconds.
          
          Scans Base mainnet for recent USDC Transfer events to `X402_RECIPIENT_ADDRESS`. Matches transfers to pending deposit rows by `amountUsdc`. On match: flips deposit to `verified`, increments `api_keys.credit_balance` via the `debit_credits` Postgres fn (reversed), and invalidates the Redis balance cache.
          
          Also expires any pending deposits past their `expires_at` timestamp.
          
          **Do not call this directly in production.** In local dev, the QStash `upstash-signature: dev-bypass` header skips signature verification (only when `QSTASH_CURRENT_SIGNING_KEY` is unset).
      security: []
      parameters:
        - name: upstash-signature
          in: header
          required: true
          description: QStash HMAC signature for request authentication. Use `dev-bypass` in local dev when signing keys are unset.
          schema:
            type: string
      responses:
        "200":
          description: Cron run completed. Returns verified + expired counts.
          content:
            application/json:
              schema:
                type: object
                required:
                  - verified
                  - expired
                properties:
                  verified:
                    type: integer
                    minimum: 0
                    description: Pending deposits flipped to verified this run.
                  expired:
                    type: integer
                    minimum: 0
                    description: Pending deposits flipped to expired this run.
        "401":
          description: QStash signature verification failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal Server Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
