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.
- Package:
solvela-sdkon PyPI - Source:
sdks/python/in the monorepo - Python: 3.10+
Installation
pip install solvela-sdkSolana 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_urlorrpc_urlmust usehttps://. Plainhttp://to a remote host raisesClientErrorat construction. - Unknown payment schemes from the gateway raise
ClientErrorrather than silently mis-branching. - Malformed amount strings in the 402 body raise
ClientError, not bareValueError.
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:
- Sends the request.
- Receives 402 with the price quote.
- Picks a compatible scheme (
exactorescrow, respectingprefer_escrow). - Calls
signer.sign_payment(...)to build and sign the SPL transfer. - Retries with the
Payment-Signatureheader.
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.
| Field | Type | Description |
|---|---|---|
id | str | Canonical model identifier (e.g. "openai/gpt-4o") |
provider | str | Upstream provider ("openai", "anthropic", etc.) |
display_name | str | Human-readable label |
context_window | int | Maximum input tokens |
supports_streaming / supports_tools / supports_vision / reasoning | bool | Capability flags |
input_usdc_per_million / output_usdc_per_million | float | Provider rate in USDC per million tokens (human units, not atomic) |
currency / fee_percent | str / int | Platform 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")| Error | Raised when |
|---|---|
PaymentRequiredError | Gateway returns 402 and no Signer is configured. Carries payment_required. |
PaymentRejectedError | Gateway returns 402 again after a signed retry. Carries reason and (optional) payment_required. |
AmountExceedsMaxError | Gateway's quoted amount exceeds the local max_payment_amount cap. Carries amount and max_amount as AtomicUsdc. |
RecipientMismatchError | Gateway's pay_to doesn't match the configured expected_recipient. |
InsufficientBalanceError | Wallet balance is too low for the quoted amount. Carries have / need as AtomicUsdc. |
GatewayError | Non-200/402 HTTP response. Carries status and message. |
SignerError | The configured Signer failed to build or sign the payment transaction. |
WalletError | Wallet operation failed (e.g. mnemonic parse, keypair derivation). |
TimeoutError | HTTP timeout exceeded. |
ClientError | Base — 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.