gas sponsorship quickstart.md

ZeroDev Gas Sponsorship Quickstart

Set up gasless EVM transactions with ZeroDev (ERC-4337) so your app pays gas for users

With ZeroDev and Dynamic, you can sponsor gas so your users don't pay transaction fees. This page is the minimal path to get sponsorship working.

Prerequisites

1. Add the ZeroDev extension

Install the package and add the extension when creating your client. You need the EVM extension as well:

theme={"system"}
npm install @dynamic-labs-sdk/zerodev @dynamic-labs-sdk/evm
theme={"system"}
import { createDynamicClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";
import { addZerodevExtension } from "@dynamic-labs-sdk/zerodev";

const dynamicClient = createDynamicClient({
  environmentId: "YOUR_ENVIRONMENT_ID",
});

addEvmExtension();
addZerodevExtension();

See Adding ZeroDev Extension for details.

2. Send a sponsored transaction

Use sendUserOperation with a wallet account. Sponsorship is on by default when you use a wallet account, so your users don't pay gas:

theme={"system"}
import { sendUserOperation } from "@dynamic-labs-sdk/zerodev";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { getPrimaryWalletAccount } from "@dynamic-labs-sdk/client";
import { parseEther } from "viem";

const walletAccount = getPrimaryWalletAccount();

if (walletAccount && isEvmWalletAccount(walletAccount)) {
  const receipt = await sendUserOperation({
    walletAccount,
    calls: [
      {
        to: "0xRecipientAddress...",
        value: parseEther("0.01"),
        data: "0x",
      },
    ],
  });
  console.log("Sponsored tx sent:", receipt.userOpHash);
}

That's the basic flow: enable in dashboard → add extension → call sendUserOperation with a wallet account.

Optional: check if an operation can be sponsored

To show different UI when sponsorship isn't available (e.g. unsupported network or paymaster limit), use canSponsorUserOperation before sending:

theme={"system"}
import { canSponsorUserOperation, sendUserOperation } from "@dynamic-labs-sdk/zerodev";
import type { BatchCall } from "@dynamic-labs-sdk/zerodev";
import { parseEther } from "viem";

const calls: BatchCall[] = [
  { to: "0xRecipientAddress...", value: parseEther("0.01"), data: "0x" },
];

const canSponsor = await canSponsorUserOperation({ walletAccount, calls });

if (canSponsor) {
  await sendUserOperation({ walletAccount, calls });
} else {
  // Fallback: let user pay gas, or show a message
}

See canSponsorUserOperation for details.

When sponsorship is used

Scenario Sponsored?
sendUserOperation with walletAccount (default) Yes (withSponsorship: true)
sendUserOperation with withSponsorship: false No — user pays gas
sendUserOperation with kernelClient only Depends on how the kernel client was created

React

In React, set up the extensions at module level and call sendUserOperation inside a button handler:

theme={"system"}
// dynamicClient.ts — module-level setup
import { createDynamicClient } from '@dynamic-labs-sdk/client';
import { addEvmExtension } from '@dynamic-labs-sdk/evm';
import { addZerodevExtension } from '@dynamic-labs-sdk/zerodev';

export const dynamicClient = createDynamicClient({ environmentId: 'YOUR_ENVIRONMENT_ID' });
addEvmExtension();
addZerodevExtension();
theme={"system"}
// SponsoredSendButton.tsx
import { sendUserOperation, canSponsorUserOperation } from '@dynamic-labs-sdk/zerodev';
import { isEvmWalletAccount } from '@dynamic-labs-sdk/evm';
import { useGetWalletAccounts } from '@dynamic-labs-sdk/react-hooks';
import type { BatchCall } from '@dynamic-labs-sdk/zerodev';
import { useState } from 'react';
import { parseEther } from 'viem';

function SponsoredSendButton({ recipientAddress }) {
  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const walletAccount = walletAccounts.find(isEvmWalletAccount);
  const [opHash, setOpHash] = useState('');

const handleSend = async () => {
    if (!walletAccount) return;

const calls: BatchCall[] = [
      { to: recipientAddress, value: parseEther('0.01'), data: '0x' },
    ];
    const canSponsor = await canSponsorUserOperation({ walletAccount, calls });

if (canSponsor) {
      const receipt = await sendUserOperation({ walletAccount, calls });
      setOpHash(receipt.userOpHash);
    } else {
      console.warn('Sponsorship not available for this operation');
    }
  };

return (
    <div>
      <button onClick={handleSend} disabled={!walletAccount}>Send (Gasless)</button>
      {opHash && <p>Op hash: {opHash}</p>}
    </div>
  );
}