This guide assumes you are using Zerodev as the natively integrated provider. Please refer to our other guides for other providers as needed.

Dashboard Settings Overview

7702 vs 4337

  • Default (7702) — embedded wallet delegates to a smart account at the same address.
  • Legacy (4337) — smart account deployed at a different address.
Learn more in our 7702 section.
Advanced: You can specify a bundler/paymaster RPC. See specifying a bundler/paymaster RPC for more details.

New Users Only vs All Users

  • New users only — Issue smart wallets only for users created after this setting is enabled.
  • All users — Issue smart wallets for all users. Existing users will receive a smart wallet the next time they log in.

Wallet vs Signer (Legacy - 4337 settings)

  • Show Smart Wallet only - Interact only with the smart-contract wallet in the SDK. Users will need to send assets to an external wallet if exporting their key.
  • Show Smart Wallet & Signer - Display the smart wallet and signer wallet as separate wallets in the SDK, allowing switching between them to more easily export assets.

Advanced Configuration

You can utilize the full functionality of ZeroDev inside Dynamic - everything from session keys to gas policies. Learn more in the ZeroDev Docs.
This section is currently React only.

Delegate to a custom smart account

In order to delegate to a custom smart account, you need to sign the delegation authorization manually like so:
import { useDynamicContext} from '@dynamic-labs/sdk-react-core';
import { isDynamicWaasConnector, isEthereumWallet } from '@dynamic-labs/wallet-connector-core';

const { primaryWallet } = useDynamicContext();
  const handleAuthorize = async () => {
    const connector = primaryWallet?.connector;
    if (!primaryWallet || !isEthereumWallet(primaryWallet)) {
      console.log('Primary wallet is not available or not an Ethereum wallet');
      return null;
    }

    if (!connector || !isDynamicWaasConnector(connector)) {
      console.log('Connector is not a WaaS wallet connector');
      return null;
    }

    try {
      console.log('Starting authorization process...');

      const smartAccountAddress = '0xd6CEDDe84be40893d153Be9d467CD6aD37875b28';
      const sepoliaChainId = 11155111;

      const walletClient = await primaryWallet.getWalletClient();

      console.log('Signing authorization for', smartAccountAddress);
      const authorization = await connector.signAuthorization({
        address: smartAccountAddress,
        chainId: sepoliaChainId,
        nonce: 0,
      });

      console.log('Authorization signed:', authorization);

      const connectorAddress = await connector.getAddress();
      console.log('Connector address:', connectorAddress);

      const hash = await walletClient.sendTransaction({
        authorizationList: [authorization],
        to: connectorAddress as `0x${string}`,
        value: BigInt(0),
      });

      console.log('Transaction hash:', hash);
      return hash;
    } catch (err) {
      console.error('Error during authorization:', err);
      return null;
    }
  };

Bundled transactions

For advanced use cases like bundling multiple transactions into a single user operation, you can interact directly with the kernel client.
const connector = primaryWallet?.connector;

const kernelClient = connector.getAccountAbstractionProvider({
  withSponsorship: true,
});

const userOpHash = await kernelClient.sendUserOperation({
  callData: await kernelClient.account.encodeCalls([
    {
      data: "0x",
      to: zeroAddress,
      value: BigInt(0),
    },
    {
      data: "0x",
      to: zeroAddress,
      value: BigInt(0),
    },
  ]),
});
Note that there is a delay between loading the page and the ZeroDev kernel client being available. To ensure that the kernel client is available, please await one of the following methods: getAddress(), getConnectedAccounts() or getNetwork() before calling getAccountAbstractionProvider().

Specifying a bundler/paymaster RPC

Use ZeroDevSmartWalletConnectorsWithConfig and pass in values for bundlerRpc and paymasterRpc:
import { ZeroDevSmartWalletConnectorsWithConfig } from "@dynamic-labs/ethereum-aa";

<DynamicContextProvider
  settings={{
    environmentId: "YOUR_ENV_ID",
    walletConnectors: [
      ZeroDevSmartWalletConnectorsWithConfig({
        bundlerRpc: "CUSTOM_BUNDLER_RPC",
        paymasterRpc: "CUSTOM_PAYMASTER_RPC",
      }),
    ],
  }}
