Upgrade Path from Biconomy v2 to Nexus | Post Feb-28/26

This guide explains how Biconomy clients can safely upgrade existing ERC-4337 Accounts v2 to the Nexus / Modular Execution Environment (MEE) stack by running a self-contained upgrade script.

The script acts as a mini bundler, allowing you to submit a single UserOperation directly to the EntryPoint, without relying on Biconomy’s hosted bundler infrastructure.

⚠️ Important
Biconomy will be shutting down bundler operations on February 28th.

The script is only required for the one-time upgrade transaction, so expected transaction volume is very low.

For broader architecture and Nexus concepts, refer to docs.biconomy.io.


Withdraw Paymaster Funds

We request that all clients withdraw their Paymaster "gas tank" funds before the 28th of February. The funds will remain availablea for withdrawal after that date, but the UI for enabling this functionality will be removed.

You can call the withdrawTo method with the owner wallet via the explorer. Example: https://basescan.org/address/0x00000f79b7faf42eebadba19acc07cd08af44789#writeContract%23F12

EPv6 Paymaster: 0x00000f79b7faf42eebadba19acc07cd08af44789
EPv7 Paymasters:
0x0000006087310897e0BFfcb3f0Ed3704f7146852 (Base and OP)
0x00000072a5F551D6E80b2f6ad4fB256A27841Bbc (All other chains)


Sample Server

This is a template server achieving the capabilities of the "mini bundler". You can use it as reference to build your solution.

GitHub - bcnmy/upgrade-server: Upgrading v2 Accounts to Nexus after the shutdown of the Biconomy Bundler service
Upgrading v2 Accounts to Nexus after the shutdown of the Biconomy Bundler service - bcnmy/upgrade-server

What This Script Does

At a high level, the script:

  1. Upgrades the account implementation to Nexus
  2. Initializes the Nexus & Modular Execution Environment
  3. Wraps everything into a single ERC-4337 UserOperation
  4. Submits the operation directly to the EntryPoint

All steps execute atomically.


Step 1 — Encode the Implementation Upgrade

The first step prepares calldata to upgrade the account’s implementation contract.

const updateImplementationCalldata = encodeFunctionData({
    abi: [
        {
            name: 'updateImplementation',
            type: 'function',
            stateMutability: 'nonpayable',
            inputs: [{ name: 'newImplementation', type: 'address' }],
            outputs: [],
        },
    ],
    functionName: 'updateImplementation',
    args: [versionConfig.implementationAddress],
});

This updates the account logic in-place, preserving:


Step 2 — Prepare Nexus Initialization Data

Once the implementation is upgraded, the account must be initialized as a Nexus account with a default validator.

Encode Nexus initialization

const initData = encodeFunctionData({
    abi: [
        {
            name: 'initNexusWithDefaultValidator',
            type: 'function',
            stateMutability: 'nonpayable',
            inputs: [{ type: 'bytes', name: 'data' }],
            outputs: [],
        },
    ],
    functionName: 'initNexusWithDefaultValidator',
    args: [ownerAddress as `0x${string}`],
});

Wrap initialization with bootstrap data

const initDataWithBootstrap = encodeAbiParameters(
    [
        { name: 'bootstrap', type: 'address' },
        { name: 'initData', type: 'bytes' },
    ],
    [versionConfig.bootStrapAddress, initData]
);

Encode initializeAccount

const initializeNexusCalldata = encodeFunctionData({
    abi: [
        {
            name: 'initializeAccount',
            type: 'function',
            stateMutability: 'nonpayable',
            inputs: [{ type: 'bytes', name: 'data' }],
            outputs: [],
        },
    ],
    functionName: 'initializeAccount',
    args: [initDataWithBootstrap],
});

This activates the Modular Execution Environment (MEE).


Step 3 — Execute Both Actions Atomically

Both the implementation upgrade and Nexus initialization are executed by the account itself using executeBatch.

const executeCalldata = encodeFunctionData({
    abi: [
        {
            name: 'executeBatch',
            type: 'function',
            stateMutability: 'nonpayable',
            inputs: [
                { name: 'dest', type: 'address[]' },
                { name: 'value', type: 'uint256[]' },
                { name: 'func', type: 'bytes[]' },
            ],
            outputs: [],
        },
    ],
    functionName: 'executeBatch',
    args: [
        [smartAccountAddress, smartAccountAddress],
        [0n, 0n],
        [updateImplementationCalldata, initializeNexusCalldata],
    ],
});

