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:
| Client | Role | Native code? |
|---|---|---|
LeaderClient | Recommended. 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) |
MarketplaceClient | JWT minting only, against POST /v1/leases/{id}/token, with retry and background auto-refresh. | No (pure TS, native fetch) |
TpuClient | Low-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:
- Book and pay for a lease covering the current epoch (see Booking and the leases API).
- Reveal the trader credentials once via
POST /v1/leases/{id}/credentials. The response carriesapi_token(the bearer key, a bare 26-character ULID with no prefix, shown exactly once),tee_endpoint_url(the validator's host-proxy), andtls_ca_cert(PEM). Rotate the key later withPOST /v1/leases/{id}/api-keys. Details in Authentication. - 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:
| Field | Meaning |
|---|---|
marketplaceApiBase | Marketplace API base URL. The client POSTs {base}/v1/leases/{leaseId}/token to mint the JWT. |
apiKey | Trader API token, sent as Authorization: Bearer on the mint call. |
leaseId | Lease public id (ULID). Must be active and cover the current epoch. |
teeEndpoint | Host-proxy gRPC endpoint with scheme (http:// or https://). Plain host:port is rejected. |
rpcUrl | Solana RPC used by the underlying TPU client for leader discovery. |
Behavior notes:
sendTransactionis 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(), furthersendTransactioncalls 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 code | Failing 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
MarketplaceAuthErrorimmediately (bad, revoked, or rotated API key). - 403 throws
MarketplaceForbiddenErrorimmediately. The backend's error slugs here arelease-not-activeandlease-epoch-out-of-range(see Errors). - Other 4xx (404
lease-not-found, 422, ...) throwsMarketplaceServerErrorwithout retry. - 5xx retries, then throws
MarketplaceServerErrorafter the budget is exhausted. - Transport failures retry, then throw
MarketplaceNetworkErrorwith 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:
| Field | Meaning |
|---|---|
endpoint | Host-proxy gRPC endpoint as plain host:port (e.g. localhost:50051). Do not prefix a scheme. |
tls | Optional { rootCerts?, privateKey?, certChain? } (PEM Buffers). Omitted = insecure channel, dev / docker-compose only. In production pass at least rootCerts (the lease's tls_ca_cert). |
protoPath | Override for the bundled .proto. Used by tests. |
deadlineMs | Per-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 asauthorization: Bearer <jwt>gRPC metadata, and returns the 64-byte Ed25519 signature as aUint8Array.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.
| Class | Thrown by | Meaning |
|---|---|---|
MarketplaceError | base class | Every marketplace-side error extends this |
MarketplaceAuthError | fetchJwt | 401, carries detail |
MarketplaceForbiddenError | fetchJwt | 403, carries detail |
MarketplaceServerError | fetchJwt | Persistent 5xx after retries, or any other 4xx. Carries status + detail |
MarketplaceNetworkError | fetchJwt | Transport failure after retries. Carries underlying |
TpuError | base class | Every host-proxy-side error extends this |
TpuRpcError | all TpuClient calls | Non-OK gRPC status. Carries the numeric code (e.g. UNAUTHENTICATED = 16, RESOURCE_EXHAUSTED = 8, UNAVAILABLE = 14) and details |
TpuInvalidPayloadError | signCertificateVerify | Transcript hash is not 32 bytes |
TpuInvalidSignatureError | signCertificateVerify | Returned signature is not 64 bytes. Carries actualLength |
LeaderClientUnsupportedPlatformError | LeaderClient.create | No 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:
| Variable | Example |
|---|---|
SWQOS_API_BASE | http://localhost:8080 (prod: https://api.swqos.dev) |
SWQOS_API_KEY | trader api_token from the credentials reveal |
SWQOS_LEASE_ID | ULID of an active lease |
SWQOS_HOST_PROXY | localhost:50051 (TpuClient wants bare host:port, LeaderClient wants it with scheme) |
SWQOS_RPC_URL | https://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, andHealth, and does not forward transactions to leaders. The full client-side QUIC handshake (TEE-signedCertificateVerifystraight to the leader) ships only throughLeaderClient'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-platformoptionalDependencies. - No TLS CA pinning on
LeaderClientin v0.1.0. Unlike the Python SDK'stee_tls_ca_pemoption,LeaderClientOptionshas no CA field, soLeaderClienttargets the dev / plaintext host-proxy path today. For a CA-pinned production connection use the Rust SDK, orTpuClientwithtls.rootCertsfor the oracle-only path. - Building the native module from source against the sibling repo's
current master can fail: the Rust
client-sdkconnect 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'sCargo.lockuntil the binding is updated. - Proto is a one-way mirror.
proto/staked_quic.protois 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.