> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# MetaMask Delegation Framework (EIP-7710) with Dynamic

> Use a Dynamic embedded wallet as a MetaMask smart account delegator: grant scoped authority to an agent or another smart account to execute transactions on your behalf.

## Overview

[EIP-7710](https://eips.ethereum.org/EIPS/eip-7710) is the on-chain delegation standard that underpins the MetaMask Delegation Framework. It lets a smart account (the **delegator**) sign a delegation granting a second account (the **delegate**) the right to execute transactions on its behalf — subject to optional caveats like an ERC-20 spend cap.

The common pattern with Dynamic is a user's embedded wallet acting as delegator and an agent wallet (or another user's embedded wallet) acting as delegate. Both sides are MetaMask smart accounts.

**Signer vs smart account:** Each smart account is controlled by an underlying **signer**—a viem `Account` whose address appears in `walletClient.account`. The signer holds the key material (MPC for embedded wallets). The **smart account address** (`delegatorSmartAccount.address`, `delegateSmartAccount.address`) is the on-chain contract that holds assets and appears in delegation `from` / `to` fields. Do not confuse the two addresses.

This guide uses the JavaScript SDK throughout.

<Note>
  The delegator account must be deployed on-chain before any delegation can be redeemed. The guide covers deployment via a direct factory call — no bundler required on the delegator side.
</Note>

## Stack

| Piece                                    | Role                                                                       |
| ---------------------------------------- | -------------------------------------------------------------------------- |
| `@dynamic-labs-sdk/client`               | Dynamic client, `getPrimaryWalletAccount`                                  |
| `@dynamic-labs-sdk/evm`                  | EVM extensions and type guards                                             |
| `@dynamic-labs-sdk/evm/viem`             | `createWalletClientForWalletAccount`                                       |
| `@metamask/smart-accounts-kit`           | `toMetaMaskSmartAccount`, `createDelegation`, `ScopeType`, `ExecutionMode` |
| `@metamask/smart-accounts-kit/contracts` | `DelegationManager`                                                        |
| `viem` + `viem/account-abstraction`      | Chain types, ABI encoding, bundler client                                  |

## Dynamic's role

The MetaMask Delegation Framework requires a smart account as the delegator. Normally you'd use a plain private key as the signer for that smart account — Dynamic replaces that key with an embedded wallet backed by MPC. Everything else (smart accounts, delegation signing, on-chain redemption) is standard MetaMask SDK behaviour, documented in the [MetaMask Smart Accounts Kit guides](https://docs.metamask.io/smart-accounts-kit/guides/delegation/execute-on-smart-accounts-behalf/).

## Setup

