authenticateJwt with the session signer → create and sign with backUpToDynamic: true → refresh → re-authenticate at the refresh limit.
- Email OTP (human user)
- Private key (autonomous agent)
This script collects the OTP code on stdin; in production, collect it through your own channel.
import readline from 'node:readline/promises';
import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';
import {
createAuthClient,
generateSessionKeyPair,
signSessionMessage,
} from '@dynamic-labs-wallet/node';
import { ThresholdSignatureScheme } from '@dynamic-labs-wallet/core';
const environmentId = process.env.DYNAMIC_ENVIRONMENT_ID!;
// 1. Session key pair — persist privateKeyJwk in your secrets vault.
const { publicKeyHex, privateKeyJwk } = await generateSessionKeyPair();
// 2. Sign the user in with email OTP, binding the session public key into the JWT.
const auth = createAuthClient({ environmentId });
const { verificationUUID } = await auth.email.sendOtp('user@example.com');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const code = await rl.question('OTP code sent to the user: ');
rl.close();
const { jwt } = await auth.email.verifyOtp({
verificationUUID,
code,
sessionPublicKey: publicKeyHex,
});
// 3. Authenticate the wallet client with the JWT and the session signer.
const client = new DynamicEvmWalletClient({
environmentId,
enableMPCAccelerator: false, // Set to true only on AWS Nitro Enclave-compatible infrastructure
});
await client.authenticateJwt(jwt, {
getSessionSignature: (message) => signSessionMessage(message, privateKeyJwk),
});
// 4. Create a wallet for the user, backed up to Dynamic. First run only —
// on later runs, load the persisted walletMetadata and externalServerKeyShares
// instead of creating a new wallet.
const { walletMetadata, externalServerKeyShares } = await client.createWalletAccount({
thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
password: process.env.WALLET_PASSWORD,
backUpToDynamic: true,
});
console.log('Wallet created:', walletMetadata.accountAddress);
// Persist walletMetadata (durable store) and externalServerKeyShares (secrets vault).
// 5. Sign on the user's behalf.
const signature = await client.signMessage({
message: 'Hello from your agent',
walletMetadata,
externalServerKeyShares,
password: process.env.WALLET_PASSWORD,
});
console.log('Signed:', signature);
// 6. Long-running sessions: refresh and persist the replacement JWT.
try {
const refreshedJwt = await client.refreshAuthToken();
// Overwrite the stored JWT with refreshedJwt.
} catch {
// Refresh limit (refreshExp) reached or session invalid: repeat step 2,
// then authenticateJwt with the new token.
}
No human in the loop: the agent signing token signs the SIWE message, and re-authentication at the refresh limit reuses the same
signIn function programmatically.import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';
import {
createAuthClient,
generateSessionKeyPair,
signSessionMessage,
} from '@dynamic-labs-wallet/node';
import { ThresholdSignatureScheme } from '@dynamic-labs-wallet/core';
import { privateKeyToAccount } from 'viem/accounts';
const environmentId = process.env.DYNAMIC_ENVIRONMENT_ID!;
// 1. Session key pair — persist privateKeyJwk in your secrets vault.
const { publicKeyHex, privateKeyJwk } = await generateSessionKeyPair();
// The agent signing token is the agent's identity — load it from your vault or KMS.
const agentAccount = privateKeyToAccount(process.env.AGENT_SIGNING_TOKEN as `0x${string}`);
// 2. Sign in with the agent signing token, binding the session public key into the JWT.
const auth = createAuthClient({
environmentId,
appOrigin: 'https://yourapp.example.com',
});
const signIn = () =>
auth.wallet.signIn({
address: agentAccount.address,
signMessage: (message) => agentAccount.signMessage({ message }),
sessionPublicKey: publicKeyHex,
statement: 'Autonomous agent sign-in',
});
const { jwt } = await signIn();
// 3. Authenticate the wallet client with the JWT and the session signer.
const client = new DynamicEvmWalletClient({
environmentId,
enableMPCAccelerator: false, // Set to true only on AWS Nitro Enclave-compatible infrastructure
});
await client.authenticateJwt(jwt, {
getSessionSignature: (message) => signSessionMessage(message, privateKeyJwk),
});
// 4. Create a wallet for the agent's user, backed up to Dynamic. First run
// only — on later runs, load the persisted walletMetadata and
// externalServerKeyShares instead of creating a new wallet.
const { walletMetadata, externalServerKeyShares } = await client.createWalletAccount({
thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
password: process.env.WALLET_PASSWORD,
backUpToDynamic: true,
});
console.log('Wallet created:', walletMetadata.accountAddress);
// Persist walletMetadata (durable store) and externalServerKeyShares (secrets vault).
// 5. Sign as the agent's user.
const signature = await client.signMessage({
message: 'Hello from your agent',
walletMetadata,
externalServerKeyShares,
password: process.env.WALLET_PASSWORD,
});
console.log('Signed:', signature);
// 6. Long-running sessions: refresh; past the refresh limit (refreshExp),
// sign in again — no human needed.
try {
const refreshedJwt = await client.refreshAuthToken();
// Overwrite the stored JWT with refreshedJwt.
} catch {
const { jwt: newJwt } = await signIn();
await client.authenticateJwt(newJwt, {
getSessionSignature: (message) => signSessionMessage(message, privateKeyJwk),
});
}
npx tsx --env-file=.env agent.ts