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

  1. Go to the Dynamic Dashboard
  2. Navigate to SettingsEmbedded Wallets
  3. Make sure the EVM chains you want to sponsor are enabled
  4. 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:

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: