When using account abstraction in your project to sponsor gas, batch transactions, or any other account abstraction features, you will likely want to get a ZeroDev kernel client to perform these operations.
You can use the @dynamic-labs/zerodev-extension package to create a ZeroDev kernel client for a wallet.
Here is how you can set up and create a kernel client.
The @dynamic-labs/zerodev-extension depends on the Viem Extension, so before
going through this setup, make sure to have the Viem Extension set up and
working. Viem Extension Setup Setup
Install ZeroDevExtension
Install the @dynamic-labs/zerodev-extension package:
npx expo install @dynamic-labs/zerodev-extension 
Resolve File Resolution Error (Optional)
When running the ZeroDevExtension in your React Native application, you might encounter an error where Metro cannot resolve the paymasterClient.js file. This issue occurs because Metro tries to load paymasterClient.js, but the actual file is named paymasterClient.ts.
To fix this, you need to customize Metro’s resolver in your metro.config.js file to handle TypeScript file extensions properly.
Generate metro.config.js for Expo Projects
If you don’t have a metro.config.js file in your project, you can generate one using the following command:
npx expo customize metro.config.js
Customize Metro Resolver
Add the following code to your metro.config.js file to instruct Metro to resolve .ts files when it cannot find the corresponding .js files:
const { getDefaultConfig } = require('expo/metro-config')
const config = getDefaultConfig(__dirname)
/**
 * Custom resolver to handle ZeroDev imports
 */
config.resolver.resolveRequest = (context, moduleName, platform) => {
  try {
    return context.resolveRequest(context, moduleName, platform)
  } catch (error) {
    if (moduleName.endsWith('.js')) {
      const tsModuleName = moduleName.replace(/\.js$/, '.ts')
      return context.resolveRequest(context, tsModuleName, platform)
    }
    throw error
  }
}
module.exports = config
.ts files when it fails to find .js files, which fixes the resolution error for the paymasterClient file.
Events Polyfill
ZeroDevExtension requires a polyfill for the Node.js events module. You can install the events polyfill using one of the following commands:
Integrate with your Dynamic client
To include the ZeroDev module in your Dynamic client, you need to extend the client with the ZeroDev extension:
import { createClient } from '@dynamic-labs/client'
import { ReactNativeExtension } from '@dynamic-labs/react-native-extension'
import { ViemExtension } from '@dynamic-labs/viem-extension'
import { ZeroDevExtension } from '@dynamic-labs/zerodev-extension'
import 'fast-text-encoding' // Only required for Expo SDK 53 and below
const environmentId = process.env.EXPO_PUBLIC_ENVIRONMENT_ID as string
if (!environmentId) {
  throw new Error('EXPO_PUBLIC_ENVIRONMENT_ID is required')
}
export const client = createClient({
  environmentId,
  appLogoUrl: 'https://demo.dynamic.xyz/favicon-32x32.png',
  appName: 'React Native Stablecoin App',
})
  .extend(ReactNativeExtension())
  .extend(ViemExtension())
  .extend(ZeroDevExtension())
The fast-text-encoding import is only required when using Expo SDK 53 and below. Expo SDK 54 and above have built-in text encoding support.
Usage
Now that you have the ZeroDev module in your Dynamic client, you can get the ZeroDev kernel client by using the client.zeroDev.createKernelClient method. Here is an example:
import { PaymasterTypeEnum } from '@dynamic-labs/ethereum-aa'
import { ZerodevBundlerProvider } from '@dynamic-labs/sdk-api-core'
import { mainnet } from 'viem/chains'
import { client } from './dynamicClient'
export const getPrimaryWalletKernelClient = async () => {
  const primaryWallet = client.wallets.primary
  if (!primaryWallet) {
    throw new Error('No primary wallet')
  }
  try {
    const kernelClient = await client.zeroDev.createKernelClient({
      wallet: primaryWallet,
      chainId: mainnet.id,
      paymaster: PaymasterTypeEnum.SPONSOR,
      bundlerProvider: ZerodevBundlerProvider.Pimlico,
    })
    return kernelClient
  } catch (error) {
    console.error('Failed to create kernel client:', error)
    throw new Error(
      `Failed to create kernel client: ${error instanceof Error ? error.message : 'Unknown error'}`
    )
  }
}
 
