Send Gasless Transactions
Configure account abstraction with a paymaster so users never pay gas
Send Gasless Transactions
Sponsor gas fees for your users through the ERC-4337 paymaster protocol. The paymaster pays gas on behalf of the user, so they can interact with the blockchain without holding any native tokens.
Prerequisites
@nerochain/mpc-sdkinstalled with theetherspeer dependency- A NERO project with an API key
- The SDK configured with the Pedersen protocol (required for smart accounts)
npm install @nerochain/mpc-sdk ethers @noble/curves @noble/hashesStep 1 -- Configure the SDK with Pedersen Protocol
Gasless transactions require ERC-4337 smart accounts, which use the Pedersen DKG protocol. The wsUrl parameter is required for real-time MPC coordination during key generation and signing.
import { NeroMpcAuthProvider } from "@nerochain/mpc-sdk/react";
function App() {
return (
<NeroMpcAuthProvider
config={{
backendUrl: "https://your-api.example.com",
apiKey: "your-project-api-key",
protocol: "pedersen",
wsUrl: "wss://your-ws.example.com",
chainId: 689,
}}
>
<GaslessDemo />
</NeroMpcAuthProvider>
);
}Step 2 -- Set Up the Smart Account Infrastructure
Import the Account Abstraction utilities from @nerochain/mpc-sdk/aa. These connect to the NERO testnet bundler and paymaster services.
import {
SimpleAccount,
BundlerClient,
PaymasterClient,
} from "@nerochain/mpc-sdk/aa";
const NERO_TESTNET = {
bundlerUrl: "https://bundler-testnet.nerochain.io",
paymasterUrl: "https://paymaster-testnet.nerochain.io",
entryPointAddress: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
factoryAddress: "0x9406Cc6185a346906296840746125a0E44976454",
chainId: 689,
};
const bundler = new BundlerClient(NERO_TESTNET.bundlerUrl);
const paymaster = new PaymasterClient(NERO_TESTNET.paymasterUrl);Step 3 -- Build and Send a Sponsored UserOperation
The full flow: build a UserOperation, request paymaster sponsorship, sign it with MPC, and submit it through the bundler.
import { useState } from "react";
import {
useNeroWallet,
useNeroMpcAuth,
} from "@nerochain/mpc-sdk/react";
import {
SimpleAccount,
BundlerClient,
PaymasterClient,
} from "@nerochain/mpc-sdk/aa";
const NERO_TESTNET = {
bundlerUrl: "https://bundler-testnet.nerochain.io",
paymasterUrl: "https://paymaster-testnet.nerochain.io",
entryPointAddress: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
factoryAddress: "0x9406Cc6185a346906296840746125a0E44976454",
chainId: 689,
};
function GaslessDemo() {
const { sdk } = useNeroMpcAuth();
const { wallet } = useNeroWallet();
const [txHash, setTxHash] = useState<string | null>(null);
const [status, setStatus] = useState<string>("idle");
const [error, setError] = useState<string | null>(null);
const sendGaslessTransaction = async () => {
if (!sdk || !wallet) return;
setStatus("building");
setError(null);
try {
const bundler = new BundlerClient(NERO_TESTNET.bundlerUrl);
const paymaster = new PaymasterClient(NERO_TESTNET.paymasterUrl);
const account = new SimpleAccount({
ownerAddress: wallet.eoaAddress,
factoryAddress: NERO_TESTNET.factoryAddress,
entryPointAddress: NERO_TESTNET.entryPointAddress,
chainId: NERO_TESTNET.chainId,
});
// 1. Build the UserOperation
setStatus("estimating gas");
const userOp = await account.buildUserOperation({
to: "0xRecipientAddress",
value: "0x0",
data: "0x",
});
// 2. Estimate gas via the bundler
const gasEstimate = await bundler.estimateUserOperationGas(
userOp,
NERO_TESTNET.entryPointAddress,
);
userOp.callGasLimit = gasEstimate.callGasLimit;
userOp.verificationGasLimit = gasEstimate.verificationGasLimit;
userOp.preVerificationGas = gasEstimate.preVerificationGas;
// 3. Request paymaster sponsorship
setStatus("requesting sponsorship");
const paymasterData = await paymaster.getPaymasterData(
userOp,
NERO_TESTNET.entryPointAddress,
NERO_TESTNET.chainId,
);
userOp.paymasterAndData = paymasterData.paymasterAndData;
// 4. Compute the UserOperation hash and sign with MPC
setStatus("signing");
const userOpHash = account.computeUserOpHash(
userOp,
NERO_TESTNET.entryPointAddress,
NERO_TESTNET.chainId,
);
const signature = await sdk.signMessage(userOpHash);
userOp.signature = signature;
// 5. Submit to the bundler
setStatus("submitting");
const opHash = await bundler.sendUserOperation(
userOp,
NERO_TESTNET.entryPointAddress,
);
// 6. Wait for the receipt
setStatus("confirming");
const receipt = await bundler.waitForUserOperationReceipt(opHash);
setTxHash(receipt.receipt.transactionHash);
setStatus("confirmed");
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setStatus("error");
}
};
return (
<div>
<h2>Gasless Transaction</h2>
{wallet && (
<p>Smart Account: {wallet.smartWalletAddress ?? wallet.eoaAddress}</p>
)}
<button onClick={sendGaslessTransaction} disabled={status !== "idle" && status !== "confirmed" && status !== "error"}>
{status === "idle" ? "Send Gasless Tx" : status}
</button>
{txHash && (
<p>
Transaction:{" "}
<a
href={`https://testnet.neroscan.io/tx/${txHash}`}
target="_blank"
rel="noreferrer"
>
{txHash.slice(0, 10)}...{txHash.slice(-8)}
</a>
</p>
)}
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}What the User Sees
- They click "Send Gasless Tx" -- no wallet balance needed
- The status progresses through: estimating gas, requesting sponsorship, signing, submitting, confirming
- A transaction hash appears with a link to the block explorer
- The user paid zero gas fees -- the paymaster covered everything
How It Works Under the Hood
- Build --
SimpleAccountencodes the target call intoexecute(address, uint256, bytes)calldata and checks whether the smart account needs on-chain deployment (first tx includesinitCode) - Estimate -- The bundler returns
callGasLimit,verificationGasLimit, andpreVerificationGas - Sponsor -- The paymaster receives the UserOperation via
pm_sponsorUserOperationand returnspaymasterAndDatacontaining its on-chain sponsorship commitment - Sign -- The UserOperation hash is signed through the MPC threshold protocol (the private key is never reconstructed)
- Submit -- The bundler packages the UserOperation and submits it to the EntryPoint contract on-chain
- Execute -- The EntryPoint validates the signature, deducts gas from the paymaster's stake, and calls the smart account's
executefunction
Using the EIP-1193 Provider (Simpler Path)
If you prefer the standard eth_sendTransaction interface, the Pedersen protocol handles UserOperation construction, paymaster sponsorship, and bundler submission automatically:
import { useNeroMpcAuth } from "@nerochain/mpc-sdk/react";
import { BrowserProvider } from "ethers";
function SimpleGaslessDemo() {
const { sdk } = useNeroMpcAuth();
const send = async () => {
if (!sdk) return;
const provider = new BrowserProvider(sdk.getProvider());
const signer = await provider.getSigner();
const tx = await signer.sendTransaction({
to: "0xRecipientAddress",
value: 0n,
data: "0x",
});
const receipt = await tx.wait();
console.log("Confirmed:", receipt?.hash);
};
return <button onClick={send}>Send via Provider</button>;
}When the SDK is configured with protocol: "pedersen" and a paymaster URL is set in the chain configuration, the provider automatically wraps eth_sendTransaction calls into sponsored UserOperations.
NERO Network Endpoints
| Network | Bundler | Paymaster |
|---|---|---|
| Testnet | https://bundler-testnet.nerochain.io | https://paymaster-testnet.nerochain.io |
| Mainnet | https://bundler-mainnet.nerochain.io | https://paymaster-mainnet.nerochain.io |
| Contract | Address |
|---|---|
| EntryPoint | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
| SimpleAccountFactory | 0x9406Cc6185a346906296840746125a0E44976454 |
Error Handling
try {
const paymasterData = await paymaster.getPaymasterData(userOp, entryPoint, chainId);
} catch (err) {
if (err.message.includes("insufficient")) {
// Paymaster stake is depleted -- contact the project admin
} else if (err.message.includes("rejected")) {
// Paymaster refused to sponsor this operation (policy rules)
}
}Common failure modes:
| Error | Cause | Fix |
|---|---|---|
| Paymaster rejects UserOp | Sponsorship policy doesn't cover this operation | Check your paymaster configuration in the dashboard |
| Gas estimation fails | Invalid calldata or target contract reverts | Verify the target contract address and function |
| Bundler rejects UserOp | Nonce mismatch or signature invalid | Ensure the smart account nonce is current |