{
  "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.\n\n**v0.3.0 — Sprint 6:** Stripe subscription rail removed. Monetisation is now fully crypto-native:\n1. **Prepaid USDC credits** — buy credit packs at `/console/credits`. Each `/v1/*` call debits 1 credit. Balance verified via on-chain Base Transfer events.\n2. **x402 per-call micropayment** — autonomous agents pay per-call in USDC on Base without a pre-provisioned key.\n\n**Auth paths:**\n1. **Bearer token** — `Authorization: Bearer cv_<env>_<token>`. Issue keys at https://cloven.cloud/console.\n2. **x402 micropayment** — first call returns 402 with `PaymentRequirements`; agent signs USDC transfer on Base and retries with `X-Payment: <base64-proof>`.\n\n**Credit quotas (prepaid):**\n- Starter: 1,000 credits / $1 USDC.\n- Hobby: 10,000 credits / $8 USDC (20% discount).\n- Pro: 100,000 credits / $70 USDC (30% discount).\n- Team: 1,000,000 credits / $600 USDC (40% discount).\n\n**Free-tier fallback:** Keys with zero credits fall through to the 100 calls/day sliding-window free tier (crypto pack only).\n\nAll 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.\n\nAuth: `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).\n\nx402 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.\n\nPhase 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.\n\nThe `brief` field is empty for historical snapshots — the briefer runs daily, not per-compaction. Use `/v1/brief` for the current narrated brief.\n\nHistorical 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.\n\n**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).\n\n**Frame types:**\n- `event: pulse` — emitted every 15 seconds with the current cached state.\n- `event: delta` — emitted when `generatedAt` advances (a new compaction landed upstream).\n- `event: error` — non-fatal error string; stream stays open.\n- `: keepalive <epoch_ms>` — SSE comment frame emitted every 25 seconds to prevent proxy connection timeouts.\n\nThe 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.\n\nUseful 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`.\n\nAvailable tools: `cloven.fresh`, `cloven.brief`, `cloven.search`, `cloven.snapshot`, `cloven.subscribe`, `cloven.cite`.\n\nAuth: `Authorization: Bearer cv_*` (prepaid credit key) or `X-Payment: <base64>` (x402 per-call USDC path). Unauthenticated first call returns 402 with `PaymentRequirements`.\n\nThe 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.\n\nAuth: 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.\n\nTier 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.\n\nThe `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.\n\nThe 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.\n\nAlternatively the caller can fast-path verify by POSTing the `txHash` to `/api/console/credits/{depositId}`.\n\nAuth: 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.\n\nAuth: 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`.\n\nAuth: 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.\n\nThe route reads the receipt from Base, checks the Transfer event matches (recipient + amount), and flips the deposit to `verified` + increments `credit_balance` atomically.\n\nAuth: 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.\n\nScans 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.\n\nAlso expires any pending deposits past their `expires_at` timestamp.\n\n**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"
                }
              }
            }
          }
        }
      }
    }
  }
}