{
  "openapi": "3.1.0",
  "info": {
    "title": "Solvela Gateway",
    "description": "Solana-native AI agent payment gateway. OpenAI-compatible LLM chat completions paid per request in USDC-SPL over the x402 protocol — no API key, no account, just a wallet. A rule-based smart router selects a model per request, an exact-match response cache returns prior answers at zero upstream cost, and a trustless on-chain escrow scheme is available for prepaid sessions.",
    "version": "0.1.0",
    "license": { "name": "BUSL-1.1", "identifier": "BUSL-1.1" },
    "contact": { "email": "partnerships@solvela.ai", "url": "https://solvela.ai" },
    "x-guidance": "Pay per request in USDC-SPL on Solana via x402 — no API key or account, just a wallet. POST /v1/chat/completions with no PAYMENT-SIGNATURE header to receive a 402 challenge quoting the USDC cost; sign the quoted `exact` (or `escrow`) payment and resubmit the same request with the signed payload in the PAYMENT-SIGNATURE header. Model catalog at GET /v1/models; x402 discovery at /openapi.json and /.well-known/x402."
  },
  "servers": [
    { "url": "https://api.solvela.ai", "description": "Production" },
    { "url": "https://solvela-gateway.fly.dev", "description": "Direct Fly host" }
  ],
  "paths": {
    "/v1/chat/completions": {
      "post": {
        "operationId": "createChatCompletion",
        "summary": "Create an OpenAI-compatible chat completion",
        "description": "OpenAI-compatible chat completion. Without a `PAYMENT-SIGNATURE` header the gateway returns HTTP 402 with an x402 challenge: a legacy snake_case JSON body plus a canonical x402 v2 challenge (camelCase) in the `PAYMENT-REQUIRED` response header, both quoting the USDC cost on Solana mainnet. Sign the quoted `exact` (or `escrow`) payment with your wallet and resubmit the same request with the signed payment payload in the `PAYMENT-SIGNATURE` header to receive the completion.",
        "x-payment-info": {
          "protocols": ["x402"],
          "price": { "mode": "dynamic", "currency": "USD", "min": 0.000001, "max": 1.00, "description": "Non-binding discovery hint; the authoritative per-request price is the dynamic x402 402 challenge (mode=dynamic). Real cost depends on the resolved model and token counts." }
        },
        "security": [{}, { "x402Payment": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ChatCompletionRequest" },
              "example": {
                "model": "auto",
                "messages": [
                  { "role": "user", "content": "What is 2+2?" }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chat completion (or an SSE stream when `stream` is true).",
            "headers": {
              "X-Solvela-Receipt": {
                "description": "Path of the retrievable payment receipt (`/v1/receipts/{receipt_id}`) for this PAID completion. Present only when the request settled a payment AND the gateway has receipt storage configured; absent on free-tier ($0) responses and on gateways without a database. For SSE streams the header is decided before the body starts. The UUIDv4 id is a bearer capability — anyone holding it can read the receipt.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChatCompletionResponse" }
              },
              "text/event-stream": {
                "schema": { "type": "string", "description": "Server-sent events: `data: {chunk}` lines terminated by `data: [DONE]`." }
              }
            }
          },
          "402": {
            "description": "Payment required. Two distinct bodies share this status: (1) the x402 **challenge** (`PaymentRequired`, snake_case fields) when the request carries no `PAYMENT-SIGNATURE` header — sign and resubmit; (2) the standard **error envelope** (`Error`, with `error.type` of `payment_required` or `invalid_payment`) when a payment header was present but could not be decoded or verified — do not blindly retry. The `PAYMENT-REQUIRED` response header accompanies the challenge form.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "description": "Base64-encoded canonical x402 v2 challenge as camelCase JSON (`x402Version`, `accepts[].payTo`, `accepts[].maxTimeoutSeconds`, …) — note the JSON *body* uses snake_case; the two casings are intentional and must not be mixed. Carries only `exact` scheme entries, each with `extra: {\"decimals\": 6}`. Present whenever an `exact` scheme is offered (i.e., on every challenge).",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    { "$ref": "#/components/schemas/PaymentRequired" },
                    { "$ref": "#/components/schemas/Error" }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`error.type`: `bad_request`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "404": {
            "description": "Unknown model ID, alias, or profile (`error.type`: `model_not_found`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "415": {
            "description": "Image content sent to a model without vision capability (`error.type`: `unsupported_media_type`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Rate limited (`error.type`: `rate_limit_exceeded` or `rate_limited`). Honor `retry-after` before retrying.",
            "headers": {
              "retry-after": { "description": "Seconds until the rate-limit window resets.", "schema": { "type": "integer" } },
              "x-ratelimit-limit": { "description": "Requests allowed per window.", "schema": { "type": "integer" } },
              "x-ratelimit-remaining": { "description": "Requests remaining in the current window (always 0 on a 429).", "schema": { "type": "integer" } },
              "x-ratelimit-reset": { "description": "Seconds until the window resets.", "schema": { "type": "integer" } }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "5XX": {
            "description": "Upstream or gateway failure: 502 (`provider_error`), 503 (`upstream_unavailable` — all providers down), 500 (`settlement_failed`, `internal_error`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/models": {
      "get": {
        "operationId": "listModels",
        "summary": "List available models with pricing and capabilities",
        "description": "Returns the model catalog with per-token pricing and capabilities. No payment required.",
        "security": [],
        "responses": {
          "200": {
            "description": "Model list.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ModelList" }
              }
            }
          }
        }
      }
    },
    "/v1/receipts/{receipt_id}": {
      "get": {
        "operationId": "getReceipt",
        "summary": "Fetch a payment receipt by id",
        "description": "Returns the client-facing receipt for a paid request: payer wallet, payment scheme, transaction reference, and the amounts actually charged (atomic USDC integers are canonical; decimal strings are derived). The unguessable UUIDv4 receipt id — issued in the `X-Solvela-Receipt` response header on paid responses — is the only credential: treat it as a bearer capability. Unknown and malformed ids both return the same 404, and there is no listing endpoint. Free ($0) requests produce no payment and therefore no receipt.",
        "security": [],
        "parameters": [
          {
            "name": "receipt_id",
            "in": "path",
            "required": true,
            "description": "UUIDv4 receipt id from the `X-Solvela-Receipt` header.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "The receipt.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Receipt" }
              }
            }
          },
          "404": {
            "description": "Unknown or malformed receipt id (`error.type` = `not_found`). The two cases are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "Rate limited (`error.type`: `rate_limit_exceeded`). This public route carries a stricter per-client-IP cap than the generic limiter (default 20/min) to bound receipt-id scanning. Honor `retry-after` before retrying.",
            "headers": {
              "retry-after": { "description": "Seconds until the rate-limit window resets.", "schema": { "type": "integer" } },
              "x-ratelimit-limit": { "description": "Requests allowed per window.", "schema": { "type": "integer" } },
              "x-ratelimit-remaining": { "description": "Requests remaining in the current window (always 0 on a 429).", "schema": { "type": "integer" } },
              "x-ratelimit-reset": { "description": "Seconds until the window resets.", "schema": { "type": "integer" } }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": {
            "description": "Receipt storage is not configured on this gateway (`error.type` = `service_unavailable`) — receipts cannot exist here at all, so no per-id 404 is implied.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "health",
        "summary": "Report gateway and dependency health status",
        "description": "Liveness/readiness probe. No payment required.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "status": { "type": "string", "enum": ["ok", "degraded", "error"] } },
                  "required": ["status"]
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "x402Payment": {
        "type": "apiKey",
        "in": "header",
        "name": "PAYMENT-SIGNATURE",
        "description": "x402 payment payload: JSON (raw or base64-encoded) of the form `{ x402_version, resource: {url, method}, accepted: <one entry from the 402 challenge's accepts[]>, payload: { transaction } | { deposit_tx, service_id, agent_pubkey } }`, where `transaction`/`deposit_tx` is a base64-encoded signed Solana versioned transaction. Omit the header to receive the 402 challenge quoting the price."
      }
    },
    "schemas": {
      "ChatCompletionRequest": {
        "type": "object",
        "required": ["model", "messages"],
        "properties": {
          "model": {
            "type": "string",
            "description": "Model ID (e.g. `openai/gpt-4o`), alias (e.g. `sonnet`), or routing profile (`auto`, `eco`, `premium`, `free`). Call `GET /v1/models` for available IDs.",
            "example": "auto"
          },
          "messages": {
            "type": "array",
            "description": "Conversation messages in OpenAI chat format, ordered oldest to newest (roles: system, user, assistant, tool, developer).",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/ChatMessage" }
          },
          "max_tokens": { "type": "integer", "minimum": 1, "description": "Max output tokens; clamped to the model limit." },
          "temperature": { "type": "number", "minimum": 0, "maximum": 2 },
          "top_p": { "type": "number", "minimum": 0, "maximum": 1 },
          "stream": { "type": "boolean", "default": false, "description": "Stream the response as Server-Sent Events." },
          "tools": { "type": "array", "items": { "type": "object" }, "description": "OpenAI-style tool/function definitions." },
          "tool_choice": { "description": "OpenAI-style tool choice." }
        }
      },
      "ChatMessage": {
        "type": "object",
        "required": ["role"],
        "properties": {
          "role": { "type": "string", "enum": ["system", "user", "assistant", "tool", "developer"] },
          "content": {
            "description": "Plain string, an array of content parts (text and image_url) for vision-capable models, or null/absent on assistant turns that carry only `tool_calls`. The gateway maps absent and null content to the empty string on input.",
            "anyOf": [
              { "type": "string" },
              { "type": "array", "items": { "type": "object" } },
              { "type": "null" }
            ]
          },
          "name": { "type": "string" },
          "tool_calls": {
            "type": "array",
            "description": "Tool calls requested by the model (assistant messages only). Reply with a `role: tool` message carrying the matching `tool_call_id`.",
            "items": { "$ref": "#/components/schemas/ToolCall" }
          },
          "tool_call_id": { "type": "string" }
        }
      },
      "ToolCall": {
        "type": "object",
        "required": ["id", "type", "function"],
        "properties": {
          "id": { "type": "string", "description": "Unique identifier for this tool call; echo it back as `tool_call_id` on the follow-up `role: tool` message." },
          "type": { "type": "string", "example": "function" },
          "function": {
            "type": "object",
            "description": "The function the model wants invoked.",
            "required": ["name", "arguments"],
            "properties": {
              "name": { "type": "string", "description": "Function name, matching a `tools[].function.name` from the request." },
              "arguments": { "type": "string", "description": "JSON-encoded function arguments." }
            }
          }
        }
      },
      "ChatCompletionResponse": {
        "type": "object",
        "required": ["id", "object", "created", "model", "choices"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "example": "chat.completion" },
          "created": { "type": "integer" },
          "model": { "type": "string" },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["index", "message"],
              "properties": {
                "index": { "type": "integer" },
                "message": { "$ref": "#/components/schemas/ChatMessage" },
                "finish_reason": { "type": ["string", "null"] }
              }
            }
          },
          "usage": {
            "type": ["object", "null"],
            "required": ["prompt_tokens", "completion_tokens", "total_tokens"],
            "properties": {
              "prompt_tokens": { "type": "integer" },
              "completion_tokens": { "type": "integer" },
              "total_tokens": { "type": "integer" }
            }
          }
        }
      },
      "PaymentRequired": {
        "type": "object",
        "description": "x402 challenge returned with HTTP 402 when no `PAYMENT-SIGNATURE` header is present. Field names are snake_case; the canonical camelCase rendering travels in the `PAYMENT-REQUIRED` response header.",
        "required": ["x402_version", "resource", "accepts", "cost_breakdown", "error"],
        "properties": {
          "x402_version": { "type": "integer", "example": 2 },
          "resource": {
            "type": "object",
            "required": ["url", "method"],
            "properties": {
              "url": { "type": "string", "example": "/v1/chat/completions" },
              "method": { "type": "string", "example": "POST" }
            }
          },
          "accepts": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": ["scheme", "network", "amount", "asset", "pay_to", "max_timeout_seconds"],
              "properties": {
                "scheme": { "type": "string", "enum": ["exact", "escrow"] },
                "network": { "type": "string", "example": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" },
                "amount": { "type": "string", "description": "Atomic units (USDC has 6 decimals)." },
                "asset": { "type": "string", "description": "USDC SPL mint address." },
                "pay_to": { "type": "string", "description": "Recipient wallet address." },
                "max_timeout_seconds": { "type": "integer", "example": 300 },
                "escrow_program_id": { "type": "string", "description": "Escrow program ID. Present only on `scheme: escrow` entries; absent otherwise." }
              }
            }
          },
          "cost_breakdown": {
            "type": "object",
            "required": ["provider_cost", "platform_fee", "total", "currency", "fee_percent"],
            "properties": {
              "provider_cost": { "type": "string" },
              "platform_fee": { "type": "string" },
              "total": { "type": "string" },
              "currency": { "type": "string", "example": "USDC" },
              "fee_percent": { "type": "integer", "example": 5 }
            }
          },
          "error": { "type": "string" },
          "extensions": {
            "type": "object",
            "description": "Optional, additive discovery metadata. Present on the live `/v1/chat/completions` 402 challenge body; absent on the canonical camelCase `PAYMENT-REQUIRED` header. Carries the static Coinbase-Bazaar block (`extensions.bazaar`) so x402 discovery indexers (x402scan, agentcash) read the resource as invocable — a non-canonical challenge-embed because Solvela self-settles rather than running on Coinbase's facilitator. NOT part of the value path: clients sign `accepts`, never `extensions`; money fields, verification, and settlement are byte-unchanged. Identical on every challenge (no wallet/amount/time data).",
            "properties": {
              "bazaar": {
                "type": "object",
                "description": "Coinbase-Bazaar discovery descriptor: `info` (x402scan invocability gate) plus `schema` whose `properties.input.properties.body` is a JSON Schema of the chat request and `properties.output.properties.example` is a representative `chat.completion` response (agentcash schema extraction)."
              }
            }
          }
        }
      },
      "ModelList": {
        "type": "object",
        "required": ["object", "data"],
        "properties": {
          "object": { "type": "string", "example": "list" },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "object", "provider", "display_name", "context_window", "capabilities", "pricing"],
              "properties": {
                "id": { "type": "string" },
                "object": { "type": "string", "example": "model" },
                "provider": { "type": "string" },
                "display_name": { "type": "string" },
                "context_window": { "type": "integer" },
                "capabilities": {
                  "type": "object",
                  "required": ["streaming", "tools", "vision", "reasoning"],
                  "properties": {
                    "streaming": { "type": "boolean" },
                    "tools": { "type": "boolean" },
                    "vision": { "type": "boolean" },
                    "reasoning": { "type": "boolean" }
                  }
                },
                "pricing": {
                  "type": "object",
                  "required": ["input_per_million", "output_per_million", "currency", "fee_percent"],
                  "properties": {
                    "input_per_million": { "type": "number" },
                    "output_per_million": { "type": "number" },
                    "currency": { "type": "string", "example": "USDC" },
                    "fee_percent": { "type": "integer", "example": 5 }
                  }
                }
              }
            }
          }
        }
      },
      "Receipt": {
        "type": "object",
        "description": "Client-facing payment receipt for a paid request. Atomic-USDC integers (6 decimals) are canonical; the `*_usdc` decimal strings are derived from them. `amount_paid_atomic` is what the payer was actually billed and equals `cost_breakdown.total_atomic` except when an escrow semantic-cache discount realised on-chain. It is the billed amount from the gateway ledger's perspective — identical to the spend ledger — and can differ from the raw on-chain transfer amount when an agent overpays the 402 quote.",
        "required": ["receipt_id", "created_at", "model", "payment_scheme", "payer_wallet", "amount_paid_atomic", "amount_paid_usdc", "cost_breakdown"],
        "properties": {
          "receipt_id": { "type": "string", "format": "uuid" },
          "created_at": { "type": "string", "format": "date-time", "description": "When the receipt was recorded (request completion time, UTC)." },
          "model": { "type": "string", "description": "Model ID (chat path) or marketplace service ID (services proxy path).", "example": "openai/gpt-4o" },
          "payment_scheme": { "type": "string", "description": "x402 scheme that settled the payment.", "example": "exact" },
          "tx_signature": { "type": "string", "description": "Payment transaction reference as recorded on the spend ledger (the signed transaction carried in the payment payload). Absent when no reference was extractable." },
          "payer_wallet": { "type": "string", "description": "Payer wallet (base58 pubkey) extracted from the signed payment." },
          "amount_paid_atomic": { "type": "integer", "minimum": 0, "description": "Amount actually billed, atomic USDC. Canonical." },
          "amount_paid_usdc": { "type": "string", "example": "0.002625" },
          "cost_breakdown": { "$ref": "#/components/schemas/ReceiptCostBreakdown" },
          "vendor": { "$ref": "#/components/schemas/ReceiptVendorSettlement" }
        }
      },
      "ReceiptCostBreakdown": {
        "type": "object",
        "description": "Agent-facing cost breakdown that produced the bill: provider cost + platform fee = total. On vendor-settled services the agent fee is 0 (the vendor absorbs the platform fee — see `vendor`).",
        "required": ["provider_cost_atomic", "provider_cost_usdc", "platform_fee_atomic", "platform_fee_usdc", "total_atomic", "total_usdc", "currency"],
        "properties": {
          "provider_cost_atomic": { "type": "integer", "minimum": 0 },
          "provider_cost_usdc": { "type": "string", "example": "0.002500" },
          "platform_fee_atomic": { "type": "integer", "minimum": 0 },
          "platform_fee_usdc": { "type": "string", "example": "0.000125" },
          "total_atomic": { "type": "integer", "minimum": 0 },
          "total_usdc": { "type": "string", "example": "0.002625" },
          "currency": { "type": "string", "example": "USDC" }
        }
      },
      "ReceiptVendorSettlement": {
        "type": "object",
        "description": "Vendor-settlement evidence, present only when the request hit a marketplace service with a per-service `vendor_wallet`: the agent's transfer settled `settled_atomic` directly to the vendor on-chain, and Solvela's platform fee is recorded as an off-chain receivable against the vendor (never charged to the agent).",
        "required": ["vendor_wallet", "settled_atomic", "settled_usdc", "fee_receivable_atomic", "fee_receivable_usdc"],
        "properties": {
          "vendor_wallet": { "type": "string", "description": "Vendor wallet (base58 pubkey) the payment settled to." },
          "settled_atomic": { "type": "integer", "minimum": 0 },
          "settled_usdc": { "type": "string", "example": "0.020000" },
          "fee_receivable_atomic": { "type": "integer", "minimum": 0 },
          "fee_receivable_usdc": { "type": "string", "example": "0.001000" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["type", "message"],
            "properties": {
              "type": {
                "type": "string",
                "description": "Machine-readable error kind. Known values: `bad_request`, `model_not_found`, `not_found`, `payment_required`, `invalid_payment`, `settlement_failed`, `forbidden`, `unsupported_media_type`, `rate_limited`, `rate_limit_exceeded`, `provider_error`, `upstream_unavailable`, `service_unavailable`, `internal_error`. New values may be added; treat unknown values as retriable-or-not by HTTP status.",
                "example": "invalid_payment"
              },
              "message": { "type": "string" }
            }
          }
        }
      }
    }
  }
}
