Skip to main content

Overview

Delegated access allows your server to perform wallet operations (signing messages and transactions) on behalf of users with their permission. This is useful for automating workflows while maintaining security through user-approved delegation. This guide covers the server-side implementation using the Node EVM SDK. For conceptual background on delegated access and the delegation workflow, see Delegated Access Overview.

Prerequisites

Before implementing delegated access, you need:

How Delegation Works

  1. User approves delegation request in your application (client-side)
  2. Dynamic sends delegation credentials to your webhook endpoint:
    • walletId: The user’s wallet ID
    • walletApiKey: Temporary API key for this wallet
    • keyShare: Server key share for signing operations
  3. Your server stores these credentials securely
  4. Your server uses these credentials to perform signing operations

Creating the Delegated Client

First, create a delegated EVM wallet client using your server API key:
import { createDelegatedEvmWalletClient } from '@dynamic-labs-wallet/node-evm';

const delegatedClient = createDelegatedEvmWalletClient({
  environmentId: 'your-environment-id',
  apiKey: 'your-server-api-key',
});

Configuration Options

const delegatedClient = createDelegatedEvmWalletClient({
  environmentId: 'your-environment-id', // Required
  apiKey: 'your-server-api-key', // Required
  baseMPCRelayApiUrl: 'https://mpc-relay.dynamic.xyz', // Optional: custom MPC relay URL
});

Signing Messages

Use the delegated client to sign messages on behalf of users. You’ll need the delegation credentials received from your webhook.

Basic Message Signing

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

const signature = await delegatedSignMessage(delegatedClient, {
  walletId: 'wallet-id-from-webhook',
  walletApiKey: 'wallet-api-key-from-webhook',
  keyShare: keyShareFromWebhook, // ServerKeyShare object
  message: 'Hello, World!',
});

console.log('Message signed:', signature);

Signing Transactions

Sign EVM transactions using delegated access. The transaction must be a valid Viem TransactionSerializable object.

Basic Transaction Signing

import { delegatedSignTransaction } from '@dynamic-labs-wallet/node-evm';
import type { TransactionSerializable } from 'viem';

const transaction: TransactionSerializable = {
  to: '0xRecipientAddress',
  value: BigInt('1000000000000000000'), // 1 ETH in wei
  chainId: 1
};

const signedTx = await delegatedSignTransaction(delegatedClient, {
  walletId: 'wallet-id-from-webhook',
  walletApiKey: 'wallet-api-key-from-webhook',
  keyShare: keyShareFromWebhook,
  transaction,
});

console.log('Signed transaction:', signedTx);

Complete Transaction Flow

import { delegatedSignTransaction } from '@dynamic-labs-wallet/node-evm';
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(),
});

const transaction = {
  to: '0xRecipientAddress',
  value: BigInt('1000000000000000000'),
  chainId: 1,
};

const nonce = await publicClient.getTransactionCount({
  address: walletAddress,
});

const gasEstimate = await publicClient.estimateGas({
  account: walletAddress,
  to: transaction.to,
  value: transaction.value,
});

const fullTransaction = {
  ...transaction,
  nonce,
  gasLimit: gasEstimate,
  maxFeePerGas: BigInt('20000000000'),
  maxPriorityFeePerGas: BigInt('1000000000'),
};

const signedTx = await delegatedSignTransaction(delegatedClient, {
  walletId: 'wallet-id-from-webhook',
  walletApiKey: 'wallet-api-key-from-webhook',
  keyShare: keyShareFromWebhook,
  transaction: fullTransaction,
});

const txHash = await publicClient.sendRawTransaction({
  serializedTransaction: signedTx as `0x${string}`,
});

console.log('Transaction hash:', txHash);

Security Considerations

Credential Storage

  • Never log or expose delegation credentials (wallet API key, key share)
  • Store credentials encrypted at rest in your database
  • Use secure environment variables for server API keys
  • Implement credential rotation policies

Best Practices

class DelegatedWalletService {
  private client: DelegatedEvmWalletClient;

  constructor() {
    this.client = createDelegatedEvmWalletClient({
      environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
      apiKey: process.env.DYNAMIC_API_KEY!,
      debug: process.env.NODE_ENV === 'development',
    });
  }

  async signMessage(userId: string, message: string): Promise<string> {
    const credentials = await this.getEncryptedCredentials(userId);
    
    if (!credentials) {
      throw new Error('No delegation credentials found');
    }

    try {
      const signature = await delegatedSignMessage(this.client, {
        walletId: credentials.walletId,
        walletApiKey: credentials.walletApiKey,
        keyShare: credentials.keyShare,
        message,
      });

      await this.auditLog({
        userId,
        action: 'sign_message',
        message,
        timestamp: new Date(),
      });

      return signature;
    } finally {
      this.clearSensitiveData(credentials);
    }
  }

  private async getEncryptedCredentials(userId: string) {
    // Implementation (important-comment)
  }

  private clearSensitiveData(credentials: any) {
    // Implementation (important-comment)
  }

  private async auditLog(data: any) {
    // Implementation (important-comment)
  }
}
I