Skip to main content

Gate Chain Account Abstraction Bundler API Integration Guide

This document explains how to submit a UserOperation to the Gate Chain Bundler using the ERC-4337 Account Abstraction standard.

Core goal: construct a UserOperation, get gas estimates, sign it, and submit it on-chain via the Gate Chain Bundler.


1. Network Parameters

Gate Chain Mainnet

ParameterValue
Chain ID86
RPC (HTTP, recommended)https://evm.nodeinfo.cc
RPC (HTTP, fallback)https://evm-1.nodeinfo.cc / https://evm.gatenode.cc
RPC (WSS)wss://evm-ws.gatenode.cc
Bundlerhttps://gatechain-bundler.gatenode.cc
EntryPoint v0.8Query via eth_supportedEntryPoints (standard address: 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108)
Block Explorerhttps://www.gatescan.org/gatechain
Gas TokenGT
AA StandardERC-4337 v0.7

Gate Chain Testnet (Meteora)

ParameterValue
Chain ID85
RPC (HTTP)https://meteora-evm.gatenode.cc
RPC (WSS)wss://meteora-ws.gatenode.cc
Bundlerhttps://gatechain-meteora-bundler.gatenode.cc
Block Explorerhttps://www.gatescan.org/gatechain-testnet

2. About Gate Chain

Gate Chain is an EVM-compatible Layer 1 blockchain.

Gate Chain serves as the settlement layer for Gate Layer L2. Both networks use the same AA integration approach, but differ in network parameters and gas fee models — see Section 4 for details.


3. ERC-4337 Concepts

ERC-4337 allows smart contract accounts to initiate transactions without an EOA. Instead of raw transactions, users submit UserOperation objects to a Bundler.

Standard execution flow:

  1. A user or dApp constructs and signs a UserOperation
  2. The Bundler collects UserOperations from the alt-mempool
  3. The Bundler packages them into a single transaction
  4. The transaction is sent to the EntryPoint contract
  5. EntryPoint validates and executes each operation

Paymaster is optional. When paymaster is empty, the smart account pays gas from its own EntryPoint deposit (pre-funded via depositTo()). When paymaster is set, the Paymaster contract sponsors the gas on behalf of the user.


4. Gas Fee Mechanism

Gate Chain is a Layer 1 network and uses the standard EIP-1559 gas model.

The fee for each transaction is calculated as:

totalFee = gasUsed × (baseFee + priorityFee)
ComponentDescription
baseFeeNetwork base fee, dynamically adjusted per block (EIP-1559)
priorityFeeOptional tip to the validator; can be zero or a small value

For UserOperations, preVerificationGas primarily covers the Bundler's calldata overhead, while callGasLimit and verificationGasLimit are based on actual on-chain execution costs. Always use eth_estimateUserOperationGas to get accurate values rather than estimating manually.


5. EntryPoint Contract Address

Query the Bundler to get the active EntryPoint address:

curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_supportedEntryPoints",
"params": []
}'

Example response:

{
"jsonrpc": "2.0",
"id": 1,
"result": ["0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108"]
}

Always resolve the EntryPoint address at runtime via eth_supportedEntryPoints. Do not hard-code it. Addresses differ across versions, and hard-coding causes silent validation failures.


6. UserOperation Fields (v0.7)

ERC-4337 v0.7 splits initCode and paymasterAndData into separate fields.

FieldTypeDescription
senderaddressSmart account address
nonceuint256From EntryPoint.getNonce(sender, key)
factoryaddressAccount factory address (first deployment only; omit if already deployed)
factoryDatabytesFactory calldata (first deployment only; set to 0x if already deployed)
callDatabytesEncoded call to execute on the smart account
callGasLimituint256Gas limit for the execution phase
verificationGasLimituint256Gas limit for the validation phase
preVerificationGasuint256Overhead gas covering the Bundler's packaging cost
maxFeePerGasuint256Max gas price (baseFee + priorityFee)
maxPriorityFeePerGasuint256Max tip to the validator
paymasteraddressPaymaster contract address; empty for self-pay
paymasterVerificationGasLimituint256Gas for paymaster validation (set to 0x0 if no paymaster)
paymasterPostOpGasLimituint256Gas for paymaster post-op (set to 0x0 if no paymaster)
paymasterDatabytesPaymaster-specific data (set to 0x if no paymaster)
signaturebytesOwner signature over the UserOperation hash
eip7702Authobject(Optional) EIP-7702 authorization tuple — see Section 10

7. Integration Workflow

Step 1 — Query EntryPoint

curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_supportedEntryPoints","params":[]}'

Step 2 — Get Priority Fee

curl -s "<GATECHAIN_NODE_RPC>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"eth_maxPriorityFeePerGas","params":[]}'

Use the returned value as maxPriorityFeePerGas. Add the current baseFee to get maxFeePerGas.

