← Spruce/Paymaster for Developers
ADI Testnet · ERC-4337 v0.7
ERC-4337 · Account abstraction

Sponsor gas for your users

Add paymaster-backed gas sponsorship to your dApp in minutes. Native (ADI) or ERC20 gas payment. Backend-controlled signer, EntryPoint v0.7 compatible. Use our API or run your own.

Two paymaster types

Native (zero-balance accounts) or ERC20 gas. Same API, switch with one parameter.

Self-host or use our API

Deploy your own with our open scripts or call our hosted endpoints for testing.

ERC-4337 v0.7

Compatible with standard EntryPoint and any bundler or custom submit path.

Quick start

Get contract addresses, build a UserOperation, request sponsorship, then submit.

1

Get deployments

javascript
const res = await fetch('/api/erc4337/deployments')
const { entryPoint, nativePaymaster, erc20Paymaster, mockERC20 } = await res.json()
2

Build your UserOp (without paymasterAndData)

Include: sender, nonce, initCode, callData, accountGasLimits, preVerificationGas, gasFees. Use empty paymasterAndData/signature for the hash step.

javascript
const partialUserOp = {
  sender: smartAccountAddress,
  nonce: nonce.toString(),
  initCode: initCodeHex,
  callData: callDataHex,
  accountGasLimits: packedGasLimits,
  preVerificationGas: preVerificationGas.toString(),
  gasFees: packedGasFees,
  paymasterAndData: '0x',
  signature: '0x',
}
3

Request sponsorship

javascript
const sponsorRes = await fetch('/api/erc4337/sponsor', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'native',  // or 'erc20'
    paymasterAddress: nativePaymaster,
    userOp: {
      sender: partialUserOp.sender,
      nonce: partialUserOp.nonce,
      initCode: partialUserOp.initCode,
      callData: partialUserOp.callData,
      accountGasLimits: partialUserOp.accountGasLimits,
      preVerificationGas: partialUserOp.preVerificationGas,
      gasFees: partialUserOp.gasFees,
    },
    validitySeconds: 300,
    // maxTokenCost: '1000000000000000000'  // required for type: 'erc20'
  }),
})
const { paymasterAndData } = await sponsorRes.json()
4

Attach paymasterAndData, sign, and submit

javascript
const opWithPaymaster = { ...partialUserOp, paymasterAndData }
// Get userOpHash from EntryPoint.getUserOpHash(opWithPaymaster), sign with owner
const finalOp = { ...opWithPaymaster, signature: ownerSignature }

const submitRes = await fetch('/api/erc4337/submit', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    userOp: {
      ...finalOp,
      nonce: finalOp.nonce.toString(),
      preVerificationGas: finalOp.preVerificationGas.toString(),
    },
    beneficiary: null,
  }),
})
const { txHash } = await submitRes.json()

API reference

All endpoints are relative to your app origin. No API key required for the default setup.

GET/api/erc4337/deployments
Returns EntryPoint, NativePaymaster, ERC20Paymaster, MockERC20 addresses and chainId. Used by the frontend to know which paymaster to request.
Response
json
{
  "deployed": true,
  "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
  "nativePaymaster": "0x...",
  "erc20Paymaster": "0x...",
  "mockERC20": "0x...",
  "chainId": 99999
}
POST/api/erc4337/sponsor
Returns signed paymasterAndData for the given UserOp. Backend signer never leaves the server. Required body: type, paymasterAddress, userOp (UserOp fields only). Optional: validitySeconds (default 300), maxTokenCost (required for type: 'erc20').
Request body (native)
json
{
  "type": "native",
  "paymasterAddress": "0x...",
  "userOp": {
    "sender": "0x...",
    "nonce": "0",
    "initCode": "0x",
    "callData": "0x...",
    "accountGasLimits": "0x...",
    "preVerificationGas": "60000",
    "gasFees": "0x..."
  },
  "validitySeconds": 300
}
Response
json
{
  "paymasterAndData": "0x...",
  "validUntil": 1731234567,
  "sponsorAddress": "0x..."
}
POST/api/erc4337/submit
Submits a fully built UserOp (with paymasterAndData and signature) via EntryPoint.handleOps. Uses the same sponsor wallet as beneficiary. Numeric fields can be strings (e.g. nonce, preVerificationGas) for JSON.
Request body
json
{
  "userOp": {
    "sender": "0x...",
    "nonce": "0",
    "initCode": "0x",
    "callData": "0x...",
    "accountGasLimits": "0x...",
    "preVerificationGas": "60000",
    "gasFees": "0x...",
    "paymasterAndData": "0x...",
    "signature": "0x..."
  },
  "beneficiary": null
}
Response
json
{ "txHash": "0x..." }
POST/api/erc4337/mint
Mints MockERC20 (GTKN) to an address. Used in the ERC20 flow to fund the smart account. Body: to, amount (wei string).

Integration example

Minimal fetch-based integration you can drop into any frontend (React, Next, vanilla).

sponsor-and-submit.tstypescript
async function sponsorAndSubmitUserOp(partialUserOp: {
  sender: string
  nonce: string
  initCode: string
  callData: string
  accountGasLimits: string
  preVerificationGas: string
  gasFees: string
}, paymasterAddress: string, type: 'native' | 'erc20' = 'native') {
  const sponsorRes = await fetch('/api/erc4337/sponsor', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      type,
      paymasterAddress,
      userOp: partialUserOp,
      validitySeconds: 300,
      ...(type === 'erc20' && { maxTokenCost: '1000000000000000000' }),
    }),
  })
  if (!sponsorRes.ok) {
    const err = await sponsorRes.json()
    throw new Error(err.error || 'Sponsorship failed')
  }
  const { paymasterAndData } = await sponsorRes.json()

  // In your app: get userOpHash from EntryPoint.getUserOpHash({ ...partialUserOp, paymasterAndData, signature: '0x' })
  // then sign with owner and build finalOp = { ...partialUserOp, paymasterAndData, signature }

  const finalOp = {
    ...partialUserOp,
    paymasterAndData,
    signature: '0x...', // from your owner signer
  }

  const submitRes = await fetch('/api/erc4337/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      userOp: {
        ...finalOp,
        nonce: finalOp.nonce.toString(),
        preVerificationGas: finalOp.preVerificationGas.toString(),
      },
      beneficiary: null,
    }),
  })
  if (!submitRes.ok) {
    const err = await submitRes.json()
    throw new Error(err.error || 'Submit failed')
  }
  const { txHash } = await submitRes.json()
  return txHash
}

Try it in the Playground

Run Flow A (native sponsorship) and Flow B (ERC20 gas) end-to-end with live logs. No code required — just load deployments and click Run.

Open Playground
Paymaster contracts and scripts: contracts/erc4337/ and scripts/erc4337/. Deploy with node scripts/erc4337/deploy.js. Sponsor key: ADI_CHAIN_PRIVATE_KEY (server-only).