This guarantees:


Step 4 — Construct the ERC-4337 UserOperation

Fetch the nonce from EntryPoint

const nonce = await readContract(walletClient, {
    address: entryPointAddress,
    abi: [
        {
            name: 'getNonce',
            type: 'function',
            stateMutability: 'view',
            inputs: [
                { name: 'sender', type: 'address' },
                { name: 'key', type: 'uint192' },
            ],
            outputs: [{ type: 'uint256' }],
        },
    ],
    functionName: 'getNonce',
    args: [smartAccountAddress, 0n],
});

Build the UserOperation

const userOp: any = {
    sender: smartAccountAddress,
    nonce,
    initCode: '0x',
    callData: executeCalldata,
    callGasLimit: 800_000n,
    verificationGasLimit: 500_000n,
    preVerificationGas: 100_000n,
    maxFeePerGas: 20_000_000n,
    maxPriorityFeePerGas: 1_000_000n,
    paymasterAndData: '0x',
    signature: '0x',
};

Step 5 — Sign the UserOperation

Your users can sign the user operation in a regular way - with their regular Biconomy account SDK. This operation serves just to enable them an upgrade path when the Bundler is shut down.

Compute the UserOperation hash

const userOpHash = await readContract(walletClient, {
    address: entryPointAddress,
    abi: [
        {
            type: 'function',
            name: 'getUserOpHash',
            stateMutability: 'view',
            inputs: [
                {
                    name: 'userOp',
                    type: 'tuple',
                    components: [
                        { name: 'sender', type: 'address' },
                        { name: 'nonce', type: 'uint256' },
                        { name: 'initCode', type: 'bytes' },
                        { name: 'callData', type: 'bytes' },
                        { name: 'callGasLimit', type: 'uint256' },
                        { name: 'verificationGasLimit', type: 'uint256' },
                        { name: 'preVerificationGas', type: 'uint256' },
                        { name: 'maxFeePerGas', type: 'uint256' },
                        { name: 'maxPriorityFeePerGas', type: 'uint256' },
                        { name: 'paymasterAndData', type: 'bytes' },
                        { name: 'signature', type: 'bytes' },
                    ],
                },
            ],
            outputs: [{ type: 'bytes32' }],
        },
    ],
    functionName: 'getUserOpHash',
    args: [userOp],
});

Sign using the existing account signer

const biconomyAccount = useBiconomyStore.getState().biconomyAccount;

const signature: any =
    await biconomyAccount?.signUserOperationHash(userOpHash);

const signatureWithModuleAddress =
    biconomyAccount?.getSignatureWithModuleAddress(signature);

userOp.signature = signatureWithModuleAddress;

Step 6 — Submit the UserOperation (Mini Bundler Mode)

Instead of relying on a hosted bundler, the script directly calls handleOps on the EntryPoint.

const handleOpsCalldata = encodeFunctionData({
    abi: [
        {
            name: 'handleOps',
            type: 'function',
            stateMutability: 'nonpayable',
            inputs: [
                {
                    name: 'ops',
                    type: 'tuple[]',
                    components: [
                        { name: 'sender', type: 'address' },
                        { name: 'nonce', type: 'uint256' },
                        { name: 'initCode', type: 'bytes' },
                        { name: 'callData', type: 'bytes' },
                        { name: 'callGasLimit', type: 'uint256' },
                        { name: 'verificationGasLimit', type: 'uint256' },
                        { name: 'preVerificationGas', type: 'uint256' },
                        { name: 'maxFeePerGas', type: 'uint256' },
                        { name: 'maxPriorityFeePerGas', type: 'uint256' },
                        { name: 'paymasterAndData', type: 'bytes' },
                        { name: 'signature', type: 'bytes' },
                    ],
                },
                { name: 'beneficiary', type: 'address' },
            ],
            outputs: [],
        },
    ],
    functionName: 'handleOps',
    args: [[userOp], walletClient.account.address],
});
const hash = await walletClient.sendTransaction({
    to: entryPointAddress,
    data: handleOpsCalldata,
    value: 0n,
    type: 'eip1559',
    gas: 1_000_000n,
});

This is why the script functions as a mini bundler:


Next Steps

After execution, the account:

For full Nexus documentation and follow-up integrations, see:

👉 docs.biconomy.io