Get Started

Send your first user operation

Stackup operates on an open-source foundation that is wholly in sync with the ERC-4337 standard. This ensures seamless interoperability with other services that adhere to the same ERC-4337 protocol and allows for the option of self-hosting.

What you'll learn

This guide walks you though a simple interaction with Stackup's services: sending a User Operation with gas paid by a Paymaster. For a fuller view of Stackup's solutions check out our guides.

Skip to 5:05 in the above video to skip to writing code.

Every call to Stackup's API must include an API key. After you create a Stackup account, you can generate API keys for each of your applications. From you Stackup dashboard you can modify the access settings of each of your API keys.

1. Initialize a paymaster

Install the userop.js and ethers libraries. Before you start building your User Operation, you need to specify the Paymaster you are going to use. Make sure the paymaster is toggled "on" for your API key.

import { ethers } from "ethers";
import { Presets, Client } from "userop";

const paymasterRpcUrl = "";
const paymasterContext = {type: "payg"};
const paymaster = Presets.Middleware.verifyingPaymaster(

2. Create a builder

To create a User Operation, enter the following code snippet into a new app and run it. This will create a user operation builder based on the Kernel Smart Account and display the address of the new account.

const signer = new ethers.Wallet(signingKey);
const rpcUrl = "";

// Initialize the Builder
const builder = await Presets.Builder.Kernel.init(
  paymasterMiddleware: paymaster
const address = builder.getSender();
console.log(`Account address: ${address}`);

If everything worked, you will get this response in the console:

Account address: 0x000...000

The preset contains middleware functions. Middleware functions are executed in the order they are created, which is why the paymaster middleware has to be created first.

3. Create the call data

Call data contain the instructions that will be executed on-chain. This user operation contains two contract calls: one to approve an ERC-20 token for transfer, and then the transfer of the token.

const token = "TOKEN_ADDRESS";
const value = "VALUE_TO_SEND";

// Read the ERC-20 token contract
const ERC20_ABI = require("./abi.json");
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const erc20 = new ethers.Contract(token, ERC20_ABI, provider);
const decimals = await Promise.all([erc20.decimals()]);
const amount = ethers.utils.parseUnits(value, decimals);

// Create the calls
const approve = {
  to: token,
  value: ethers.constants.Zero,
  data: erc20.interface.encodeFunctionData("approve", [to, amount]),

const send = {
  to: token,
  value: ethers.constants.Zero,
  data: erc20.interface.encodeFunctionData("transfer", [to, amount]),

const calls = [approve, send];

4. Send the user operation to the bundler network

Create a Bundler client and send the User Operation.

// Build & send
const client = await Client.init(rpcUrl);
const res = await client.sendUserOperation(builder.executeBatch(calls), {
  onBuild: (op) => console.log("Signed UserOperation:", op),

console.log(`UserOpHash: ${res.userOpHash}`);
console.log("Waiting for transaction...");
const ev = await res.wait();
console.log(`Transaction hash: ${ev?.transactionHash ?? null}`);

You've now sent your first user operation!

What’s Next