WDK
EVM Gasless Transactions in WDK: ERC-4337 and EIP-7702 Gasless Transactions
Tether's Developer Relations

EVM Gasless Transactions in WDK: ERC-4337 and EIP-7702 Gasless Transactions

Raquel Carrasco Gonzalez
Article byRaquel Carrasco GonzalezDeveloper Relations Services

In this post, we will cover Gasless transactions on EVM Chains using the WDK wallet modules.

Users on Ethereum (or any EVM chain) shouldn't need to buy ETH just to move USD₮. In this post, we'll set up gasless transactions on EVM chains using WDK, with two different modules depending on the account model you want:

  • @tetherto/wdk-wallet-evm-erc-4337 for smart contract accounts (the "classic" account abstraction approach)
  • @tetherto/wdk-wallet-evm-7702-gasless for users who want to keep a regular EOA address, but delegate execution via EIP-7702 and still get the full ERC-4337 gasless flow

By the end, you'll send USD₮ on Ethereum and pay the fee in USD₮, or sponsor it entirely so the user pays nothing.

Step 1: Understand How Gasless Works on EVM

EVM gasless transactions are built on ERC-4337:

  1. Instead of a normal transaction, your wallet creates a UserOperation
  2. A bundler collects UserOperations from many users and batches them into a single on-chain transaction
  3. A paymaster pays the ETH gas for the batch
  4. The paymaster is repaid, either in an ERC-20 token (like USD₮) that is deducted from the user's balance in the same transaction, or by a sponsorship policy that your application funds

So where does EIP-7702 fit in? Classic ERC-4337 requires deploying a smart contract account for each user, which means a new address, different from their EOA, being a different address from the @tetherto/wdk-wallet-evm module or other wallet providers. EIP-7702 lets an ordinary EOA temporarily delegate its execution to a smart-account implementation. The user keeps their address; under the hood, transactions still flow through the bundler and paymaster as UserOperations.

Which module should you use?


wdk-wallet-evm-erc-4337

wdk-wallet-evm-7702-gasless

Account address

Smart contract account (new address)

The user's existing EOA, previous

Gasless via paymaster token

Sponsored
(user pays zero)

Depends on paymaster

✅ built-in (isSponsored: true)

Chain requirements

ERC-4337 infrastructure

ERC-4337 infrastructure + EIP-7702 support on the chain

If you're starting a new project on a chain that supports EIP-7702, the wdk-wallet-evm-7702-gasless module is the recommended path: same address as a normal wallet, gasless underneath.

Step 2: Get Your API Keys

The bundler and paymaster are third-party infrastructure, so this is where you need credentials. Two popular options:

Option A: Candide

Candide serves the bundler and paymaster from a single URL, so you only need one endpoint, no separate paymasterUrl. The chain is selected by chain ID in the path.

Steps:

  1. You can start immediately with the public endpoint; rate-limited, no key required: https://api.candide.dev/public/v3/{chainId} (e.g. .../v3/1 for Ethereum mainnet)
  2. For production, go to the Candide dashboard and create an account
  3. Create an API key and, if you want to sponsor gas for your users, create a sponsorship policy; you'll get a policy ID (e.g. sp_my_policy)
    • Click on "Create API Key" to generate your first APY key.
Candide empty APY key section
    • Name your API key
Candide name your new API key
    • Select the chains you would like to work with.
Candide select chain API Key
    • And your API key is created!
APY key created success message
    • You will be able to find it in your dashboard.
Candide generated API Key dashboard

Usage in code:

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-aa', WalletManagerEvmErc4337, {
    chainId: 1, // Ethereum mainnet — required, not inferred from the provider
    provider: 'https://eth.drpc.org',
    bundlerUrl: 'https://api.candide.dev/public/v3/1',
    paymasterUrl: 'https://api.candide.dev/public/v3/1',
    paymasterAddress: '0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba',
    safeModulesVersion: '0.3.0',
    paymasterToken: {
      address: '0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT on Ethereum
    },
    transferMaxFee: 100000n // cap: 0.1 USDT (6 decimals)
  })


Option B: Pimlico

Pimlico provides separate bundler and paymaster URLs.

Steps:

  1. Go to the Pimlico dashboard and sign up
  2. Create an API key
    • Go to API Keys and
Pimlico API Key empty Dashboard
    • Give a name to your new API Key
Pimlico name your new API Key
    • Your new API Key is generated and visible in your dashboard
Pimlico API key generated and visible in your dashboard.
    • Click on "RPC URLs" to obtain the URL link for your desired chain.
Pimlico Chain Selection URL


    • Your endpoints follow this format (chain selected by ID in the path): https://api.pimlico.io/v2/{chainId}/rpc?apikey=YOUR_KEY
    • If your provider serves the paymaster from a different URL than the bundler, set paymasterUrl separately; otherwise, WDK defaults it to bundlerUrl.

Usage in code:

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-aa', WalletManagerEvmErc4337, {
    chainId: 1, // Ethereum mainnet — required, not inferred from the provider
    provider: 'https://eth.drpc.org',
   bundlerUrl: `https://api.pimlico.io/v1/ethereum/rpc?apikey=${PIMLICO_API_KEY}`,
  paymasterUrl: `https://api.pimlico.io/v2/ethereum/rpc?apikey=${PIMLICO_API_KEY}`,
  paymasterAddress: '0x777777777777AeC03fd955926DbF81597e66834C',
    safeModulesVersion: '0.3.0',
    paymasterToken: {
      address: '0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT on Ethereum
    },
    transferMaxFee: 100000n // cap: 0.1 USDT (6 decimals)
  })


One more thing: the delegation address (7702 module only)

The EIP-7702 module needs a delegationAddress that is the smart-account implementation your users' EOAs delegate to. Treat this address as security-sensitive: it controls execution for the account. Verify it against the WDK documentation for your target chain before using it with real funds.


Usage in code:

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-7702', WalletManagerEvm7702Gasless, {
    provider: 'https://eth.drpc.org',
    delegationAddress: '<VERIFIED_DELEGATION_ADDRESS>',
    bundlerUrl: 'https://api.candide.dev/api/v3/1/YOUR_API_KEY',
	isSponsored: true,
	sponsorshipPolicyId: 'sp_my_policy',

  })


Step 3: Register the Gasless Wallet

Install the module in your project. We support Node.js and Bare runtimes. For more information, check Introduction to WDK: Tether's Wallet Development Kit.


npm install @tetherto/wdk @tetherto/wdk-wallet-evm-erc-4337

or

npm install @tetherto/wdk @tetherto/wdk-wallet-evm-7702-gasless

Next, you will have to register the desired installed module with WDK.

Option A: wdk-wallet-evm-erc-4337

The ERC-4337 module generates a Smart Account: a smart contract wallet (a Safe, under the hood) deployed on-chain for each user that supports full account abstraction.

Unlike a regular wallet, its address is a contract address, different from the EOA your seed phrase would normally produce, and it's deployed automatically the first time it transacts. As we mentioned, if you want to keep your EOA address, you should use the wdk-wallet-evm-7702-gasless module, as we will detail in the next section.

To achieve a gasless experience, paying gas in USD₮, you only have to configure the paymaster: point paymasterToken at the USD₮ contract, and every transaction's gas is quoted, charged, and settled in USD₮.

The module also supports two other gas modes, the fully sponsored transactions (isSponsored: true, your app covers the cost) and classic native-coin fees (useNativeCoins: true), so you can switch modes later, even per transaction, without changing wallets.

import WDK from '@tetherto/wdk'
import WalletManagerEvmErc4337 from '@tetherto/wdk-wallet-evm-erc-4337'

