Session Key Helpers
Why session keys exist in SYMMIO
Every user-driven action in SYMMIO — open a position, close a position, set / edit / delete a TP or SL — is submitted as an EIP-712 signed message to the solver / conditional-order handler. That’s the security model: the contract and off-chain services only accept messages the user has explicitly authorized.
Signing every one of those with the connected wallet (MetaMask, WalletConnect, Coinbase Wallet) means a popup on every action:
- Open a position → popup.
- Adjust a TP → popup.
- Cancel an SL → popup.
- Partial close → popup.
- Bulk close five positions → five popups.
That’s unusable for trading. A trader clicking through fifty popups a session is fifty chances to hit “reject” by accident, fifty context switches away from the price. Traders on centralized exchanges never see this friction; SYMMIO can’t afford to either.
Session keys fix it. The flow:
- User connects the wallet — MetaMask etc. — and authorizes a session key once (via
grantDelegationon-chain). One wallet popup. - The session key is a locally generated EVM keypair the app holds in memory (and optionally persists via a consumer-owned storage adapter).
- Every subsequent open / close / TP / SL / edit signs silently in the background with the session key. Zero popups.
- When the session expires (default: 1 year) or the user rotates it, they re-authorize once and continue.
The trader sees a normal exchange-style flow. The wallet is only touched on connect, delegation, deposit, and withdraw — never on the trading hot path.
What this package does — and doesn’t
@symmio/session-key is the framework-agnostic runtime that owns:
- Key generation (
createSessionKey,sessionKeyFromPrivateKey) — creates the local EVM keypair. - Manager (
createSessionKeyManager) — loads, generates, rotates, signs, destroys. Exposes state and subscription for framework integrations. - Transfer payloads (
encodeSessionKeyTransferPayload/decodeSessionKeyTransferPayload) — short-lived envelopes for moving a key to another device. - Constants — TTLs, versions.
It does not own:
- Storage — the package defines a
SessionKeyStoragecontract; the consumer picks localStorage, IndexedDB, memory, or a remote store. - Encryption / decryption — see the warning below.
- DOM globals — runs equally well in Node scripts, workers, and browsers.
⚠️ No encryption
The private key is stored as plaintext Hex in the SessionKeyRecord. @symmio/session-key does not encrypt anything. It hands your storage adapter a raw private key and expects it back on load. Pay attention — add encryption if you need it.
Consumer responsibility:
- Browser apps — encrypt
record.privateKeybefore writing to storage (WebCrypto + a user-derived key, WebAuthn PRF, biometrics-gated key, or Passkey PRF). Decrypt insideload(). - Bots / scripts — read the private key from a secrets manager (Vault, GCP Secret Manager, KMS), never from a checked-in file.
- Never log the record. Never send it over an untrusted channel. Never expose it in a URL fragment.
The manager exposes getPrivateKey() only for explicit device-transfer flows. Treat it like a wallet mnemonic.
Where session keys plug into SDK actions
Session keys are wired into @symmio/trading-react through SymmioProvider’s getWalletClient resolver. Every SDK write that passes from: sessionKeyAddress routes signing through the session-key manager; everything else routes to the wagmi-connected wallet.
Actions that sign via the session key when configured:
- TP/SL —
useSetQuoteTpSl,useDeleteQuoteTpSl(EIP-712 conditional-order message). - Instant Open —
useInstantOpen,useInstantOpenAuto,useInstantOpenWithTpSl(EIP-712 open + optional TP/SL leg). - Instant Close —
useInstantClose,useInstantCloseAuto, and their bulk variants. - Delegated instant withdraw — after
useGrantDelegationauthorizes the session key on-chain for the withdraw selector.
Actions that stay on the connected wallet (regardless of session-key config):
- Wallet connect / disconnect / chain switch — not signing, just wagmi calls.
- Grant delegation — the very act of authorizing the session key; signed by the owner wallet.
- Deposit / classic withdraw — non-instant on-chain writes routed through the owner wallet.
Session keys are normal EVM keys — not chain-tied. One session key per owner covers every supported chain unless the app has a specific reason to isolate.
import {
createSessionKey,
createSessionKeyManager,
decodeSessionKeyTransferPayload,
encodeSessionKeyTransferPayload,
sessionKeyFromPrivateKey,
SESSION_KEY_TRANSFER_PAYLOAD_VERSION,
type SessionKeyStorage,
} from "@symmio/session-key";
const storage: SessionKeyStorage = {
async load(owner) {
return loadRecordForOwner(owner);
},
async save(owner, record) {
await saveRecordForOwner(owner, record);
},
async remove(owner) {
await removeRecordForOwner(owner);
},
async getMetadata(owner) {
return loadMetadataForOwner(owner);
},
};
const manager = createSessionKeyManager({ storage });
const state = await manager.initialize(owner);
const signature = await manager.sign("hello");Storage Boundary
@symmio/session-key exports the storage interface only:
interface SessionKeyStorage {
load(owner: Address): Promise<SessionKeyRecord | null>;
save(owner: Address, record: SessionKeyRecord): Promise<void>;
remove(owner: Address): Promise<void>;
getMetadata(owner: Address): Promise<SessionKeyMetadata | null>;
}
interface SessionKeyRecord {
owner: Address;
privateKey: Hex;
address: Address;
createdAt: number;
expiresAt: number;
label?: string;
}The manager passes a plain SessionKeyRecord — including the raw private key as Hex — straight to your adapter. @symmio/session-key performs no encryption; the adapter must add it. See the No encryption warning above.
For browser apps: encrypt record.privateKey before writing to localStorage / IndexedDB, decrypt inside load(). For scripts / backend tools: use a secrets manager (Vault, KMS) instead of a local file.
Storage should normally be keyed by owner address, not chain ID. A session key can sign for the same owner across chains, and chain-specific authorization belongs in the contracts or delegated permission layer.
Only use getPrivateKey() for explicit device-transfer or export flows.
createSessionKeyManager
Create the runtime manager.
const manager = createSessionKeyManager({
storage,
defaultTtlMs: SESSION_KEY_EXPIRY_MS, // default: one year
now: () => Date.now(), // default; override for tests
});Options
| Option | Type | Default | Notes |
|---|---|---|---|
storage | SessionKeyStorage | required | Consumer-owned persistence adapter. |
defaultTtlMs | number | SESSION_KEY_EXPIRY_MS (1 year) | Lifetime applied to newly generated / imported keys. |
now | () => number | Date.now | Injectable clock for tests. |
Methods
| Method | Notes |
|---|---|
initialize(owner, options?) | Load an existing non-expired key or generate + persist a new one. |
importPrivateKey(owner, key, opts?) | Import an existing private key and persist it for owner. |
rotate(owner, options?) | Replace any existing key for owner with a newly generated one. |
sign(message) | Sign a raw string message. Returns SessionKeySignature with duration ms. |
signTypedData(params) | Sign EIP-712 typed data. Returns the Hex signature. |
destroy(owner?) | Clear memory; when owner is passed, remove the stored key. |
getState() | Snapshot of { isReady, isExpired, publicAddress, expiresAt }. |
isReady() | Quick boolean for “session key loaded”. |
getAddress() | Loaded session-key public address, or null. |
getPrivateKey() | Loaded raw private key — for explicit device-transfer only. |
getMetadata(owner) | Stored public metadata via the adapter (no in-memory load required). |
subscribe(listener) | Subscribe to state changes. Returns an unsubscribe function. |
getSnapshot() | Framework-integration snapshot (returns the loaded address or null). |
SessionKeyState
The shape getState() returns:
interface SessionKeyState {
isReady: boolean; // valid key currently in memory
isExpired: boolean; // last loaded record was expired
publicAddress: Address | null;
expiresAt: number | null; // ms since epoch
}Key Material Helpers
Use createSessionKey() when you only need a fresh key pair and do not need manager state.
const key = createSessionKey();Use sessionKeyFromPrivateKey() when importing a private key outside the manager.
const key = sessionKeyFromPrivateKey(privateKey);Transfer Payloads
Encode + decode short-lived payloads for explicit device-handoff flows (QR code, deep link, cross-tab):
const raw = encodeSessionKeyTransferPayload({
version: SESSION_KEY_TRANSFER_PAYLOAD_VERSION,
owner,
sessionPrivateKey: manager.getPrivateKey(),
sessionAddress: manager.getAddress() ?? undefined,
createdAt: Date.now(),
expiresAt: Date.now() + SESSION_KEY_TRANSFER_MAX_AGE_MS,
});
const decoded = decodeSessionKeyTransferPayload(raw);
await manager.importPrivateKey(decoded.owner, decoded.sessionPrivateKey!);Payloads expire after SESSION_KEY_TRANSFER_MAX_AGE_MS (two minutes by default). decode throws when the payload is older than that or its version doesn’t match SESSION_KEY_TRANSFER_PAYLOAD_VERSION.
Never ship a transfer payload over an untrusted channel — the private key is right there. Prefer local QR scans or same-device deep links.
Constants
| Constant | Value | Notes |
|---|---|---|
SESSION_KEY_EXPIRY_MS | 365 * 24 * 60 * 60_000 | Default TTL for newly generated / imported keys (1 year). |
SESSION_KEY_TRANSFER_PAYLOAD_VERSION | 1 | Current transfer payload schema. Bump if the encoding changes. |
SESSION_KEY_TRANSFER_MAX_AGE_MS | 120_000 | Default validity window for transfer payloads (2 minutes). |
Integration with SymmioProvider
The typical wiring — session-key wallet signs when from matches, otherwise wagmi wallet:
function useAppGetWalletClient(manager: SessionKeyManager) {
return useCallback(
async ({ chainId, from }: { chainId: number; from?: Address }) => {
const sessionAddress = manager.getAddress();
if (sessionAddress && from?.toLowerCase() === sessionAddress.toLowerCase()) {
return sessionKeyWalletClient(manager, chainId);
}
return await getWalletClient(wagmiConfig, { chainId });
},
[manager],
);
}
<SymmioProvider getWalletClient={useAppGetWalletClient(manager)}>{children}</SymmioProvider>;sessionKeyWalletClient(manager, chainId) is a small helper the consuming app owns — it turns the session-key private key into a viem walletClient bound to the chain. Every SDK write that sets from to the session key’s address then signs via this path without a wallet prompt.
Related
- SymmioProvider — where
getWalletClientis injected. - TP/SL — the primary consumer of session-key signing.
- Instant Open / Instant Close — same.
- InstantLayer —
useGrantDelegationauthorizes the session key on-chain.