{
  "openapi": "3.1.0",
  "info": {
    "title": "dox402 - Pay-Per-Use AI Inference Gateway",
    "version": "1.0.0",
    "description": "Pay-per-use AI inference via the x402 payment protocol. No signup, no API key — authenticate with your Ethereum wallet (SIWE/EIP-4361), pay with USDC on Base Mainnet, and get streamed AI responses. Each wallet gets its own Durable Object instance with isolated balance, history, and rate limiting.",
    "contact": {
      "url": "https://github.com/iglesiasbrandon/dox402"
    },
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://inference-gate.iglesias-brandon.workers.dev",
      "description": "Production"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "summary": "Liveness probe",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "const": "ok" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/payment-info": {
      "get": {
        "summary": "Payment address and network details",
        "operationId": "getPaymentInfo",
        "description": "Returns the USDC receiving address, network, and minimum top-up amount. Use this to construct on-chain payments.",
        "responses": {
          "200": {
            "description": "Payment configuration",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PaymentInfo" }
              }
            }
          }
        }
      }
    },
    "/auth/nonce": {
      "get": {
        "summary": "Generate one-time nonce for SIWE signing",
        "operationId": "getAuthNonce",
        "description": "Step 1 of authentication: request a nonce, then construct an EIP-4361 (SIWE) message containing this nonce for wallet signing.",
        "parameters": [
          {
            "name": "wallet",
            "in": "query",
            "required": true,
            "description": "Ethereum wallet address (0x + 40 hex chars)",
            "schema": {
              "type": "string",
              "pattern": "^0x[0-9a-fA-F]{40}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Nonce generated (valid for 5 minutes, single use)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["nonce"],
                  "properties": {
                    "nonce": {
                      "type": "string",
                      "description": "32-byte hex nonce (64 chars)"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid wallet address format",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/auth/login": {
      "post": {
        "summary": "Verify SIWE signature and issue session",
        "operationId": "postAuthLogin",
        "description": "Step 2 of authentication: submit the signed EIP-4361 message. On success, returns {token, expiresAt} in the response body and sets an HttpOnly session cookie (ig_session). Use the token as Authorization: Bearer <token> for API access. The token is valid for 24 hours.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["message", "signature"],
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "EIP-4361 (SIWE) message string containing the nonce from /auth/nonce"
                  },
                  "signature": {
                    "type": "string",
                    "description": "EIP-191 personal_sign hex signature (0x-prefixed)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authentication successful. JWT returned in body and set as HttpOnly cookie.",
            "headers": {
              "Set-Cookie": {
                "description": "HttpOnly session cookie (ig_session=JWT; HttpOnly; SameSite=Strict; Secure; Path=/)",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["token", "expiresAt"],
                  "properties": {
                    "token": {
                      "type": "string",
                      "description": "JWT session token for Authorization: Bearer header"
                    },
                    "expiresAt": {
                      "type": "integer",
                      "description": "Token expiry as Unix timestamp (seconds)"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON body or missing fields",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Signature verification failed or nonce invalid",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "summary": "Clear session cookie",
        "operationId": "postAuthLogout",
        "description": "Clears the ig_session HttpOnly cookie by setting Max-Age=0.",
        "responses": {
          "200": {
            "description": "Session cookie cleared",
            "headers": {
              "Set-Cookie": {
                "description": "Expired session cookie (Max-Age=0)",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/infer": {
      "post": {
        "summary": "Run AI inference",
        "operationId": "postInfer",
        "description": "Submit a prompt for AI text generation. Supports two response modes: set Accept: application/json for a synchronous JSON response (recommended for agents), or Accept: text/event-stream (default) for SSE streaming with heartbeat keepalive. Requires authentication and a positive token balance. If balance is zero and no payment proof is provided, returns 402 with payment instructions.",
        "security": [
          { "BearerAuth": [] },
          { "CookieAuth": [] },
          { "SiwxAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InferRequest" }
            }
          }
        },
        "parameters": [
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "description": "Set to 'application/json' for synchronous JSON response (recommended for agents). Defaults to SSE stream if omitted.",
            "schema": { "type": "string", "enum": ["application/json", "text/event-stream"] }
          },
          {
            "name": "PAYMENT-SIGNATURE",
            "in": "header",
            "required": false,
            "description": "Base64-encoded PaymentProof JSON. Include when topping up balance alongside inference.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Inference response. Content type depends on Accept header: application/json for synchronous response, text/event-stream for SSE.",
            "headers": {
              "Set-Cookie": {
                "description": "Session cookie (only present when authenticated via SIWX header)",
                "schema": { "type": "string" }
              },
              "X-Session-Expires": {
                "description": "Session token expiry as Unix timestamp (only present when authenticated via SIWX header)",
                "schema": { "type": "string" }
              },
              "X-Payment-Status": {
                "description": "Set to 'provisional' when payment was credited via grace mode (RPC was unreachable)",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Synchronous JSON response (when Accept: application/json)",
                  "properties": {
                    "response": { "type": "string", "description": "Complete generated text" },
                    "usage": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "prompt_tokens": { "type": "integer" },
                        "completion_tokens": { "type": "integer" }
                      }
                    },
                    "cost": { "type": "integer", "description": "Tokens charged for this request (1 token = 1 µUSDC)" },
                    "model": { "type": "string", "description": "Workers AI model ID used" },
                    "balance": { "type": "integer", "description": "Updated token balance after deduction" }
                  }
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE stream. Data events: data: {\"response\":\"...chunk...\"}. Keepalive: :keepalive. Terminal: data: [DONE]."
                }
              }
            }
          },
          "401": { "description": "Missing or invalid session token / SIWX header", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "402": {
            "description": "Insufficient balance. Response includes payment instructions and SIWX auth extension.",
            "headers": { "PAYMENT-REQUIRED": { "description": "Base64-encoded PaymentRequired JSON (x402 spec)", "schema": { "type": "string" } } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentRequired" } } }
          },
          "403": { "description": "walletAddress in body does not match authenticated session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "413": { "description": "Total input (prompt + history + RAG context) exceeds the selected model's context window", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": {
            "description": "Rate limit exceeded (60 requests per minute per wallet)",
            "headers": { "Retry-After": { "description": "Seconds until the rate limit window resets", "schema": { "type": "string" } } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/deposit": {
      "post": {
        "summary": "Top up token balance with USDC payment proof",
        "operationId": "postDeposit",
        "description": "Submit an on-chain USDC payment proof to add tokens to your balance without running inference. Supports grace mode: if RPC is unreachable, tokens are provisionally granted and verified asynchronously.",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DepositRequest" } } } },
        "responses": {
          "200": {
            "description": "Deposit successful",
            "content": { "application/json": { "schema": {
              "type": "object",
              "properties": {
                "ok": { "type": "boolean", "const": true },
                "credited": { "type": "integer", "description": "Tokens credited from this deposit" },
                "tokens": { "type": "integer", "description": "New total token balance" },
                "provisional": { "type": "boolean", "description": "True if tokens were granted provisionally (RPC unreachable)" }
              }
            } } }
          },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "402": { "description": "Payment proof verification failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/balance": {
      "get": {
        "summary": "Get token balance and usage stats",
        "operationId": "getBalance",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "responses": {
          "200": { "description": "Balance and usage statistics", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BalanceResponse" } } } },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/history": {
      "get": {
        "summary": "Get conversation history",
        "operationId": "getHistory",
        "description": "Returns the stored conversation messages for this wallet. Capped at 20 messages (10 exchanges).",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "responses": {
          "200": { "description": "Conversation history", "content": { "application/json": { "schema": { "type": "object", "properties": { "history": { "type": "array", "items": { "$ref": "#/components/schemas/ConversationMessage" } } } } } } },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      },
      "delete": {
        "summary": "Clear conversation history",
        "operationId": "deleteHistory",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "responses": {
          "200": { "description": "History cleared", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "const": true } } } } } },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/documents": {
      "post": {
        "summary": "Upload a document for RAG",
        "operationId": "postDocument",
        "description": "Upload a text document to be chunked, embedded, and stored in Vectorize for retrieval-augmented generation. Embedding cost is deducted from wallet balance. Max 50 documents per wallet, max 100KB per document.",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "content"],
                "properties": {
                  "title": { "type": "string", "maxLength": 200, "description": "Document title (max 200 chars)" },
                  "content": { "type": "string", "maxLength": 102400, "description": "Raw text content (max 100KB)" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Document uploaded and embedded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DocumentMeta" } } } },
          "400": { "description": "Invalid input (missing title/content or content too large)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "402": { "description": "Insufficient balance for embedding cost", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "409": { "description": "Document limit reached (max 50 per wallet)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      },
      "get": {
        "summary": "List uploaded documents",
        "operationId": "getDocuments",
        "description": "Returns metadata for all documents uploaded by this wallet, ordered by creation date (newest first).",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "responses": {
          "200": { "description": "Document list", "content": { "application/json": { "schema": { "type": "object", "properties": { "documents": { "type": "array", "items": { "$ref": "#/components/schemas/DocumentMeta" } } } } } } },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/documents/reindex": {
      "post": {
        "summary": "Re-upsert all document vectors",
        "operationId": "postDocumentsReindex",
        "description": "Re-embeds and re-upserts all documents for this wallet into Vectorize. Useful when Vectorize metadata indexes were created after initial vector upserts, making existing vectors unfilterable.",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "responses": {
          "200": { "description": "Reindex complete", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "const": true }, "reindexed": { "type": "integer", "description": "Number of documents re-indexed" }, "totalChunks": { "type": "integer", "description": "Total vector chunks upserted" } } } } } },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/documents/{documentId}": {
      "delete": {
        "summary": "Delete a document and its embeddings",
        "operationId": "deleteDocument",
        "description": "Deletes a document, its chunks from SQL, and its vectors from Vectorize. Does not refund the embedding cost.",
        "security": [{ "BearerAuth": [] }, { "CookieAuth": [] }],
        "parameters": [
          { "name": "documentId", "in": "path", "required": true, "description": "UUID of the document to delete", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Document deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "const": true }, "deletedChunks": { "type": "integer", "description": "Number of vector chunks removed" } } } } } },
          "401": { "description": "Missing or invalid session token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Document not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Session token obtained via POST /auth/login. Valid for 24 hours. Flow: 1) GET /auth/nonce?wallet=0x... 2) Sign EIP-4361 message with wallet 3) POST /auth/login with {message, signature}"
      },
      "CookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "ig_session",
        "description": "HttpOnly session cookie set by POST /auth/login. Contains the same JWT as BearerAuth. Preferred for browser clients."
      },
      "SiwxAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "SIGN-IN-WITH-X",
        "description": "Base64-encoded SIWX payload for single-request authentication (x402 SIWX spec). Contains {message, signature, chainId, type, address}. Server sets ig_session cookie in the response for subsequent requests."
      }
    },
    "schemas": {
      "ErrorResponse": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" } } },
      "InferRequest": {
        "type": "object",
        "required": ["prompt", "walletAddress"],
        "properties": {
          "prompt": { "type": "string", "description": "The user prompt for AI text generation" },
          "walletAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$", "description": "Must match the authenticated wallet address" },
          "maxTokens": { "type": "integer", "default": 512, "maximum": 2048, "description": "Maximum tokens to generate" },
          "model": { "type": "string", "default": "@cf/meta/llama-3.1-8b-instruct", "enum": ["@cf/meta/llama-3.1-8b-instruct", "@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/google/gemma-3-12b-it", "@cf/mistral/mistral-7b-instruct-v0.2", "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b"], "description": "Workers AI model ID" },
          "useRag": { "type": "boolean", "default": false, "description": "Opt-in RAG augmentation. When true, the user prompt is embedded and matched against uploaded documents to inject relevant context." },
          "systemPrompt": { "type": "string", "maxLength": 2000, "description": "Optional persistent instructions prepended as a system message (e.g., 'respond in Spanish', 'you are a Python expert'). Max 2000 chars." }
        }
      },
      "DepositRequest": {
        "type": "object",
        "required": ["walletAddress", "proof"],
        "properties": {
          "walletAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$", "description": "Must match the authenticated wallet address" },
          "proof": { "type": "string", "description": "Base64-encoded PaymentProof JSON" }
        }
      },
      "PaymentRequired": {
        "type": "object",
        "description": "x402 payment requirements returned with HTTP 402",
        "properties": {
          "version": { "type": "string", "const": "1" },
          "scheme": { "type": "string", "const": "exact" },
          "network": { "type": "string", "example": "base-mainnet" },
          "paymentAddress": { "type": "string", "description": "USDC receiving address" },
          "asset": { "type": "string", "const": "USDC" },
          "amount": { "type": "string", "description": "USDC amount in smallest unit (6 decimals). 1000 = $0.001" },
          "balanceTokens": { "type": "integer", "description": "Tokens added per payment" },
          "maxAgeSeconds": { "type": "integer", "description": "Payment proof validity window" },
          "description": { "type": "string" },
          "extensions": { "type": "object", "properties": { "sign-in-with-x": { "$ref": "#/components/schemas/SiwxExtension" } } }
        }
      },
      "PaymentProof": {
        "type": "object",
        "description": "Proof of on-chain USDC payment (submitted via PAYMENT-SIGNATURE header or in deposit body)",
        "required": ["txHash", "from", "amount", "timestamp", "signature"],
        "properties": {
          "txHash": { "type": "string", "description": "On-chain transaction hash" },
          "from": { "type": "string", "description": "Payer wallet address (0x-prefixed)" },
          "amount": { "type": "string", "description": "USDC amount in smallest unit" },
          "timestamp": { "type": "integer", "description": "Unix seconds when proof was constructed" },
          "signature": { "type": "string", "description": "EIP-191 personal_sign over canonical proof message" }
        }
      },
      "SiwxPayload": {
        "type": "object",
        "description": "SIGN-IN-WITH-X header payload (base64-encoded JSON)",
        "required": ["message", "signature", "chainId", "type", "address"],
        "properties": {
          "message": { "type": "string", "description": "EIP-4361 message string" },
          "signature": { "type": "string", "description": "Hex-encoded EIP-191 signature (0x-prefixed)" },
          "chainId": { "type": "string", "example": "eip155:8453", "description": "CAIP-2 chain identifier" },
          "type": { "type": "string", "example": "eip191", "description": "Signature type" },
          "address": { "type": "string", "description": "Wallet address (0x-prefixed for EVM)" }
        }
      },
      "SiwxExtension": {
        "type": "object",
        "description": "SIWX auth discovery extension included in 402 responses",
        "properties": {
          "supportedChains": { "type": "array", "items": { "type": "object", "properties": { "chainId": { "type": "string", "example": "eip155:8453" }, "type": { "type": "string", "example": "eip191" } } } },
          "info": { "type": "object", "properties": { "domain": { "type": "string" }, "uri": { "type": "string" }, "version": { "type": "string" }, "statement": { "type": "string" }, "nonce": { "type": "string", "description": "One-time nonce for SIWE message construction" }, "issuedAt": { "type": "string", "format": "date-time" }, "expirationTime": { "type": "string", "format": "date-time" } } }
        }
      },
      "BalanceResponse": {
        "type": "object",
        "properties": {
          "tokens": { "type": "integer", "description": "Current token balance" },
          "totalDeposited": { "type": "integer", "description": "Total tokens deposited" },
          "totalSpent": { "type": "integer", "description": "Total tokens spent on inference" },
          "totalRequests": { "type": "integer" },
          "totalFailedRequests": { "type": "integer", "description": "Requests where billing was skipped (AI failure, empty response, stream error)" },
          "provisionalTokens": { "type": "integer", "description": "Outstanding provisional tokens pending RPC re-verification" }
        }
      },
      "DocumentMeta": {
        "type": "object",
        "description": "Metadata for an uploaded RAG document",
        "properties": {
          "id": { "type": "string", "description": "Document UUID" },
          "title": { "type": "string", "description": "Document title" },
          "charCount": { "type": "integer", "description": "Character count of the original text" },
          "chunkCount": { "type": "integer", "description": "Number of text chunks created for embedding" },
          "createdAt": { "type": "integer", "description": "Unix timestamp (ms) when the document was uploaded" },
          "embeddingCostTokens": { "type": "integer", "description": "Embedding cost deducted from balance in tokens" }
        }
      },
      "ConversationMessage": {
        "type": "object",
        "properties": {
          "role": { "type": "string", "enum": ["user", "assistant"] },
          "content": { "type": "string" },
          "meta": { "type": "object", "description": "Usage metadata (assistant messages only)", "properties": { "cost": { "type": "number", "description": "Tokens charged for this response" }, "model": { "type": "string", "description": "Workers AI model ID used" } } }
        }
      },
      "PaymentInfo": {
        "type": "object",
        "properties": {
          "paymentAddress": { "type": "string", "description": "USDC receiving address on Base Mainnet" },
          "network": { "type": "string", "example": "base-mainnet" },
          "asset": { "type": "string", "const": "USDC" },
          "usdcContract": { "type": "string", "description": "USDC ERC-20 contract address on Base" },
          "minimumTokens": { "type": "integer", "description": "Minimum top-up amount in tokens" },
          "tokensPerUSDC": { "type": "integer", "description": "Tokens per 1 USDC (1,000,000)" }
        }
      }
    }
  }
}
