This page explains how Polyester authentication works underneath the SDK's APIs. For the task-oriented version, see the Authentication guide.
Identity: the smart account
A Polyester account is a Safe smart account on Polyester Chain. The wallet key your user holds (MetaMask, Turnkey, a raw private key) is the owner of that Safe — it controls the account but is not the account itself.
The Safe's address is computed deterministically from the owner address, a salt nonce, and the environment's Safe deployment configuration. That has two consequences the SDK leans on:
- No deployment needed to authenticate. The address exists (counterfactually) before any
on-chain transaction. Different
saltNoncevalues derive different accounts (used for subaccounts). - Signatures must verify against an undeployed contract. The SDK produces ERC-6492-wrapped Safe signatures, which carry the factory data a verifier needs to check a signature for a contract that isn't deployed yet.
The AccountSigner interface abstracts all of this: an accountAddress, an optional ownerAddress, an environment fingerprint, and a signMessage function. Anything that can
produce a valid Safe signature can be an account signer — the built-in createPolyesterAccountSigner covers the standard single-owner case with zero RPC calls.
Wallet login: nonce → signature → JWT
- The client requests a login nonce for the smart-account address
(
auth.requestLoginNonce). Nonces are single-use and expire in ~5 minutes. - The account signer signs the nonce message.
- The signed nonce is exchanged for a JWT session token bound to the account.
- The browser client stores the token (in memory by default, or in a cookie via
createCookieAuthTokenStorage), attaches it asAuthorization: Bearer …on every request, and uses it to authorize private realtime channels.
Session tokens are environment-fingerprint-bound in storage: a token minted on one environment is
invisible to clients constructed for another. Refreshing a session (auth.refreshSession) is the
same signature dance with a fresh nonce.
API keys: Ed25519 request signing
API keys skip JWTs entirely — every request is signed individually:
- You generate an Ed25519 keypair locally (
apiKeys.generateKeypair) and register only the public key. The secret never leaves your process. - For each request the SDK builds a canonical string —
timestamp \n method \n path \n sorted-query \n sha256(body)— signs it with the secret key, and sends three headers:X-API-KEY-ID,X-API-TIMESTAMP,X-API-SIGNATURE. - The backend verifies the signature against the registered public key.
Because the timestamp is part of the signature, replay windows are tight; because the body hash is included, payloads are tamper-evident. Keys can carry IP whitelists, expirations, and policies (scoped markets, actions, and limits).
WebSocket authentication reuses the same contract: connection- and subscription-token requests are signed the same way.
Sessions on servers
Browser sessions surface to your server as cookies. The SDK deliberately splits what they prove:
- The auth token cookie holds the JWT — real proof, verified by the backend on every call.
- The session cookie holds display data (username, active account, addresses) — unsigned,
client-writable, and typed as display-only (
ServerSessionSnapshot).
PolyesterServerClient reads both, uses the JWT for calls, and exposes verifySession() for a
backend-confirmed identity. Authorization is always the backend's decision; the SDK just carries
intent.
Assurance layers: MFA
Some operations require more than a valid session. Polyester has two elevation mechanisms, and the SDK maps each to a distinct error:
- Session elevation (
SessionElevationRequiredError) — the session itself must have passed an MFA challenge recently. Complete a challenge with purpose"sessionElevation"; the session is upgraded. - Fresh step-up (
StepUpRequiredError) — a one-use proof bound to a single protected request. Complete a challenge with purpose"freshStepUp"and retry the call with the returnedstepUpToken(sent as theX-Auth-Step-Upheader).
Factors are TOTP or passkeys, with one-time recovery codes as backup — all managed through client.mfa. Subaccount owners can additionally require MFA from delegated members.
Subaccounts and delegation
Subaccounts are full smart accounts derived from the same owner with different salt nonces, tied to the main account. Creating one is itself a signed operation (proof you control the new address). Access can be delegated to other users with roles, and constrained with policy templates — see Accounts & balances.