Skip to main content

Your First Gasless Smart Account Transaction

Create a Smart Account and send gasless batched transactions in under 10 minutes

This guide walks you through creating your first smart account and sending multiple gasless transactions in a single UserOperation. You'll learn the core concepts of account abstraction while building a practical example that requires no ETH for gas fees.

What You'll Build

By the end of this tutorial, you'll have:

  • Created a smart contract wallet (smart account)
  • Batched two NFT minting transactions into one operation
  • Sent your first gasless UserOperation using a paymaster

What is a UserOperation?

A UserOperation is how smart accounts interact with Ethereum. Unlike regular transactions that go directly to the network, UserOperations are:

  • Batched: Multiple transactions can be grouped together
  • Gas flexible: Can be sponsored or paid with any ERC-20 token
  • Programmable: Include custom validation and execution logic

Quickstart

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ and npm/yarn
  • Basic TypeScript knowledge - We'll explain the smart account concepts
Complete Example

You can also fork the complete code and follow along.

Step 1: Project Setup

Create Project Directory

mkdir candide-smart-account
cd candide-smart-account
npx tsc --init

Install Dependencies

npm install typescript --save-dev
npm install abstractionkit dotenv

What each package does:

  • abstractionkit - Candide's SDK for smart account operations
  • dotenv - Loads environment variables from .env file

Configure Environment Variables

Create a .env file in your project root:

.env
CHAIN_ID=11155111
BUNDLER_URL=https://api.candide.dev/public/v3/sepolia
JSON_RPC_NODE_PROVIDER=https://ethereum-sepolia-rpc.publicnode.com
PAYMASTER_URL=https://api.candide.dev/public/v3/sepolia
SPONSORSHIP_POLICY_ID= # optional

PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE
PUBLIC_ADDRESS=YOUR_PUBLIC_ADDRESS_HERE

Create Main Script

Create an index.ts file:

index.ts
async function main(): Promise<void> {
// We'll build our smart account logic here
}

main().catch(console.error);

Step 2: Create Your Smart Account

Understanding Smart Accounts

Smart accounts are smart contracts. Unlike regular wallets (EOAs), they can:

  • Execute multiple transactions atomically
  • Have custom validation logic
  • Be controlled by multiple signers or different signature schemes

In this guide, we will use Safe's implementation, which is battle-tested and widely adopted.

Generate Account Address

index.ts
import * as dotenv from "dotenv";
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

async function main(): Promise<void> {
// Load environment variables
dotenv.config();

const ownerPublicAddress = process.env.PUBLIC_ADDRESS as string;

// Create a new smart account controlled by your EOA
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);

console.log("Smart Account Address:", smartAccount.accountAddress);
}

main().catch(console.error);

Test Your Setup

Run the code to generate your smart account address:

npx ts-node index.ts

Expected output:

Smart Account Address: 0x32afdcfa1e3bfe70d03ecb55b5c8045c26515c9d
How It Works

The initializeNewAccount() method deterministically computes your smart account address locally, using only the provided inputs—no network calls or API requests are made. The account itself is not deployed at this stage; deployment happens automatically when you send your first transaction.

Step 3: Prepare Your Transactions

What We're Building

We'll create two NFT minting transactions and batch them together. This demonstrates the power of smart accounts - executing multiple operations atomically.

Understanding Transaction Data

Every transaction needs three things:

  • to: The contract address to interact with
  • value: ETH amount to send (0 for most contract calls)
  • data: Encoded function call data

Build the Transactions

Add this to your index.ts (after the account creation code):

index.ts
import {
SafeAccountV0_3_0 as SafeAccount,
MetaTransaction,
getFunctionSelector,
createCallData,
} from "abstractionkit";

// Inside your main() function, after creating smartAccount:

// NFT contract we'll interact with (a test contract on Sepolia)
const nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336";

// Prepare the function call data
const mintFunctionSignature = 'mint(address)';
const mintFunctionSelector = getFunctionSelector(mintFunctionSignature);
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress] // Mint NFT to our smart account
);

// Create two identical transactions (we'll mint 2 NFTs)
const transaction1: MetaTransaction = {
to: nftContractAddress,
value: 0n, // No ETH needed for minting
data: mintTransactionCallData,
};

const transaction2: MetaTransaction = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
};

console.log("Prepared 2 NFT minting transactions");
Why Batch Transactions?

Instead of sending two separate transactions, we'll batch them into one UserOperation. This gives your app UX a one click experience.

Step 4: Create UserOperation

index.ts
const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string;
const bundlerUrl = process.env.BUNDLER_URL as string;

// Create the UserOperation (batching both transactions)
let userOperation = await smartAccount.createUserOperation(
[transaction1, transaction2], // Batch both NFT mints together
jsonRpcNodeProvider, // Used for nonce and gas price data
bundlerUrl // Used for gas limit estimation
);

Step 5: Get Paymaster Data

We'll use a paymaster to sponsor the gas fees, making this transaction completely gasless for the user.

index.ts
import {
SafeAccountV0_3_0 as SafeAccount,
MetaTransaction,
getFunctionSelector,
createCallData,
CandidePaymaster,
} from "abstractionkit";

