Search docs

Find a documentation page

TypeScript SDK

Reference for @swqos/staked-quic-sdk: LeaderClient, MarketplaceClient, and TpuClient for Node.js traders.

What ships

@swqos/staked-quic-sdk v0.1.0 (MIT, ESM-only, Node.js >= 20) is the trader-facing TypeScript SDK for the 2QUIC Marketplace. It contains three clients:

ClientRoleNative code?
LeaderClientRecommended. Full v1 path: mint JWT, TEE gRPC handshake, QUIC to the current Solana leader with the TEE-signed CertificateVerify, forward the transaction.Yes (napi-rs binding to the Rust staked-quic-tpu-client)
MarketplaceClientJWT minting only, against POST /v1/leases/{id}/token, with retry and background auto-refresh.No (pure TS, native fetch)
TpuClientLow-level gRPC client for the host-proxy signing oracle (signer.v1.StakedQuicSigner: Health, GetCertificate, SignCertificateVerify).No (pure TS, @grpc/grpc-js)

Runtime dependencies are @grpc/grpc-js, @grpc/proto-loader, and zod. The gRPC .proto schema ships inside the package at proto/staked_quic.proto and is loaded dynamically at startup, so there is no codegen step at install time.

Install

v0.1.0 is not published to npm. Consume it via the file: protocol from a local checkout of the open-source provider repo (staked-quic-connection-provider, which carries the SDKs under packages/):

# from your consumer project, with the provider repo checked out side-by-side
pnpm add file:../staked-quic-connection-provider/packages/typescript/staked-quic-sdk
# or with an absolute path
pnpm add file:/abs/path/to/staked-quic-connection-provider/packages/typescript/staked-quic-sdk

npm install file:... and yarn add file:... work identically.

The prebuilt native binary for LeaderClient covers only darwin-arm64 (Apple Silicon) in v0.1.0. On any other platform, clone the repo, install a recent Rust toolchain, and run pnpm build:native inside packages/typescript/staked-quic-sdk. MarketplaceClient and TpuClient are pure TypeScript and run everywhere Node 20+ runs. Importing the package never touches the native binary: an unsupported platform only fails at LeaderClient.create(), with a LeaderClientUnsupportedPlatformError.

Prerequisites

