SDKs

Python SDK

Async-first Python client for Solvela with built-in Solana signing and transparent x402 payments

solvela-sdk is the official Python SDK for the Solvela gateway. It's async-only, dataclass-typed, and handles the x402 payment handshake transparently when a Signer is configured.

Installation

pip install solvela-sdk

Solana wallet, keypair signing, and RPC integration are built in — no extras required.

Quick start

Every example here is async. Wrap with asyncio.run or call from inside an async function.

import asyncio
from solvela import SolvelaClient, ClientConfig

async def main():
    client = SolvelaClient(config=ClientConfig(gateway_url="http://localhost:8402"))

    models = await client.models()
    for m in models[:3]:
        print(f"{m.id}{m.display_name}  (${m.input_usdc_per_million}/M in)")

asyncio.run(main())

Without a Signer, paid endpoints raise PaymentRequiredError on the first call so the agent can react.

Configuration

ClientConfig is a dataclass; ClientBuilder is a fluent equivalent. Both are exported from solvela.

Security defaults

  • Non-loopback gateway_url or rpc_url must use https://. Plain http:// to a remote host raises ClientError at construction.
  • Unknown payment schemes from the gateway raise ClientError rather than silently mis-branching.
  • Malformed amount strings in the 402 body raise ClientError, not bare ValueError.

The chat flow

SolvelaClient.chat() runs the default path as a single request. Opt-in flags add steps: balance guard → session lookup → cache check → request → quality check → cache store → session record.

ChatResponse is OpenAI-compatible — response.id, response.choices[i].message, response.usage.

With a signer wired up, the SDK automatically:

  1. Sends the request.
  2. Receives 402 with the price quote.
  3. Picks a compatible scheme (exact or escrow, respecting prefer_escrow).
  4. Calls signer.sign_payment(...) to build and sign the SPL transfer.
  5. Retries with the Payment-Signature header.

If the gateway returns 402 again after the signed retry, the SDK raises PaymentRejectedError carrying the second PaymentRequired body so the caller can inspect why it was rejected.

Streaming

async for chunk in client.chat_stream(request):
    print(chunk.choices[0].delta.content or "", end="", flush=True)
print()

chat_stream runs the same preflight payment handshake as chat(). A post-signing 402 on the streaming POST is converted to PaymentRejectedError so streaming callers can distinguish "needs signing" from "signed and rejected."

Models and pricing

models = await client.models()  # list[ModelInfo]

for m in models:
    print(
        f"{m.id:<35} "
        f"in={m.input_usdc_per_million:>5.2f}  "
        f"out={m.output_usdc_per_million:>5.2f}  "
        f"ctx={m.context_window:>7}  "
        f"tools={m.supports_tools}  vision={m.supports_vision}  reasoning={m.reasoning}"
    )

ModelInfo mirrors the gateway's /v1/models shape — capabilities and pricing are flattened off the nested objects the gateway emits.

FieldTypeDescription
idstrCanonical model identifier (e.g. "openai/gpt-4o")
providerstrUpstream provider ("openai", "anthropic", etc.)
display_namestrHuman-readable label
context_windowintMaximum input tokens
supports_streaming / supports_tools / supports_vision / reasoningboolCapability flags
input_usdc_per_million / output_usdc_per_millionfloatProvider rate in USDC per million tokens (human units, not atomic)
currency / fee_percentstr / intPlatform fee terms

Cost estimation

quote = await client.estimate_cost("openai/gpt-4o")
print(f"Estimated total: {quote.cost_breakdown.total} {quote.cost_breakdown.currency}")
print(f"Provider: {quote.cost_breakdown.provider_cost}, fee: {quote.cost_breakdown.platform_fee}")

estimate_cost triggers a 402 against the model and returns the parsed PaymentRequired without signing.

Error hierarchy

All SDK errors inherit from ClientError. Catch the base for "anything the SDK threw," catch a subclass for specific recovery.

from solvela import (
    ClientError,
    PaymentRequiredError,
    PaymentRejectedError,
    AmountExceedsMaxError,
    RecipientMismatchError,
    InsufficientBalanceError,
    GatewayError,
    SignerError,
    WalletError,
    TimeoutError as SolvelaTimeoutError,
)

