Skip to main content

Gate Layer Account Abstraction Bundler API Integration Guide

This document explains how to submit a UserOperation to the Gate Layer 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 Layer Bundler.


1. Network Parameters

Gate Layer Mainnet

ParameterValue
Chain ID10088
RPC (HTTP)https://gatelayer-mainnet.gatenode.cc
RPC (WSS)wss://gatelayer-ws-mainnet.gatenode.cc
Bundlerhttps://gatelayer-bundler.gatenode.cc
EntryPoint(0.8)0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108
Block Explorerhttps://www.gatescan.org/gatelayer
Gas TokenGT
AA StandardERC-4337 v0.7

Gate Layer Testnet

ParameterValue
Chain ID10087
RPC (HTTP)https://gatelayer-testnet.gatenode.cc
RPC (WSS)wss://gatelayer-ws-testnet.gatenode.cc
Bundlerhttps://gatelayer-testnet-bundler.gatenode.cc
Block Explorerhttps://www.gatescan.org/gatelayer-testnet
Faucethttps://www.gatechain.io/docs/GateLayer/GettingStarted/GetTestnetGT

2. 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.


3. EntryPoint Contract

Query the Bundler to get the active EntryPoint address:

curl -s "https://gatelayer-bundler.gatenode.cc" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_supportedEntryPoints",
"params": []
}'

Response:

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

Always resolve the EntryPoint address at runtime via eth_supportedEntryPoints. Hard-coding addresses across versions causes silent validation failures.


4. Gas Fee Mechanism

Gate Layer runs on OP Stack. Every transaction pays three fee components, all denominated in GT:

totalFee = l2ExecutionFee + l1DataFee + operatorFee

L2 Execution Fee — standard EIP-1559 fee for on-chain execution:

l2ExecutionFee = gasUsed × (baseFee + priorityFee)

L1 Data Fee — cost to post transaction data to Gate Chain via EIP-4844 blobs:

l1DataFee = estimatedCompressedSize × (baseFeeScalar × l1BaseFee × 16 + blobFeeScalar × l1BlobBaseFee) / 10¹²

Current Gate Layer parameters: baseFeeScalar = 113016, blobFeeScalar = 801949.

Operator Fee — currently zero (operatorFeeScalar = 0, operatorFeeConstant = 0). No additional operator surcharge is applied.


5. UserOperation Structure (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, else omit)
factoryDatabytesCalldata for factory (first deployment only, else 0x)
callDatabytesEncoded call to execute on the smart account
callGasLimituint256Gas limit for execution phase
verificationGasLimituint256Gas limit for validation phase
preVerificationGasuint256Overhead gas (bundler cost, L1 data fee component)
maxFeePerGasuint256Max gas price (base fee + priority fee)
maxPriorityFeePerGasuint256Max tip to sequencer
paymasteraddressPaymaster address, or empty for self-pay
paymasterVerificationGasLimituint256Gas for paymaster validation (0 if no paymaster)
paymasterPostOpGasLimituint256Gas for paymaster post-op (0 if no paymaster)
paymasterDatabytesPaymaster-specific data (0x if no paymaster)
signaturebytesOwner signature over the UserOperation hash
eip7702Authobject(Optional) EIP-7702 authorization tuple — see Section 9

6. Integration Workflow

Step 1 — Query EntryPoint

curl -s "https://gatelayer-bundler.gatenode.cc" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_supportedEntryPoints","params":[]}'

Step 2 — Get Priority Fee

curl -s "https://gatelayer-mainnet.gatenode.cc" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"eth_maxPriorityFeePerGas","params":[]}'

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

Step 3 — Estimate Gas

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

curl -s "https://gatelayer-bundler.gatenode.cc" \
-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. An empty 0x causes ECDSA.recover() to revert and breaks estimation. Use the dummy above or any valid-format 65-byte hex that doesn't match the owner.

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 key:

userOpHash = keccak256(abi.encode(
keccak256(packed(userOp fields)),
entryPointAddress,
chainId
))
signature = owner.signMessage(userOpHash) // add ETH prefix

