This guide provides a detailed comparison between using UltraRelay vs GelatoRelay with Kernel-based smart wallets, focusing on the minimal code changes required to switch relay services while maintaining the same account types.
This migration guide covers switching from UltraRelay to GelatoRelay while keeping the same Kernel account implementations:
- Account Type: Kernel Account with EIP-7702 support (unchanged)
- Relay Change: UltraRelay → GelatoRelay
- SDK Change: ZeroDev SDK → Gelato Smart Wallet SDK
- Account Type: Kernel Account (ERC-4337) (unchanged)
- Relay Change: UltraRelay → GelatoRelay
- SDK Change: ZeroDev SDK → Gelato Smart Wallet SDK
- Account Type: Remains the same (Kernel accounts)
- Primary Change: Relay service from UltraRelay to GelatoRelay
- Secondary Change: SDK from ZeroDev to Gelato (required for GelatoRelay integration)
- Functionality: Same smart wallet capabilities, different relay infrastructure
This guide focuses on relay service migration - switching from UltraRelay to GelatoRelay while maintaining the same Kernel account types. The SDK change is necessary because GelatoRelay requires the Gelato Smart Wallet SDK for integration.
What stays the same:
- Kernel account types (EIP-7702 or ERC-4337)
- Smart wallet functionality and capabilities
- User experience and transaction flow
What changes:
- Relay service: UltraRelay → GelatoRelay
- SDK: ZeroDev SDK → Gelato Smart Wallet SDK
- API endpoints and configuration
// Step 1: Create entry point and kernel version
const entryPoint = getEntryPoint("0.7");
const kernelVersion = KERNEL_V3_3_BETA;
// Step 2: Create wallet client for authorization
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
// Step 3: Sign authorization for EIP-7702
const authorization = await walletClient.signAuthorization({
account,
contractAddress: KernelVersionToAddressesMap[kernelVersion].accountImplementationAddress,
});
// Step 4: Create validator
const validator = await signerToEcdsaValidator(publicClient, {
entryPoint,
kernelVersion,
signer: account,
});
// Step 5: Create kernel account
const kernelAccount = await createKernelAccount(publicClient as any, {
address: account.address,
eip7702Auth: authorization,
entryPoint,
kernelVersion,
plugins: { sudo: validator },
});
// Single step: Create kernel account using Gelato's kernel function
const account = await kernel({
owner: signer,
client: publicClient,
eip7702: true,
});
Migration Change: Replace the 5-step ZeroDev account creation with Gelato's single kernel()
function call.
const kernelClient = createKernelAccountClient({
account: kernelAccount,
chain: baseSepolia,
bundlerTransport: http(process.env.NEXT_PUBLIC_ULTRA_RELAY_URL || ""),
paymaster: undefined,
userOperation: {
estimateFeesPerGas: async ({ bundlerClient }) => {
return getUserOperationGasPrice(bundlerClient);
},
},
});
// Step 1: Create wallet client
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
// Step 2: Create Gelato smart wallet client
const smartWalletClient = await createGelatoSmartWalletClient(
walletClient,
{
apiKey: process.env.NEXT_PUBLIC_SPONSOR_API_KEY || "",
}
);
Migration Change: Replace ZeroDev's createKernelAccountClient
with Gelato's two-step process using createWalletClient
and createGelatoSmartWalletClient
.
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
] as const;
const callData = await kernelClient.account.encodeCalls(calls);
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
];
const preparedCalls = await smartWalletClient.prepare({
payment: sponsored(process.env.NEXT_PUBLIC_SPONSOR_API_KEY || ""),
calls,
});
Migration Change:
- Replace ZeroDev's
encodeCalls()
with Gelato'sprepare()
method
const userOpHash = await kernelClient.sendUserOperation({
callData,
maxFeePerGas: BigInt(0),
maxPriorityFeePerGas: BigInt(0),
});
const userOpReceipt = await kernelClient.waitForUserOperationReceipt({
hash: userOpHash,
});
const hash = userOpReceipt.receipt.transactionHash;
const results = await smartWalletClient.send({ preparedCalls });
const hash = await results?.wait();
Migration Change:
- Replace ZeroDev's
sendUserOperation()
andwaitForUserOperationReceipt()
with Gelato'ssend()
andwait()
- Change from
userOpHash
toresults.id
for logging
// BEFORE: ZeroDev SDK + Ultra Relay
const entryPoint = getEntryPoint("0.7");
const kernelVersion = KERNEL_V3_3_BETA;
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
const authorization = await walletClient.signAuthorization({
account,
contractAddress: KernelVersionToAddressesMap[kernelVersion].accountImplementationAddress,
});
const validator = await signerToEcdsaValidator(publicClient, {
entryPoint,
kernelVersion,
signer: account,
});
const kernelAccount = await createKernelAccount(publicClient as any, {
address: account.address,
eip7702Auth: authorization,
entryPoint,
kernelVersion,
plugins: { sudo: validator },
});
const kernelClient = createKernelAccountClient({
account: kernelAccount,
chain: baseSepolia,
bundlerTransport: http(process.env.NEXT_PUBLIC_ULTRA_RELAY_URL || ""),
paymaster: undefined,
userOperation: {
estimateFeesPerGas: async ({ bundlerClient }) => {
return getUserOperationGasPrice(bundlerClient);
},
},
});
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
] as const;
const callData = await kernelClient.account.encodeCalls(calls);
const userOpHash = await kernelClient.sendUserOperation({
callData,
maxFeePerGas: BigInt(0),
maxPriorityFeePerGas: BigInt(0),
});
const userOpReceipt = await kernelClient.waitForUserOperationReceipt({
hash: userOpHash,
});
const hash = userOpReceipt.receipt.transactionHash;
// AFTER: Gelato Smart Wallet SDK + Kernel Wallet
const account = await kernel({
owner: signer,
client: publicClient,
eip7702: true,
});
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
const smartWalletClient = await createGelatoSmartWalletClient(
walletClient,
{
apiKey: process.env.SPONSOR_API_KEY || "",
}
);
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
];
const preparedCalls = await smartWalletClient.prepare({
payment: sponsored(process.env.SPONSOR_API_KEY || ""),
calls,
});
const results = await smartWalletClient.send({ preparedCalls });
const hash = await results?.wait();
- Remove
@zerodev/sdk
imports - Add
@gelatonetwork/smartwallet
imports
- Replace ZeroDev account creation with Gelato's
kernel()
function - Remove entry point and kernel version configuration
- Remove authorization signing
- Remove validator creation
- Replace
createKernelAccountClient
withcreateWalletClient
+createGelatoSmartWalletClient
- Update bundler transport configuration
- Remove paymaster configuration
- Replace
encodeCalls()
withprepare()
- Replace
sendUserOperation()
withsend()
- Replace
waitForUserOperationReceipt()
withwait()
- Update transaction hash extraction
- Update error messages and logging
- Ensure retry logic remains compatible
The main differences when migrating from UltraRelay to GelatoRelay are:
- Account Creation: ZeroDev requires 5 steps vs Gelato's single function call
- Client Setup: ZeroDev uses one client vs Gelato's two-client approach
- Transaction Flow: ZeroDev uses
encodeCalls()
+sendUserOperation()
vs Gelato'sprepare()
+send()
- Relay Service: UltraRelay → GelatoRelay (primary change)
- SDK: ZeroDev SDK → Gelato Smart Wallet SDK (required for GelatoRelay integration)
// Step 1: Get entry point and kernel version
const entryPoint = getEntryPoint("0.7");
const kernelVersion = KERNEL_V3_1;
// Step 2: Create wallet client for authorization
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
// Step 3: Create validator
const validator = await signerToEcdsaValidator(publicClient, {
entryPoint,
kernelVersion,
signer: account,
});
// Step 4: Create kernel account
const kernelAccount = await createKernelAccount(publicClient as any, {
entryPoint,
kernelVersion,
plugins: { sudo: validator },
});
// Single step: Create kernel account using Gelato's kernel function
const account = await kernel({
owner: signer,
client: publicClient,
eip7702: false,
});
Migration Change: Replace the 4-step ZeroDev account creation with Gelato's single kernel()
function call.
const kernelClient = createKernelAccountClient({
account: kernelAccount,
chain: baseSepolia,
bundlerTransport: http(process.env.NEXT_PUBLIC_ULTRA_RELAY_URL || ""),
paymaster: undefined,
userOperation: {
estimateFeesPerGas: async ({ bundlerClient }) => {
return getUserOperationGasPrice(bundlerClient);
},
},
});
// Step 1: Create wallet client
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
// Step 2: Create Gelato smart wallet client
const smartWalletClient = await createGelatoSmartWalletClient(
walletClient,
{
apiKey: process.env.NEXT_PUBLIC_SPONSOR_API_KEY || "",
}
);
Migration Change: Replace ZeroDev's createKernelAccountClient
with Gelato's two-step process using createWalletClient
and createGelatoSmartWalletClient
.
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
] as const;
const callData = await kernelClient.account.encodeCalls(calls);
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
];
const preparedCalls = await smartWalletClient.prepare({
payment: sponsored(process.env.NEXT_PUBLIC_SPONSOR_API_KEY || ""),
calls,
});
Migration Change:
- Replace ZeroDev's
encodeCalls()
with Gelato'sprepare()
method
const userOpHash = await kernelClient.sendUserOperation({
callData,
maxFeePerGas: BigInt(0),
maxPriorityFeePerGas: BigInt(0),
});
const userOpReceipt = await kernelClient.waitForUserOperationReceipt({
hash: userOpHash,
});
const hash = userOpReceipt.receipt.transactionHash;
const results = await smartWalletClient.send({ preparedCalls });
const hash = await results?.wait();
Migration Change:
- Replace ZeroDev's
sendUserOperation()
andwaitForUserOperationReceipt()
with Gelato'ssend()
andwait()
- Change from
userOpHash
toresults.id
for logging
// BEFORE: ZeroDev SDK + Ultra Relay
const entryPoint = getEntryPoint("0.7");
const kernelVersion = KERNEL_V3_3_BETA;
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
const validator = await signerToEcdsaValidator(publicClient, {
entryPoint,
kernelVersion,
signer: account,
});
const kernelAccount = await createKernelAccount(publicClient as any, {
entryPoint,
kernelVersion,
plugins: { sudo: validator },
});
const kernelClient = createKernelAccountClient({
account: kernelAccount,
chain: baseSepolia,
bundlerTransport: http(process.env.NEXT_PUBLIC_ULTRA_RELAY_URL || ""),
paymaster: undefined,
userOperation: {
estimateFeesPerGas: async ({ bundlerClient }) => {
return getUserOperationGasPrice(bundlerClient);
},
},
});
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
] as const;
const callData = await kernelClient.account.encodeCalls(calls);
const userOpHash = await kernelClient.sendUserOperation({
callData,
maxFeePerGas: BigInt(0),
maxPriorityFeePerGas: BigInt(0),
});
const userOpReceipt = await kernelClient.waitForUserOperationReceipt({
hash: userOpHash,
});
const hash = userOpReceipt.receipt.transactionHash;
// AFTER: Gelato Smart Wallet SDK + Kernel Wallet
const account = await kernel({
owner: signer,
client: publicClient,
eip7702: false,
});
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(""),
});
const smartWalletClient = await createGelatoSmartWalletClient(
walletClient,
{
apiKey: process.env.SPONSOR_API_KEY || "",
}
);
const calls = [
{
to: "0x0000000000000000000000000000000000000000" as `0x${string}`,
value: BigInt(0),
data: "0x",
},
];
const preparedCalls = await smartWalletClient.prepare({
payment: sponsored(process.env.SPONSOR_API_KEY || ""),
calls,
});
const results = await smartWalletClient.send({ preparedCalls });
const hash = await results?.wait();
- Remove
@zerodev/sdk
imports - Add
@gelatonetwork/smartwallet
imports
- Replace ZeroDev account creation with Gelato's
kernel()
function - Remove entry point and kernel version configuration
- Remove validator creation
- Replace
createKernelAccountClient
withcreateWalletClient
+createGelatoSmartWalletClient
- Update bundler transport configuration
- Remove paymaster configuration
- Replace
encodeCalls()
withprepare()
- Replace
sendUserOperation()
withsend()
- Replace
waitForUserOperationReceipt()
withwait()
- Update transaction hash extraction
- Update error messages and logging
- Ensure retry logic remains compatible
The main differences when migrating from UltraRelay to GelatoRelay are:
- Account Creation: ZeroDev requires 4 steps vs Gelato's single function call
- Client Setup: ZeroDev uses one client vs Gelato's two-client approach
- Transaction Flow: ZeroDev uses
encodeCalls()
+sendUserOperation()
vs Gelato'sprepare()
+send()
- Relay Service: UltraRelay → GelatoRelay (primary change)
- SDK: ZeroDev SDK → Gelato Smart Wallet SDK (required for GelatoRelay integration)