Skip to main content
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.
EVM Gas Sponsorship works only with V3 MPC embedded wallets (the wallets Dynamic creates for your users). It does not work with external wallets like MetaMask.

Quick start

This is everything you need to sponsor a transaction. The SDK handles the underlying setup for you automatically.
1

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
2

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.
The first time a wallet sends a sponsored transaction, the SDK does a one-time on-chain setup (EIP-7702 delegation) for you automatically. You don’t need to do anything — it just works. See Advanced if you want to control that step yourself.

What goes in calls

Each entry in the calls array describes one action the transaction should perform. Most apps only need a single call.
FieldTypeDescription
targetHexThe address you’re sending to (a recipient or a contract).
dataHexThe action to run on the target. Use 0x for a plain native-token transfer.
valuebigintAmount of native token (in wei) to send with the call. Use parseEther to convert from a human-readable amount.

React example

In React, use useWalletAccounts to get the user’s embedded wallet, then call sendSponsoredTransaction from a button handler. 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 { useWalletAccounts } from '@dynamic-labs-sdk/react-hooks';
import { useState } from 'react';
import { parseEther } from 'viem';

function SponsoredSendButton({ recipientAddress }) {
  const walletAccounts = useWalletAccounts();
  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: recipientAddress, data: '0x', value: parseEther('0.01') },
        ],
      });
      setTransactionHash(transactionHash);
    } catch (err) {
      if (err instanceof SponsorTransactionError) {
        setError('Gas sponsorship failed');
      }
    }
  };

  return (
    <div>
      <button onClick={handleSend} disabled={!walletAccount}>
        Send (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 transaction ended in a terminal failure status (FAILED, CANCELED, EXPIRED, REJECTED)
  • 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)

Advanced usage

Everything below is optional. Reach for it only when you need more control than the quick start gives you — custom UI, pre-signing, or managing the one-time delegation step yourself.

How it works under the hood

When you call sendSponsoredTransaction, the SDK:
  1. Builds an EIP-712 intent describing the batch of calls and a deadline
  2. Signs the intent with the user’s embedded wallet (and, if delegation is needed, an EIP-7702 authorization for the wallet’s EOA)
  3. Relays the signed intent to Dynamic’s sponsorship backend
  4. Polls the relayer until the request reaches a terminal state, then returns the on-chain transaction hash
EIP-7702 delegation is what lets a relayer submit a batch of calls on the user’s behalf. The user’s EOA is delegated, once, to a Dynamic-operated relayer contract.

Supported chains

Dynamic operates relayers on the following EVM chains. Mainnet
ChainChain ID
Ethereum Mainnet1
Base8453
Testnet
ChainChain ID
Ethereum Sepolia11155111
Base Sepolia84532

Splitting sign and send

Reuse a pre-signed intent (for example, to send it from a different process) by calling signSponsoredTransaction first and passing the result to sendSponsoredTransaction:
import {
  signSponsoredTransaction,
  sendSponsoredTransaction,
} from '@dynamic-labs-sdk/evm';

const signedTransaction = await signSponsoredTransaction({
  walletAccount,
  calls,
});

const { transactionHash } = await sendSponsoredTransaction({ signedTransaction });
By default the signed intent is valid for 10 minutes. Override with validForSeconds:
const signedTransaction = await signSponsoredTransaction({
  walletAccount,
  calls,
  validForSeconds: 60,
});

Reusing a nonce

By default each signed intent gets a fresh single-use bitmap nonce. Pass nonce (a bigint, matching the nonce on the signed result) to reuse one across intents — sign several intents with the same nonce so at most one can ever land on-chain (cancel-replace). The value is used as-is, with no on-chain validation:
const signedTransaction = await signSponsoredTransaction({
  walletAccount,
  calls,
  nonce: 42n,
});
nonce works the same way on relaySponsoredTransaction and sendSponsoredTransaction.

Managing EIP-7702 delegation yourself

Before a wallet can submit a sponsored transaction, its EOA must be delegated to the Dynamic relayer contract via an EIP-7702 authorization. The quick start handles this automatically on the first transaction. These three helpers exist for when you want explicit control over the delegation step.
When sendSponsoredTransaction is called on a wallet that hasn’t been delegated yet, the SDK signs an EIP-7702 authorization automatically and attaches it to the first call. You only need these helpers when you want explicit control over the delegation step.

Checking delegation status

Use is7702DelegationActive to check whether delegation is already active on the wallet’s current network. Results are cached per wallet + chain in an in-memory registry, so repeated calls are cheap:
import { is7702DelegationActive } from '@dynamic-labs-sdk/evm';

const isActive = await is7702DelegationActive({ walletAccount });

Signing an authorization

sign7702Authorization signs an EIP-7702 authorization for the Dynamic delegation contract using the wallet’s active network (or an explicit chainId override). Pass the result to signSponsoredTransaction or sendSponsoredTransaction to attach it to the next sponsored call:
import {
  sign7702Authorization,
  signSponsoredTransaction,
} from '@dynamic-labs-sdk/evm';

const authorization = await sign7702Authorization({ walletAccount });

const signedTransaction = await signSponsoredTransaction({
  walletAccount,
  calls,
  authorization,
});

Activating delegation up-front

For flows that want delegation persisted on-chain before the first user-facing sponsored transaction (e.g. during onboarding), call activate7702Delegation. It sends an empty sponsored transaction that only carries the EIP-7702 authorization, then returns the on-chain transaction hash. Subsequent sponsored transactions no longer need to include an authorization:
import {
  activate7702Delegation,
  is7702DelegationActive,
} from '@dynamic-labs-sdk/evm';

if (!(await is7702DelegationActive({ walletAccount }))) {
  const { transactionHash } = await activate7702Delegation({ walletAccount });
  console.log('Delegation activated:', transactionHash);
}
You can also pass a pre-signed authorization to activate7702Delegation to reuse one obtained from sign7702Authorization.

Polling the relay status yourself

For custom UI (e.g. a progress bar across SUBMITTEDCONFIRMED), call getEVMSponsoredTransactionStatus directly on the requestId returned by relaySponsoredTransaction:
import {
  relaySponsoredTransaction,
  getEVMSponsoredTransactionStatus,
} from '@dynamic-labs-sdk/evm';

const { requestId } = await relaySponsoredTransaction({ walletAccount, calls });

const { status, transactionHash, errorMessage } =
  await getEVMSponsoredTransactionStatus({ requestId });
status is one of PENDING, SENT, SUBMITTED, CONFIRMED, FAILED, CANCELED, EXPIRED, or REJECTED. transactionHash is set once the relay broadcasts the transaction. For the common “wait until done” case, use waitForSponsoredTransaction — it polls every 2 seconds and resolves on CONFIRMED (timeout: 60s):
import { waitForSponsoredTransaction } from '@dynamic-labs-sdk/evm';

const { transactionHash } = await waitForSponsoredTransaction({ requestId });

Limitations

LimitationDetails
Wallet typeEmbedded wallets only (V3 MPC)
MechanismEIP-7702 delegation to the Dynamic relayer contract
Intent validity10 minutes by default; configurable via validForSeconds
Polling timeoutwaitForSponsoredTransaction resolves or throws within 60 seconds
BatchingOne signed intent per sendSponsoredTransaction call; each intent can contain multiple calls

Helpers

  • sign7702Authorization — sign an EIP-7702 authorization for the Dynamic delegation contract
  • is7702DelegationActive — check whether delegation is active for a wallet on its current network
  • activate7702Delegation — activate delegation on-chain via a single empty sponsored transaction