const paymasterUrl = process.env.PAYMASTER_URL as string;
const paymaster = new CandidePaymaster(paymasterUrl);
const sponsorshipPolicyId = process.env.SPONSORSHIP_POLICY_ID;

let [paymasterUserOperation, _sponsorMetadata] = await paymaster.createSponsorPaymasterUserOperation(
userOperation,
bundlerUrl,
sponsorshipPolicyId // sponsorshipPolicyId will have no effect if empty
)

userOperation = paymasterUserOperation;

Step 6: Sign and Submit

Sign the UserOperation

Your smart account needs to be authorized by your EOA. Add this signing code:

index.ts
const chainId = BigInt(process.env.CHAIN_ID as string);
const privateKey = process.env.PRIVATE_KEY as string;

// Sign the UserOperation with your private key
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey], // Array because Safe supports multiple signers
chainId,
);

console.log("UserOperation signed successfully");

Submit to Network

Now send your UserOperation to the bundler:

index.ts
// Submit the UserOperation
const sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation,
bundlerUrl
);

console.log("UserOperation submitted! Waiting for confirmation...");

Wait for Confirmation

Track your UserOperation until it's included in a block:

index.ts
let userOperationReceiptResult = await sendUserOperationResponse.included()

console.log("Useroperation receipt received.")
console.log(userOperationReceiptResult)

if (userOperationReceiptResult.success) {
console.log("Two Nfts were minted. The transaction hash is : " + userOperationReceiptResult.receipt.transactionHash)
} else {
console.log("Useroperation execution failed")
}

Run Your Complete Code

Execute your smart account transaction:

npx ts-node index.ts

Expected output:

Example Receipt Result
Useroperation sent. Waiting to be included ......
Useroperation receipt received.
{
userOpHash: '0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514',
entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
sender: '0xb8741a449d50ed0dcfe395287f85be152884c8d9',
nonce: 0n,
paymaster: '0x3fe285dcd76bcce4ac92d38a6f2f8e964041e020',
actualGasCost: 8078496n,
actualGasUsed: 504906n,
success: true,
logs: '[{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000007b44a0000000000000000000000000000000000000000000000000000000000007b44a","logIndex":"0x9a","removed":false,"topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000003fe285dcd76bcce4ac92d38a6f2f8e964041e020"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"}]',
receipt: {
blockHash: '0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c',
blockNumber: 10419807n,
from: '0x3cfdc212769c890907bce93d3d8c2c53de6a7a89',
cumulativeGasUsed: 6978990n,
gasUsed: 507053n,
logs: '[{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x90","removed":false,"topics":["0xecdf3a3effea5783a3c4c2140e677577666428d44ed9d474a0b3a4c9943f8440","0x000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b4037"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008ecd4ec46d4d2a6b64fe960b3d64e8b94b2234eb000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b40370000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bdbc5fbc9ca8c3f514d073ec3de840ac84fc6d31","logIndex":"0x91","removed":false,"topics":["0x141df868a6331af528e38c83b7aa03edc19be66e37ae67f9285bf4f8e3c6a1a8","0x0000000000000000000000004e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x4e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x00000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762","logIndex":"0x92","removed":false,"topics":["0x4f51faf6c4561ff95f067657e43439f0f856d97c04d9ec9070a6199ad418e235","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000004e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec670000000000000000000000003fe285dcd76bcce4ac92d38a6f2f8e964041e020","logIndex":"0x93","removed":false,"topics":["0xd51a9c61267aa6196961883ecf5ff2da6619c37dac0fa92122513fb32c032d2d","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x94","removed":false,"topics":["0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b403700000000000000000000000038869bf66a61cf6bdb996a6ae40d5853fd43b526000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001048d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000b200d9de104e3386d9a45a61bce269c43e48b534e4e7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041249c58b00d9de104e3386d9a45a61bce269c43e48b534e4e7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041249c58b000000000000000000000000000000000000000000000000000000000000000000000000000000000000","logIndex":"0x95","removed":false,"topics":["0xb648d3644f584ed1c2232d53c46d87e693586486ad0d1175f8656013110b714e"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xd9de104e3386d9a45a61bce269c43e48b534e4e7","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x96","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000000000000000000000000000000000000000000056"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xd9de104e3386d9a45a61bce269c43e48b534e4e7","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x97","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000000000000000000000000000000000000000000057"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x98","removed":false,"topics":["0x6895c13664aa4f67288b25d7a21d7aaa34916e355fb9b6fae0a139a9085becb8","0x000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b4037"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x3fe285dcd76bcce4ac92d38a6f2f8e964041e020","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000000000000000000000000000000000000000000000","logIndex":"0x99","removed":false,"topics":["0xa050a122b4c0e369e3385eb6b7cccd8019638b2764de67bec0af99130ddf8471","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000000000000000000000000000000000000000000000"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000007b44a0000000000000000000000000000000000000000000000000000000000007b44a","logIndex":"0x9a","removed":false,"topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000003fe285dcd76bcce4ac92d38a6f2f8e964041e020"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"}]',
logsBloom: '0x0000040000009000000000000000010080000000000000000000000000000000000800000000000000020001000004040010000000000000800002000000000000001000000000000000000c0002000000000000010000080040000000000000020000000a0000000500002000000800008000000100000000000014000000000800010020000200008000000040000000000200000400000000000000000000000004000000000000500000000004000210000000000000000002001000000020200082000000000001000008000000000000002060000000100000000026000000082000010000000000000008100220000000000000000000000010000200',
transactionHash: '0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa',
transactionIndex: 29n,
effectiveGasPrice: 16n
}
}
Two Nfts were minted. The transaction hash is : 0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa

🎉 Congratulations! You've successfully sent your first batched gasless smart account transaction.

Full Example

Below is the complete example code that you can copy directly to implement the functionality described in the tutorial.

index.js
import * as dotenv from 'dotenv'

import {
SafeAccountV0_3_0 as SafeAccount,
MetaTransaction,
CandidePaymaster,
getFunctionSelector,
createCallData,
} from "abstractionkit";

async function main(): Promise<void> {
//get values from .env
dotenv.config()
const chainId = BigInt(process.env.CHAIN_ID as string)
const bundlerUrl = process.env.BUNDLER_URL as string
const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string
const paymasterURL = process.env.PAYMASTER_URL as string
const ownerPublicAddress = process.env.PUBLIC_ADDRESS as string
const ownerPrivateKey = process.env.PRIVATE_KEY as string

//initializeNewAccount only needed when the smart account
//have not been deployed yet for its first useroperation.
//You can store the accountAddress to use it to initialize
//the SafeAccount object for the following useroperations
let smartAccount = SafeAccount.initializeNewAccount(
[ownerPublicAddress],
)

//After the account contract is deployed, no need to call initializeNewAccount
//let smartAccount = new SafeAccount(accountAddress)

console.log("Account address(sender) : " + smartAccount.accountAddress)

//create two meta transaction to mint two NFTs
//you can use favorite method (like ethers.js) to construct the call data
const nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336";
const mintFunctionSignature = 'mint(address)';
const mintFunctionSelector = getFunctionSelector(mintFunctionSignature);
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress]
);
const transaction1 :MetaTransaction ={
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

const transaction2 :MetaTransaction ={
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

//createUserOperation will determine the nonce, fetch the gas prices,
//estimate gas limits and return a useroperation to be signed.
//you can override all these values using the overrides parameter.
let userOperation = await smartAccount.createUserOperation(
[
//You can batch multiple transactions to be executed in one useroperation.
transaction1, transaction2,
],
jsonRpcNodeProvider, //the node rpc is used to fetch the current nonce and fetch gas prices.
bundlerUrl, //the bundler rpc is used to estimate the gas limits.
)

// Get paymaster data to sponsor the transaction
const paymasterUrl = process.env.PAYMASTER_URL as string;
const paymaster: CandidePaymaster = new CandidePaymaster(paymasterUrl);
const sponsorshipPolicyId = process.env.SPONSORSHIP_POLICY_ID;

let [paymasterUserOperation, _sponsorMetadata] = await paymaster.createSponsorPaymasterUserOperation(
userOperation,
bundlerUrl,
sponsorshipPolicyId // sponsorshipPolicyId will have no effect if empty
)
userOperation = paymasterUserOperation;

console.log("Transaction will be sponsored - no ETH required!");

//Safe is a multisig that can have multiple owners/signers
//signUserOperation will create a signature for the provided
//privateKeys
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[ownerPrivateKey],
chainId
)
console.log(userOperation)

//use the bundler rpc to send a userOperation
//sendUserOperation will return a SendUseroperationResponse object
//that can be awaited for the useroperation to be included onchain
const sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation, bundlerUrl
)

console.log("Useroperation sent. Waiting to be included ......")
//included will return a UserOperationReceiptResult when
//useroperation is included onchain
let userOperationReceiptResult = await sendUserOperationResponse.included()

console.log("Useroperation receipt received.")
console.log(userOperationReceiptResult)
if(userOperationReceiptResult.success){
console.log("Two Nfts were minted. The transaction hash is : " + userOperationReceiptResult.receipt.transactionHash)
}else{
console.log("Useroperation execution failed")
}
}

main()

Troubleshooting

"Insufficient funds" error

  • Make sure your smart account address has enough Sepolia ETH, or are using a paymaster to sponsor gas fees

"Invalid signature" error

  • Verify your PRIVATE_KEY matches your PUBLIC_ADDRESS
  • Ensure you're using the correct chain ID

Network/timeout errors

  • Try a different node endpoint if the current one is down
  • Check your internet connection

Getting Help

If you're still stuck:

You can view your transaction on block explorers that support UserOperations like Blockscout.

What's Next?

Now that you've mastered the basics, explore more advanced smart account features:

  1. Pay Gas in ERC-20 - Let users pay fees with USDC or other tokens
  2. Passkeys - Let users secure their account with biometrics login
  3. Enable Account Recovery - Let users add recovery methods through guardians or traditional sms/email