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
| Parameter | Value |
|---|---|
| Chain ID | 86 |
| 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 |
| Bundler | https://gatechain-bundler.gatenode.cc |
| EntryPoint v0.8 | Query via eth_supportedEntryPoints (standard address: 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108) |
| Block Explorer | https://www.gatescan.org/gatechain |
| Gas Token | GT |
| AA Standard | ERC-4337 v0.7 |
Gate Chain Testnet (Meteora)
| Parameter | Value |
|---|---|
| Chain ID | 85 |
| RPC (HTTP) | https://meteora-evm.gatenode.cc |
| RPC (WSS) | wss://meteora-ws.gatenode.cc |
| Bundler | https://gatechain-meteora-bundler.gatenode.cc |
| Block Explorer | https://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:
- A user or dApp constructs and signs a
UserOperation - The Bundler collects
UserOperations from the alt-mempool - The Bundler packages them into a single transaction
- The transaction is sent to the EntryPoint contract
- 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)
| Component | Description |
|---|---|
baseFee | Network base fee, dynamically adjusted per block (EIP-1559) |
priorityFee | Optional 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.
| Field | Type | Description |
|---|---|---|
sender | address | Smart account address |
nonce | uint256 | From EntryPoint.getNonce(sender, key) |
factory | address | Account factory address (first deployment only; omit if already deployed) |
factoryData | bytes | Factory calldata (first deployment only; set to 0x if already deployed) |
callData | bytes | Encoded call to execute on the smart account |
callGasLimit | uint256 | Gas limit for the execution phase |
verificationGasLimit | uint256 | Gas limit for the validation phase |
preVerificationGas | uint256 | Overhead gas covering the Bundler's packaging cost |
maxFeePerGas | uint256 | Max gas price (baseFee + priorityFee) |
maxPriorityFeePerGas | uint256 | Max tip to the validator |
paymaster | address | Paymaster contract address; empty for self-pay |
paymasterVerificationGasLimit | uint256 | Gas for paymaster validation (set to 0x0 if no paymaster) |
paymasterPostOpGasLimit | uint256 | Gas for paymaster post-op (set to 0x0 if no paymaster) |
paymasterData | bytes | Paymaster-specific data (set to 0x if no paymaster) |
signature | bytes | Owner signature over the UserOperation hash |
eip7702Auth | object | (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
| Method | Description |
|---|---|
eth_supportedEntryPoints | Returns active EntryPoint contract addresses |
eth_estimateUserOperationGas | Estimates gas limits for a UserOperation |
eth_sendUserOperation | Submits a signed UserOperation to the mempool |
eth_getUserOperationReceipt | Returns execution result (null if pending) |
eth_getUserOperationByHash | Returns full UserOperation details by hash |
Standard EVM Methods (call GateChain RPC)
| Method | Description |
|---|---|
eth_getBlockByNumber | Fetch the latest block to read baseFeePerGas |
eth_call | Simulate a contract call without sending a transaction |
eth_getTransactionReceipt | Query 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
| Code | Name | Cause | Resolution |
|---|---|---|---|
AA10 | Sender already exists | factory provided but account is already deployed | Remove factory and factoryData fields |
AA13 | Init code failed | Factory call reverted during account creation | Check factory address and factoryData encoding |
AA21 | Didn't pay prefund | Smart account has insufficient EntryPoint deposit | Call entryPoint.depositTo() before submitting |
AA23 | Reverted on validation | validateUserOp() returned failure | Check signature, nonce, and account validation logic |
AA25 | Invalid account nonce | Nonce mismatch | Re-query EntryPoint.getNonce() and rebuild the UserOp |
AA31 | Paymaster deposit too low | Paymaster has insufficient EntryPoint balance | Replenish the Paymaster deposit |
AA33 | Reverted on paymaster validation | validatePaymasterUserOp() failed | Check paymasterData and Paymaster policy conditions |
AA40 | Over verification gas limit | verificationGasLimit is too low | Re-run eth_estimateUserOperationGas |
AA51 | Prefund below actual gas cost | maxFeePerGas is below current network levels | Re-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.