const seedPhrase = 'your seed phrase here' // only for development and testing

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-aa', WalletManagerEvmErc4337, {
    chainId: 1, // Ethereum mainnet — required, not inferred from the provider
    provider: 'https://eth.drpc.org',
    bundlerUrl: 'https://api.candide.dev/public/v3/1',
    paymasterUrl: 'https://api.candide.dev/public/v3/1',
    paymasterAddress: '0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba',
    safeModulesVersion: '0.3.0',
    paymasterToken: {
      address: '0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT on Ethereum
    },
    transferMaxFee: 100000n // cap: 0.1 USDT (6 decimals)
  })

What's happening:

  • chainId: the network ID (1 = Ethereum, 11155111 = Sepolia Testnet). The module needs it for fee estimation and smart account initialization.
⚠️ Note the difference from other WDK modules: the standard EVM module and the EIP-7702 module (Option B) infer the chain from the provider, but here chainId is required, the module must know the chain upfront to compute your smart account's address and build valid UserOperations.
  • provider: the RPC endpoint for blockchain reads. You can also pass an ordered array of URLs for automatic failover (with optional retries, default 3).
  • bundlerUrl / paymasterUrl: the two ERC-4337 services from Step 2. With Candide they're the same endpoint; with Pimlico they're separate URLs. Or the endpoints from the provider of your preference.
  • paymasterAddress: the paymaster smart contract that fronts the gas. It must match your paymaster service. Candide's is 0x8b1f...75ba, Pimlico's is 0x7777...834C. Mixing a Candide URL with Pimlico's address (or vice versa) will fail.
  • safeModulesVersion: the smart account is a Safe under the hood; this pins the Safe modules version ('0.3.0' recommended, '0.2.0' also valid).
  • paymasterToken.address: the ERC-20 used to repay gas (USD₮ here). This single field is what makes the account gasless.
  • transferMaxFee: a fee cap in paymaster-token base units (100000 = 0.1 USD₮). If the quoted fee exceeds it, the transfer throws with "Exceeded maximum fee" instead of silently overpaying.

The rest of the flow is the standard WDK pattern: getAccount('ethereum-aa', 0), quoteSendTransaction(), sendTransaction().

One thing to keep in mind: getAddress() returns the smart account's contract address, not the EOA derived from your seed phrase. Fund that address with USD₮, tokens sent to the underlying EOA won't be visible to the smart account.

Beyond paying gas in USD₮, the module supports two other gas payment modes: sponsored transactions (isSponsored: true with an optional sponsorshipPolicyId , your app covers the fee, and the user pays nothing) and classic native-coin fees (useNativeCoins: true).

Note that only one mode should be active at a time, but you can override the mode per call. This is important for every account's first transaction: the Smart Account is a smart contract, and it isn't deployed on-chain until the account transacts for the first time. That deployment requires a small amount of ETH in the account, so if you create a fresh account, load it with USD₮, and try to send, the transaction fails. The gasless flow only kicks in once the account is deployed. Rather than asking users to buy ETH (which defeats the whole point of this post), the recommended pattern is to sponsor that first transaction with isSponsored: true as a per-call override: your app absorbs the one-time deployment cost, and every transaction after that runs gasless in USD₮.

Option B: wdk-wallet-evm-7702-gasless

The EIP-7702 module takes a different approach: the user keeps their regular EOA address. Instead of deploying a smart contract account, EIP-7702 lets the EOA delegate its execution to a smart-account implementation (the delegationAddress), so the address is the same as the one your seed phrase would produce with a standard EVM wallet, but transactions still flow through the ERC-4337 bundler and paymaster as UserOperations. WDK handles the delegation, UserOperation signing, and paymaster approvals automatically on every operation.

This solves the two friction points from Option A: there's no chainId to configure (the chain is inferred from the provider), and there's no contract deployment, so no expensive first transaction that needs ETH or sponsorship. The trade-off is that the chain itself must support EIP-7702.

The 7702 module has two fee modes; pick one:

Mode A: Paymaster token (user pays gas in USD₮)

import WDK from '@tetherto/wdk'
import WalletManagerEvm7702Gasless from '@tetherto/wdk-wallet-evm-7702-gasless'