try:
    response = await client.chat(request)
except PaymentRequiredError as exc:
    pr = exc.payment_required
    print(f"Payment needed: {pr.cost_breakdown.total} {pr.cost_breakdown.currency}")
except PaymentRejectedError as exc:
    print(f"Rejected: {exc.reason}; body: {exc.payment_required}")
except AmountExceedsMaxError as exc:
    print(f"Quote {exc.amount} exceeds local cap {exc.max_amount}")
except GatewayError as exc:
    print(f"Gateway HTTP {exc.status}: {exc.message}")
except SolvelaTimeoutError as exc:
    print(f"Timed out after {exc.timeout_secs}s")
ErrorRaised when
PaymentRequiredErrorGateway returns 402 and no Signer is configured. Carries payment_required.
PaymentRejectedErrorGateway returns 402 again after a signed retry. Carries reason and (optional) payment_required.
AmountExceedsMaxErrorGateway's quoted amount exceeds the local max_payment_amount cap. Carries amount and max_amount as AtomicUsdc.
RecipientMismatchErrorGateway's pay_to doesn't match the configured expected_recipient.
InsufficientBalanceErrorWallet balance is too low for the quoted amount. Carries have / need as AtomicUsdc.
GatewayErrorNon-200/402 HTTP response. Carries status and message.
SignerErrorThe configured Signer failed to build or sign the payment transaction.
WalletErrorWallet operation failed (e.g. mnemonic parse, keypair derivation).
TimeoutErrorHTTP timeout exceeded.
ClientErrorBase — also raised directly for wire-format violations (unknown scheme, malformed amount).

Balance monitoring

For long-running agents, BalanceMonitor polls USDC balance in the background and wires transitions back into the client's balance guard.

from solvela import BalanceMonitor

monitor = BalanceMonitor(
    fetch_balance=client.usdc_balance,
    poll_interval=30.0,
    low_balance_threshold=0.50,             # USDC
    on_low_balance=lambda b: print(f"Low: ${b:.4f}"),
    on_balance_change=client.balance_state_setter(),
)
monitor.start()
# ... agent loop ...
monitor.stop()

When the polled balance hits 0.0 and ClientConfig.free_fallback_model is set, the chat balance guard swaps the requested model for the free fallback automatically. On RPC failure, on_balance_change(None) clears the cached balance to "unknown" so a stale 0.0 doesn't lock callers into the fallback during an outage.

OpenAI-compatible interface

For code that already targets the OpenAI Python SDK, OpenAICompat provides a drop-in shim.

from solvela import SolvelaClient
from solvela.openai_compat import OpenAICompat

client = SolvelaClient()
openai = OpenAICompat(client)

response = await openai.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello!"}],
    max_tokens=100,
)
print(response.choices[0].message.content)

openai.chat.completions.create(stream=True, ...) returns the same AsyncIterator[ChatChunk] as SolvelaClient.chat_stream().

Custom signers

The default KeypairSigner builds and signs Solana SPL transfers. Implement the Signer ABC to plug in HSM-backed signing, multi-sig flows, or a custodial backend.

from solvela import Signer
from solvela.types import AtomicUsdc, PaymentAccept, PaymentPayload, Resource

class MyCustomSigner(Signer):
    async def sign_payment(
        self,
        amount_atomic: AtomicUsdc,
        recipient: str,
        resource: Resource,
        accepted: PaymentAccept,
    ) -> PaymentPayload:
        # Build, sign, and return a PaymentPayload your way.
        ...

AtomicUsdc is a type-only NewType over int; pass AtomicUsdc(123) at the boundary to make mypy track the unit. Wire amounts (accept.amount) are decimal strings in atomic USDC (1 USDC = 1,000,000).

Cancellation and timeouts

ClientConfig.timeout (default 180s) caps every HTTP call. Cancel an in-flight chat() by cancelling the surrounding asyncio task — httpx propagates cancellation through cleanly.

Note

Source: sdks/python/ in the monorepo. The canonical chat-flow entry point is src/solvela/client.py; a 60-line end-to-end smoke test lives at scripts/smoke.py. See also x402 Protocol for the underlying payment flow.