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
| Parameter | Value |
|---|---|
| Chain ID | 10088 |
| RPC (HTTP) | https://gatelayer-mainnet.gatenode.cc |
| RPC (WSS) | wss://gatelayer-ws-mainnet.gatenode.cc |
| Bundler | https://gatelayer-bundler.gatenode.cc |
| EntryPoint(0.8) | 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 |
| Block Explorer | https://www.gatescan.org/gatelayer |
| Gas Token | GT |
| AA Standard | ERC-4337 v0.7 |
Gate Layer Testnet
| Parameter | Value |
|---|---|
| Chain ID | 10087 |
| RPC (HTTP) | https://gatelayer-testnet.gatenode.cc |
| RPC (WSS) | wss://gatelayer-ws-testnet.gatenode.cc |
| Bundler | https://gatelayer-testnet-bundler.gatenode.cc |
| Block Explorer | https://www.gatescan.org/gatelayer-testnet |
| Faucet | https://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:
- 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.
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.
| Field | Type | Description |
|---|---|---|
sender | address | Smart account address |
nonce | uint256 | From EntryPoint.getNonce(sender, key) |
factory | address | Account factory address (first deployment only, else omit) |
factoryData | bytes | Calldata for factory (first deployment only, else 0x) |
callData | bytes | Encoded call to execute on the smart account |
callGasLimit | uint256 | Gas limit for execution phase |
verificationGasLimit | uint256 | Gas limit for validation phase |
preVerificationGas | uint256 | Overhead gas (bundler cost, L1 data fee component) |
maxFeePerGas | uint256 | Max gas price (base fee + priority fee) |
maxPriorityFeePerGas | uint256 | Max tip to sequencer |
paymaster | address | Paymaster address, or empty for self-pay |
paymasterVerificationGasLimit | uint256 | Gas for paymaster validation (0 if no paymaster) |
paymasterPostOpGasLimit | uint256 | Gas for paymaster post-op (0 if no paymaster) |
paymasterData | bytes | Paymaster-specific data (0x if no paymaster) |
signature | bytes | Owner signature over the UserOperation hash |
eip7702Auth | object | (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
| 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 |
Numeric Format
All numeric values in requests and responses use hex strings with 0x prefix. Example: gasLimit: 21000 → "0x5208".
12. Error Reference
| Code | Name | Cause | Resolution |
|---|---|---|---|
AA10 | Sender already exists | factory provided but account already deployed | Omit factory and factoryData |
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 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 paymaster deposit |
AA33 | Reverted on paymaster validation | validatePaymasterUserOp() failed | Check paymaster data and policy conditions |
AA40 | Over verification gas limit | verificationGasLimit too low | Re-run eth_estimateUserOperationGas |
AA51 | Prefund below actual gas cost | maxFeePerGas too low for current network conditions | Re-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
| Item | Value |
|---|---|
| Mainnet Bundler | https://gatelayer-bundler.gatenode.cc |
| Mainnet Chain ID | 10088 |
| Testnet Chain ID | 10087 |
| AA Standard | ERC-4337 v0.7 |
| EntryPoint v0.8 | Query via eth_supportedEntryPoints |
| Gas Token | GT |
| Numeric Format | All hex with 0x prefix |
| Core Flow | eth_supportedEntryPoints → build → eth_estimateUserOperationGas → sign → eth_sendUserOperation → eth_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