Search docs

Find a documentation page

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}/credentials reveals three values exactly once:

    FieldWhat 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.dev in production, http://localhost:8080 in 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_at is UNIX seconds and mirrors the exp claim.
  • 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-exceeded with a Retry-After header. A refresh-based loop never gets near this limit.
  • The aud claim is your lease's tee_endpoint_url, so a JWT works against exactly one TEE. Other claims include lease_id, val_pk (the validator identity), exp / nbf / jti, and quota.
  • Shared leases get a reduced handshake quota. The quota.max_handshakes_per_min claim is the full budget (default 100/min) on an exclusive lease, and max(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:

StatusSlugWhen it happensWhat to do
409lease-slot-takenBooking 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.
409shared-capacity-fullEvery shared seat is taken for at least one epoch in the requested range.Pick another window, or book exclusive.
422shared-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.
403lease-not-activeMinting 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.
429rate-limit-exceededOver 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