@gelatocloud/gasless
v0.0.12
Published
Gelato Gasless SDK: All-in-one solution for gasless transactions
Readme
@gelatocloud/gasless
All-in-one solution for gasless transactions.
Features
- Three abstraction levels - Choose the optimal integration model for your application::
- Turbo Relayer: The fastest, most efficient way to submit transactions on-chain with zero gas overhead, ideal for latency-sensitive workflows.
- Turbo Relayer with Smart Account - Leverage Smart Accounts for streamlined transaction encoding and signing while retaining Turbo-level performance.
- ERC-4337 Bundler - A fully compliant ERC-4337 bundler for native Account Abstraction flows.
- Sponsorship via Gas Tank - Support for sponsored transactions using your Gas Tank.
- 2D nonce support - Advanced nonce management using both
nonceandnonceKeyfor parallelized execution. - Type-safe - Implemented on top of viem, offering complete TypeScript type safety and developer ergonomics.
- Synchronous methods: Send transaction and get the receipt in a single call
- WebSockets: Live notifications and updates via WebSockets
Learn more in our docs
Installation
pnpm install viem @gelatocloud/gaslessPeer dependencies: viem
Quick Start
- Get your API key at https://app.gelato.cloud/
- Set your environment variable:
export GELATO_API_KEY="your-api-key"Usage
Turbo Relayer
Direct gasless transaction relay without smart accounts. Best for simple sponsored transactions.
Synchronous:
import { createGelatoEvmRelayerClient } from '@gelatocloud/gasless';
import { baseSepolia } from 'viem/chains';
const relayer = createGelatoEvmRelayerClient({
apiKey: process.env.GELATO_API_KEY,
testnet: true
});
// Send and wait for inclusion in one call
const receipt = await relayer.sendTransactionSync({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...'
});
console.log(`Transaction hash: ${receipt.transactionHash}`);Asynchronous:
import { createGelatoEvmRelayerClient } from '@gelatocloud/gasless';
import { baseSepolia } from 'viem/chains';
const relayer = createGelatoEvmRelayerClient({
apiKey: process.env.GELATO_API_KEY,
testnet: true
});
// Send transaction (returns immediately with ID)
const id = await relayer.sendTransaction({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...'
});
// Poll for status separately
const receipt = await relayer.waitForReceipt({ id });
console.log(`Transaction hash: ${receipt.transactionHash}`);Account (Gelato Smart Account)
Gelato's smart account implementation with ERC-7821 delegation pattern.
Synchronous:
import {
createGelatoSmartAccountClient,
toGelatoSmartAccount } from '@gelatocloud/gasless';
import { createPublicClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';
const owner = privateKeyToAccount('0xYourPrivateKey...');
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http()
});
// Create a Gelato smart account
const account = toGelatoSmartAccount({ client: publicClient, owner });
// Create the smart account client
const client = await createGelatoSmartAccountClient({
account,
apiKey: process.env.GELATO_API_KEY
});
// Send and wait for inclusion in one call
const receipt = await client.sendTransactionSync({
calls: [
{ to: '0xContract1...', data: '0xCalldata1...' },
{ to: '0xContract2...', data: '0xCalldata2...' }
]// Optional: nonce or nonceKey for 2D nonce management
});
console.log(`Transaction hash: ${receipt.transactionHash}`);Asynchronous:
import {
createGelatoSmartAccountClient,
toGelatoSmartAccount } from '@gelatocloud/gasless';
// ... same setup as above ...
// Send transaction (returns immediately with ID)
const id = await client.sendTransaction({
calls: [{ to: '0xContract...', data: '0xCalldata...' }] });
// Poll for status separately
const receipt = await client.waitForReceipt({ id });
console.log(`Transaction hash: ${receipt.transactionHash}`);Bundler (ERC-4337)
Compatible with any ERC-4337 smart account.
import { createGelatoBundlerClient } from '@gelatocloud/gasless';
import { to7702SimpleSmartAccount } from 'permissionless/accounts';
import { createPublicClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';
const owner = privateKeyToAccount('0xYourPrivateKey...');
const client = createPublicClient({
chain: baseSepolia,
transport: http()
});
// Create a smart account
const account = await to7702SimpleSmartAccount({
client,
owner
});
// Create the Gelato bundler client
const bundler = await createGelatoBundlerClient({
account,
client,
apiKey: process.env.GELATO_API_KEY,
sponsored: true
});
// Send a user operation
const hash = await bundler.sendUserOperation({
calls: [{ to: '0x...', data: '0x...' }]
});
// Wait for the receipt
const { receipt } = await bundler.waitForUserOperationReceipt({ hash });
console.log(`Transaction hash: ${receipt.transactionHash}`);WebSocket Subscriptions
WebSockets are enabled by default. Methods automatically race WebSocket notifications against HTTP polling for the fastest result. To disable:
const relayer = createGelatoEvmRelayerClient({
apiKey: process.env.GELATO_API_KEY,
ws: { disable: true }
});Relayer — single transaction:
const id = await relayer.sendTransaction({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...'
});
const subscription = await relayer.ws.subscribe({ id });
subscription.on('success', (data) => {
console.log(`Included in block ${data.receipt.blockNumber}`);
});
subscription.on('reverted', (data) => {
console.log(`Reverted: ${data.receipt.blockNumber}`);
});
// Cleanup when done
await relayer.ws.unsubscribe(subscription.subscriptionId);
relayer.ws.disconnect();Relayer — global (all transactions):
const subscription = await relayer.ws.subscribe();
subscription.on('submitted', (data) => console.log(`${data.id} submitted`));
subscription.on('success', (data) => console.log(`${data.id} success`));
subscription.on('rejected', (data) => console.log(`${data.id} rejected`));Bundler — same patterns apply via bundler.ws:
const subscription = await bundler.ws.subscribe({ id: userOpHash });
subscription.on('success', (data) => console.log(data.receipt));Events:
| Event | Description |
|-------------|-------------------------------------|
| pending | Transaction pending |
| submitted | Submitted to network |
| success | Successfully included on-chain |
| rejected | Rejected by relayer |
| reverted | Reverted on-chain |
Cleanup:
await relayer.ws.unsubscribe(subscription.subscriptionId);
relayer.ws.disconnect();Get Balance
Check your Gas Tank balance. Available on all client types.
const { balance, decimals, unit } = await relayer.getBalance();
// Also: client.getBalance(), bundler.getBalance()Sync vs Async
The SDK offers two ways to send transactions:
| | Sync (sendTransactionSync) | Async (sendTransaction + WS) |
|---|---|---|
| Returns | Final TransactionReceipt | ID (Hex) immediately |
| Lifecycle events | None (handled internally) | All: pending, submitted, success, rejected, reverted |
| Tx hash on (re)submission | Not exposed | hash field on every submitted event |
| Control | Minimal — fire and forget | Full — react to each status change via WS subscription |
| Reorg Protection | None | Yes, a new update event is published |
Sync — call sendTransactionSync to send the transaction and get the receipt in the same call. If the transaction is not included, the SDK will handle it internally and return a final TransactionReceipt by racing http polls and ws updates. Simplest approach when you just need the result:
const receipt = await relayer.sendTransactionSync({ chainId, to, data });Async — call sendTransaction to get a ID immediately, then subscribe via WebSocket for granular status updates. WS subscriptions give you full control over the transaction lifecycle — you can react to resubmissions, display tx hashes to users in real time, handle rejections immediately, and more. The submitted event includes the transaction hash each time the relay (re)submits the transaction (e.g. with bumped gas):
const id = await relayer.sendTransaction({ chainId, to, data });
const subscription = await relayer.ws.subscribe({ id });
subscription.on('submitted', (data) => {
console.log(`Tx hash: ${data.hash}`); // available on every (re)submission
});
subscription.on('success', (data) => {
console.log(`Confirmed in block ${data.receipt.blockNumber}`);
});API Reference
Relayer API
createGelatoEvmRelayerClient(config)
Creates a low-level relayer client for direct transaction submission.
const client = createGelatoEvmRelayerClient({
apiKey: string, // Your Gelato API key
testnet: boolean // true for testnets, false for mainnet
});Methods:
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| sendTransaction | { chainId, to, data, authorizationList?, context? } | Promise<Hex> | Submit a transaction |
| sendTransactionSync | { chainId, to, data, timeout?, pollingInterval?, throwOnReverted?, ... } | Promise<TransactionReceipt> | Send and wait for receipt |
| getStatus | { id: string } | Promise<Status> | Get transaction status |
| waitForReceipt | { id: string, timeout?, pollingInterval?, throwOnReverted? } | Promise<TransactionReceipt> | Wait for receipt, throws on failure |
| getBalance | - | Promise<Balance> | Get Gas Tank balance |
| getCapabilities | - | Promise<Capabilities> | Get supported chains |
| getFeeData | { chainId, gas, l1Fee? } | Promise<FeeData> | Get network fee data |
Account API
toGelatoSmartAccount(params)
Creates a Gelato smart account using ERC-7821 delegation.
const account = toGelatoSmartAccount({
client: Client, // viem public client
owner: Account // EOA that owns the smart account
});createGelatoSmartAccountClient(config)
Creates a client for interacting with a Gelato smart account.
const client = await createGelatoSmartAccountClient({
apiKey: string, // Your Gelato API key
account: SmartAccount // From toGelatoSmartAccount()
});Methods:
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| sendTransaction | { calls, nonce?, nonceKey?} | Promise<Hex> | Send transaction(s) |
| sendTransactionSync | { calls, nonce?, nonceKey?, timeout?, pollingInterval?, throwOnReverted?, ... } | Promise<TransactionReceipt> | Send and wait for receipt |
| getStatus | { id: string } | Promise<Status> | Get transaction status |
| waitForReceipt | { id: string, timeout?, pollingInterval?, throwOnReverted? } | Promise<TransactionReceipt> | Wait for receipt, throws on failure |
| getBalance | - | Promise<Balance> | Get Gas Tank balance |
| getCapabilities | - | Promise<Capabilities> | Get supported chains |
Nonce Options:
nonce: Explicit nonce valuenonceKey: Key for 2D nonce (allows parallel transactions)
Polling Configuration:
All synchronous methods (sendTransactionSync, sendUserOperationSync, waitForReceipt) support customizable polling behavior:
timeout(optional): Maximum wait time in milliseconds- Default:
120000(2 minutes) - Must not exceed
600000(10 minutes)
- Default:
pollingInterval(optional): Frequency to check status in milliseconds- Default:
1000(1 second)
- Default:
Example:
// Wait up to 30 seconds, checking every 500ms
const receipt = await relayer.sendTransactionSync({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...',
timeout: 30000,
pollingInterval: 500
});Bundler API
createGelatoBundlerClient(config)
Creates an ERC-4337 bundler client. Extends viem's BundlerClient.
const bundler = await createGelatoBundlerClient({
account: SmartAccount, // Any ERC-4337 smart account
client: Client, // viem public client
apiKey: string, // Your Gelato API key
sponsored: boolean, // Whether to use sponsored payment via Gas Tank
pollingInterval?: number // Polling interval in ms
});Methods:
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| sendUserOperation | { calls } | Promise<Hex> | Send a user operation |
| sendUserOperationSync | { calls, timeout?, pollingInterval? } | Promise<UserOperationReceipt> | Send and wait for receipt |
| waitForUserOperationReceipt | { hash } | Promise<{ receipt }> | Wait for receipt |
| getBalance | - | Promise<Balance> | Get Gas Tank balance |
| estimateUserOperationGas | UserOperationParams | Promise<GasEstimate> | Estimate gas |
| prepareUserOperation | UserOperationParams | Promise<UserOperation> | Prepare operation |
| getUserOperationGasPrice | - | Promise<GasPrice> | Get current gas prices |
| getUserOperationQuote | UserOperationParams | Promise<Quote> | Get fee quote |
Types
StatusCode
enum StatusCode {
Pending = 100, // Transaction pending
Submitted = 110, // Submitted to network
Success = 200, // Successfully included
Rejected = 400, // Rejected by relayer
Reverted = 500 // Reverted on-chain
}ErrorCode
enum ErrorCode {
// JSON-RPC
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
TimeoutError = -32070,
// Relayer
Unauthorized = 4100,
UnsupportedPaymentToken = 4202,
InsufficientPayment = 4200,
InsufficientBalance = 4205,
UnsupportedChain = 4206,
UnknownTransactionId = 4208,
InvalidAuthorizationList = 4210,
SimulationFailed = 4211,
// Bundler
ValidationFailed = -32500,
PaymasterValidationFailed = -32501,
InvalidSignature = -32507,
ExecutionFailed = -32521
}Call
type Call = {
to: Address; // Target contract address
data?: Hex; // Calldata (optional)
value?: bigint; // ETH value (optional)
};Status Handling
const status = await client.getStatus({ id: hash });
switch (status.status) {
case StatusCode.Success:
console.log('Success:', status.receipt.transactionHash);
break;
case StatusCode.Rejected:
console.log('Rejected:', status.message);
break;
case StatusCode.Reverted:
console.log('Reverted:', status.data);
break;
}Error Handling
Timeout Errors
Synchronous methods (sendTransactionSync, waitForReceipt) throw TimeoutError when operations don't complete within the configured timeout:
import { TimeoutError } from '@gelatocloud/gasless';
try {
const receipt = await relayer.sendTransactionSync({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...',
timeout: 10000
});
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Transaction timed out:', error.message);
// Transaction may still be pending - you can retry with longer timeout
// or use async methods to check status manually
} else {
console.error('Other error:', error);
}
}Revert Handling
By default, sendTransactionSync and waitForReceipt return the receipt even when a transaction reverts on-chain. Set throwOnReverted: true to throw a TransactionRevertedError instead:
import { TransactionRevertedError } from '@gelatocloud/gasless';
try {
const receipt = await relayer.sendTransactionSync({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...',
throwOnReverted: true
});
} catch (error) {
if (error instanceof TransactionRevertedError) {
console.error('Transaction reverted:', error.receipt.transactionHash);
console.error('Error message:', error.erroMessage);
console.error('Error data:', error.errorData);
// Also available: error.id, error.chainId, error.createdAt
}
}Available on both relayer and account clients via sendTransactionSync and waitForReceipt. Properties on TransactionRevertedError: receipt, id, chainId, createdAt, errorData, errorMessage.
Simulation Errors
When the relayer simulates a transaction before submission and the simulation reverts, a SimulationFailedRpcError is thrown with error code 4211. This happens before the transaction is sent on-chain:
import { SimulationFailedRpcError } from '@gelatocloud/gasless';
try {
const receipt = await relayer.sendTransactionSync({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...'
});
} catch (error) {
if (error instanceof SimulationFailedRpcError) {
console.error('Simulation reverted:', error.message);
console.error('Revert data:', error.revertData);
// error.params contains the original { to, chainId, data } sent
}
}Properties on SimulationFailedRpcError: revertData, params, code (4211).
Automatic Retries
Relayer methods (sendTransaction, sendTransactionSync) support automatic retries when specific error codes are encountered. This is useful for transient failures like simulation errors (4211) that may succeed on a subsequent attempt.
const receipt = await relayer.sendTransactionSync(
{
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...'
},
{
retries: {
max: 3, // Retry up to 3 times (default: 0, max: 5)
delay: 1000, // Wait 1s between retries (default: 200ms)
backoff: 'fixed', // 'exponential' (default) or 'fixed'
errorCodes: [4211] // Error codes that trigger a retry (default: [4211])
}
}
);Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| max | number | 0 | Maximum number of retries (0–5). Clamped to 5. |
| delay | number | 200 | Base delay in milliseconds before each retry. |
| backoff | 'fixed' \| 'exponential' | 'exponential' | Backoff strategy. 'fixed' keeps constant delay; 'exponential' doubles each retry (delay × 2^attempt). |
| maxDelay | number | 10000 | Maximum delay in ms. Caps exponential backoff so it never exceeds this value. |
| errorCodes | number[] | [4211] | RPC error codes that trigger a retry. Default is SimulationFailedRpcError. |
Works with both async and sync relayer methods:
// Async
const id = await relayer.sendTransaction(
{ chainId, to, data },
{ retries: { max: 3 } }
);
// Sync
const receipt = await relayer.sendTransactionSync(
{ chainId, to, data },
{ retries: { max: 3 } }
);Only errors with a matching code property are retried. All other errors are thrown immediately.
Automatic Fallback on Timeout
When sendTransactionSync rpc request times out, the SDK automatically falls back to polling for the transaction status. If you see a warning message like:
Transaction 0x... sync call timed out, falling back to polling for completion. DO NOT RETRY this transaction.This means your transaction was successfully submitted but the sync method timed out. The SDK will continue polling for completion automatically. Do not retry the operation as this could result in duplicate transactions.
Recovery Strategies
If a timeout occurs:
- Wait for automatic fallback:
sendTransactionSyncautomatically polls after timeout - Check status manually: Use
getStatus({ id })to check if transaction is still processing if needed - Retry with longer timeout: Increase
timeoutand callwaitForReceiptagain - Use async methods: Switch to async pattern for more control
Configuration Limits
The SDK enforces the following limits to prevent denial of service:
import {
TimeoutError,
TransactionRevertedError,
SimulationFailedRpcError,
MIN_TIMEOUT,
MAX_TIMEOUT,
MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL
} from '@gelatocloud/gasless';
console.log(MIN_TIMEOUT); // 1000ms (1 second)
console.log(MAX_TIMEOUT); // 600000ms (10 minutes)
console.log(MIN_POLLING_INTERVAL); // 100ms
console.log(MAX_POLLING_INTERVAL); // 300000ms (5 minutes)You can set default timeout and polling interval at the client level:
const relayer = createGelatoEvmRelayerClient({
apiKey: process.env.GELATO_API_KEY,
timeout: 30000, // Default 30 second timeout
pollingInterval: 500 // Default 500ms polling interval
});
// Methods use client defaults unless overridden
const receipt = await relayer.sendTransactionSync({
chainId: baseSepolia.id,
to: '0xTargetContract...',
data: '0xCalldata...',
// timeout: 60000 // Optional: override client default
});Requirements
- Node.js >= 23
- Peer dependencies:
viem,zod - Optional:
typescript
Examples
See the /examples directory for complete working examples:
examples/relayer/sponsored- Direct relayer usageexamples/relayer/get-balance- Get Gas Tank balanceexamples/account/sponsored- Gelato smart accountexamples/bundler/sponsored- ERC-4337 bundlerexamples/bundler/get-balance- Get Gas Tank balance (bundler)examples/relayer/ws- Relayer WebSocket usageexamples/bundler/ws- Bundler WebSocket usageexamples/account/ws- Account WebSocket usage
Run an example:
cd examples/account/sponsored
npm install
GELATO_API_KEY=your-key npm run devLinks
License
MIT
