Zero Gas Mass Payment with SBC
This guide demonstrates how to implement gasless mass payments using SBC's custom paymaster and bundler. With this functionality, you can send SBC tokens to multiple recipients in a single transaction without paying gas fees.
Prerequisites
- Access to SBC's bundler and paymaster services
- Sufficient SBC tokens in your wallet
Visit the SBC Dashboard to get an API Key as well as SBC on Base Sepolia. Feel free to reach out to us on Telegram to ask any questions.
Contract Addresses
Base Sepolia (Testnet)
- SBC Token:
0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16
How It Works
A mass payment is a single UserOperation carrying a batch of calls:
- One
permitcall, letting the smart account move SBC on the owner's behalf - One
transferFromcall per recipient - Zero gas fees, because SBC's paymaster sponsors the UserOperation
All of it settles in one transaction, for up to 200 recipients.
We'll send 0.01 SBC to each of 2 addresses.
Implementation Steps
1. Imports and setup
import {
createPublicClient,
createWalletClient,
custom,
encodeFunctionData,
erc20Abi,
http,
parseSignature,
parseUnits,
type Hex,
} from "viem";
import { baseSepolia } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { createPaymasterClient } from "viem/account-abstraction";
import { toSimpleSmartAccount } from "permissionless/accounts";
import { createSmartAccountClient } from "permissionless";
import { Token } from "@uniswap/sdk-core";
// Helper that builds the EIP-2612 permit signature.
// Source: https://github.com/stablecoinxyz/masspay/blob/main/src/lib/masspay.ts#L232
import { getPermitSignature } from "./lib/masspay";
const chain = baseSepolia;
const SBC_TOKEN_ADDRESS = "0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16" as Hex;
const SBC_DECIMALS = 6;
// getPermitSignature reads the token's name and address to build the EIP-712 domain
const SBC = new Token(chain.id, SBC_TOKEN_ADDRESS, SBC_DECIMALS, "SBC", "Stable Coin");
// Visit https://dashboard.stablecoin.xyz to get an API key
const PAYMASTER_API_URL = `https://api.aa.stablecoin.xyz/rpc/v1/baseSepolia/${process.env.SBC_PAYMASTER_API_KEY}`;
const publicClient = createPublicClient({
chain,
transport: http(),
});2. Create a SimpleAccount smart account from your wallet (owner)
const [ownerAddress] = await (window as any).ethereum.request({
method: "eth_requestAccounts",
});
const owner = createWalletClient({
account: ownerAddress as Hex,
chain,
transport: custom((window as any).ethereum),
});
const simpleAccount = await toSimpleSmartAccount({
client: publicClient,
owner,
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
});3. Create a Paymaster Client from our Paymaster URL
const pmClient = createPaymasterClient({
transport: http(PAYMASTER_API_URL),
});4. Create the Smart Account Bundler Client with our Paymaster Client
const smartAccountClient = createSmartAccountClient({
account: simpleAccount,
chain,
bundlerTransport: http(PAYMASTER_API_URL),
paymaster: pmClient,
userOperation: {
estimateFeesPerGas: async () => {
const gasPrice = await publicClient.getGasPrice();
return {
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: gasPrice,
};
},
},
});5. Prepare the calldata
const txs = [
{ to: "0x124b082e8DF36258198da4Caa3B39c7dFa64D9cE" as Hex, value: 0.01 },
{ to: "0xB5f6fECd59dAd3d5bA4Dfe8FcCA6617CE71B99f9" as Hex, value: 0.01 },
];
const txnBigInts = txs.map((tx) => ({
to: tx.to,
value: parseUnits(tx.value.toString(), SBC_DECIMALS),
}));
// Build the calls array, sending from the owner wallet to each `to` address.
const calls = txnBigInts.map((tx) => ({
to: SBC_TOKEN_ADDRESS,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "transferFrom",
args: [owner.account.address, tx.to, tx.value],
}),
}));6. Compute the permit signature
The smart account is the one submitting the transfers, so the owner permits it to spend the total.
// Sum the total value of the mass pay
const totalValue = txnBigInts.reduce((acc, tx) => acc + tx.value, 0n);
// Set a 30 min deadline for the signature (seconds since epoch)
const deadline = Math.floor(Date.now() / 1000) + 60 * 30;
// The SimpleAccount's (counterfactual) address is the spender
const senderAddress = simpleAccount.address;
const signature = await getPermitSignature(
chain,
owner,
SBC,
owner.account.address,
senderAddress,
totalValue,
deadline,
);
const { r, s, v } = parseSignature(signature);
// Encode the permit call and prepend it to the batch
const permitData = encodeFunctionData({
abi: erc20PermitAbi,
functionName: "permit",
args: [owner.account.address, senderAddress, totalValue, deadline, v, r, s],
});
calls.unshift({
to: SBC_TOKEN_ADDRESS,
data: permitData,
});7. Send the batch call with our Smart Account Client
const userOpHash = await smartAccountClient.sendUserOperation({ calls });
const receipt = await smartAccountClient.waitForUserOperationReceipt({
hash: userOpHash,
});
console.log(`User Op Hash: ${receipt.userOpHash}`);Performance Considerations
- Gas Limits: Mass payments are gas intensive as the information needed (addresses) take up room in the calldata. Each chain has its own maximum gas per block.
- Transaction Limits: The maximum number of recipients in a single batch is 200.
Next Steps
- Learn about NFT Minting with gasless transactions