Skip to main content
With a JWT in hand, authenticate the wallet client and operate wallets as the user: create, sign, and keep the session alive.
Before this: a Dynamic user JWT minted with your session public key bound in — see Sign In with Email OTP or Sign In with a Private Key — and access to the session private key.

Authenticate the wallet client

import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';
import { signSessionMessage } from '@dynamic-labs-wallet/node';

const client = new DynamicEvmWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
  enableMPCAccelerator: false, // Set to true only on AWS Nitro Enclave-compatible infrastructure
});

await client.authenticateJwt(jwt, {
  getSessionSignature: (message) => signSessionMessage(message, privateKeyJwk),
});
authenticateJwt installs the JWT as the bearer token for all subsequent calls. There is no token exchange: the client only checks the token’s structure (a compact JWS with three segments); the server validates it on every request. getSessionSignature keeps key custody with you. The session private key never enters the SDK — the SDK sends messages to your callback and uses the signatures it returns. signSessionMessage implements the callback for a stored JWK; a KMS- or HSM-backed signer works the same way as long as it returns the same encoding.
getSessionSignature must return the signature as lowercase hex: the raw 64-byte ECDSA P-256 r‖s, exactly as signSessionMessage produces. Base64 output corrupts the proof — base64 can contain /, which breaks the /-delimited composite the SDK sends to the keyshares relay.

Create wallets and sign

With the signer configured, use the client as usual. When you pass backUpToDynamic: true, the SDK automatically attaches a signed-session proof to the backup and recovery calls — it signs the JWT’s session ID and a single-use server nonce with your callback. Without a signer, those calls fail with a 400 from the keyshares relay for agent JWT sessions.
import { ThresholdSignatureScheme } from '@dynamic-labs-wallet/core';

const { walletMetadata, externalServerKeyShares } = await client.createWalletAccount({
  thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
  password: process.env.WALLET_PASSWORD,
  backUpToDynamic: true,
});
// Persist walletMetadata (durable store) and externalServerKeyShares (secrets vault) —
// see Storage & Security for the full persistence matrix.

const signature = await client.signMessage({
  message: 'Hello from your agent',
  walletMetadata,
  externalServerKeyShares,
  password: process.env.WALLET_PASSWORD,
});

Reuse an existing wallet

Creation is a one-time step. On later runs, load the persisted walletMetadata and externalServerKeyShares from your stores instead of creating a new wallet, and sign directly:
const walletMetadata = await loadWalletMetadata(userId); // your durable store
const externalServerKeyShares = await loadKeyShares(userId); // your secrets vault

const signature = await client.signMessage({
  message: 'Hello again from your agent',
  walletMetadata,
  externalServerKeyShares,
  password: process.env.WALLET_PASSWORD,
});
Calling createWalletAccount on every run creates a new wallet each time. Create once, persist the returned artifacts, and reuse them for the life of the wallet.

Refresh long-running sessions

User JWTs are short-lived. refreshAuthToken refreshes the JWT using the current session, installs the replacement on the client, and returns it so you can persist it. The session signer you passed to authenticateJwt is preserved across refreshes.
const refreshedJwt = await client.refreshAuthToken();
// Overwrite the stored JWT — the previous token is superseded.
Sign-in returns expiresAt — schedule refreshes ahead of it instead of waiting for a 401. The server caps how long a session can be extended via the JWT’s refreshExp claim. Past that limit, refreshAuthToken fails with a 401 and a new sign-in is required — refresh cannot extend a session indefinitely. For a human user, that means re-approving (a new OTP code or SIWE signature). For an autonomous agent with an agent signing token, re-authentication is just another programmatic sign-in.
try {
  await client.refreshAuthToken();
} catch {
  // Refresh limit reached or session invalid: run the sign-in flow again,
  // then call authenticateJwt with the new token.
}
refreshAuthToken also throws a descriptive error when the environment is configured for cookie-based auth: refresh responses then carry no JWT in the body, and agent flows require header-based JWTs.

Next steps

API reference

Last modified on July 9, 2026