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. On the server, the wallet client signs a sponsored transaction with the user’s embedded-wallet shares, relays the signed intent to Dynamic’s sponsorship backend, and waits for it to land on-chain. For the basic case you don’t need to understand relayers or delegation. Enable it in the dashboard and call one method.
EVM Gas Sponsorship works only with V3 MPC embedded wallets (the wallets Dynamic creates for your users). It does not work with externally imported keys that you sign for directly.

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 wallet’s walletMetadata, the chainId, an rpcUrl, and a list of calls (what you want the transaction to do). It signs the intent, relays it, waits for the transaction to land on-chain, and returns the transaction hash.
import { authenticatedEvmClient } from './client';
import { parseEther } from 'viem';

const evmClient = await authenticatedEvmClient();

// Load the metadata + shares you persisted at creation time.
const walletMetadata = JSON.parse(await redis.get(`wallet:${accountAddress}`));
const externalServerKeyShares = await vault.read(`wallet:${accountAddress}/shares`);

const { transactionHash } = await evmClient.sendSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453, // Base
  rpcUrl: process.env.BASE_RPC_URL,
  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);
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. For that to work it needs an rpcUrl so it can read the wallet’s delegation state and EOA nonce. If you omit rpcUrl, either pass autoDelegate: false or supply a pre-signed authorization (see Managing EIP-7702 delegation 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.

Sending an ERC-20 token (USDC)

To move an ERC-20 token instead of native gas, point target at the token contract, encode a transfer call as data, and leave value at 0n (no native token moves):
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem';

// USDC on Base (6 decimals)
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

const { transactionHash } = await evmClient.sendSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
  calls: [
    {
      target: USDC_BASE,
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'transfer',
        args: [recipientAddress, parseUnits('10', 6)], // 10 USDC
      }),
      value: 0n,
    },
  ],
});

Batching multiple calls

A single sponsored transaction can carry several calls, executed in order within one on-chain transaction. This is handy for batching: for example, paying multiple recipients at once.
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem';

const payouts = [
  { to: '0x1111111111111111111111111111111111111111', amount: '10' },
  { to: '0x2222222222222222222222222222222222222222', amount: '25' },
];

const { transactionHash } = await evmClient.sendSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
  calls: payouts.map(({ to, amount }) => ({
    target: USDC_BASE,
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: 'transfer',
      args: [to, parseUnits(amount, 6)],
    }),
    value: 0n,
  })),
});

Relaying on behalf of an end user

By default the transaction is relayed as the authenticated service user. To relay on behalf of one of your end users (the wallet’s owner), pass their userId:
const { transactionHash } = await evmClient.sendSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
  calls,
  userId: endUser.id,
});
userId works the same way on relaySponsoredTransaction.

Advanced usage

Everything below is optional. Reach for it only when you need more control than the quick start gives you: pre-signing, custom polling, 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 shares (and, if delegation is needed, an EIP-7702 authorization for the wallet’s EOA)
  3. Relays the signed intent to Dynamic’s sponsorship backend and gets back a requestId
  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 (0x0000Fb7702036ff9f76044a501ac1aA74cbab16b).

Supported chains

Dynamic operates relayers on the following EVM chains. Mainnet
ChainChain ID
Ethereum Mainnet1
Base8453
Optimism10
Arbitrum One42161
BNB Smart Chain56
Testnet
ChainChain ID
Ethereum Sepolia11155111
Base Sepolia84532

Splitting sign and send

Reuse a pre-signed intent (for example, to sign in one process and relay from another) by calling signSponsoredTransaction first and passing the result to sendSponsoredTransaction:
const signedTransaction = await evmClient.signSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
  calls,
});

const { transactionHash } = await evmClient.sendSponsoredTransaction({ signedTransaction });
signSponsoredTransaction returns a SignedSponsoredTransaction, a JSON-serializable payload (calls, chainId, deadline, nonce, relayer, signature, walletAddress, and an optional authorization) you can hand to relaySponsoredTransaction or sendSponsoredTransaction. By default the signed intent is valid for 10 minutes. Override with validForSeconds:
const signedTransaction = await evmClient.signSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
  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):