const seedPhrase = 'your seed phrase here' // only for development and testing

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-7702', WalletManagerEvm7702Gasless, {
    provider: 'https://eth.drpc.org',
    delegationAddress: '<VERIFIED_DELEGATION_ADDRESS>',
    bundlerUrl: 'https://api.candide.dev/api/v3/1/YOUR_API_KEY',
    paymasterAddress: '0x888888888888Ec68A58AB8094Cc1AD20Ba3D2402',
    paymasterToken: {
      address: '0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT on Ethereum
    },
    transferMaxFee: 100000n // cap: 0.1 USDT (6 decimals)
  })

What's happening:

  • provider: the RPC endpoint. You can also pass an ordered array of RPC URLs for automatic failover (with optional retries, default 3).
  • delegationAddress: the smart-account implementation the EOA delegates to via EIP-7702.
  • bundlerUrl: your Candide (or Pimlico) endpoint. paymasterUrl defaults to bundlerUrl if not set.
  • paymasterToken.address: the ERC-20 used to pay fees, USD₮ here.
  • paymasterAddress (optional): pins the expected paymaster contract. If the RPC response returns a different paymaster, WDK throws a ConfigurationError. A nice safety net.
  • transferMaxFee (optional): a fee cap in token base units. If the quoted fee exceeds it, the transfer throws instead of silently overpaying.

Mode B: Sponsored (user pays zero)

import WDK from '@tetherto/wdk'
import WalletManagerEvm7702Gasless from '@tetherto/wdk-wallet-evm-7702-gasless'

const seedPhrase = 'your seed phrase here' // only for development and testing

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-7702', WalletManagerEvm7702Gasless, {
    provider: 'https://eth.drpc.org',
    delegationAddress: '<VERIFIED_DELEGATION_ADDRESS>',
    bundlerUrl: 'https://api.candide.dev/api/v3/1/YOUR_API_KEY',
	isSponsored: true,
	sponsorshipPolicyId: 'sp_my_policy',

  })

What's happening:

  • isSponsored: true switches to sponsorship mode: your application (via the sponsorship policy you created in the dashboard) covers the gas.
  • In this mode, quoteSendTransaction() and quoteTransfer() return fee: 0n . The user genuinely pays nothing.
  • You can even override the fee mode per call: a sponsored account can pay one specific transaction with a paymaster token by passing { isSponsored: false, paymasterToken: { address: '...' } } as a second argument to sendTransaction().

Step 4: Send Your First Gasless Transaction

Mode A: Paymaster token

In the paymaster token, we have two options: use the wdk-wallet-evm-erc-4337 or the @tetherto/wdk-wallet-evm-7702-gasless.

For this example, we will use the wdk-wallet-evm-erc-4337 module, since we will be using the @tetherto/wdk-wallet-evm-7702-gaslessmodule in the next example, for sponsored transactions. Remember that the syntax is the same in both cases.

import WDK from '@tetherto/wdk'
import WalletManagerEvmErc4337 from '@tetherto/wdk-wallet-evm-erc-4337'

const seedPhrase = 'your seed phrase here' // only for development and testing

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-aa', WalletManagerEvmErc4337, {
    chainId: 1, // Ethereum mainnet — required, not inferred from the provider
    provider: 'https://eth.drpc.org',
    bundlerUrl: 'https://api.candide.dev/public/v3/1',
    paymasterUrl: 'https://api.candide.dev/public/v3/1',
    paymasterAddress: '0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba',
    safeModulesVersion: '0.3.0',
    paymasterToken: {
      address: '0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT on Ethereum
    },
    transferMaxFee: 100000n // cap: 0.1 USDT (6 decimals)
  })

// Get the first account (index 0) on Ethereum
const ethAccount = await wdk.getAccount('ethereum-aa', 0)
const ethAddress = await ethAccount.getAddress()
console.log('Ethereum address:', ethAddress)

