Use private-key sign-in when the sign-in is authorized by a signature over a sign-in message — currently SIWE (Sign-In With Ethereum), so the signing address must be an EVM address. The signer is usually the agent itself — a fully autonomous sign-in with an agent signing token: a secp256k1 private key that the agent holds, which serves as its identity. SIWE is only the mechanism; the agent signing token is the credential. The signer can also be a human user signing in their own wallet UI. For the human-relayed alternative, see Sign In with Email OTP.
Generate a session key pair
import { generateSessionKeyPair } from '@dynamic-labs-wallet/node';
const { publicKeyHex, privateKeyJwk } = await generateSessionKeyPair();
// Store privateKeyJwk in your secrets vault. The SDK does not persist it.
generateSessionKeyPair returns a P-256 key pair: publicKeyHex (33-byte compressed public key, lowercase hex) and privateKeyJwk (the private key as a JWK). The SDK stores nothing — you persist the JWK in your secrets vault.
The returned JWK is extractable so it can be handed back to you — it is a convenience, not a security boundary. For non-extractable custody, skip generateSessionKeyPair: generate a P-256 key in your KMS, HSM, or enclave, pass its compressed public key hex at sign-in, and back getSessionSignature with the external signer.
Create the auth client
createAuthClient performs Dynamic sign-in flows headlessly — the server-side counterpart to the JS client SDK’s sign-in. It holds configuration only: no secrets, no persistence. Set appOrigin to the origin your sign-ins present as; it must be on your environment’s allowlisted CORS origins.
import { createAuthClient } from '@dynamic-labs-wallet/node';
const auth = createAuthClient({
environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
appOrigin: 'https://yourapp.example.com',
});
Sign in with wallet.signIn
wallet.signIn runs the whole flow in one call — it fetches a nonce, builds the sign-in message (deriving its domain and URI from appOrigin; pass domain and uri per call to override), collects the signature from your signMessage callback, and verifies it to mint the JWT. The callback is the only difference between the two modes: the agent signs with its own key, or a human signs in your app’s UI.
Pass sessionPublicKey at sign-in. This binds the session key into the minted JWT and is what makes the signed-session proof verifiable. If you omit it, wallet creation and signing with backUpToDynamic: true fail at the backup and recovery calls. This is the most commonly missed step.
Autonomous agent
Human user
No human in the loop: the agent signs with its agent signing token. If the agent does not have one yet, generate it once and store it in your secrets vault or KMS:import { generatePrivateKey } from 'viem/accounts';
const agentSigningToken = generatePrivateKey(); // 32-byte secp256k1 private key, 0x-prefixed hex
// One-time setup: store it in your secrets vault or KMS. Generating a new one
// creates a new Dynamic user — it will not see the previous user's wallets.
On every run, load the stored token and sign in with it:import { privateKeyToAccount } from 'viem/accounts';
const agentAccount = privateKeyToAccount(process.env.AGENT_SIGNING_TOKEN as `0x${string}`);
const { jwt, expiresAt } = await auth.wallet.signIn({
address: agentAccount.address,
signMessage: (message) => agentAccount.signMessage({ message }),
sessionPublicKey: publicKeyHex,
});
The Dynamic user is keyed to this public address, so use the same agent signing token every run to keep operating the same user’s wallets — and re-authentication at the refresh limit needs no human either. For custody requirements, see Storage & Security. The user signs with their own wallet (for example, in your app’s UI) — how you collect that signature is your implementation:// The public address of the wallet that will sign the message.
const walletAddress = '0xUserPublicAddress';
const { jwt, expiresAt } = await auth.wallet.signIn({
address: walletAddress,
signMessage: (message) => collectSignatureFromUser(message), // your implementation
sessionPublicKey: publicKeyHex,
statement: 'Authorize this agent to use your Dynamic wallet',
});
In the JS client SDK this whole flow is a single connectAndVerifyWithWalletProvider() call, because the SDK is connected to the user’s wallet and signs internally. Server-side there is no wallet provider, so collecting the user’s signature is your application’s job.
Sign-in resolves to { jwt, expiresAt, userId }. Sign-in fails with a descriptive error when address is not a 0x-prefixed 20-byte hex address, when neither appOrigin nor domain/uri is provided, when the signature or nonce is invalid or expired, when the account requires MFA (the headless client does not support MFA), or when the environment uses cookie-based auth (the response carries no JWT in the body).
For custom message construction, auth.wallet also exposes the underlying primitives — getNonce, createMessage, and verify. See the Wallet sign-in reference.
What you must persist
The SDK is stateless by design — your store is the source of truth. After sign-in you hold two artifacts to keep safe: the session private key (secrets vault or KMS) and the JWT (update it on every refresh). See Storage & Security for the full persistence matrix.
Next step
Hand the JWT and the session private key to the wallet client: Use Agent Wallets.
API reference