Skip to main content
Agent wallets (also called API wallets) are permissioned signers that hold no funds but can execute Hyperliquid actions on behalf of a master account. With Dynamic’s Node SDK you manage the master wallet server-side via MPC, while agent wallets are lightweight ephemeral keypairs generated with viem. This pattern lets you:
  • Keep funds locked in a secure MPC master account
  • Rotate agent keys without disrupting active trading
  • Give each bot or strategy its own isolated nonce space
  • Preserve consolidated fee tiers and account-level PnL under one master account
If you are building a browser or React Native app where a human user controls the master wallet, see the Hyperliquid SDK integration recipe instead. This recipe is for server-side / agentic use cases where the master wallet is managed programmatically.

How it works

┌──────────────────────────────────────────┐
│  Dynamic Node SDK (MPC)                  │
│  Master wallet — holds funds, signs      │
│  approveAgent once per agent             │
└───────────────────┬──────────────────────┘
                    │ approveAgent()

┌──────────────────────────────────────────┐
│  viem-generated keypair                  │
│  Agent wallet — holds no funds,          │
│  signs all trade orders                  │
└──────────────────────────────────────────┘
Dynamic’s Node SDK creates one MPC wallet per chain — ideal for the master account that secures your funds. Agent wallets are generated with generatePrivateKey from viem and stored in your secrets manager.
The master wallet must be funded on Hyperliquid before approveAgent can be called. Fund it via the Hyperliquid app (mainnet) or testnet app first. The testnet faucet requires a prior mainnet deposit — the simplest path is to use a pre-funded account or test on mainnet with a small amount.

Installation

npm install @dynamic-labs-wallet/node-evm @nktkas/hyperliquid viem

Environment setup

# .env
DYNAMIC_ENVIRONMENT_ID=your-environment-id
DYNAMIC_AUTH_TOKEN=your-auth-token
IS_TESTNET=true

Step 1: Initialize the Dynamic client

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

export const getEvmClient = async () => {
  const client = new DynamicEvmWalletClient({
    environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
  });
  await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);
  return client;
};

Step 2: Create or retrieve the master wallet

For this pattern it makes sense to use a single master wallet per chain — one account that holds funds and signs agent approvals. Create it once, persist the address, and reuse it on every subsequent run.
import { ThresholdSignatureScheme } from '@dynamic-labs-wallet/node';
import { privateKeyToAccount } from 'viem/accounts';
import { getEvmClient } from './client';

// Persist this after first creation (database, env var, etc.)
const MASTER_WALLET_ADDRESS = process.env.MASTER_WALLET_ADDRESS ?? '';

export const getOrCreateMasterWallet = async () => {
  const evmClient = await getEvmClient();

  if (MASTER_WALLET_ADDRESS) {
    return evmClient.getWallet({ accountAddress: MASTER_WALLET_ADDRESS });
  }

  const wallet = await evmClient.createWalletAccount({
    thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
    backUpToClientShareService: true,
  });
  // Store wallet.accountAddress and wallet.walletId in your database
  return wallet;
};

// Export as a viem account for signing Hyperliquid actions
export const getMasterViemAccount = async (masterAddress: string) => {
  const evmClient = await getEvmClient();
  const { derivedPrivateKey } = await evmClient.exportPrivateKey({
    accountAddress: masterAddress,
  });
  // derivedPrivateKey may omit the 0x prefix — normalise it
  const key = derivedPrivateKey?.startsWith('0x')
    ? derivedPrivateKey
    : (`0x${derivedPrivateKey ?? ''}` as `0x${string}`);
  return privateKeyToAccount(key as `0x${string}`);
};
Never log or persist the derived private key. Use getMasterViemAccount to get an in-memory signer only, and discard the key immediately after use.

Step 3: Create or load the agent wallet

import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';

export const createAgentWallet = () => {
  const privateKey = generatePrivateKey();
  const account = privateKeyToAccount(privateKey);
  // Store privateKey in your secrets manager and account.address in your database
  return { privateKey, account };
};
Agent keys should be stored in a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) and looked up by address or bot ID at runtime. The key itself never needs to touch your application database.

Step 4: Approve the agent on Hyperliquid

The approveAgent transaction is signed by the master wallet and recorded on-chain. It only needs to be called once per agent address — checking extraAgents first keeps the function idempotent.
import * as hl from '@nktkas/hyperliquid';
import { getMasterViemAccount } from './masterWallet';

