The SDK supports three authentication paths. Pick the one that matches where your code runs:
| You are building… | Use | Auth mechanism |
|---|---|---|
| A bot, script, or backend service | PolyesterClient | Ed25519 API key |
| A web app users sign into with a wallet | PolyesterBrowserClient | Account signer + JWT session |
| A server rendering pages for signed-in users | PolyesterServerClient | Session cookies forwarded from the browser |
For the underlying model — smart accounts, nonces, signing contracts, MFA — see Authentication model.
API keys (bots and services)
An API key is an Ed25519 keypair. The secret key never leaves your process: the SDK signs each
request locally and sends the signature in headers (X-API-KEY-ID, X-API-TIMESTAMP, X-API-SIGNATURE).
Create a key
Generate the keypair client-side and register only the public key. You need an already authenticated client (for example a browser session, or an existing key) to create new keys:
const { publicKey, secretKey } = await client.apiKeys.generateKeypair();
const apiKey = await client.apiKeys.create({
label: "trading-bot",
publicKeyEd25519: publicKey.bytes,
ipWhitelist: ["203.0.113.7"], // optional
});
// Persist secretKey.hex somewhere safe — the SDK never sends it anywhere.
console.log(apiKey?.keyId, secretKey.hex);apiKeys.create may throw StepUpRequiredError. Complete an MFA challenge and retry with options.stepUpToken — see Error handling.Use the key
import { PolyesterClient, POLYESTER_TESTNET_ENVIRONMENT, evmHexToBytes } from "@polyester/sdk";
const client = new PolyesterClient({
environment: POLYESTER_TESTNET_ENVIRONMENT,
auth: {
kind: "api-key-ed25519",
getKeyId: () => process.env.POLYESTER_API_KEY_ID ?? null,
getSecretKey: () => evmHexToBytes(process.env.POLYESTER_API_SECRET_HEX ?? "0x"),
},
});
const me = await client.auth.me();
console.log("authenticated as", me.username);Both getters may return promises, so keys can be fetched from a secrets manager on first use.
Keys can be scoped and constrained with policies (client.apiKeys.policies) — market allowlists,
action lists, and notional limits.
Wallet login (browsers)
PolyesterBrowserClient manages the full wallet login flow: request a nonce, sign it with an account signer, exchange the signature for a JWT session, and persist it.
1. Create an account signer
An account signer represents the user's Polyester smart account. If you hold a viem LocalAccount (from a browser wallet, Turnkey, or a private key), derive one — no RPC calls
needed:
import { createPolyesterAccountSigner } from "@polyester/sdk/account-signer";
import { POLYESTER_TESTNET_ENVIRONMENT } from "@polyester/sdk";
const accountSigner = createPolyesterAccountSigner({
environment: POLYESTER_TESTNET_ENVIRONMENT,
owner: ownerAccount, // a viem LocalAccount
});The signer computes the Safe smart-account address deterministically and produces ERC-6492 wrapped signatures, so login works even before the account is deployed on-chain.
2. Create the client and log in
import { PolyesterBrowserClient, POLYESTER_TESTNET_ENVIRONMENT, createCookieAuthTokenStorage } from "@polyester/sdk";
const client = new PolyesterBrowserClient({
environment: POLYESTER_TESTNET_ENVIRONMENT,
accountSigner, // or a factory: () => Promise<AccountSigner | null>
tokenStorage: createCookieAuthTokenStorage(), // persist across reloads; default is in-memory
});
const result = await client.auth.login({ provider: "metamask" });
console.log("logged in as", result.username, "until", result.expiresAt);accountSigner also accepts a lazy factory, useful when the wallet connects after the client is
constructed; you can also swap it later with client.setAccountSigner(signer).
3. Sessions across reloads
// On app start: restore a persisted session if the token is still valid.
const restored = await client.auth.restoreSession();
// Keep an eye on expiry and refresh with a fresh signature when needed.
if (client.auth.getSessionTimeToExpiry() < 5 * 60_000) {
await client.auth.refreshSession();
}
// End the session and disconnect private realtime channels.
await client.auth.logout();The browser auth service also emits events (authenticated, loggedOut, stateChange, …) via client.auth.events, and exposes the current state synchronously with client.auth.getState() —
useful for driving UI.
Sessions are environment-bound: a token minted against one environment is ignored when the
client is constructed for another (the environment's fingerprint must match).
Switching accounts and subaccounts
client.auth.switchAccount(subaccountId);After a switch, service calls that omit the account input are scoped to the active subaccount
automatically. See Accounts & balances for the full scoping
rules.
Server-side sessions
On a server handling a signed-in user's request, build the client from the request cookies — the browser session's bearer token rides along:
import { createPolyesterServerClientFromRequest, POLYESTER_TESTNET_ENVIRONMENT } from "@polyester/sdk";
const client = createPolyesterServerClientFromRequest({
environment: POLYESTER_TESTNET_ENVIRONMENT,
request, // the incoming Request
});
const me = await client.verifySession(); // null when not authenticatedSee Server-side usage for display sessions, verification, and SSR hydration.
Supplying your own JWT
If you already manage tokens (a custom auth proxy, tests), pass a JWT provider directly to the core client:
const client = new PolyesterClient({
environment: POLYESTER_TESTNET_ENVIRONMENT,
auth: { kind: "jwt", getToken: () => myTokenStore.current },
});Realtime private channels reuse whichever auth provider the client was configured with; override
per-connection behavior with the realtime config option if you need different headers for
WebSocket auth.