Trader integration guide
Mint signing JWTs against an active lease and route transactions through the TEE-signed QUIC path with the TypeScript SDK, the Rust crates, or raw HTTP.
This guide is API-first: every snippet below runs against the real marketplace API and the real SDK surfaces. If you have not booked a lease yet, start with the booking guide.
Prerequisites
-
An active lease. Book in the dashboard at
/book(or via the API, see the raw HTTP walkthrough below) and pay the Solana Pay request within 15 minutes. -
Your per-lease credentials. There is no account-level API token. After the lease activates, a one-shot
POST /v1/leases/{id}/credentialsreveals three values exactly once:Field What it is tee_endpoint_urlThe validator's TEE signing endpoint your client connects to api_tokenThe Bearer token for all marketplace API calls from your bot, scoped to this lease, shown exactly once tls_ca_certCA PEM for pinning the TEE endpoint's TLS certificate The dashboard lease page runs this reveal for you and offers a "Copy as .env" button that emits the names used throughout this page:
MARKETPLACE_LEASE_ID=01JD... MARKETPLACE_TEE_URL=https://tee.validator.example MARKETPLACE_API_TOKEN=01JD... -
Base URL.
https://api.swqos.devin production,http://localhost:8080in local dev.
The credentials reveal is one-shot. A second call returns
404 credentials-already-shown. If you lose the token, rotate with
POST /v1/leases/{id}/api-keys: it revokes the old token immediately
and returns a fresh one (a concurrent rotation race returns
409 concurrent-rotation, just retry). See
Authentication.
In the current release the tls_ca_cert field returns a placeholder
PEM. The Rust SDK requires the real host-proxy CA certificate (the
cert.pem printed by the validator's deploy), so obtain it from your
validator out of band until the reveal carries the real CA.
Mint a signing JWT
The TEE never sees your marketplace credentials. It accepts a short-lived ES256 JWT that the marketplace mints against your active lease:
curl -s -X POST "https://api.swqos.dev/v1/leases/$MARKETPLACE_LEASE_ID/token" \
-H "Authorization: Bearer $MARKETPLACE_API_TOKEN"
{
"data": {
"jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9...",
"expires_at": 1781430000
}
}
What you need to know about this token:
- ES256 (ECDSA P-256), signed by the marketplace through AWS KMS.
- TTL is 600 seconds (10 minutes).
expires_atis UNIX seconds and mirrors theexpclaim. - Refresh at
expires_at - 60s, not on every request. Both shipped SDKs implement exactly this schedule for you. - Rate limit: 12 mints per minute per API key. Exceeding it returns
429 rate-limit-exceededwith aRetry-Afterheader. A refresh-based loop never gets near this limit. - The
audclaim is your lease'stee_endpoint_url, so a JWT works against exactly one TEE. Other claims includelease_id,val_pk(the validator identity),exp/nbf/jti, andquota. - Shared leases get a reduced handshake quota. The
quota.max_handshakes_per_minclaim is the full budget (default 100/min) on an exclusive lease, andmax(1, floor(budget / seats))on a shared lease. Example: 100/min budget with 4 seats stamps 25/min per co-tenant. The host-proxy enforces it per lease.
Errors on this endpoint: 403 lease-not-active (payment not confirmed
yet, or the lease ended), 403 lease-epoch-out-of-range (the current
Solana epoch is outside your [epoch_start, epoch_end] window), and
404 lease-not-found (no such lease owned by the caller).
Connect with the TypeScript SDK
The real package is @swqos/staked-quic-sdk (v0.1.0). It is not on npm
yet, so install it with the file: protocol from a checkout of the
open-source provider repo (staked-quic-connection-provider):
# Consumer project side-by-side with the provider repo checkout
pnpm add file:../staked-quic-connection-provider/packages/typescript/staked-quic-sdk
Requirements: Node.js >= 20, ESM only. The package ships three clients.
MarketplaceClient mints JWTs, TpuClient is the low-level gRPC client
for the host-proxy signer service, and LeaderClient is the full
one-object path (mint JWT, QUIC handshake to the current leader,
sendTransaction), backed by a native binding to the Rust TPU client
that is prebuilt for darwin-arm64 only in v0.1.0. Full surface in the
TypeScript SDK reference.
import { MarketplaceClient, TpuClient } from "@swqos/staked-quic-sdk";
const marketplace = new MarketplaceClient({
apiBase: "https://api.swqos.dev",
apiKey: process.env.MARKETPLACE_API_TOKEN!,
leaseId: process.env.MARKETPLACE_LEASE_ID!,
});
// One-shot mint, then keep a fresh JWT in the background (refreshes at exp - 60s,
// retries 5xx/network errors with 200/400/800ms backoff).
const { jwt, expiresAt } = await marketplace.fetchJwt();
marketplace.startAutoRefresh();
// Low-level signing-oracle client against the host-proxy.
// endpoint is plain host:port, no http:// or https:// prefix.
const tpu = new TpuClient({
endpoint: "tee.validator.example:443",
tls: { rootCerts: caPem }, // omit only in local dev (insecure channel)
});
const health = await tpu.health(); // { provisioned, pubkeySha256 }
const { certificate, publicKey } = await tpu.getCertificate(); // 249-byte cert + Ed25519 key
// transcriptHash is the 32-byte SHA-256 of your TLS transcript. The SDK wraps it
// in the 130-byte CertificateVerify payload and returns the 64-byte signature.
const signature = await tpu.signCertificateVerify(transcriptHash, jwt);
tpu.close();
marketplace.stopAutoRefresh();
Honest v1 scope: the pure-TypeScript TpuClient does not run a QUIC
stack in Node. It drives the host-proxy's gRPC signer service and the
host-proxy does the QUIC forwarding. Use LeaderClient when you want
the SDK to handle the whole path in one object. Its failures come back
tagged for deterministic branching: [MINT_JWT_FAILED],
[TEE_CONNECT_FAILED], [TPU_INIT_FAILED], [SEND_TX_FAILED],
[TX_DESERIALIZE_FAILED], [CLIENT_CLOSED].
Connect with Rust
The Rust crates live in the open-source sibling repo. Add them as Git dependencies:
cargo add staked-quic-client-sdk \
--git ssh://git@github.com/nodexpert-labs/staked-quic-connection-provider.git \
--branch master
cargo add staked-quic-tpu-client \
--git ssh://git@github.com/nodexpert-labs/staked-quic-connection-provider.git \
--branch master
staked-quic-client-sdk exposes StakedQuicSignerClient with exactly
three RPCs: get_certificate(), sign_certificate_verify(payload), and
health(). There is no Rust marketplace REST client, so book leases and
mint JWTs with plain HTTP (the raw HTTP walkthrough below maps 1:1 to
reqwest calls). staked-quic-tpu-client runs its own QUIC client and
presents the TEE-signed CertificateVerify directly to the current
leader:
use staked_quic_tpu_client::{CertMode, StakedQuicSignerClient, TpuClient, TpuConfig};
// 1. Connect to the TEE signing oracle. The endpoint must be https://
// (the SDK rejects cleartext) and the CA PEM is pinned.
let ca_pem = std::fs::read("tee-ca.pem")?;
let sdk = StakedQuicSignerClient::connect(&tee_endpoint, &lease_jwt, &ca_pem).await?;
// 2. QUIC TPU client that performs the handshake under the validator's stake.
let client = TpuClient::new(&rpc_url, CertMode::Staked(sdk), TpuConfig::default()).await?;
println!("staked identity from TEE: {}", hex::encode(client.cert_pubkey()));
// 3. Build and sign the transaction with YOUR trader wallet, then send.
let signature = client.send_transaction(&tx).await?;
The sibling repo ships a runnable end-to-end example at
staked-quic-tpu-client/examples/send_one_staked.rs that sends one memo
transaction over the staked lane:
# In a checkout of staked-quic-connection-provider
cargo run -p staked-quic-tpu-client --example send_one_staked -- \
--keypair trader.json \
--tee-endpoint "$MARKETPLACE_TEE_URL" \
--tee-token "$LEASE_JWT" \
--tee-ca-pem tee-ca.pem \
--rpc-url https://api.testnet.solana.com
--keypair is the trader wallet that signs and pays the transaction.
The validator identity never leaves the TEE. --tee-token takes the
JWT you minted above, not the marketplace API token. Full surface in
the Rust SDK reference.
Raw HTTP walkthrough (any language)
Everything above the QUIC layer is plain REST, so any language with an
HTTP client can book, pay, and mint. Responses are wrapped in a
{ "data": ..., "meta": ... } envelope.
Your very first lease has no API token yet, so book it in the dashboard
at /book under your signed-in session. Once you hold one per-lease
token, your bot can book follow-up leases with it:
API=https://api.swqos.dev # http://localhost:8080 in local dev
AUTH="Authorization: Bearer $MARKETPLACE_API_TOKEN"
# 1. Book. Idempotency-Key is REQUIRED and must be a 26-char ULID,
# unique per logical request. Reuse the same key on retries.
curl -s -X POST "$API/v1/leases" \
-H "$AUTH" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01J8ZX5N4S0000000000000000" \
-d '{
"validator_id": "<validator ULID from GET /v1/validators>",
"epoch_start": 820,
"epoch_count": 5,
"mode": "exclusive"
}'
{
"data": {
"lease_id": "01JD...",
"payment": {
"solana_pay_url": "solana:https%3A%2F%2Fapi.swqos.dev%2Fv1%2Fsolana-pay%2F01JD...",
"amount_lamports": 5000000000,
"memo": "01JD..."
}
}
}
mode defaults to exclusive when omitted. Pay by opening
solana_pay_url with a Solana Pay wallet within 15 minutes. The wallet
fetches the exact transaction the marketplace built (the atomic
validator/platform split plus the memo) and you sign it. Payment
details, epoch math, and the exclusive-vs-shared trade-off are in the
booking guide.
# 2. Poll until the settlement worker confirms the payment on-chain.
# pending_payment -> active, typically seconds after finality.
curl -s "$API/v1/leases/$LEASE_ID" -H "$AUTH"
# 3. One-shot credentials reveal (returns api_token exactly once).
curl -s -X POST "$API/v1/leases/$LEASE_ID/credentials" -H "$AUTH"
# 4. Mint a signing JWT (repeat at expires_at - 60s).
curl -s -X POST "$API/v1/leases/$LEASE_ID/token" -H "$AUTH"
While the lease is pending_payment, GET /v1/leases/{id} keeps
projecting the payment instructions, so the flow is resumable if your
process dies before paying.
Idempotency rules on POST /v1/leases: a missing key is
400 idempotency-key-missing, anything that is not a 26-character
ULID (including a UUID) is 400 idempotency-key-malformed, and
replaying the same key with a different body is a 422. Replays with
the same key and body return the cached response for 24 hours.
Error handling
All errors are RFC 7807 application/problem+json with a slug in the
type URL. The extensions object appears only when an error has
case-specific fields to add, such as conflicting_lease_id on
lease-slot-taken. Error responses carry no request_id (only the
meta of the success envelope does). The codes a trader actually hits:
| Status | Slug | When it happens | What to do |
|---|---|---|---|
| 409 | lease-slot-taken | Booking or extending an exclusive range that overlaps an existing lease, or a shared request blocked by an exclusive overlap. extensions.conflicting_lease_id names the blocker. | Pick another epoch window or validator. Do not retry the same request. |
| 409 | shared-capacity-full | Every shared seat is taken for at least one epoch in the requested range. | Pick another window, or book exclusive. |
| 422 | shared-not-offered | "mode": "shared" against a validator that does not offer shared leases. | Book exclusive, or pick a validator with shared_enabled: true in GET /v1/validators. |
| 403 | lease-not-active | Minting a JWT before payment confirms, or after the lease window ends. The credentials and extension endpoints return the same slug as a 422. | Poll GET /v1/leases/{id} until status is active. |
| 429 | rate-limit-exceeded | Over 12 token mints per minute per key, or over 10 bookings per minute. | Back off for Retry-After seconds and watch the X-RateLimit-* headers. |
The full catalogue, including the booking-time validation errors and the Solana Pay wallet errors, is in the error reference.
Next steps
LeaderClient, MarketplaceClient, and TpuClient in full, with error types and platform notes.
The staked-quic-client-sdk and staked-quic-tpu-client crates from the open-source sibling repo.
The staked-quic-sdk Python package: the same LeaderClient trio over PyO3.
Every lease endpoint: booking, extension, credentials, token, rating, auto-renew.