>
  {/* ... your app */}
</DynamicContextProvider>;
For more info, see: Pimlico Paymaster documentation

Specifying a bundler

To specify a bundler, use ZeroDevSmartWalletConnectorsWithConfig and pass in a value for bundlerProvider:
import { ZeroDevSmartWalletConnectorsWithConfig } from "@dynamic-labs/ethereum-aa";

<DynamicContextProvider
  settings={{
    environmentId: "YOUR_ENV_ID",
    walletConnectors: [
      ZeroDevSmartWalletConnectorsWithConfig({ bundlerProvider: "STACKUP" }),
    ],
  }}
>
  {/* ... your app */}
</DynamicContextProvider>;
For more info, see: https://docs.zerodev.app/meta-infra/rpcs#bundler—paymaster-rpcs

Retrieving the Kernel Client

import { isZeroDevConnector } from '@dynamic-labs/ethereum-aa';

const App = () => {
  const { primaryWallet } = useDynamicContext();

  useEffect(() => {
    const { connector } = primaryWallet;

    const getKernelClient = async () => {
      if (!isZeroDevConnector(connector)) {
        return;
      }

      // ensure that the kernel client has been loaded successfully
      await connector.getNetwork();

      const params = {
        // if you have gas sponsorship enabled, set `withSponsorship` to `true`, else omit
        withSponsorship: true
      };
      const kernelClient = connector.getAccountAbstractionProvider(params);
    }
  ...
}

Using with Viem & Ethers

You can use viem or ethers with account abstraction to sign messages or send sponsored transaction with no extra configuration, it also works with our wagmi integration.

Going Further

Once you’ve tested things out and want to deploy to a live network, you will need to do the following:
  1. Add your credit card to ZeroDev under Account Settings > Billing
  2. Create a new ZeroDev project and select a live network
  3. Copy your new ZeroDev project id and paste it into your Dynamic Dashboard a. We recommend using your Dynamic Sandbox environment for testing your testnet setup, and using your Dynamic Live environment for production.

Restricting Access to your ZeroDev Project

In order to restrict access to your ZeroDev project id to allow only dynamic to use it you can add dynamic’s static IP address’s to your projects IP allowlist. See Dynamic’s Static IP Addresses for more information. ZeroDev Access Control

Examples

This section is currently React only.

[Legacy 4337] Get smart wallet address vs signer address

For Legacy (4337) implementations, the wallet connector will return your smart wallet address, that address will be used in the Dynamic UI and is the main address you will interact with. But you can fetch the signer address by using the wallet connector’s eoaConnector property and then fetching the address there.
This example only applies to Legacy (4337) implementations. With 7702, the smart wallet and signer addresses are the same.
import { useEffect, useState } from "react";
import {
  useDynamicContext,
  DynamicContextProvider,
  DynamicWidget,
} from "@dynamic-labs/sdk-react-core";
import {
  isZeroDevConnector,
  ZeroDevSmartWalletConnectors,
} from "@dynamic-labs/ethereum-aa";
import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";

const SignerAddress = () => {
  const { primaryWallet } = useDynamicContext();
  const [signerAddress, setSignerAddress] = useState("");

  useEffect(() => {
    if (!primaryWallet) {
      return;
    }

    const {
      connector,
       address, // This is your smart wallet address
    } = primaryWallet;

    if (!isZeroDevConnector(connector)) {
      return;
    }

    const signerConnector = connector.eoaConnector;

    if (!signerConnector) {
      return;
    }

    const getAddress = async () => {
      const address = await signerConnector.getAddress();

      if (!address) {
        return;
      }

      setSignerAddress(address);
    };
    getAddress();
  }, [primaryWallet]);

  return <span>My Signer address: {signerAddress}</span>;
};

const App = () => (
  <DynamicContextProvider
    settings={{
      environmentId: "YOUR_ENVIRONMENT_ID",
      walletConnectors: [
        EthereumWalletConnectors,
        ZeroDevSmartWalletConnectors,
      ],
    }}
  >
    <DynamicWidget />

    <SignerAddress />
  </DynamicContextProvider>
);

export default App;
For more information about ZeroDev’s AA features, go to ZeroDev’s documentation

Using ERC20 Paymaster

When you need to sponsor transactions using ERC20 tokens instead of native gas tokens, you’ll need to use the ERC20 paymaster functionality. This requires creating the Kernel client directly and handling the ERC20 approval for the paymaster.
ERC20 paymaster requires additional setup beyond the standard sponsored transactions. You’ll need to create the Kernel client manually and handle token approvals.
import { FC, FormEventHandler, useState } from 'react';
import { getERC20PaymasterApproveCall } from '@zerodev/sdk';
import { createZeroDevPaymasterClient } from '@zerodev/sdk/clients';
import { getEntryPoint, KERNEL_V3_3 } from '@zerodev/sdk/constants';
import { Hex, http, parseEther } from 'viem';
import { createEcdsaKernelAccountClientWith7702 } from '@dynamic-labs/ethereum-aa';
import { chainsMap, isEthereumWallet } from '@dynamic-labs/ethereum-core';
import { useDynamicContext, useSmartWallets } from '@dynamic-labs/sdk-react-core';
import { isDynamicWaasConnector } from '@dynamic-labs/wallet-connector-core';

export const ERC20PaymasterExample: FC = () => {
  const { primaryWallet } = useDynamicContext();
  const { getEOAWallet } = useSmartWallets();

  const [address, setAddress] = useState('');
  const [amount, setAmount] = useState('');
  const [erc20TokenAddress, setErc20TokenAddress] = useState(
    '0x036CbD53842c5426634e7929541eC2318f3dCF7e' // Example token address
  );
  const [result, setResult] = useState('');

  if (!primaryWallet || !isEthereumWallet(primaryWallet)) {
    return <div>ERC20 paymaster is only supported for Ethereum wallets</div>;
  }

  const onSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault();

    try {
      setResult('Creating ZeroDev kernel client...');

      const eoaWallet = getEOAWallet(primaryWallet);
      if (!eoaWallet) throw new Error('No EOA wallet found');
      if (!isDynamicWaasConnector(eoaWallet.connector)) {
        throw new Error('Connector is not a Dynamic Waas connector');
      }

      // Get the signer from the primary wallet
      const signer = await eoaWallet.connector.getWalletClient();
      const network = await primaryWallet.getNetwork();
      if (!network) throw new Error('No network found');

      const chain = chainsMap[network.toString()];
      if (!chain) throw new Error(`Unsupported chain: ${network}`);

      // ZeroDev configuration - replace with your actual project ID
      const projectId = 'YOUR_ZERODEV_PROJECT_ID';
      const entryPoint = getEntryPoint('0.7');

      setResult('Creating kernel client with ERC20 paymaster...');

      // Create the kernel client with EIP-7702 and ERC20 paymaster, use createEcdsaKernelAccountClient if you are not using EIP-7702
      const kernelClient = await createEcdsaKernelAccountClientWith7702({
        apiKernelVersion: KERNEL_V3_3, // Use KERNEL_V3_3 for EIP-7702 compatibility or get this info from our dashboard
        chain,
        enableKernelV3Migration: true, // If you want to migrate users to the new kernel version
        entryPoint,
        kernelVersion: KERNEL_V3_3, // Use KERNEL_V3_3 for EIP-7702 compatibility or get this info from our dashboard
        paymaster: erc20TokenAddress as Hex, // Use your ERC20 token address here
        projectId,
        signer,
      });

      // Create paymaster client for approval
      const paymasterClient = createZeroDevPaymasterClient({
        chain,
        transport: http(`https://rpc.zerodev.app/api/v2/paymaster/${projectId}`),
      });

      setResult('Sending transaction with ERC20 approval...');

      // Prepare the transaction with batched approval
      const value = parseEther(amount);
      const callData = await kernelClient.account.encodeCalls([
        // The ERC20 approval for paymaster
        await getERC20PaymasterApproveCall(paymasterClient, {
          approveAmount: parseEther('0.1'),
          entryPoint,
          gasToken: erc20TokenAddress as Hex,
        }),
        // The actual transaction call
        {
          data: '0x' as Hex,
          to: address as Hex,
          value,
        },
      ]);

      // Send the user operation
      const hash = await kernelClient.sendUserOperation({ callData });
      setResult(`Transaction sent! User operation hash: ${hash}`);

      // Wait for the transaction receipt
      const receipt = await kernelClient.waitForUserOperationReceipt({ hash });
      setResult(`Transaction confirmed! Receipt: ${JSON.stringify(receipt, null, 2)}`);
    } catch (error) {
      setResult(`Transaction failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  };

  return (
    <form onSubmit={onSubmit}>
      <div>
        <label htmlFor="address">Target address</label>
        <input
          value={address}
          onChange={({ target }) => setAddress(target.value)}
          name="address"
          id="address"
          type="text"
          required
          placeholder="0x..."
        />
      </div>

      <div>
        <label htmlFor="amount">Transaction amount (in ETH)</label>
        <input
          value={amount}
          onChange={({ target }) => setAmount(target.value)}
          name="amount"
          id="amount"
          type="text"
          required
          placeholder="0.05"
        />
      </div>

      <div>
        <label htmlFor="erc20TokenAddress">ERC20 Paymaster Token Address</label>
        <input
          value={erc20TokenAddress}
          onChange={({ target }) => setErc20TokenAddress(target.value)}
          name="erc20TokenAddress"
          id="erc20TokenAddress"
          type="text"
          required
          placeholder="0xA0b86a33E6441b8b5d865Db8a6E05e70b4cfB4be"
        />
      </div>

      <button type="submit">
        Send with ZeroDev (EIP-7702 + ERC20 Paymaster)
      </button>

      {result && (
        <pre style={{ whiteSpace: 'pre-wrap' }}>
          {result}
        </pre>
      )}
    </form>
  );
};

Key Points for ERC20 Paymaster Implementation:

  1. Manual Kernel Client Creation: Unlike standard sponsored transactions, ERC20 paymaster requires creating the kernel client manually using createEcdsaKernelAccountClientWith7702 or createEcdsaKernelAccountClient.
  2. ERC20 Approval: The paymaster needs approval to spend the ERC20 tokens for gas. Use getERC20PaymasterApproveCall to generate the approval call.
  3. Batched Transactions: Combine the ERC20 approval and your actual transaction into a single user operation using encodeCalls.
  4. Prerequisites:
    • Your wallet must have sufficient ERC20 tokens to pay for gas
    • The ERC20 token must be configured in your ZeroDev paymaster configuration
Make sure to replace YOUR_ZERODEV_PROJECT_ID with your actual ZeroDev project ID. You can find this in your ZeroDev dashboard.

FAQ

Yes, but not today with Dynamic. We are working on developing new flows to make managing existing EOA wallets with SCWs a smooth transition.
It depends which provider you choose. For example, with ZeroDev you have the following options:
  • Arbitrum One
  • Avalanche
  • Base
  • Binance Smart Chain
  • Ethereum
  • Optimism
  • Polygon
Each provider will handle things differently, so it’s always better to check directly with them. For example, with ZeroDev you can’t change the network after deployment. With Alchemy, it might be possible in a roundabout way.
With Dynamic, you will need to use either ZeroDev or Alchemy. If you have alternative AA providers, please reach out via our slack.
This is an advanced feature, but there is no additional cost from Dynamic beyond the advanced tier itself. The providers do take a transaction fee, which you can see on their respective pricing pages.
We are adding customers one at a time for a few weeks, after which we’ll open it up to the general public as GA.
Private keys are managed by the EOA, not the SCW. Every SCW has a Signer, or Owner, which is the EOA.
The only way the SCW can be recovered is if the EOA is recovered. The SCW is a smart contract, and the EOA is the owner of the SCW. If the EOA is lost, the SCW is lost.
It’s a common misconception that AA wallets are inherently non-custodial. In fact, whether a wallet is AA or not has nothing to do with whether it’s custodial. It’s the signers that determine whether a wallet is custodial.That is, if you use a non-custodial signer such as local private keys to manage your AA wallet, then it’s non-custodial. At the same time, if you use a custodial key provider such as Fireblocks to manage your AA wallet, then it’s custodial.In any case, whoever has custody over the signers has custody over the wallet.
We have wrapped our providers so you get all the benefit, and more. For example, we handle the transaction hashing so it’s user friendly and are able to show the SCW in our widgets and connectors.