Fetch the current baseFee from the Gate Chain RPC:

curl -s "<GATECHAIN_NODE_RPC>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",false]}'

Read the baseFeePerGas field from the response.

Step 3 — Estimate Gas

Send a UserOperation with all gas fields set to 0x0 and a dummy signature:

curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "eth_estimateUserOperationGas",
"params": [
{
"sender": "<SENDER_ADDRESS>",
"nonce": "<NONCE_HEX>",
"callData": "<CALL_DATA_HEX>",
"callGasLimit": "0x0",
"verificationGasLimit": "0x0",
"preVerificationGas": "0x0",
"maxPriorityFeePerGas": "<MAX_PRIORITY_FEE_HEX>",
"maxFeePerGas": "<MAX_FEE_HEX>",
"signature": "0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000011b"
},
"<ENTRY_POINT_ADDRESS>"
]
}'

When no factory or paymaster is needed, omit those fields entirely. Passing empty strings ("") for address fields causes the Bundler to reject the request with Invalid params.

About the dummy signature: gas estimation requires a validly formatted 65-byte ECDSA signature. Passing an empty 0x causes ECDSA.recover() to revert and breaks estimation. Use the dummy above or any valid-format 65-byte hex that does not match the owner's key.

Example response:

{
"jsonrpc": "2.0",
"id": 3,
"result": {
"preVerificationGas": "<HEX>",
"verificationGasLimit": "<HEX>",
"callGasLimit": "<HEX>",
"paymasterVerificationGasLimit": "0x0"
}
}

Step 4 — Sign

Compute the UserOperation hash and sign with the account owner's private key:

userOpHash = keccak256(abi.encode(
keccak256(packed(userOp fields)),
entryPointAddress,
chainId // GateChain Mainnet: 86
))
signature = owner.signMessage(userOpHash) // ETH signed message prefix required

Step 5 — Submit

curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "eth_sendUserOperation",
"params": [
{
"sender": "<SENDER_ADDRESS>",
"nonce": "<NONCE_HEX>",
"callData": "<CALL_DATA_HEX>",
"callGasLimit": "<CALL_GAS_LIMIT_HEX>",
"verificationGasLimit": "<VERIFICATION_GAS_LIMIT_HEX>",
"preVerificationGas": "<PRE_VERIFICATION_GAS_HEX>",
"maxPriorityFeePerGas": "<MAX_PRIORITY_FEE_HEX>",
"maxFeePerGas": "<MAX_FEE_HEX>",
"signature": "<SIGNED_USER_OPERATION_HEX>"
},
"<ENTRY_POINT_ADDRESS>"
]
}'

Example response:

{
"jsonrpc": "2.0",
"id": 4,
"result": "<USER_OPERATION_HASH>"
}

Step 6 — Poll for Receipt

curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "eth_getUserOperationReceipt",
"params": ["<USER_OPERATION_HASH>"]
}'

Returns null while pending. Poll every 3–5 seconds with exponential backoff. A confirmed result includes transactionHash and "success": true.

Gate Chain's block time is approximately 4-30 seconds, so confirmation is typically faster than on Ethereum mainnet.


8. Self-Pay vs. Paymaster

Self-pay (no Paymaster)

Omit all paymaster and factory fields from the UserOperation. Do not pass empty strings ("") for address fields — the Bundler will reject them. Simply leave these fields out of the JSON object.

Before submitting, ensure the smart account has sufficient GT deposited in the EntryPoint:

entryPoint.depositTo{value: amount}(smartAccountAddress);

Paymaster-sponsored

When a Paymaster sponsors gas, populate the following fields:

"paymaster": "<PAYMASTER_CONTRACT_ADDRESS>",
"paymasterVerificationGasLimit": "<HEX>",
"paymasterPostOpGasLimit": "<HEX>",
"paymasterData": "<PAYMASTER_SIGNED_DATA_HEX>"

paymasterData is provided by the Paymaster service after it validates and co-signs the UserOperation.


9. SDK Quick-Start (quick-start.js)

For production use, quick-start.js abstracts the raw JSON-RPC flow.

Install

npm install viem @aa-sdk/core @account-kit/smart-contracts

Define Gate Chain Chain

