10-Minute ERC-4337 Testnet Quickstart
10-Minute ERC-4337 Testnet Quickstart
Use this guide to prove one sponsored smart-account action on BSC Testnet (chain ID 97). It intentionally uses your own sponsor account, API key, policy, and test smart account. MegaFuel does not provide a shared funded policy: a shared policy would not test your permissions or safely isolate sponsorship spend.
What you need
- a MegaFuel API key with permission to create and fund a Testnet policy;
- one supported SimpleAccount-compatible wallet implementation;
- a fresh owner key and its counterfactual or deployed smart-account address;
- an ERC-20 balance at that smart account, and no BNB at that smart account; and
curlandjq. Your wallet/reference client performs the account signature.
Never place an owner private key or API key in a policy request, source file, browser console, or support ticket.
1. Select a version and verify the endpoints
export POLICY_API='https://open-platform-ap.nodereal.io/<API_KEY>/megafuel/97'
export PAYMASTER_URL='https://bsc-megafuel-testnet.nodereal.io/4337/paymaster'
export BUNDLER_URL='https://bsc-megafuel-testnet.nodereal.io/4337/bundler'
curl -sS "$BUNDLER_URL" -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_supportedEntryPoints","params":[]}' | jqChoose exactly one returned EntryPoint. Keep it unchanged in the paymaster requests, estimate, UserOperation hash, account signature, and submission. The same endpoints support v0.6, v0.7, and v0.8; their operation encodings are not interchangeable.
2. Create and verify a dedicated public policy
Create a public Testnet policy with a finite time window and enable4337: true. Use the normal Policy Management fields for the spending limits appropriate to your test.
NOW=$(date +%s)
END=$((NOW + 86400))
curl -sS "$POLICY_API" -H 'content-type: application/json' --data "{
\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"pm_createPolicy\",\"params":[{
\"name\":\"erc4337-quickstart-$(date +%s)\",
\"chainID\":97,\"type\":0,\"start\":$NOW,\"end\":$END,
\"enable4337\":true
}]
}" | jqSave the returned UUID as POLICY_UUID. Fund the policy through the approved sponsor policy-management flow, then confirm it is active, public, 4337-enabled, and has a positive remainingBalance:
export POLICY_UUID='<UUID_FROM_CREATE_POLICY>'
curl -sS "$POLICY_API" -H 'content-type: application/json' --data "{
\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"pm_getPolicyByUuid\",\"params\":[\"$POLICY_UUID\"]
}" | jq '.result | {uuid, activated, enable4337, maxGasCost, remainingBalance}'Do not continue until the policy reports activated: true, enable4337: true, and a positive available balance. Policy credit is an off-chain MegaFuel balance: sending BNB directly to an EntryPoint or Paymaster contract does not fund a policy. See Policy funding and limits.
3. Whitelist the exact inner action
Set these shell values for a BEP-20 transfer test:
export SMART_ACCOUNT='0x<YOUR_SMART_ACCOUNT>'
export TOKEN='0x<TOKEN_CONTRACT>'
export RECEIVER='0x<TRANSFER_RECEIVER>'
export TRANSFER_SELECTOR='0xa9059cbb'For a SimpleAccount operation execute(TOKEN, 0, transfer(RECEIVER, amount)), the policy evaluates the inner call. Do not whitelist the EntryPoint or Paymaster as the target.
# Sender, inner target, inner method, and token receiver.
for RULE in \
"FromAccountWhitelist:$SMART_ACCOUNT" \
"ToAccountWhitelist:$TOKEN" \
"ContractMethodSigWhitelist:$TRANSFER_SELECTOR" \
"BEP20ReceiverWhiteList:$RECEIVER"; do
TYPE=${RULE%%:*}; VALUE=${RULE#*:}
curl -sS "$POLICY_API" -H 'content-type: application/json' --data "{
\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"pm_addToWhitelist\",\"params\":[{
\"policyUuid\":\"$POLICY_UUID\",\"whitelistType\":\"$TYPE\",\"values\":[\"$VALUE\"]
}]
}" | jq
doneFor this first test, make the UserOperation match one active public policy only. Do not leave an older broad policy active for the same sender and action; policy-selection precedence is not a partner-facing routing contract.
4. Send the operation
For a complete, copyable standard-SimpleAccount client that supports v0.6, v0.7,
and v0.8, start with the reference client. It performs this same ordered flow; configure it with your factory, owner, policy rules, and intended inner call.
Follow the exact sequence in Wallet integration:
- Build the version-correct operation. Include factory data only for a counterfactual account.
- Call
pm_getPaymasterStubData. - Call
eth_estimateUserOperationGaswith a structurally valid dummy account signature and omitted account gas limits. - Apply the returned gas values, then call
pm_getPaymasterData. - Compute the chosen EntryPoint’s UserOperation hash, obtain the account’s own signature, and call
eth_sendUserOperation.
The accepted result is a userOpHash; it is not a transaction hash or evidence of inclusion.
5. Verify the terminal result and accounting
Poll both the standard receipt and MegaFuel lifecycle endpoint:
export USER_OP_HASH='0x<RETURNED_USER_OP_HASH>'
curl -sS "$BUNDLER_URL" -H 'content-type: application/json' --data "{
\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"eth_getUserOperationReceipt\",\"params\":[\"$USER_OP_HASH\"]
}" | jq
curl -sS "$BUNDLER_URL" -H 'content-type: application/json' --data "{
\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"nr_getUserOperationStatus\",\"params\":[\"$USER_OP_HASH\"]
}" | jqSuccess is a non-null receipt with success: true, an outer transactionHash, and an actualGasCost. Poll the policy every five seconds for up to 60 seconds: sponsoredGasfee must increase and remainingBalance must decrease by that same actualGasCost. See Lifecycle and client rules for terminal states and retry behavior.
Updated about 4 hours ago

