NERO
Guides

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-sdk installed with the ethers peer 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/hashes

Step 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

  1. They click "Send Gasless Tx" -- no wallet balance needed
  2. The status progresses through: estimating gas, requesting sponsorship, signing, submitting, confirming
  3. A transaction hash appears with a link to the block explorer
  4. The user paid zero gas fees -- the paymaster covered everything

How It Works Under the Hood

  1. Build -- SimpleAccount encodes the target call into execute(address, uint256, bytes) calldata and checks whether the smart account needs on-chain deployment (first tx includes initCode)
  2. Estimate -- The bundler returns callGasLimit, verificationGasLimit, and preVerificationGas
  3. Sponsor -- The paymaster receives the UserOperation via pm_sponsorUserOperation and returns paymasterAndData containing its on-chain sponsorship commitment
  4. Sign -- The UserOperation hash is signed through the MPC threshold protocol (the private key is never reconstructed)
  5. Submit -- The bundler packages the UserOperation and submits it to the EntryPoint contract on-chain
  6. Execute -- The EntryPoint validates the signature, deducts gas from the paymaster's stake, and calls the smart account's execute function

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

NetworkBundlerPaymaster
Testnethttps://bundler-testnet.nerochain.iohttps://paymaster-testnet.nerochain.io
Mainnethttps://bundler-mainnet.nerochain.iohttps://paymaster-mainnet.nerochain.io
ContractAddress
EntryPoint0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
SimpleAccountFactory0x9406Cc6185a346906296840746125a0E44976454

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:

ErrorCauseFix
Paymaster rejects UserOpSponsorship policy doesn't cover this operationCheck your paymaster configuration in the dashboard
Gas estimation failsInvalid calldata or target contract revertsVerify the target contract address and function
Bundler rejects UserOpNonce mismatch or signature invalidEnsure the smart account nonce is current

On this page