The SDK is a thin typed layer over two wire protocols:
- ConnectRPC over HTTP for requests
- Centrifugo over WebSocket for realtime
Everything else exists to make those two safe to use.
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, ... (every API domain)Environment: validated, frozen description of where to connect. See Environments.
Transports: publicApi is unauthenticated. authApi attaches auth (JWT header or Ed25519
request signature) through an interceptor. An error-mapping interceptor wraps both, so RPC
failures surface as typed PolyesterErrors.
Services: one class per API domain, exposed as getters. Built on first access and memoized, so creating a client is cheap. That matters when a server builds one per request.
RealtimeClient: one shared subscription multiplexer. Channels are refcounted. The WebSocket engine (~300 KB) loads on the first subscription via dynamic import.
Catalog: reference data every service uses for symbol lookups and decimal scale conversions. See Catalog & precision.
Validation at both edges
Every service method validates both ways with strict schemas.
Inputs are checked before the request is built. Unknown keys, bad enums, and excess decimal
precision throw ValidationError or CatalogConversionError locally. No network round-trip.
Outputs are parsed into plain TypeScript shapes. Scaled bigints, proto enums, and oneofs do not
leak through. You get decimal strings and string literal unions. Unknown numeric proto enum
values on read paths become "unspecified", leaving the rest of the response readable. Input
enums remain strictly validated.
If it typechecks and does not throw, what you sent is what the API received.
The decimal-string surface
On the wire: scaled integers (price ticks, per-asset quantities).
In the SDK: plain decimal strings in both directions.
- Outputs convert exactly. No rounding. Trailing zeros trimmed.
- Inputs convert strictly. Excess precision errors. Never silent rounding.
number is never used for money.
Why three clients
PolyesterClient is the whole machine. The two subclasses add an auth strategy and a subaccount resolver (the hook that defaults service calls 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 + tokens | active account from auth state |
PolyesterServerClient | Bearer token from cookies | main, or display-session active account (opt-in) |
Bundle discipline
The root export stays lean. Heavy graphs sit behind subpaths so a market-data-only shell never bundles them:
| Subpath | Isolated weight |
|---|---|
@polyester/sdk/account-signer | viem ABI / typed-data 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 |
@polyester/sdk/errors | error classes without the rest of the client graph |
Realtime follows the same idea at runtime with dynamic import.
Extension points
interceptors: ConnectRPC interceptors on every request (logging, tracing, mock headers)wireFormat:"binary"(default) or"json"for readable debuggingrealtime: override WebSocket auth header derivationcatalog/catalogSnapshot/catalogCell: control reference-data storage, hydration, and reactivity