Python SDK
Reference for staked-quic-sdk v0.1.0: LeaderClient, MarketplaceClient, TpuClient, and the REST calls that stay outside the SDK.
What ships in v0.1.0
The first-party Python package is staked-quic-sdk v0.1.0 (module
staked_quic_sdk, MIT). It requires Python 3.11 or newer, and the
PyO3 extension is built with abi3-py311 so one wheel covers 3.11,
3.12, and 3.13. Runtime dependencies are httpx, pydantic v2,
grpcio, and protobuf.
Three clients ship today:
| Client | Style | What it does |
|---|---|---|
LeaderClient | async | Full v1 path: mints the lease JWT, handshakes with the TEE host-proxy, opens QUIC to the current Solana leader with the TEE-signed CertificateVerify, forwards your transaction. Backed by a PyO3 binding to the Rust staked-quic-tpu-client. |
MarketplaceClient | sync | JWT minting only, against POST /v1/leases/{id}/token, with retries and background auto-refresh. |
TpuClient | sync | Low-level gRPC client for the host-proxy's signer.v1.StakedQuicSigner service (Health, GetCertificate, SignCertificateVerify). |
Use LeaderClient unless you only need one piece (a JWT-only flow, a
custom QUIC stack, or a signing-oracle smoke test).
v0.1.0 is not published to PyPI, and the prebuilt native extension
covers only darwin-arm64 (Apple Silicon). Every install today is a
build from source with maturin, which needs a Rust toolchain. If
the native extension is missing for your platform,
import staked_quic_sdk raises
LeaderClientUnsupportedPlatformError with build instructions.
Install (from source)
git clone <repo>
cd packages/python/staked-quic-sdk
python -m venv .venv && source .venv/bin/activate
pip install maturin
maturin develop --release # builds the PyO3 extension + installs the package editable
Once a wheel is published, pip install staked-quic-sdk will be all
consumers need. That has not happened yet.
Prerequisites: where your credentials come from
You need an active lease that covers the current epoch. Book one via
the Book button on a validator card or detail page (it lands on
/book/{validatorId}) or via POST /v1/leases (see the REST section
below), then pay the Solana Pay request. Once the lease is active,
reveal the trader credentials exactly once with
POST /v1/leases/{id}/credentials. The response carries api_token
(cleartext, only returned here), tee_endpoint_url, and tls_ca_cert.
A second call returns 404 credentials-already-shown. Rotate a
lost key with POST /v1/leases/{id}/api-keys.
Today the backend returns a hardcoded placeholder string
(PLACEHOLDER_CA_CERT_PEM) in tls_ca_cert, not a real PEM. The
enclave-issued CA cert is not wired through the API yet. Passing
that placeholder as tee_tls_ca_pem fails the TLS trust-anchor
parse, so until the real cert lands you must obtain the host-proxy's
TLS CA PEM out of band (for example from your validator operator or
the local dev TEE rig).
Each LeaderClientOptions field maps to a concrete source:
| Field | Where it comes from |
|---|---|
marketplace_api_base | https://api.swqos.dev in production, http://localhost:8080 for local dev |
api_key | api_token from the one-shot credentials reveal (or key rotation) |
lease_id | returned by POST /v1/leases at booking, also shown in the lease list on the trader dashboard at /trader |
tee_endpoint | tee_endpoint_url from the credentials reveal, with scheme (https://...), plain host:port is rejected |
rpc_url | any Solana RPC endpoint, used for leader discovery |
tee_tls_ca_pem | the host-proxy's TLS CA cert PEM, pinned as the trust anchor. The credentials reveal's tls_ca_cert field will carry it once the real cert ships, today it is a placeholder (see the note above), so source the PEM out of band |
Quickstart: LeaderClient
LeaderClient is created via the async factory LeaderClient.create.
Construction performs three network calls in order: mint JWT against
the marketplace API, gRPC handshake with the host-proxy, RPC query for
cluster nodes. Plan a few hundred milliseconds for a cold start, then
send_transaction reuses the connection pool.
import asyncio
import os
from staked_quic_sdk import LeaderClient, LeaderClientOptions
async def main() -> None:
client = await LeaderClient.create(
LeaderClientOptions(
marketplace_api_base="https://api.swqos.dev",
api_key=os.environ["SWQOS_API_KEY"],
lease_id=os.environ["SWQOS_LEASE_ID"],
tee_endpoint="https://tee.example.com:443", # host-proxy, WITH scheme
rpc_url="https://api.testnet.solana.com",
tee_tls_ca_pem=open("tee-cert.pem").read(), # host-proxy CA PEM, sourced out of band today
)
)
try:
# 32-byte Ed25519 key the TEE presents in its X.509 cert.
# Compare it with the leased validator's identity before sending value.
print("staked identity:", client.cert_pubkey().hex())
# tx_bytes is the bincode wire form of a signed Solana transaction
# (what solders produces when you serialize a signed Transaction).
# signature = await client.send_transaction(tx_bytes) # -> base58 str
finally:
await client.close()
asyncio.run(main())
send_transaction(tx_bytes) returns the base58 transaction signature
on success. Sending is fire-and-forget at the QUIC layer, so confirm
the transaction yourself via your RPC. Always await client.close()
when done to free the QUIC and gRPC sockets (it is idempotent).
Failures surface as a single RuntimeError whose message starts with a
[REASON_CODE] tag so callers can branch deterministically:
| Reason code | Meaning |
|---|---|
[MINT_JWT_FAILED] | The JWT mint against the marketplace API failed (bad key, lease not active, epoch out of range) |
[TEE_CONNECT_FAILED] | The gRPC handshake with the host-proxy failed |
[TPU_INIT_FAILED] | TPU client construction failed (RPC unreachable, leader discovery) |
[SEND_TX_FAILED] | The QUIC send failed |
[TX_DESERIALIZE_FAILED] | tx_bytes is not a valid bincode-serialized transaction |
[CLIENT_CLOSED] | A method was called after close() |
LeaderClientOptions suppresses the auto-derived __repr__ so
api_key never leaks into logs or tracebacks. The custom repr prints
api_key='***'.
MarketplaceClient: JWT minting only
MarketplaceClient mints the short-lived ES256 lease JWT against
POST /v1/leases/{lease_id}/token, sending Authorization: Bearer <api_key>. The JWT's default TTL is 600 seconds. fetch_jwt() makes
up to 4 attempts with 200 / 400 / 800 ms backoff on transport errors
and 5xx responses, and returns a JwtBundle with jwt (compact ES256
JWT) and expires_at (UNIX seconds, a copy of the exp claim).
import os
from staked_quic_sdk import (
MarketplaceAuthError,
MarketplaceClient,
MarketplaceForbiddenError,
MarketplaceNetworkError,
MarketplaceServerError,
)
client = MarketplaceClient(
api_base="https://api.swqos.dev",
api_key=os.environ["SWQOS_API_KEY"],
lease_id=os.environ["SWQOS_LEASE_ID"],
)
try:
bundle = client.fetch_jwt()
print(bundle.jwt, bundle.expires_at)
except MarketplaceAuthError:
# 401: API key is missing, invalid, or revoked.
raise
except MarketplaceForbiddenError:
# 403: key is valid but cannot mint for this lease
# (lease-not-active, lease-epoch-out-of-range).
raise
except (MarketplaceServerError, MarketplaceNetworkError):
# Persistent 5xx or transport failure after retries.
raise
To keep a fresh JWT in the background, use the auto-refresher. It
re-mints 60 seconds before exp (Design Doc D7) and swallows transient
refresh failures so the timer survives outages:
client.start_auto_refresh()
try:
bundle = client.get_cached_jwt() # latest JwtBundle, or None before first fetch
# ... pass bundle.jwt as the Bearer JWT to the host-proxy ...
finally:
client.close() # cancels the refresher and closes the httpx client
MarketplaceClient is also a context manager:
with MarketplaceClient(...) as client: closes cleanly on exit.
The token endpoint is rate limited at 12 requests per minute per API
key. Do not poll fetch_jwt() in a loop, use start_auto_refresh()
and read get_cached_jwt().
TpuClient: the signing oracle only
TpuClient talks gRPC to the host-proxy directly. The endpoint is a
plain host:port with no scheme. Without a tls argument it opens an
insecure channel (local dev only). In production, pass
TlsConfig(root_certs=...) with the host-proxy's cert PEM. The
per-call deadline defaults to 5 seconds (deadline_s).
import hashlib
from staked_quic_sdk import (
MarketplaceClient,
TlsConfig,
TpuClient,
TpuRpcError,
)
marketplace = MarketplaceClient(api_base=..., api_key=..., lease_id=...)
with open("tee-cert.pem", "rb") as f:
root_certs = f.read()
with TpuClient(endpoint="tee.example.com:443", tls=TlsConfig(root_certs=root_certs)) as tpu:
try:
bundle = marketplace.fetch_jwt()
health = tpu.health() # HealthStatus(provisioned, pubkey_sha256)
assert health.provisioned, "enclave not provisioned yet"
cert = tpu.get_certificate() # 249-byte Firedancer mock X.509 + 32-byte pubkey
transcript_hash = hashlib.sha256(b"hello swqos").digest() # 32 bytes
signature = tpu.sign_certificate_verify(transcript_hash, bundle.jwt)
assert len(signature) == 64 # Ed25519
except TpuRpcError as err:
# err.code is a grpc.StatusCode: UNAUTHENTICATED, RESOURCE_EXHAUSTED,
# UNAVAILABLE, ...
raise
Details worth knowing:
sign_certificate_verify(transcript_hash, jwt)builds the 130-byte TLS 1.3 CertificateVerify payload around the 32-byte SHA-256 transcript hash and forwards the JWT in the gRPCauthorizationmetadata header. Layout: 64 bytes of0x20, the ASCII label"TLS 1.3, client CertificateVerify\0", then the hash. The helperbuild_certificate_verify_payload(transcript_hash)is exported if you need the payload yourself.get_certificate()andhealth()are unauthenticated, no JWT required.- Typed errors:
TpuInvalidPayloadError(transcript hash not 32 bytes),TpuRpcError(non-OK gRPC status, carries.codeand.details),TpuInvalidSignatureError(response signature not 64 bytes). - Instances hold an open gRPC channel. Call
close()or use the context manager. Sharing one instance across threads is safe.
Runnable example
examples/hello_world.py in the package exercises both paths
(LeaderClient, then MarketplaceClient + TpuClient). It reads:
| Env var | Example |
|---|---|
SWQOS_API_BASE | http://localhost:8080 |
SWQOS_API_KEY | trader API key |
SWQOS_LEASE_ID | ULID of an active lease |
SWQOS_HOST_PROXY | localhost:50051 (TpuClient wants no scheme, LeaderClient wants it with scheme) |
SWQOS_RPC_URL | https://api.testnet.solana.com |
SWQOS_TEE_TLS_CA_PEM | PEM of the TEE's TLS cert (the LeaderClient path) |
python examples/hello_world.py
What stays outside the SDK: the REST surface
Browsing validators, booking, paying, and extending leases are plain
REST calls, not SDK methods. Use httpx (already a dependency)
directly. Two things trip people up: the list envelope is nested
(data.data), and the Idempotency-Key header must be a ULID. A
UUID4 is rejected with 400.
import os
import httpx
from ulid import ULID # pip install python-ulid
API_BASE = os.environ.get("SWQOS_API_BASE", "https://api.swqos.dev")
# 1. Browse validators (public, no auth).
r = httpx.get(f"{API_BASE}/v1/validators", params={"limit": 20})
r.raise_for_status()
page = r.json()["data"] # {"data": [...], "next_cursor": ...}
for v in page["data"]:
print(v["validator_id"], v["display_name"], v["uptime_30d_pct"])
# 2. Book a lease (trader auth + ULID idempotency key).
headers = {
"Authorization": f"Bearer {os.environ['SWQOS_API_KEY']}",
"Idempotency-Key": str(ULID()), # must parse as a ULID, UUIDs get a 400
}
body = {
"validator_id": "01HXYVAL00000000000000000A",
"epoch_start": 901,
"epoch_count": 3, # 1 to 30
"mode": "exclusive", # optional: "exclusive" (default) or "shared"
}
r = httpx.post(f"{API_BASE}/v1/leases", json=body, headers=headers)
r.raise_for_status()
lease = r.json()["data"]
print(lease["lease_id"], lease["payment"]["solana_pay_url"])
The booking response carries payment with solana_pay_url,
amount_lamports, and memo. Pay within the 15-minute
pending-payment window or the slot is released. The full booking and
payment flow, including filters like region, price_min_lamports,
price_max_lamports, and available_now on the validator list, is
covered in Booking a lease and the
API reference. Error bodies are RFC 7807
Problem Details, see Errors.
If you want generated models, the OpenAPI spec is committed at
docs/openapi.json in the repo and served live at
GET /v1/openapi.json.
Development
From packages/python/staked-quic-sdk:
python3.11 -m venv .venv && source .venv/bin/activate
pip install maturin
pip install ".[dev]" # BEFORE maturin develop, or the editable install is replaced
maturin develop --release # rebuild after any change to crate/
pytest # 23 tests: marketplace + tpu + leader-client
ruff check src tests examples
cd crate && cargo clippy --all-targets -- -D warnings
The pytest suite covers error paths only. The canonical success-path
test lives at sdk-manual-test/rust/ in the repo, driving the
upstream staked-quic-tpu-client against the docker-compose dev
stack. The gRPC stubs under src/staked_quic_sdk/_proto/ are
pre-generated from proto/staked_quic.proto, regenerate them with
scripts/generate_proto.py when the sibling proto changes.