You can also use the ZerodevBundlerProvider.Alchemy or ZerodevBundlerProvider.Gelato provider to use the Alchemy or Gelato bundler services.You can either use PaymasterTypeEnum.SPONSOR to sponsor all transactions or PaymasterTypeEnum.NONE to send a non-sponsored transaction.
Examples
Sponsored transactions allow users to interact with your dApp without paying gas fees. The gas costs are covered by your application through a paymaster service.
Here’s how to send a sponsored transaction:
import { encodeFunctionData, parseUnits } from 'viem'
import { getPrimaryWalletKernelClient } from './dynamicClient'
const sendSponsoredTransaction = async () => {
  const kernelClient = await getPrimaryWalletKernelClient()
  // Example: Transfer USDC tokens
  const USDC_CONTRACT = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // ETH Mainnet USDC
  const ERC20_ABI = [
    {
      inputs: [
        { internalType: 'address', name: 'to', type: 'address' },
        { internalType: 'uint256', name: 'amount', type: 'uint256' }
      ],
      name: 'transfer',
      outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
      stateMutability: 'nonpayable',
      type: 'function'
    }
  ]
  const recipientAddress = '0x1234567890123456789012345678901234567890'
  const amount = '10' // 10 USDC
  try {
    const usdcAmount = parseUnits(amount, 6)
    const transferData = encodeFunctionData({
      abi: ERC20_ABI,
      functionName: 'transfer',
      args: [recipientAddress as `0x${string}`, usdcAmount],
    })
    const userOpHash = await kernelClient.sendUserOperation({
      callData: await kernelClient.account.encodeCalls([
        {
          to: USDC_CONTRACT as `0x${string}`,
          value: BigInt(0),
          data: transferData,
        },
      ]),
    })
    console.log('User operation sent, hash:', userOpHash)
    console.log('Waiting for transaction confirmation...')
    const { receipt } = await kernelClient.waitForUserOperationReceipt({
      hash: userOpHash,
    })
    const transactionHash = receipt.transactionHash
    console.log('Transaction completed:', transactionHash)
    return transactionHash
  } catch (error) {
    console.error('Failed to send sponsored transaction:', error)
    throw error
  }
}
import { PaymasterTypeEnum } from '@dynamic-labs/ethereum-aa'
import { ZerodevBundlerProvider } from '@dynamic-labs/sdk-api-core'
import { client } from './dynamicClient'
import { base } from 'viem/chains'
const sendCustomSponsoredTransaction = async () => {
  const primaryWallet = client.wallets.primary
  if (!primaryWallet) {
    throw new Error('No primary wallet')
  }
  // Create kernel client with custom configuration
  const kernelClient = await client.zeroDev.createKernelClient({
    wallet: primaryWallet,
    chainId: base.id, // ETH Mainnet
    paymaster: PaymasterTypeEnum.SPONSOR, // Sponsor all gas costs
    bundlerProvider: ZerodevBundlerProvider.Pimlico, // Use Pimlico bundler
  })
  // Your transaction logic here
  const userOpHash = await kernelClient.sendUserOperation({
    callData: await kernelClient.account.encodeCalls([
      {
        to: '0x1234567890123456789012345678901234567890',
        value: BigInt(0),
        data: '0x', // Your transaction data
      },
    ]),
  })
  return userOpHash
}
Batch Transactions
Batch transactions allow you to execute multiple operations in a single transaction, reducing gas costs and improving user experience.
Batch Transaction Example
This example demonstrates how you can use a ZeroDev kernel client to perform a batched transaction:
import { encodeFunctionData } from 'viem'
import { PaymasterTypeEnum } from '@dynamic-labs/ethereum-aa'
import { ZerodevBundlerProvider } from '@dynamic-labs/sdk-api-core'
import { getPrimaryWalletKernelClient } from './dynamicClient'
import { client } from './dynamicClient'
const contractAddress = '0x123'
const contractABI = [
  {
    inputs: [{ internalType: 'address', name: '_to', type: 'address' }],
    name: 'mint',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
]
const sendBatchedTransactions = async () => {
  const primaryWallet = client.wallets.primary
  if (!primaryWallet) {
    throw new Error('No primary wallet')
  }
  const kernelClient = await getPrimaryWalletKernelClient()
  const { account } = kernelClient
  const hash = await kernelClient.sendUserOperation({
    callData: await account.encodeCalls([
      {
        data: encodeFunctionData({
          abi: contractABI,
          args: [primaryWallet.address],
          functionName: 'mint',
        }),
        to: contractAddress,
        value: BigInt(0),
      },
      {
        data: encodeFunctionData({
          abi: contractABI,
          args: [primaryWallet.address],
          functionName: 'mint',
        }),
        to: contractAddress,
        value: BigInt(0),
      },
    ]),
  })
  return hash
}