evm gas sponsorship.md
EVM Gas Sponsorship
Sponsor EVM transaction fees for your users with Dynamic's built-in gas sponsorship feature.
EVM Gas Sponsorship is an enterprise-only feature. Contact us to learn more about upgrading your plan.
Normally, a user needs to hold some of a network's native token (like ETH) to pay the "gas" fee on every transaction. Gas sponsorship lets your app pay those fees instead, so users can transact without ever topping up a wallet. This is one of the most common ways to remove friction for people new to crypto.
Dynamic has gas sponsorship built in. For the basic case you don't need to understand relayers, delegation, or any of the underlying mechanics — flip a switch in the dashboard and call one function.
Quick start
This is everything you need to sponsor a transaction. The SDK handles the underlying setup for you automatically.
Steps
Turn on gas sponsorship in the dashboard
- Go to the Dynamic Dashboard
- Navigate to Settings → Embedded Wallets
- Make sure the EVM chains you want to sponsor are enabled
- Toggle on EVM Gas Sponsorship
Send a sponsored transaction
Call sendSponsoredTransaction with the user's wallet and a list of calls (what you want the transaction to do). It signs, sends, waits for the transaction to land on-chain, and returns the transaction hash.
import { sendSponsoredTransaction } from '@dynamic-labs-sdk/evm';
import { parseEther } from 'viem';
const sendSponsoredTx = async (walletAccount, recipientAddress) => {
const { transactionHash } = await sendSponsoredTransaction({
walletAccount,
calls: [
{
target: recipientAddress, // who/what you're sending to
data: '0x', // '0x' = a plain token transfer
value: parseEther('0.01'), // amount of native token to send
},
],
});
console.log('Sponsored transaction confirmed:', transactionHash);
};
That's it — the user pays no gas, and you didn't have to think about delegation or relayers.
What goes in calls
Each entry in the calls array describes one action the transaction should perform. Most apps only need a single call.
| Field | Type | Description |
|---|---|---|
target |
Hex |
The address you're sending to (a recipient or a contract). |
data |
Hex |
The action to run on the target. Use 0x for a plain native-token transfer. |
value |
bigint |
Amount of native token (in wei) to send with the call. Use parseEther to convert from a human-readable amount. |
Batch calls
A single sponsored transaction can carry more than one call — they're executed together, atomically (all succeed or all revert). For each call, set target to the contract (or recipient), put the encoded function call in data, and use value for any native-token amount you want to send with that call (it's 0n when the call moves no native token, like the ERC-20 transfers below).
The example below sends two USDC transfers to two different addresses in one sponsored transaction. USDC is an ERC-20 token, so each data is the calldata for its transfer(address,uint256) function, built with viem's encodeFunctionData and the standard erc20Abi viem ships:
import { sendSponsoredTransaction } from '@dynamic-labs-sdk/evm';
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem';
// USDC on Base — an ERC-20 contract with 6 decimals
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const sendTwoUsdcTransfers = async (walletAccount, recipientA, recipientB) => {
const { transactionHash } = await sendSponsoredTransaction({
walletAccount,
calls: [
{
target: USDC_ADDRESS,
value: 0n, // no native token — the transfer moves USDC, not ETH
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientA, parseUnits('5', 6)], // 5 USDC
}),
},
{
target: USDC_ADDRESS,
value: 0n,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientB, parseUnits('10', 6)], // 10 USDC
}),
},
],
});
console.log('Both transfers landed in one sponsored transaction:', transactionHash);
};
React example
In React, use useGetWalletAccounts to get the user's embedded wallet, then call sendSponsoredTransaction from a button handler. This example sponsors a USDC transfer — the same ERC-20 pattern as above, with a single call. The try/catch shows the user a friendly message if sponsorship fails (see Error handling).
import {
sendSponsoredTransaction,
isEvmWalletAccount,
SponsorTransactionError,
} from '@dynamic-labs-sdk/evm';
import { useGetWalletAccounts } from '@dynamic-labs-sdk/react-hooks';
import { useState } from 'react';
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem';
// USDC on Base — an ERC-20 contract with 6 decimals
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
function SponsoredSendButton({ recipientAddress }) {
const { data: walletAccounts = [] } = useGetWalletAccounts();
const walletAccount = walletAccounts.find(isEvmWalletAccount);
const [transactionHash, setTransactionHash] = useState('');
const [error, setError] = useState('');
const handleSend = async () => {
if (!walletAccount) return;
setError('');
try {
const { transactionHash } = await sendSponsoredTransaction({
walletAccount,
calls: [
{
target: USDC_ADDRESS,
value: 0n,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientAddress, parseUnits('5', 6)], // 5 USDC
}),
},
],
});
setTransactionHash(transactionHash);
} catch (err) {
if (err instanceof SponsorTransactionError) {
setError('Gas sponsorship failed');
}
}
};
return (
<div>
<button onClick={handleSend} disabled={!walletAccount}>
Send 5 USDC (Sponsored)
</button>
{transactionHash && <p>Hash: {transactionHash.slice(0, 20)}...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}
Error handling
If sponsorship can't go through, sendSponsoredTransaction throws a SponsorTransactionError. There is no silent fallback — if it throws, the transaction did not happen. Wrap the call in a try/catch so you can show the user a message and decide what to do next.
import {
sendSponsoredTransaction,
SponsorTransactionError,
} from '@dynamic-labs-sdk/evm';
const sendTransaction = async (walletAccount, calls) => {
try {
const { transactionHash } = await sendSponsoredTransaction({
walletAccount,
calls,
});
return { success: true, transactionHash };
} catch (error) {
if (error instanceof SponsorTransactionError) {
return { success: false, error: 'Gas sponsorship failed' };
}
return { success: false, error: error.message };
}
};
A SponsorTransactionError is thrown when:
- The sponsorship API rejects the request (sponsorship not enabled, chain not supported, or a paymaster limit was hit)
- The relay reports a terminal
failurestatus - The request times out after 60 seconds
- The wallet doesn't support sponsored transactions (e.g. an external wallet rather than a V3 MPC embedded wallet)
Supported chains
Dynamic operates relayers on the following EVM chains.
Mainnet
| Chain | Chain ID |
|---|---|
| Ethereum Mainnet | 1 |
| Base | 8453 |
| Optimism | 10 |
| Arbitrum One | 42161 |
| BNB Smart Chain | 56 |
| Robinhood Chain | 4663 |
Testnet
| Chain | Chain ID |
|---|---|
| Ethereum Sepolia | 11155111 |
| Base Sepolia | 84532 |
Going further
The quick start covers the common case. For finer control, each of these has its own reference page:
- sendSponsoredTransaction — the full API for the primary send function: every parameter, batching, nonces, and return value.
- Splitting sign & send — pre-sign an intent with
signSponsoredTransaction, relay it separately, reuse a nonce for cancel-replace, and drive custom progress UI withgetEVMSponsoredTransactionStatus/waitForSponsoredTransaction. - Managing EIP-7702 delegation — check, sign, and activate the one-time delegation yourself with
is7702DelegationActive,sign7702Authorization, andactivate7702Delegation. - EVM Server-Controlled Sponsorship — move the sponsorship decision to your backend: the user signs on the client, and your server validates and relays so you control what gets sponsored, per user and per transaction.