The SDK is a thin, strongly typed layer over two wire protocols: ConnectRPC over HTTP for requests and Centrifugo over WebSocket for realtime. Everything else exists to make those two safe and ergonomic.
The layers
PolyesterEnvironment frozen endpoint + chain configuration
│
PolyesterClient wires everything, exposes services as lazy getters
├── Transports two ConnectRPC transports: publicApi and authApi
├── RealtimeClient one shared WebSocket multiplexer
├── ClientCatalog reference data: assets, pairs, scales, routes
└── Services orders, marketData, balances, … (26 domains)- Environment — a validated, frozen description of where to connect (see Environments).
- Transports —
publicApicarries unauthenticated traffic;authApiattaches auth (JWT header or Ed25519 request signature) via an interceptor. An error-mapping interceptor wraps both, so every RPC failure surfaces as a typedPolyesterError. - Services — one class per API domain, exposed as getters on the client. Services are constructed on first access and memoized, so creating a client is nearly free — important when a server builds one per request.
- RealtimeClient — a single shared subscription multiplexer. Channels are refcounted; the WebSocket engine itself (~300 KB) is dynamically imported on the first subscription.
- Catalog — the reference-data store every service consults for symbol lookups and decimal scale conversions (see Catalog & precision).
Validation at both edges
Every service method validates in both directions with strict schemas:
- Inputs are checked before the request is built — unknown keys, wrong enum values, and
excess decimal precision throw a
ValidationError/CatalogConversionErrorlocally, with no network round-trip. - Outputs are parsed into plain, documented TypeScript shapes. Wire-level representations (scaled bigints, proto enums, oneofs) never leak through: you get decimal strings and string literal unions.
This means the compiler and the runtime agree: if it typechecks and doesn't throw, what you sent is exactly what the API received.
The decimal-string surface
The wire protocol carries scaled integers (price ticks, per-asset scaled quantities). The SDK's public surface carries plain decimal strings in both directions:
- Outputs convert exactly — no rounding, trailing zeros trimmed.
- Inputs convert strictly — excess precision is an error, never rounded away.
Floating-point number is never used for money.
Why three clients
PolyesterClient is the whole machine. The two subclasses only add an authentication strategy
and a subaccount resolver — the hook that lets service calls default to the user's active
subaccount:
| Client | Auth strategy | Resolver default |
|---|---|---|
PolyesterClient | Whatever auth you pass (API key / JWT) | none (main account) |
PolyesterBrowserClient | Managed wallet-signer login + token storage | the active account from auth state |
PolyesterServerClient | Bearer token parsed from cookies | main, or display-session active account (opt-in) |
Bundle discipline
The root export stays lean deliberately. Heavy dependency graphs are isolated behind subpath exports so an app shell that only reads market data never bundles them:
| Subpath | Isolated weight |
|---|---|
@polyester/sdk/account-signer | viem ABI / typed-data graph for Safe signature derivation |
@polyester/sdk/smart-account | permissionless + bundler/paymaster clients |
@polyester/sdk/catalogs | snapshot builders (types are free from the root) |
@polyester/sdk/server-session | cookie/session parsing for servers |
The realtime engine follows the same principle at runtime via dynamic import.
Extension points
interceptors— ConnectRPC interceptors run on every request (logging, tracing, mock headers).wireFormat—"binary"(default) or"json"for human-readable debugging.realtime— override WebSocket auth header derivation.catalog/catalogSnapshot/catalogCell— take control of reference-data storage, hydration, and reactivity.