> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# EVM Gas Sponsorship

> Sponsor EVM transaction fees for your users from your server with Dynamic's built-in gas sponsorship feature.

<Info>
  EVM Gas Sponsorship is an enterprise-only feature. [Contact us](https://www.dynamic.xyz/talk-to-us) to learn more about upgrading your plan.
</Info>

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.

<Note>
  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.
</Note>

## Quick start

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

<Steps>
  <Step title="Turn on gas sponsorship in the dashboard">
    1. Go to the [Dynamic Dashboard](https://app.dynamic.xyz)
    2. Navigate to **Settings** → **Embedded Wallets**
    3. Make sure the EVM chains you want to sponsor are enabled
    4. Toggle on **EVM Gas Sponsorship**
  </Step>

  <Step title="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.

    ```typescript theme={"system"}
    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.
  </Step>
</Steps>

<Tip>
  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](#managing-eip-7702-delegation-yourself)).
</Tip>

### 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. |

### 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):

```typescript theme={"system"}
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.

```typescript theme={"system"}
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`:

```typescript theme={"system"}
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**

| Chain            | Chain ID |
| ---------------- | -------- |
| Ethereum Mainnet | `1`      |
| Base             | `8453`   |
| Optimism         | `10`     |
| Arbitrum One     | `42161`  |
| BNB Smart Chain  | `56`     |

**Testnet**

| Chain            | Chain ID   |
| ---------------- | ---------- |
| Ethereum Sepolia | `11155111` |
| Base Sepolia     | `84532`    |

### 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`:

```typescript theme={"system"}
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`:

```typescript theme={"system"}
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):

```typescript theme={"system"}
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`:

```typescript theme={"system"}
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`:

```typescript theme={"system"}
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`):

```typescript theme={"system"}
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:

```typescript theme={"system"}
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:

| Status      | Meaning                                                  |
| ----------- | -------------------------------------------------------- |
| `pending`   | Accepted by the relayer, not yet broadcast to the chain. |
| `submitted` | Broadcast to the chain, waiting for confirmation.        |
| `success`   | Finalized successfully on-chain.                         |
| `failure`   | Terminal 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):

```typescript theme={"system"}
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.

```typescript theme={"system"}
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

| Limitation      | Details                                                                                       |
| --------------- | --------------------------------------------------------------------------------------------- |
| Wallet type     | Embedded wallets only (V3 MPC)                                                                |
| Mechanism       | EIP-7702 delegation to the Dynamic relayer contract                                           |
| Intent validity | 10 minutes by default; configurable via `validForSeconds`                                     |
| Polling timeout | `waitForSponsoredTransaction` resolves or throws within 60 seconds by default                 |
| Batching        | One signed intent per `sendSponsoredTransaction` call; each intent can contain multiple calls |

## Related

* [`signTransaction()`](/node/reference/evm/sign-transaction) - Sign a regular (non-sponsored) EVM transaction
* [`WalletMetadata`](/node/reference/types/wallet-metadata) - The metadata object passed to every signing operation
* [`sponsorTransaction()`](/node/reference/svm/sponsor-transaction) - The Solana equivalent
* [Storage Best Practices](/node/wallets/server-wallets/storage-best-practices) - Where to cache metadata vs. vault shares