import { LocalAccountSigner, getEntryPoint } from '@aa-sdk/core';
import { createLightAccount } from '@account-kit/smart-contracts';
import {
createPublicClient,
defineChain,
http,
parseEther,
toHex,
encodeFunctionData,
getContract,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const {
PRIVATE_KEY,
RPC_URL = 'https://evm.nodeinfo.cc',
BUNDLER_URL = 'https://gatechain-bundler.gatenode.cc',
ENTRY_POINT = '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108',
LIGHT_ACCOUNT_FACTORY = '0x13E9ed32155810FDbd067D4522C492D6f68E5944',
RECIPIENT_ADDRESS = '0xRECIPIENT_ADDRESS',
CHAIN_ID = '86',
TRANSFER_GT = '0.001',
} = process.env;

if (!PRIVATE_KEY) throw new Error('Missing PRIVATE_KEY');

const chain = defineChain({
id: Number(CHAIN_ID),
name: `GateChain-${CHAIN_ID}`,
nativeCurrency: { name: 'GT', symbol: 'GT', decimals: 18 },
rpcUrls: { default: { http: [RPC_URL] } },
});

const publicClient = createPublicClient({ chain, transport: http(RPC_URL) });

async function bundlerRpc(method, params) {
const res = await fetch(BUNDLER_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
});
const { result, error } = await res.json();
if (error) throw new Error(`${method}: ${JSON.stringify(error)}`);
return result;
}

async function main() {
// 1. Create smart account
const owner = LocalAccountSigner.privateKeyToAccountSigner(PRIVATE_KEY);
const ownerAddr = await owner.getAddress();
const entryPointDef = { ...getEntryPoint(chain, { version: '0.7.0' }), address: ENTRY_POINT };

// Query the factory for the correct counterfactual address
const factoryContract = getContract({
address: LIGHT_ACCOUNT_FACTORY,
abi: [
{
name: 'getAddress',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'salt', type: 'uint256' },
],
outputs: [{ name: '', type: 'address' }],
},
],
client: publicClient,
});
const accountAddress = await factoryContract.read.getAddress([ownerAddr, 0n]);

const account = await createLightAccount({
chain,
transport: http(RPC_URL),
signer: owner,
factoryAddress: LIGHT_ACCOUNT_FACTORY,
accountAddress,
version: 'v2.0.0',
entryPoint: entryPointDef,
});

const sender = account.address;
console.log('Smart account address:', sender);

// 2. Check deployment & get initCode
const code = await publicClient.getCode({ address: sender });
const isDeployed = code && code !== '0x';
const initCode = await account.getInitCode();
const needsDeploy = !isDeployed && initCode && initCode !== '0x';

// 3. Build callData
const callData = encodeFunctionData({
abi: [
{
name: 'execute',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'dest', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'func', type: 'bytes' },
],
outputs: [],
},
],
functionName: 'execute',
args: [RECIPIENT_ADDRESS, parseEther(TRANSFER_GT), '0x'],
});

// 4. Get nonce
const nonce = await publicClient.readContract({
address: ENTRY_POINT,
abi: entryPointDef.abi,
functionName: 'getNonce',
args: [sender, 0n],
});

// 5. Gas limits & fee (whole-gwei required)
// NOTE: Adjust these values based on your transaction complexity.
// Use eth_estimateUserOperationGas for precise values in production.
const callGasLimit = 200_000n;
const verificationGasLimit = 500_000n;
const preVerificationGas = 100_000n;
const maxPriorityFeePerGas = 1_000_000_000n; // 1 gwei
const maxFeePerGas = 20_000_000_000n; // 20 gwei

// 6. Sign via EntryPoint.getUserOpHash()
const normalizedPk = PRIVATE_KEY.startsWith('0x') ? PRIVATE_KEY : `0x${PRIVATE_KEY}`;
const eoa = privateKeyToAccount(normalizedPk);
const packedForHash = {
sender,
nonce,
initCode: needsDeploy ? initCode : '0x',
callData,
accountGasLimits:
'0x' +
verificationGasLimit.toString(16).padStart(32, '0') +
callGasLimit.toString(16).padStart(32, '0'),
preVerificationGas,
gasFees:
'0x' +
maxPriorityFeePerGas.toString(16).padStart(32, '0') +
maxFeePerGas.toString(16).padStart(32, '0'),
paymasterAndData: '0x',
signature: '0x',
};
const userOpHash = await publicClient.readContract({
address: ENTRY_POINT,
abi: entryPointDef.abi,
functionName: 'getUserOpHash',
args: [packedForHash],
});
const signature = await eoa.sign({ hash: userOpHash });

// 7. Send
const hash = await bundlerRpc('eth_sendUserOperation', [
{
sender,
nonce: toHex(nonce),
callData,
callGasLimit: toHex(callGasLimit),
verificationGasLimit: toHex(verificationGasLimit),
preVerificationGas: toHex(preVerificationGas),
maxPriorityFeePerGas: toHex(maxPriorityFeePerGas),
maxFeePerGas: toHex(maxFeePerGas),
signature,
...(needsDeploy
? { factory: initCode.slice(0, 42), factoryData: '0x' + initCode.slice(42) }
: {}),
},
ENTRY_POINT,
]);
console.log('UserOp hash:', hash);

