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
- Create a Zerodev account and projects for each network you want to support, as well as a gas policy for each one. Make sure you use the V1 dashboard to configure your project.
- Visit the Sponsor Gas section of the dashboard, toggle on Zerodev and add your project IDs.
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>
);
}