// Estimate the fee first
const quote = await ethAccount.quoteSendTransaction({
  to: '0x350188fB7EB1A49d43398b610a3714844b361FC3', // Recipient
  value: 1000000000000000000n // 1 ETH in wei (BigInt)
})
console.log('Estimated fee:', Number(quote.fee) / 1e6, 'USDT')


// Send the transaction
try {
  const result = await ethAccount.sendTransaction({
    to: '0x350188fB7EB1A49d43398b610a3714844b361FC3',
    value: 1000000000000000000n
  })
  console.log('Transaction hash:', result.hash)
  console.log('Fee paid:', Number(result.fee) / 1e6, 'USDT')
} catch (error) {
  console.error('Transaction failed:', error.message)
  // Handle errors: insufficient balance, invalid recipient, network issues
}

What's happening:

  • getAccount('ethereum-aa', 0) derives account zero from your seed phrase, and getAddress() returns the Smart Account's contract address, this is the address to fund with USD₮, not the underlying EOA.
  • quoteSendTransaction() asks the bundler and paymaster what the transaction will cost, without sending anything. Since we configured paymasterToken, the fee is returned in USD₮ base units (divide by 1,000,000), even though we're sending native ETH, the gas is charged in USD₮.
  • sendTransaction() builds a UserOperation, signs it, and submits it through the bundler. The paymaster pays the ETH gas and deducts the fee from your USD₮ balance in the same operation.
  • result.hash is the UserOperation hash; result.fee is the actual fee paid, which may differ slightly from the quote if network conditions changed.
  • The catch block covers the usual failures: insufficient balance (remember the first-transaction deployment note above), invalid recipient, or bundler timeouts.

The exact same code works with the wdk-wallet-evm-7702-gasless module only the registration config changes; getAccount, quoteSendTransaction, and sendTransaction are identical.

Mode B: Sponsored (user pays zero)

Sponsored mode goes one step further: your application covers the gas entirely, and the user pays nothing. Not in ETH, not in USD₮, nothing. This is the mode for onboarding flows, promotional campaigns, or any product where you'd rather absorb a few cents of gas than introduce payment friction for the user.

The cost is controlled through a sponsorship policy, the one you created in the Candide dashboard back in Step 2. The policy defines the rules for what your app is willing to sponsor (limits, budgets), and you reference it by its ID in the config.

We'll use the @tetherto/wdk-wallet-evm-7702-gasless module, remember, the syntax is identical to the ERC-4337 module; only the registration config changes.

import WDK from '@tetherto/wdk'
import WalletManagerEvm7702Gasless from '@tetherto/wdk-wallet-evm-7702-gasless'

const seedPhrase = 'your seed phrase here' // only for development and testing

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum-7702', WalletManagerEvm7702Gasless, {
    provider: 'https://eth.drpc.org',
    delegationAddress: '<VERIFIED_DELEGATION_ADDRESS>',
    bundlerUrl: 'https://api.candide.dev/api/v3/1/YOUR_API_KEY',
	isSponsored: true,
	sponsorshipPolicyId: 'sp_my_policy',
})

// Get the first account (index 0) on Ethereum
const ethAccount = await wdk.getAccount('ethereum-7702', 0)
const ethAddress = await ethAccount.getAddress()
console.log('Ethereum address:', ethAddress)

// Estimate the fee first
const quote = await ethAccount.quoteSendTransaction({
  to: '0x350188fB7EB1A49d43398b610a3714844b361FC3', // Recipient
  value: 1000000000000000000n // 1 ETH in wei (BigInt)
})
console.log('Estimated fee:', Number(quote.fee) / 1e6, 'USDT')

// Send the transaction
try {
  const result = await ethAccount.sendTransaction({
    to: '0x350188fB7EB1A49d43398b610a3714844b361FC3',
    value: 1000000000000000000n
  })
  console.log('Transaction hash:', result.hash)
  console.log('Fee paid:', result.fee, 'wei')
} catch (error) {
  console.error('Transaction failed:', error.message)
  // Handle errors: insufficient balance, invalid recipient, network issues
}