Every client needs an active lease and a trader credential:

  1. Book and pay for a lease covering the current epoch (see Booking and the leases API).
  2. Reveal the trader credentials once via POST /v1/leases/{id}/credentials. The response carries api_token (the bearer key, a bare 26-character ULID with no prefix, shown exactly once), tee_endpoint_url (the validator's host-proxy), and tls_ca_cert (PEM). Rotate the key later with POST /v1/leases/{id}/api-keys. Details in Authentication.
  3. For LeaderClient, also have a Solana RPC URL at hand (used for leader discovery).

LeaderClient (recommended)

LeaderClient is the equivalent of the Rust SDK's TpuClient. The async factory LeaderClient.create(opts) performs two network calls in order: mint the JWT against the marketplace API, then the gRPC handshake with the host-proxy (GetCertificate). The Solana RPC is not contacted at create time: leader discovery runs inside each sendTransaction. Plan a few hundred milliseconds for a cold start. Subsequent sendTransaction calls reuse the QUIC connection pool.

import { LeaderClient } from "@swqos/staked-quic-sdk";

const client = await LeaderClient.create({
  marketplaceApiBase: "https://api.swqos.dev", // http://localhost:8080 in dev
  apiKey: process.env.SWQOS_API_KEY!,          // trader api_token (a bare ULID)
  leaseId: "01HXY00000000000000000LEAS",       // active lease ULID
  teeEndpoint: "http://localhost:50051",       // host-proxy, WITH scheme
  rpcUrl: "https://api.testnet.solana.com",    // leader discovery
});

// 32-byte Ed25519 pubkey the TEE presents in its X.509 cert.
// Assert it matches the validator you leased before sending value.
console.log("staked identity:", client.certPubkey().toString("hex"));

// txBytes = bincode wire form: web3.js `Transaction.serialize()` or
// Rust `bincode::serialize(&tx)`.
const signature = await client.sendTransaction(txBytes); // base58 string

await client.close();

Options:

FieldMeaning
marketplaceApiBaseMarketplace API base URL. The client POSTs {base}/v1/leases/{leaseId}/token to mint the JWT.
apiKeyTrader API token, sent as Authorization: Bearer on the mint call.
leaseIdLease public id (ULID). Must be active and cover the current epoch.
teeEndpointHost-proxy gRPC endpoint with scheme (http:// or https://). Plain host:port is rejected.
rpcUrlSolana RPC used by the underlying TPU client for leader discovery.

Behavior notes:

  • sendTransaction is send-only. The returned base58 signature means the bytes were handed to upcoming leaders, not that the transaction is confirmed. Poll signature status against your own RPC.
  • After close(), further sendTransaction calls fail with [CLIENT_CLOSED]. close() is idempotent.
  • Errors surface as a single rejection whose message starts with a [REASON_CODE] tag so callers can branch deterministically:
Reason codeFailing step
[MINT_JWT_FAILED]JWT mint against the marketplace API
[TEE_CONNECT_FAILED]gRPC handshake with the host-proxy
[TPU_INIT_FAILED]Building the underlying TPU client: TEE GetCertificate or QUIC endpoint setup (not RPC reachability)
[TX_DESERIALIZE_FAILED]txBytes is not a valid bincode transaction
[SEND_TX_FAILED]Leader discovery against the RPC failed (e.g. unreachable RPC), or all QUIC sends to upcoming leaders failed
[CLIENT_CLOSED]Method called after close()

MarketplaceClient (JWT only)

Use this when you only need the short-lived ES256 lease JWT, for example to feed a custom QUIC stack or the low-level TpuClient.

import { MarketplaceClient } from "@swqos/staked-quic-sdk";

const marketplace = new MarketplaceClient({
  apiBase: "https://api.swqos.dev",
  apiKey: process.env.SWQOS_API_KEY!,
  leaseId: "01HXY00000000000000000LEAS",
});

const { jwt, expiresAt } = await marketplace.fetchJwt();
// expiresAt = UNIX seconds, copy of the JWT `exp` claim

Options: apiBase, apiKey, leaseId (all required), plus optional fetch (override the global fetch, useful in tests) and refreshSkewSecs (auto-refresh skew override, default 60, leave it alone in production).

fetchJwt retry behavior

fetchJwt() POSTs {apiBase}/v1/leases/{leaseId}/token with Authorization: Bearer <apiKey> and makes up to 4 attempts (1 initial

  • 3 retries) with exponential backoff of 200ms / 400ms / 800ms. Only network errors and 5xx responses are retried:
  • 401 throws MarketplaceAuthError immediately (bad, revoked, or rotated API key).
  • 403 throws MarketplaceForbiddenError immediately. The backend's error slugs here are lease-not-active and lease-epoch-out-of-range (see Errors).
  • Other 4xx (404 lease-not-found, 422, ...) throws MarketplaceServerError without retry.
  • 5xx retries, then throws MarketplaceServerError after the budget is exhausted.
  • Transport failures retry, then throw MarketplaceNetworkError with the underlying error attached.

The successful bundle is cached in memory and readable via getCachedJwt() (returns null before the first fetch).

Auto-refresh

marketplace.startAutoRefresh();
// ... long-running process keeps a fresh JWT in marketplace.getCachedJwt() ...
marketplace.stopAutoRefresh();

startAutoRefresh() arms a background timer that re-mints the JWT at expires_at - 60s (Design Doc D7, same skew as the Rust SDK). It is idempotent, runs an immediate fetchJwt() if the cache is empty, and swallows refresh failures with a console.warn so the timer survives transient outages while the cached JWT is still valid. stopAutoRefresh() clears the timer.

The token endpoint is rate limited at 12 requests/min per API key and the JWT TTL defaults to 600s, so the auto-refresh cadence (about one call every 9 minutes) sits far below the limit. Avoid calling fetchJwt() per transaction: mint once, refresh in the background.

TpuClient (signing oracle)

TpuClient talks gRPC to the validator's host-proxy. Useful for health probes, certificate inspection, or driving your own TLS 1.3 stack against the TEE signing oracle.

import { TpuClient, buildCertificateVerifyPayload } from "@swqos/staked-quic-sdk";
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";

const tpu = new TpuClient({
  endpoint: "tee.example.com:443",                  // plain host:port, NO scheme
  tls: { rootCerts: readFileSync("tls_ca_cert.pem") }, // from the credentials reveal
});

const health = await tpu.health();                  // { provisioned, pubkeySha256 }
const { certificate, publicKey } = await tpu.getCertificate();

const transcriptHash = createHash("sha256").update(handshakeBytes).digest();
const signature = await tpu.signCertificateVerify(new Uint8Array(transcriptHash), jwt);

tpu.close();

Options:

FieldMeaning
endpointHost-proxy gRPC endpoint as plain host:port (e.g. localhost:50051). Do not prefix a scheme.
tlsOptional { rootCerts?, privateKey?, certChain? } (PEM Buffers). Omitted = insecure channel, dev / docker-compose only. In production pass at least rootCerts (the lease's tls_ca_cert).
protoPathOverride for the bundled .proto. Used by tests.
deadlineMsPer-call deadline, default 5000ms.

Methods:

  • health() returns { provisioned: boolean, pubkeySha256: Uint8Array } (SHA-256 of the loaded validator pubkey, empty until provisioned). Unauthenticated.
  • getCertificate() returns { certificate, publicKey }: the 249-byte Firedancer-format dummy X.509 certificate and the raw 32-byte Ed25519 public key. Unauthenticated.
  • signCertificateVerify(transcriptHash, jwt) takes a 32-byte SHA-256 transcript hash plus the lease JWT, builds the 130-byte TLS 1.3 CertificateVerify payload, sends the JWT as authorization: Bearer <jwt> gRPC metadata, and returns the 64-byte Ed25519 signature as a Uint8Array.
  • close() frees the gRPC channel. Idempotent.

The payload layout matches Firedancer's fd_keyguard_payload_matches_tls_cv matcher: 64 bytes of 0x20, then the ASCII label "TLS 1.3, client CertificateVerify\0", then the 32-byte hash. buildCertificateVerifyPayload(transcriptHash) is exported separately if you need the raw bytes.

Error classes

All classes are exported from the package root and meant for instanceof checks.

ClassThrown byMeaning
MarketplaceErrorbase classEvery marketplace-side error extends this
MarketplaceAuthErrorfetchJwt401, carries detail
MarketplaceForbiddenErrorfetchJwt403, carries detail
MarketplaceServerErrorfetchJwtPersistent 5xx after retries, or any other 4xx. Carries status + detail
MarketplaceNetworkErrorfetchJwtTransport failure after retries. Carries underlying
TpuErrorbase classEvery host-proxy-side error extends this
TpuRpcErrorall TpuClient callsNon-OK gRPC status. Carries the numeric code (e.g. UNAUTHENTICATED = 16, RESOURCE_EXHAUSTED = 8, UNAVAILABLE = 14) and details
TpuInvalidPayloadErrorsignCertificateVerifyTranscript hash is not 32 bytes
TpuInvalidSignatureErrorsignCertificateVerifyReturned signature is not 64 bytes. Carries actualLength
LeaderClientUnsupportedPlatformErrorLeaderClient.createNo native binary for this OS/arch. Carries platform + arch

LeaderClient runtime failures are plain Error rejections with the [REASON_CODE] message prefix listed above.

End-to-end example

The package ships a runnable example at examples/hello-world.ts (run from packages/typescript/staked-quic-sdk with pnpm tsx examples/hello-world.ts). It reads these env vars:

VariableExample
SWQOS_API_BASEhttp://localhost:8080 (prod: https://api.swqos.dev)
SWQOS_API_KEYtrader api_token from the credentials reveal
SWQOS_LEASE_IDULID of an active lease
SWQOS_HOST_PROXYlocalhost:50051 (TpuClient wants bare host:port, LeaderClient wants it with scheme)
SWQOS_RPC_URLhttps://api.testnet.solana.com (LeaderClient only)

A condensed version of both paths:

import { createHash } from "node:crypto";
import {
  LeaderClient,
  MarketplaceClient,
  TpuClient,
  TpuRpcError,
} from "@swqos/staked-quic-sdk";

const apiBase = process.env.SWQOS_API_BASE!;   // e.g. http://localhost:8080
const apiKey = process.env.SWQOS_API_KEY!;
const leaseId = process.env.SWQOS_LEASE_ID!;
const hostProxy = process.env.SWQOS_HOST_PROXY!; // e.g. localhost:50051

// Path A, recommended: full handshake to the leader.
const leader = await LeaderClient.create({
  marketplaceApiBase: apiBase,
  apiKey,
  leaseId,
  teeEndpoint: `http://${hostProxy}`,           // scheme required here
  rpcUrl: process.env.SWQOS_RPC_URL!,
});
try {
  console.log("TEE-presented pubkey:", leader.certPubkey().toString("hex"));
  // const sig = await leader.sendTransaction(txBytes);
} finally {
  await leader.close();
}

// Path B, low-level: mint a JWT and exercise the signing oracle directly.
const marketplace = new MarketplaceClient({ apiBase, apiKey, leaseId });
const tpu = new TpuClient({ endpoint: hostProxy }); // bare host:port here
try {
  const { jwt, expiresAt } = await marketplace.fetchJwt();
  console.log("jwt expires at", new Date(expiresAt * 1000).toISOString());

  const health = await tpu.health();
  console.log("provisioned:", health.provisioned);

  const cert = await tpu.getCertificate();
  console.log(`cert ${cert.certificate.length}B, pubkey ${Buffer.from(cert.publicKey).toString("hex")}`);

  const transcriptHash = createHash("sha256").update("hello swqos").digest();
  const sig = await tpu.signCertificateVerify(new Uint8Array(transcriptHash), jwt);
  console.log(`signature: ${sig.length}B Ed25519`);
} catch (err) {
  if (err instanceof TpuRpcError) {
    console.error(`host-proxy grpc.status=${err.code}: ${err.details}`);
  } else {
    throw err;
  }
} finally {
  tpu.close();
}

What the SDK does not cover

Browsing validators, booking, paying, and extending leases are plain REST and stay outside the SDK. Book your first lease from a validator listing at /validators (see booking), then automate with fetch if needed:

import { ulid } from "ulid";

const API = "https://api.swqos.dev";

// Public, no auth. NOTE the nested envelope: validators live at data.data.
const res = await fetch(`${API}/v1/validators?available_now=true&limit=20`);
const body = await res.json();
for (const v of body.data.data) {
  console.log(v.validator_id, v.display_name, v.price_per_epoch_lamports);
}

// Authenticated booking. Idempotency-Key is REQUIRED and MUST be a ULID
// (a UUID is rejected with 400).
const booking = await fetch(`${API}/v1/leases`, {
  method: "POST",
  headers: {
    authorization: `Bearer ${process.env.SWQOS_API_KEY}`,
    "content-type": "application/json",
    "idempotency-key": ulid(),
  },
  body: JSON.stringify({
    validator_id: body.data.data[0].validator_id,
    epoch_start: 712,
    epoch_count: 3,        // 1..=30
    mode: "exclusive",     // optional: "exclusive" (default) or "shared"
  }),
});
const { data } = await booking.json();
// data.lease_id, data.payment.solana_pay_url,
// data.payment.amount_lamports, data.payment.memo

The full contract is the generated OpenAPI spec, served live at GET https://api.swqos.dev/v1/openapi.json and committed in the repo at docs/openapi.json. You can point openapi-typescript at it to generate types in your own project (the marketplace does not ship a generated-types package). See the API reference and trader integration guide.

v1 notes and known limitations

Stated plainly so you can plan around them:

  • No QUIC in TypeScript. The pure-TS clients stop at the gRPC signing oracle and have no transaction path at all: the host-proxy exposes only GetCertificate, SignCertificateVerify, and Health, and does not forward transactions to leaders. The full client-side QUIC handshake (TEE-signed CertificateVerify straight to the leader) ships only through LeaderClient's native Rust binding.
  • Prebuilt native binary is darwin-arm64 only in v0.1.0. Other platforms build from source with pnpm build:native. The cross-platform prebuild matrix (darwin-x64, linux-x64-gnu, linux-arm64-gnu, win32-x64-msvc) lands when CI publishes per-platform optionalDependencies.
  • No TLS CA pinning on LeaderClient in v0.1.0. Unlike the Python SDK's tee_tls_ca_pem option, LeaderClientOptions has no CA field, so LeaderClient targets the dev / plaintext host-proxy path today. For a CA-pinned production connection use the Rust SDK, or TpuClient with tls.rootCerts for the oracle-only path.
  • Building the native module from source against the sibling repo's current master can fail: the Rust client-sdk connect signature moved to a 3-argument, https-only form while the v0.1.0 binding still calls the 2-argument form. Build against the sibling revision pinned by the binding's Cargo.lock until the binding is updated.
  • Proto is a one-way mirror. proto/staked_quic.proto is copied from the open-source TEE repo, never edited locally, and loaded dynamically at runtime.
  • No npm publish yet. Install via file: from a checkout.