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 wraps the PaymentRequired object inside an OpenAI-style error.message field as a JSON string. Parse error.message as JSON to access the fields.

{
  "error": {
    "type": "invalid_payment",
    "message": "{\"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\"}"
  }
}

Once you JSON.parse(error.message), the inner PaymentRequired looks like:

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

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 ships an UnimplementedSigner stub; implementers typically reach for gagliardetto/solana-go (or any Solana RPC client) to build and sign the transfer themselves — 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 scheme, network, and a payload field with the base58-encoded signed transaction bytes. The outer header value is base64-encoded JSON; the inner signed transaction is base58. See x402 Protocol for the exact shape.

Receive the LLM response

If the signature is valid and the nonce is fresh, the gateway broadcasts the transaction to Solana, waits for confirmation, then proxies the request to the upstream LLM and streams the response back to you in standard OpenAI format.

{
  "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).

FieldTypeDescription
provider_coststringWhat the upstream LLM charges for this request
platform_feestring5% of provider_cost, retained by Solvela
totalstringprovider_cost + platform_fee
currencystringAlways "USDC" today
fee_percentnumberPlatform fee percentage (currently 5.0)

Escrow vs Exact Payments

The accepts array may contain one or both payment schemes. Choose based on your trust model.


Session Budgets

All official SDKs support a sessionBudget option that caps total spending for the lifetime of a client instance. Once the budget is reached, the SDK throws rather than signing another payment.

Warning

Important: sessionBudget is a client-side guard only and does not communicate a limit to the server. For server-enforced limits, use enterprise team budgets. The SDK accumulates cost_breakdown.total values across all signed payments and stops signing once the sum exceeds the budget.


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 base58 encoding in the header. Parse the new 402 response and re-sign with the updated parameters.

HTTP status: 402 Payment Required (new 402)

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. The new 402 will contain the current required amount — re-sign 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.

HTTP status: 402 Payment Required (new 402, with an additional error field)

The gateway detected that your wallet does not hold enough USDC-SPL to cover the payment. Top up your wallet and retry. The 402 payload will include "error": "insufficient_balance" to distinguish this from other 402 reasons.

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.