const signedTransaction = await evmClient.signSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
  calls,
  nonce: 42n,
});
nonce works the same way on relaySponsoredTransaction and sendSponsoredTransaction.

Inspecting the active relayer

The SDK picks an available relayer automatically when signing. To look one up yourself (for example, to display it or pre-flight a chain), call getAvailableEvmGaslessRelayer:
const { relayerAddress } = await evmClient.getAvailableEvmGaslessRelayer({ chainId: 8453 });
It throws if the chain has no available relayer (e.g. the chain isn’t supported or sponsorship isn’t enabled).

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 (when you provide an rpcUrl). These helpers exist for when you want explicit control over the delegation step.

Checking delegation status

Use is7702DelegationActive to check whether delegation is already active for a wallet on a given chain. It reads the wallet’s on-chain code, so it requires an rpcUrl:
const isActive = await evmClient.is7702DelegationActive({
  walletAddress: walletMetadata.accountAddress,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
});

Signing an authorization

sign7702Authorization signs an EIP-7702 authorization for the Dynamic delegation contract. Pass the result as authorization to signSponsoredTransaction or sendSponsoredTransaction to attach it to the next sponsored call. This is useful when you want to skip auto-delegation (e.g. you’ve turned autoDelegate off, or you’re signing without an rpcUrl):
const authorization = await evmClient.sign7702Authorization({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
});

const { transactionHash } = await evmClient.sendSponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  calls,
  authorization,
});
When nonce is omitted, sign7702Authorization fetches the EOA nonce from the chain via rpcUrl; pass an explicit nonce to sign without an RPC.

Polling the relay status yourself

For custom progress reporting, call relaySponsoredTransaction (which returns a requestId immediately) and poll getEVMSponsoredTransactionStatus yourself:
const { requestId } = await evmClient.relaySponsoredTransaction({
  walletMetadata,
  externalServerKeyShares,
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL,
  calls,
});

const { status, transactionHash, errorMessage } =
  await evmClient.getEVMSponsoredTransactionStatus({ requestId });
status is one of:
StatusMeaning
pendingAccepted by the relayer, not yet broadcast to the chain.
submittedBroadcast to the chain, waiting for confirmation.
successFinalized successfully on-chain.
failureTerminal failure (errorMessage is populated).
transactionHash is set once the relay broadcasts the transaction. For the common “wait until done” case, use waitForSponsoredTransaction, which polls every 2 seconds and resolves on success (timeout: 60 seconds):
const { transactionHash } = await evmClient.waitForSponsoredTransaction({ requestId });
Override the cadence with pollInterval (ms) and timeout (ms).

Error handling

These methods throw a standard Error when sponsorship can’t go through. There is no silent fallback. Wrap the call in a try/catch so you can surface a message and decide what to do next.
try {
  const { transactionHash } = await evmClient.sendSponsoredTransaction({
    walletMetadata,
    externalServerKeyShares,
    chainId: 8453,
    rpcUrl: process.env.BASE_RPC_URL,
    calls,
  });
} catch (error) {
  // e.g. "EVM sponsored transaction relay failed" or
  //      "EVM sponsored transaction timed out waiting for terminal status"
  console.error('Sponsorship failed:', error.message);
}
An error is thrown when:
  • The sponsorship API rejects the request (sponsorship not enabled, chain not supported, or no relayer available)
  • The relay reaches a terminal failure status
  • waitForSponsoredTransaction times out (default 60 seconds) before reaching a terminal status
  • autoDelegate is on but no rpcUrl (and no pre-signed authorization) was supplied

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 by default
BatchingOne signed intent per sendSponsoredTransaction call; each intent can contain multiple calls
Last modified on June 26, 2026