<Steps>
  <Step title="Install dependencies">
    <CodeGroup>
      ```bash npm theme={"system"}
      npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @metamask/smart-accounts-kit viem
      ```

      ```bash yarn theme={"system"}
      yarn add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @metamask/smart-accounts-kit viem
      ```

      ```bash pnpm theme={"system"}
      pnpm add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @metamask/smart-accounts-kit viem
      ```
    </CodeGroup>
  </Step>

  <Step title="Initialize Dynamic and get the embedded wallet">
    Call `addWaasEvmExtension()` immediately after creating the client to register the embedded wallet provider.

    ```typescript theme={"system"}
    import { createDynamicClient, getPrimaryWalletAccount } from '@dynamic-labs-sdk/client';
    import { isEvmWalletAccount } from '@dynamic-labs-sdk/evm';
    import { addWaasEvmExtension } from '@dynamic-labs-sdk/evm/waas';
    import { createWalletClientForWalletAccount } from '@dynamic-labs-sdk/evm/viem';
    import { createPublicClient, http } from 'viem';
    import { sepolia } from 'viem/chains';

    createDynamicClient({ environmentId: 'YOUR_ENVIRONMENT_ID' });
    addWaasEvmExtension();

    // After the user authenticates:
    const walletAccount = getPrimaryWalletAccount();
    if (!walletAccount || !isEvmWalletAccount(walletAccount)) {
      throw new Error('No EVM embedded wallet found');
    }

    const walletClient = await createWalletClientForWalletAccount({ walletAccount });
    const publicClient = createPublicClient({ chain: sepolia, transport: http() });
    ```

    * **`publicClient`** — read-only RPC (bytecode checks, receipts).
    * **`walletClient`** — signs and sends transactions as the user’s embedded wallet (`walletClient.account` is `signerAccount` in the next step).
  </Step>

  <Step title="Wrap signers as MetaMask smart accounts">
    `toMetaMaskSmartAccount` turns a viem signer into a MetaMask **Hybrid** smart account. `Implementation.Hybrid` supports an EOA signer (your embedded wallet) controlling a contract wallet.

    | Field            | Meaning                                                                                                                                                                              |
    | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | `deployParams`   | Constructor args for the smart account. For Hybrid: `[ownerAddress, validators, hooks, modules]`. Use the signer’s address as owner; empty arrays `[]` accept defaults for the rest. |
    | `deploySalt`     | Salt for deterministic deployment. `'0x'` uses the default.                                                                                                                          |
    | `signer.account` | The viem account that signs UserOps and delegation EIP-712 payloads. For embedded wallets, signing routes through Dynamic’s MPC key share.                                           |

    ### Delegator (user embedded wallet)

    `signerAccount` is the authenticated user’s embedded wallet—the account behind `walletClient` from the previous step.

    ```typescript theme={"system"}
    import { toMetaMaskSmartAccount, Implementation } from '@metamask/smart-accounts-kit';

    const signerAccount = walletClient.account;
    if (!signerAccount) throw new Error('walletClient has no account');

    const delegatorSmartAccount = await toMetaMaskSmartAccount({
      client: publicClient,
      implementation: Implementation.Hybrid,
      deployParams: [signerAccount.address, [], [], []],
      deploySalt: '0x',
      signer: { account: signerAccount },
    });
    ```

    `delegatorSmartAccount.address` is the on-chain delegator contract address (not `signerAccount.address`).

    ### Delegate signer (`agentSignerAccount`)

    The **delegate** redeems the delegation—often a backend **agent** or a second embedded wallet. It needs its own viem signer, same shape as `signerAccount`.

    **`agentSignerAccount`** is that delegate signer: `agentWalletClient.account` from whichever wallet backs your agent. Common sources:

    * A **server wallet** you create for the agent (see [Agents overview](/docs/overview/agents/overview))
    * A second **embedded wallet** authenticated in your app
    * Any viem `Account` / `PrivateKeyAccount` you control

    Obtain it the same way as the user wallet—`createWalletClientForWalletAccount` for embedded wallets, or your server wallet’s viem client:

    ```typescript theme={"system"}
    // Example: agent backed by a server or second embedded wallet
    const agentWalletClient = await createWalletClientForWalletAccount({
      walletAccount: agentWalletAccount, // from getPrimaryWalletAccount() or your server wallet setup
    });

    const agentSignerAccount = agentWalletClient.account;
    if (!agentSignerAccount) throw new Error('Agent walletClient has no account');
    ```

    ### Delegate smart account

    Wrap `agentSignerAccount` the same way as the delegator:

    ```typescript theme={"system"}
    const delegateSmartAccount = await toMetaMaskSmartAccount({
      client: publicClient,
      implementation: Implementation.Hybrid,
      deployParams: [agentSignerAccount.address, [], [], []],
      deploySalt: '0x',
      signer: { account: agentSignerAccount },
    });
    ```

    `delegateSmartAccount.address` is what you pass to `createDelegation` as `to`.
  </Step>

  <Step title="Deploy the delegator smart account">
    Counterfactual smart accounts have no bytecode until deployed. The **DelegationManager** verifies delegations by calling `isValidSignature` on the delegator contract, so that contract must exist on-chain before redemption.

    `getFactoryArgs()` returns the MetaMask factory `to` address and `data` calldata. A single `sendTransaction` from the user’s `walletClient` deploys the contract—no bundler on the delegator side.

    ```typescript theme={"system"}
    const code = await publicClient.getCode({ address: delegatorSmartAccount.address });

    if (!code || code === '0x') {
      const { factory, factoryData } = await delegatorSmartAccount.getFactoryArgs();
      if (!factory || !factoryData) throw new Error('Factory args unavailable');

      await walletClient.sendTransaction({ to: factory, data: factoryData, value: 0n });
      console.log('Delegator deployed:', delegatorSmartAccount.address);
    }
    ```
  </Step>

  <Step title="Create and sign a delegation">
    `createDelegation` builds the unsigned delegation struct. Fields that often need clarification:

    | Field         | Meaning                                                                                                                                                                            |
    | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `from` / `to` | Smart account addresses (`delegatorSmartAccount.address` → `delegateSmartAccount.address`), not signer EOAs.                                                                       |
    | `environment` | Chain-specific MetaMask contract addresses (including DelegationManager). Copy from `delegatorSmartAccount.environment` so the delegation targets the same network as the account. |
    | `scope`       | On-chain **caveat** limiting what the delegate may do (here, max ERC-20 transfer per redemption).                                                                                  |

    `signDelegation` signs the struct as EIP-712 typed data through the delegator’s MPC key. **`signedDelegation`** is `{ ...delegation, signature }`—store or pass it to the delegate for redemption. The DelegationManager checks this signature on-chain.

    ```typescript theme={"system"}
    import { createDelegation, ScopeType } from '@metamask/smart-accounts-kit';
    import { parseUnits } from 'viem';

    const TOKEN_ADDRESS = '0xTOKEN_ADDRESS';

    const delegation = createDelegation({
      from: delegatorSmartAccount.address,
      to: delegateSmartAccount.address,
      environment: delegatorSmartAccount.environment,
      scope: {
        type: ScopeType.Erc20TransferAmount,
        tokenAddress: TOKEN_ADDRESS,
        maxAmount: parseUnits('10', 6), // delegate can transfer up to 10 USDC
      },
    });

    const signature = await delegatorSmartAccount.signDelegation({ delegation });
    const signedDelegation = { ...delegation, signature };
    ```

    <Note>
      For an unrestricted delegation, use `createOpenDelegation` instead. The `scope` field is required on `createDelegation`. Unrestricted delegations should only be used in trusted contexts such as agent wallets you control.
    </Note>
  </Step>

  <Step title="Redeem the delegation (delegate side)">
    The delegate smart account submits an ERC-4337 **UserOperation** (via a **bundler** RPC). The UserOp calls `redeemDelegations` on the delegate contract; the **DelegationManager** validates `signedDelegation` and executes the inner call (for example the ERC-20 `transfer`) **on the delegator’s behalf**.

    ```typescript theme={"system"}
    import { createExecution, ExecutionMode } from '@metamask/smart-accounts-kit';
    import { DelegationManager } from '@metamask/smart-accounts-kit/contracts';
    import { encodeFunctionData, parseUnits, erc20Abi } from 'viem';
    import { createBundlerClient } from 'viem/account-abstraction';
    import { http } from 'viem';

    const bundlerClient = createBundlerClient({
      client: publicClient,
      transport: http('YOUR_BUNDLER_RPC'),
    });

    const callData = encodeFunctionData({
      abi: erc20Abi,
      functionName: 'transfer',
      args: ['0xRECIPIENT', parseUnits('5', 6)],
    });

    // createExecution = one call on the delegator's behalf; ExecutionMode.SingleDefault = one batch
    const redeemCalldata = DelegationManager.encode.redeemDelegations({
      delegations: [[signedDelegation]],
      modes: [ExecutionMode.SingleDefault],
      executions: [[createExecution({ target: TOKEN_ADDRESS, callData })]],
    });

    const userOpHash = await bundlerClient.sendUserOperation({
      account: delegateSmartAccount,
      calls: [
        {
          to: delegateSmartAccount.address,
          data: redeemCalldata,
          value: 0n,
        },
      ],
    });

    await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash });
    console.log('Delegation redeemed');
    ```

    <Tip>
      Use a paymaster on the bundler client to sponsor gas for the delegate. See the [Pimlico](https://docs.pimlico.io) or [Alchemy](https://docs.alchemy.com/docs/rundler) docs for paymaster setup.
    </Tip>

    <Accordion title="EOA delegate (simpler alternative)">
      If the delegate is a plain EOA rather than a smart account, it sends a regular transaction to the DelegationManager directly — no bundler needed.

      **`delegateWalletClient`** is a viem `WalletClient` for that EOA (same pattern as `walletClient`, but using the delegate’s account as `account`). The delegate signs and sends the redemption transaction itself.

      ```typescript theme={"system"}
      import { getSmartAccountsEnvironment } from '@metamask/smart-accounts-kit';
      import { sepolia } from 'viem/chains';

      const redeemCalldata = DelegationManager.encode.redeemDelegations({
        delegations: [[signedDelegation]],
        modes: [ExecutionMode.SingleDefault],
        executions: [[createExecution({ target: TOKEN_ADDRESS, callData })]],
      });

      await delegateWalletClient.sendTransaction({
        to: getSmartAccountsEnvironment(sepolia.id).DelegationManager,
        data: redeemCalldata,
        chain: sepolia,
        account: delegateWalletClient.account,
      });
      ```
    </Accordion>
  </Step>
</Steps>

## Revoking a delegation

Send a UserOp from the delegator's smart account to disable the delegation on-chain. Reuse the same `bundlerClient` and `publicClient` from the redemption step.

```typescript theme={"system"}
import { DelegationManager } from '@metamask/smart-accounts-kit/contracts';
import { getSmartAccountsEnvironment } from '@metamask/smart-accounts-kit';
import { sepolia } from 'viem/chains';

const revokeCalldata = DelegationManager.encode.disableDelegation({
  delegation: signedDelegation,
});

await bundlerClient.sendUserOperation({
  account: delegatorSmartAccount,
  calls: [
    {
      to: getSmartAccountsEnvironment(sepolia.id).DelegationManager,
      data: revokeCalldata,
      value: 0n,
    },
  ],
});
```

## Available scope types

| `ScopeType`                 | What it constrains                              |
| --------------------------- | ----------------------------------------------- |
| `Erc20TransferAmount`       | Max ERC-20 transfer amount per redemption       |
| `NativeTokenTransferAmount` | Max native token (ETH) value per execution      |
| `AllowedTargets`            | Restrict which contract addresses can be called |
| `AllowedMethods`            | Restrict which function selectors can be called |

A single `scope` sets one caveat. Combine several by passing a `caveats` array on `createDelegation` (each entry uses a `ScopeType` like the table above).

## Network support

Use `getSmartAccountsEnvironment(chainId)` to retrieve contract addresses for the target network — it throws if the chain is unsupported. Check MetaMask's [supported networks list](https://docs.metamask.io/smart-accounts-kit/get-started/supported-networks/) before going to production.

## References

* [MetaMask Smart Accounts Kit docs](https://docs.metamask.io/smart-accounts-kit/)
* [EIP-7710 specification](https://eips.ethereum.org/EIPS/eip-7710)
* [Dynamic JS SDK — Adding EVM extensions](/docs/javascript/reference/evm/adding-evm-extensions)
* [Dynamic JS SDK — Getting a viem WalletClient](/docs/javascript/reference/evm/getting-viem-wallet-client)
* [MetaMask smart account provider (React)](/docs/react/smart-wallets/smart-wallet-providers/metamask)
