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
  • curl and jq. 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":[]}' | jq

Choose 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
  }]
}" | jq

Save 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
done

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

  1. Build the version-correct operation. Include factory data only for a counterfactual account.
  2. Call pm_getPaymasterStubData.
  3. Call eth_estimateUserOperationGas with a structurally valid dummy account signature and omitted account gas limits.
  4. Apply the returned gas values, then call pm_getPaymasterData.
  5. 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\"]
}" | jq

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


Did this page help you?