// Get the first account (index 0) on Ethereum
const ethAccount = await wdk.getAccount('ethereum-aa', 0)
const ethAddress = await ethAccount.getAddress()
console.log('Ethereum address:', ethAddress)

// Estimate the fee first
const quote = await ethAccount.quoteSendTransaction({
  to: '0x350188fB7EB1A49d43398b610a3714844b361FC3', // Recipient
  value: 1000000000000000000n // 1 ETH in wei (BigInt)
})
console.log('Estimated fee:', quote.fee, 'wei')
console.log('Estimated fee in ETH:', Number(quote.fee) / 1e18)

// Send the transaction
try {
  const result = await ethAccount.sendTransaction({
    to: '0x350188fB7EB1A49d43398b610a3714844b361FC3',
    value: 1000000000000000000n
  })
  console.log('Transaction hash:', result.hash)
  console.log('Fee paid:', Number(result.fee) / 1e6, 'USDT')
} catch (error) {
  console.error('Transaction failed:', error.message)
  // Handle errors: insufficient balance, invalid recipient, network issues
}


What's happening:

  • The config swaps paymasterAddress + paymasterToken for two fields: isSponsored: true activates sponsorship mode, and sponsorshipPolicyId points to the policy you created in the Candide dashboard, which defines the sponsorship rules and budget.
  • delegationAddress is the 7702-specific field: the smart-account implementation the user's EOA delegates to. Verify it against the WDK docs before using real funds, and note there's no chainId here; the 7702 module infers the chain from the provider.
  • getAddress() returns the user's regular EOA address, the same address a standard EVM wallet would produce from this seed phrase. No smart contract deployment, no expensive first transaction.
  • quoteSendTransaction() returns fee: 0n in sponsored mode the quote confirms the user pays nothing; the sponsor absorbs the gas.
  • sendTransaction() signs the EIP-7702 delegation and the UserOperation, and submits through the bundler. Your sponsorship policy pays the ETH gas; nothing is deducted from the user's balance.
  • The catch block still matters: transactions can fail if the recipient is invalid, the bundler times out, or the sponsorship policy rejects the operation (e.g., budget exhausted or the transaction falls outside the policy's rules).

Common errors:

  • ConfigurationError: a required field (provider, bundlerUrl, delegationAddress) is missing, or the paymaster returned by the RPC doesn't match your pinned paymasterAddress.
  • Fee cap exceeded: the quoted fee is above transferMaxFee. Either raise the cap or wait for lower network fees.
  • "Insufficient balance": in paymaster-token mode, your USD₮ balance must cover the amount + fee.
  • Bundler timeout: the bundler service is slow or unreachable. Retry in a few seconds, or configure provider failover with an array of RPC URLs.

Key Insights

  • EIP-7702 = EOA address, smart-account powers: users keep the address they know; gasless machinery runs underneath.
  • Two fee modes, one flag: isSponsored: true for zero-cost UX funded by your app; paymasterToken for "pay gas in USD₮". You can even mix them per call.
  • Pin your paymaster, cap your fees: paymasterAddress and transferMaxFee are optional but cheap insurance in production.
  • The API is the same as every other WDK chain: register → getAccount → quote → send.

What's next

Follow WDK's official X account to stay up to date with all the announcements. Join us on Discord, the best place to ask questions, share what you're building, and get help directly from the team.

All four modules are already documented and ready to use. And if you'd enjoy a more guided, step-by-step take with extra tips along the way, we've got you. Coming up:

  1. Solana Gasless Transactions in WDK
  2. TON Gasless Transactions in WDK
  3. TRON Gasless Transactions in WDK

And after that, more to come. Protocol Integration: Swaps, Bridges, and Lending with WDK, where we layer DeFi functionality on top of these gasless wallets.

Gasless + swaps + bridges + lending = a complete DeFi experience without ever touching native tokens.

Receive the latest WDK news

Click to subscribe to the WDK newsletter and stay updated!

Latest Articles & Guides