// 8. Wait for receipt
let receipt;
for (let i = 0; i < 30; i++) {
receipt = await bundlerRpc('eth_getUserOperationReceipt', [hash]);
if (receipt?.receipt?.transactionHash) break;
await new Promise((r) => setTimeout(r, 4000));
}
console.log('Tx hash:', receipt.receipt.transactionHash);
}

main().catch((err) => {
console.error('Failed:', err);
process.exit(1);
});

10. EIP-7702 Authorization Field

EIP-7702 allows an EOA to temporarily delegate its code to a smart contract for a single transaction. Combined with ERC-4337, the eip7702Auth field enables EOA accounts to acquire smart account capabilities without a separate deployment.

This field is optional. Include it only when the UserOperation requires EIP-7702 code delegation.

Field structure:

"eip7702Auth": {
"chainId": "0x56",
"address": "<DELEGATED_CONTRACT_ADDRESS>",
"nonce": "<EOA_NONCE_HEX>",
"yParity": "<0x0 or 0x1>",
"r": "<SIGNATURE_R_HEX>",
"s": "<SIGNATURE_S_HEX>"
}

chainId is 0x56 (decimal 86) for Gate Chain mainnet. When present, the Bundler adds the authorization tuple to the transaction's authorizationList and submits it as an EIP-7702 transaction type. The UserOperation hash calculation also incorporates the delegated contract address.


11. Smart Account Deployment

On first use, the smart account does not yet exist on-chain. The account address is counterfactual — deterministically derived from the factory address and owner address.

To deploy a new smart account in the same transaction as its first operation, populate factory and factoryData:

"factory": "<ACCOUNT_FACTORY_ADDRESS>",
"factoryData": "<ENCODED_CONSTRUCTOR_CALLDATA>"

Once the account is deployed (nonce > 0), omit both fields for subsequent operations.

The EntryPoint calls factory.createAccount(factoryData) during the verification phase if factory is non-empty. The deployed address must match sender, otherwise the operation is rejected.


12. RPC Method Reference

Bundler Methods

MethodDescription
eth_supportedEntryPointsReturns active EntryPoint contract addresses
eth_estimateUserOperationGasEstimates gas limits for a UserOperation
eth_sendUserOperationSubmits a signed UserOperation to the mempool
eth_getUserOperationReceiptReturns execution result (null if pending)
eth_getUserOperationByHashReturns full UserOperation details by hash

Standard EVM Methods (call GateChain RPC)

MethodDescription
eth_getBlockByNumberFetch the latest block to read baseFeePerGas
eth_callSimulate a contract call without sending a transaction
eth_getTransactionReceiptQuery an on-chain transaction receipt

Numeric Format

All numeric values in requests and responses use hex strings with a 0x prefix. Example: gasLimit: 21000"0x5208".


13. Error Reference

CodeNameCauseResolution
AA10Sender already existsfactory provided but account is already deployedRemove factory and factoryData fields
AA13Init code failedFactory call reverted during account creationCheck factory address and factoryData encoding
AA21Didn't pay prefundSmart account has insufficient EntryPoint depositCall entryPoint.depositTo() before submitting
AA23Reverted on validationvalidateUserOp() returned failureCheck signature, nonce, and account validation logic
AA25Invalid account nonceNonce mismatchRe-query EntryPoint.getNonce() and rebuild the UserOp
AA31Paymaster deposit too lowPaymaster has insufficient EntryPoint balanceReplenish the Paymaster deposit
AA33Reverted on paymaster validationvalidatePaymasterUserOp() failedCheck paymasterData and Paymaster policy conditions
AA40Over verification gas limitverificationGasLimit is too lowRe-run eth_estimateUserOperationGas
AA51Prefund below actual gas costmaxFeePerGas is below current network levelsRe-query eth_maxPriorityFeePerGas and rebuild the UserOp

14. Troubleshooting

EntryPoint address mismatch — always resolve the EntryPoint address at runtime via eth_supportedEntryPoints. Hard-coding causes silent validation failures when versions differ.

Gas estimation fails — common causes for eth_estimateUserOperationGas errors: invalid callData encoding, insufficient account balance to pass the prefund check, or a revert in the account's validation logic during simulation.

Receipt keeps returning null — the operation is still in the mempool. Poll every 3–5 seconds. If null persists beyond 1 minute (Gate Chain blocks in ~30 seconds), the operation may have been dropped due to low fees. Rebuild with updated gas values and resubmit.

Nonce conflict — each sender may have only one pending UserOperation per nonce in the mempool at a time. To replace a stuck operation, resubmit with the same nonce and at least 10% higher maxPriorityFeePerGas.

Mixing Gate Chain and Gate Layer — Gate Chain (Chain ID 86) and Gate Layer (Chain ID 10088) are independent networks. The UserOperation hash includes chainId, so signatures are not interchangeable between chains. Verify that your SDK configuration uses the correct chain definition and Bundler endpoint for the target network.

Last updated on 2026/03/20