export const approveAgent = async ({
  masterAddress,
  agentAddress,
  agentName,
  isTestnet = false,
}: {
  masterAddress: string;
  agentAddress: string;
  agentName: string;
  isTestnet?: boolean;
}) => {
  const infoClient = new hl.InfoClient({
    transport: new hl.HttpTransport({ isTestnet }),
  });

  // Idempotency check — skip if already approved
  const existingAgents = await infoClient.extraAgents({
    user: masterAddress as `0x${string}`,
  });
  const alreadyApproved = existingAgents.some(
    (a) => a.address.toLowerCase() === agentAddress.toLowerCase(),
  );
  if (alreadyApproved) {
    console.log(`Agent ${agentAddress} is already approved, skipping.`);
    return;
  }

  const masterAccount = await getMasterViemAccount(masterAddress);
  const masterHlClient = new hl.ExchangeClient({
    transport: new hl.HttpTransport({ isTestnet }),
    wallet: masterAccount,
  });
  await masterHlClient.approveAgent({
    agentAddress: agentAddress as `0x${string}`,
    agentName,
  });
};

Setting an expiry on the agent

Encode an expiry timestamp directly in the agent name using Hyperliquid’s valid_until convention. Hyperliquid enforces this server-side.
export const approveAgentWithExpiry = async ({
  masterAddress,
  agentAddress,
  agentName,
  ttlMs = 24 * 60 * 60 * 1000, // 24 hours
  isTestnet = false,
}: {
  masterAddress: string;
  agentAddress: string;
  agentName: string;
  ttlMs?: number;
  isTestnet?: boolean;
}) => {
  const masterAccount = await getMasterViemAccount(masterAddress);
  const masterHlClient = new hl.ExchangeClient({
    transport: new hl.HttpTransport({ isTestnet }),
    wallet: masterAccount,
  });
  const expirationMs = Date.now() + ttlMs;
  await masterHlClient.approveAgent({
    agentAddress: agentAddress as `0x${string}`,
    agentName: `${agentName} valid_until ${expirationMs}`,
  });
};

Step 5: Trade with the agent wallet

Once approved, the agent wallet signs all orders directly — the master wallet is not involved in individual trades.
import * as hl from '@nktkas/hyperliquid';
import { privateKeyToAccount } from 'viem/accounts';

export const placeLimitOrder = async ({
  agentPrivateKey,
  asset,
  isBuy,
  price,
  size,
  isTestnet = false,
}: {
  agentPrivateKey: string;
  asset: number;    // Hyperliquid universe index (e.g. 0 for BTC-USD perp)
  isBuy: boolean;
  price: string;
  size: string;
  isTestnet?: boolean;
}) => {
  const agentAccount = privateKeyToAccount(agentPrivateKey as `0x${string}`);
  const agentClient = new hl.ExchangeClient({
    transport: new hl.HttpTransport({ isTestnet }),
    wallet: agentAccount,
  });
  return agentClient.order({
    orders: [
      {
        a: asset,
        b: isBuy,
        p: price,
        s: size,
        r: false,
        t: { limit: { tif: 'Gtc' } },
      },
    ],
    grouping: 'na',
  });
};

Listing active agents

import * as hl from '@nktkas/hyperliquid';

export const listActiveAgents = async ({
  masterAddress,
  isTestnet = false,
}: {
  masterAddress: string;
  isTestnet?: boolean;
}) => {
  const infoClient = new hl.InfoClient({
    transport: new hl.HttpTransport({ isTestnet }),
  });
  return infoClient.extraAgents({ user: masterAddress as `0x${string}` });
};

Putting it all together

import { getOrCreateMasterWallet } from './masterWallet';
import { createAgentWallet } from './agentWallet';
import { approveAgent } from './approveAgent';
import { listActiveAgents, placeLimitOrder } from './trade';

const IS_TESTNET = process.env.IS_TESTNET === 'true';

// 1. Get or create the MPC master wallet
const master = await getOrCreateMasterWallet();
const masterAddress = master.accountAddress;

// 2. Create a new agent keypair (store privateKey in secrets manager)
const agent = createAgentWallet();

// 3. Approve the agent on Hyperliquid (idempotent)
await approveAgent({
  masterAddress,
  agentAddress: agent.account.address,
  agentName: 'MyTradingBot',
  isTestnet: IS_TESTNET,
});

// 4. List active agents
const agents = await listActiveAgents({ masterAddress, isTestnet: IS_TESTNET });
console.log('Active agents:', agents);

// 5. Place an order through the agent
await placeLimitOrder({
  agentPrivateKey: agent.privateKey,
  asset: 0,          // BTC-USD perp
  isBuy: true,
  price: '95000',
  size: '0.001',
  isTestnet: IS_TESTNET,
});

Key concepts

ConceptDetail
One master wallet per environmentDynamic’s MPC SDK enforces one wallet per chain. Create once, persist the address, reuse forever.
Agent wallets don’t hold fundsAll balances stay in the master account. Agents are approved signers only.
Idempotent approvalCheck extraAgents before calling approveAgent to avoid redundant on-chain transactions.
Agent expiryEncode expiry in the agent name as valid_until <unixMs>. Hyperliquid enforces this server-side.
Key rotationGenerate a new agent keypair, approve it, and stop using the old one. No funds move.
Nonce isolationEach agent has its own nonce sequence — multiple bots can run concurrently without conflicts.