Concepts
Payment Flow
Step-by-step guide to x402 payments
Every request to Solvela goes through a two-leg HTTP exchange. The first leg discovers the price; the second leg pays and receives the response. SDKs handle both legs automatically — this page explains what happens at each step so you can implement a custom client or debug a failing payment.
Tip
If you are using the TypeScript, Python, Rust, or Go SDK, you never need to implement this yourself. Call client.chat() and the SDK handles discovery, signing, and retry automatically.
The Full Flow
Send the initial request
Make a standard POST to any Solvela endpoint with no payment header. The body is a normal OpenAI-compatible chat completions payload.
curl -X POST https://api.solvela.ai/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"messages": [{ "role": "user", "content": "Hello" }]
}'The server will not forward this to any LLM. It responds immediately with HTTP 402.
Parse the 402 response
The 402 body is the PaymentRequired object at the top level — not wrapped in the OpenAI { "error": { ... } } envelope. Parse the response body directly as JSON.
{
"x402_version": 2,
"resource": { "url": "/v1/chat/completions", "method": "POST" },
"accepts": [
{
"scheme": "exact",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"amount": "2625",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"pay_to": "<gateway-recipient-wallet>",
"max_timeout_seconds": 300
},
{
"scheme": "escrow",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"amount": "2625",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"pay_to": "<gateway-recipient-wallet>",
"max_timeout_seconds": 300,
"escrow_program_id": "9neDHouXgEgHZDde5SpmqqEZ9Uv35hFcjtFEPxomtHLU"
}
],
"cost_breakdown": {
"provider_cost": "0.002500",
"platform_fee": "0.000125",
"total": "0.002625",
"currency": "USDC",
"fee_percent": 5
},
"error": "Payment required"
}The trailing error field is a plain string banner, not an OpenAI error object. A second accepts entry (with escrow_program_id) only appears when the gateway has an escrow program configured.
A 402 returned for an invalid payment header (malformed, replayed, expired, amount-insufficient, etc.) uses the conventional OpenAI envelope instead:
{
"error": {
"type": "invalid_payment",
"message": "transaction has already been used; each payment signature may only be submitted once"
}
}Distinguish the two shapes by checking for the top-level x402_version field.
Warning
The asset field is the SPL mint address of the token being requested, not a symbolic name. For mainnet USDC this is always EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. Verify this byte-for-byte in your client before signing — older drafts of the protocol used the literal string "USDC-SPL" and clients hardcoded against that will silently skip mint validation. The network field uses CAIP-2 form (solana:<cluster-genesis-hash>); the trailing identifier is the mainnet cluster genesis hash.
Your client should read accepts[0].amount for the atomic-unit value, accepts[0].pay_to for the destination wallet, and accepts[0].asset for the SPL mint. The max_timeout_seconds field tells you how long the signed transaction will be accepted before expiring.
Sign the Solana transaction
Construct a USDC-SPL transfer for the exact amount to the pay_to address and sign it with your wallet's private key. The TypeScript SDK uses @solana/web3.js, the Python SDK uses solders, and the Rust CLI uses the solana crate. The Go SDK's built-in KeypairSigner (solvela.NewKeypairSigner(wallet, rpcURL)) signs escrow deposits over raw Solana JSON-RPC; the exact scheme is intentionally unimplemented in Go, so for direct transfers implementers reach for gagliardetto/solana-go (or any Solana RPC client) via a custom Signer — see the Go SDK page.
The signing step never broadcasts the transaction — it only produces a signed transaction bytes object. The gateway broadcasts it after verifying the signature is valid.
Warning
Never expose your wallet's private key to a server. Signing always happens client-side. The PAYMENT-SIGNATURE header carries the signed transaction, not the private key.
Resubmit with the payment-signature header
Build a PaymentPayload JSON object containing your signed transaction, base64-encode the JSON, and resend the original request with the encoded value in the payment-signature header.
curl -X POST https://api.solvela.ai/v1/chat/completions \
-H "Content-Type: application/json" \
-H "payment-signature: <base64-encoded-PaymentPayload-JSON>" \
-d '{
"model": "auto",
"messages": [{ "role": "user", "content": "Hello" }]
}'The PaymentPayload envelope itself contains x402_version, resource, an accepted block (one entry from the 402 accepts array), and a payload field with the base64-encoded signed Solana versioned transaction bytes. The outer header value is base64-encoded JSON; the inner signed transaction is also base64 (Solana versioned-tx wire bytes). See x402 Protocol for the exact shape.
Receive the LLM response
If the signature is valid and the nonce is fresh, the gateway verifies the transaction (validate + simulate), proxies the request to the upstream LLM, and streams the response back to you in standard OpenAI format. For the default exact scheme, the on-chain broadcast is deferred until after the response is delivered — you are not charged when the provider call fails. Escrow deposits are the exception: they must land on-chain before the request is served.
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"model": "gpt-4o-mini",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "Hello! How can I help?" },
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 10, "completion_tokens": 8, "total_tokens": 18 }
}Cost Breakdown Format
The cost_breakdown object is present on every 402 response. All monetary values are decimal strings in USDC. The amount field inside accepts is in atomic units (1 USDC = 1,000,000 atomic units).
| Field | Type | Description |
|---|---|---|
provider_cost | string | What the upstream LLM charges for this request |
platform_fee | string | 5% of provider_cost, retained by Solvela |
total | string | provider_cost + platform_fee |
currency | string | Always "USDC" today |
fee_percent | number | Platform fee percentage (currently 5, serialized as an integer) |
Escrow vs Exact Payments
The accepts array may contain one or both payment schemes. Choose based on your trust model.
Spend Caps
The official SDKs expose a per-call payment cap (the maximum amount they will sign for in one chat call) rather than a session-wide budget. The cap rejects the payment client-side before signing if the gateway's quoted amount exceeds it, preventing a misconfigured or attacker-controlled 402 from charging more than expected.
Warning
Important: max_payment_amount / maxPaymentAmount is a client-side per-call guard only and does not communicate a limit to the server. For session-wide caps, track lifetime spend in your own code; for server-enforced spend ceilings, use enterprise team or wallet budgets (see Budgets).
Error Handling
HTTP status: 402 Payment Required (new 402, not 200)
The gateway could not verify the signature. Common causes: wrong pay_to address, wrong amount, or a malformed payment-signature header (the header itself must be base64-encoded JSON, and the inner payload field must be the base64-encoded signed Solana versioned transaction). Parse the new 402 response and re-sign with the updated parameters.
HTTP status: 400 Bad Request with error.type = "bad_request" and message "payment amount insufficient: paid {N} but cost is {M} atomic USDC".
The signed amount is less than the required amount in the accepts object. This can happen if prices change between your first 402 and your retry. Resend the request without payment-signature to fetch a fresh quote, then re-sign for the new amount and retry.
HTTP status: 402 Payment Required (new 402)
The signed transaction's validity window has elapsed. Solana transactions include a recent blockhash that expires after roughly 150 slots (~60 seconds). Re-sign with a fresh blockhash. SDKs handle this automatically on retry.
HTTP status: 402 Payment Required (new 402)
The gateway has already accepted this signature. Each signed transaction can only be redeemed once. Sign a new transaction with a fresh nonce.
The gateway does not pre-check your wallet balance — it submits the signed transaction and surfaces the on-chain failure. If your wallet is short of USDC-SPL (or short of SOL for the signature fee), the transfer fails at the cluster, the gateway's facilitator reports settlement failure, and you receive a generic 402 invalid_payment with message: "Payment verification failed. Check your transaction and retry.". Top up the wallet and re-sign with a fresh blockhash.
Warning
Never retry a failed payment by resubmitting the same PAYMENT-SIGNATURE value. If a signature was accepted, retrying will be rejected as a replay. If it was rejected, the underlying issue must be fixed before re-signing.