{ "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: , 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" } } } } } } } }