# Architecture

How the SDK is put together: clients, transports, services, realtime, and the catalog.

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

```text
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](https://testnet.polyester.com/docs/sdk/typescript/concepts/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 `PolyesterError`s.

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](https://testnet.polyester.com/docs/sdk/typescript/concepts/catalog-and-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 debugging
- `realtime`: override WebSocket auth header derivation
- `catalog` / `catalogSnapshot` / `catalogCell`: control reference-data storage, hydration, and reactivity