Step 5 — Submit

curl -s "https://gatelayer-bundler.gatenode.cc" \
-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>"
]
}'

Response:

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

Step 6 — Poll for Receipt

curl -s "https://gatelayer-bundler.gatenode.cc" \
-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.


7. Self-Pay vs. Paymaster

By default, no Paymaster is required. The smart account pays gas from its EntryPoint deposit.

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);

With Paymaster

When a Paymaster sponsors gas, populate:

"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.


8. 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 Layer 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://gatelayer-mainnet.gatenode.cc',
BUNDLER_URL = 'https://gatelayer-bundler.gatenode.cc',
ENTRY_POINT = '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108',
LIGHT_ACCOUNT_FACTORY = '0x13E9ed32155810FDbd067D4522C492D6f68E5944',
RECIPIENT_ADDRESS = '0xRECIPIENT_ADDRESS',
CHAIN_ID = '10088',
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);
});

9. EIP-7702 Authorization Field

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

The field is optional. Include it only when submitting a UserOperation that requires EIP-7702 code delegation.

Structure:

"eip7702Auth": {
"chainId": "<CHAIN_ID_HEX>",
"address": "<DELEGATED_CONTRACT_ADDRESS>",
"nonce": "<EOA_NONCE_HEX>",
"yParity": "<0x0_OR_0x1>",
"r": "<SIGNATURE_R_HEX>",
"s": "<SIGNATURE_S_HEX>"
}

When present, the Bundler adds the authorization to the transaction's authorizationList and submits it as an EIP-7702 transaction type. The UserOperation hash calculation also incorporates the delegated contract address.


10. Smart Account Deployment

On first use, a smart account does not yet exist on-chain. The account address is counterfactual — it is 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.

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


11. RPC Method Reference

Core 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

Numeric Format

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


12. Error Reference

CodeNameCauseResolution
AA10Sender already existsfactory provided but account already deployedOmit factory and factoryData
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 logic
AA25Invalid account nonceNonce mismatchRe-query EntryPoint.getNonce() and rebuild the UserOp
AA31Paymaster deposit too lowPaymaster has insufficient EntryPoint balanceReplenish paymaster deposit
AA33Reverted on paymaster validationvalidatePaymasterUserOp() failedCheck paymaster data and policy conditions
AA40Over verification gas limitverificationGasLimit too lowRe-run eth_estimateUserOperationGas
AA51Prefund below actual gas costmaxFeePerGas too low for current network conditionsRe-query eth_maxPriorityFeePerGas and rebuild

13. Troubleshooting

EntryPoint mismatch — always resolve the EntryPoint address at runtime via eth_supportedEntryPoints. Using a hard-coded address from a different version causes silent validation failures.

Gas estimation fails — if eth_estimateUserOperationGas returns an error, the most common causes are: invalid callData encoding, insufficient sender balance for prefund check, or a revert in the account's validation logic.

Receipt returns null — the operation is still in the mempool. Poll every 3–5 seconds. If null persists beyond 2 minutes, 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.


14. Quick Reference

ItemValue
Mainnet Bundlerhttps://gatelayer-bundler.gatenode.cc
Mainnet Chain ID10088
Testnet Chain ID10087
AA StandardERC-4337 v0.7
EntryPoint v0.8Query via eth_supportedEntryPoints
Gas TokenGT
Numeric FormatAll hex with 0x prefix
Core Floweth_supportedEntryPoints → build → eth_estimateUserOperationGas → sign → eth_sendUserOperationeth_getUserOperationReceipt

15. References

  • Gate Layer Endpoints & API: https://www.gatechain.io/docs/GateLayer/Development/EndpointsAPI/
  • Gate Layer Gas and Fee Mechanism: https://www.gatechain.io/docs/GateLayer/Concepts/GasAndFees/
  • Gate Layer Block Explorer: https://www.gatescan.org/gatelayer
  • ERC-4337 Specification: https://eips.ethereum.org/EIPS/eip-4337
  • permissionless.js: https://docs.pimlico.io/permissionless

Last updated on 2026/03/20