Skip to main content
Smart accounts (ERC-4337) are programmable wallets that enable advanced features like batching multiple transactions together and sponsoring gas fees for your users. Key benefits of CDP Smart Accounts include:

Batch transactions

Execute multiple calls in a single user operation

Gas sponsorship

Optional paymasters for gasless UX

Multi-chain support

Deploy on 8 mainnets and 2 testnets across EVM chains
For keeping the same address, EIP-7702 upgrades an existing EOA with the same smart account capabilities without creating a new contract address.

Create a smart account

Configure createOnLogin: "smart" in your provider so new users get a smart account automatically on sign-in.
import { CDPHooksProvider, useCurrentUser } from "@coinbase/cdp-hooks";

function App() {
  return (
    <CDPHooksProvider
      config={{
        projectId: "your-project-id",
        ethereum: { createOnLogin: "smart" },
      }}
    >
      <YourApp />
    </CDPHooksProvider>
  );
}

function SmartAccountInfo() {
  const { currentUser } = useCurrentUser();
  const smartAccount = currentUser?.evmSmartAccounts?.[0];
  return <p>Smart Account: {smartAccount}</p>;
}

Send a user operation

On Base Sepolia, user operations are subsidized and the smart account does not need to be funded with ETH. On Base mainnet, fund the smart account with ETH before submitting.
useSendUserOperation tracks status, data, and error through on-chain confirmation.
import { useSendUserOperation, useCurrentUser } from "@coinbase/cdp-hooks";

function SendUserOperation() {
  const { sendUserOperation, status, data, error } = useSendUserOperation();
  const { currentUser } = useCurrentUser();

  const handleSend = async () => {
    const smartAccount = currentUser?.evmSmartAccounts?.[0];
    if (!smartAccount) return;

    await sendUserOperation({
      evmSmartAccount: smartAccount,
      network: "base-sepolia",
      calls: [{ to: "0x000...000", value: 0n, data: "0x" }],
    });
  };

  return (
    <div>
      <button onClick={handleSend} disabled={status === "pending"}>
        {status === "pending" ? "Sending..." : "Send"}
      </button>
      {status === "success" && data && <p>Tx: {data.transactionHash}</p>}
      {error && <p>Error: {error.message}</p>}
    </div>
  );
}

Batch calls

Pass multiple entries in calls[] to execute them atomically in a single user operation. Calls run in order and revert together on failure.
await sendUserOperation({
  evmSmartAccount: smartAccount,
  network: "base-sepolia",
  calls: [
    { to: addr1, value: parseEther("0.001"), data: "0x" },
    { to: addr2, value: parseEther("0.001"), data: "0x" },
    { to: addr3, value: parseEther("0.001"), data: "0x" },
  ],
});

Encode contract calls

To interact with contracts, pass data using an ABI-encoded payload. This example encodes an ERC-20 transfer using viem:
Node (TypeScript)
import { encodeFunctionData } from "viem";

const erc20Abi = [
  {
    name: "transfer",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
    ],
    outputs: [{ name: "", type: "bool" }],
  },
];

const data = encodeFunctionData({
  abi: erc20Abi as const,
  functionName: "transfer",
  args: [recipient, 1_000_000n], // 1 USDC assuming 6 decimals
});

await sendUserOperation({
  evmSmartAccount: smartAccount,
  network: "base-sepolia",
  calls: [
    {
      to: usdcAddress,
      data,
      value: 0n,
    },
  ],
});

Gas sponsorship

For CDP Smart Accounts, pass a paymasterUrl to cover gas fees with any ERC-7677-compatible paymaster, use useCdpPaymaster on Base, or configure a custom paymaster in CDP Portal (see Custom paymaster via CDP Portal below).
React also supports useCdpPaymaster: true to use the CDP Paymaster on Base without providing a URL.
useCdpPaymaster is only supported on Base. You cannot specify both useCdpPaymaster and paymasterUrl.
await sendUserOperation({
  evmSmartAccount: smartAccount,
  network: "base-sepolia",
  calls: [{ to: recipient, value: 10n ** 15n, data: "0x" }],
  useCdpPaymaster: true,
  // or: paymasterUrl: "https://your-paymaster.example.com"
});

Custom paymaster via CDP Portal

Configure CDP to route gas sponsorship for CDP Smart Accounts through your own paymaster infrastructure instead of — or in addition to — the CDP-managed paymaster. The paymaster URL is stored in CDP’s backend and never sent to the client. Use a custom paymaster when you:
  • Run your own paymaster infrastructure (e.g. a proxy in front of Alchemy, Pimlico, or another provider)
  • Need gas sponsorship on EVM chains where CDP’s native paymaster doesn’t apply (only Base and Base Sepolia are natively supported)
  • Want custom sponsorship rules — per-user caps, business-logic gates, or allow/deny logic — that go beyond CDP’s built-in contract allowlists
  • Need the paymaster URL kept strictly server-side and never exposed to the frontend

How it works

Custom Paymaster URL — A per-network URL configured in CDP Portal at the project level. When CDP’s backend receives a user operation from a CDP Smart Account that needs sponsorship, it checks whether the project has a custom paymaster configured for that network and routes the request there. Paymaster Context — An arbitrary key-value map (defined by EIP-7677) configured in Portal and forwarded to your paymaster on every sponsored request. The primary use case is authentication: include a static secret in the context, and your paymaster validates it before processing requests, so a leaked URL alone can’t be abused.
User operation (CDP Smart Account)


CDP backend (cdp-service)
    │  checks project config for custom paymaster URL on this network
    │  forwards Portal context as EIP-7677 context

Your paymaster
    │  validates secret from context
    │  applies your custom sponsorship logic

Bundler → chain
Portal config is project-level and stored server-side — your frontend never sees the paymaster URL. When context is configured in Portal, omit paymaster parameters from SDK calls and CDP forwards the Portal context automatically.

Configure in CDP Portal

1

Open Paymaster Configuration

In CDP Portal, go to your non-custodial wallet project and open the Paymaster Configuration tab.
2

Add a network

Click Add network and select the EVM network you want to configure. You can add one entry per network.
3

Set your paymaster URL

Enter your paymaster’s HTTPS endpoint. This must be an ERC-7677-compatible paymaster URL.
https://paymaster.example.com/api/v1/sponsor
4

Add context key-value pairs (recommended)

Under Context, add one or more key-value pairs. CDP forwards these to your paymaster on every sponsored request.For the static-secret authentication pattern:
KeyValue
x-cdp-secreta-long-random-secret-you-generate
Context values are write-only in the Portal UI — once saved, they cannot be read back, only overwritten. Treat them like secrets: store the value somewhere safe before saving.

Validate the secret in your paymaster

Your paymaster receives the context as part of the standard EIP-7677 pm_getPaymasterStubData / pm_getPaymasterData request, under the context field. Reject requests that don’t include the expected secret.
paymaster.ts
import { createServer, type IncomingMessage } from "http";

const CDP_SECRET = process.env.CDP_PAYMASTER_SECRET; // same value you set in Portal

async function readJson(req: IncomingMessage) {
  const buffers: Buffer[] = [];
  for await (const chunk of req) buffers.push(chunk as Buffer);
  return JSON.parse(Buffer.concat(buffers).toString());
}

createServer(async (req, res) => {
  const body = await readJson(req);

  if (body.method === "pm_getPaymasterStubData" || body.method === "pm_getPaymasterData") {
    const context = body.params?.[2] ?? {};

    // Reject if the secret is missing or wrong
    if (context["x-cdp-secret"] !== CDP_SECRET) {
      res.writeHead(401);
      res.end(JSON.stringify({ error: "Unauthorized" }));
      return;
    }

    // Your sponsorship logic here
    // ...
  }
}).listen(3000);

Send user operations with Portal config

Once your paymaster URL is configured in Portal, CDP’s backend applies it automatically — you do not pass paymasterUrl in SDK calls. If context (including authentication secrets) is configured in Portal, no paymaster parameters are required in SDK calls.
import { CdpClient } from "@coinbase/cdp-sdk";

const cdp = new CdpClient();
const owner = await cdp.evm.getOrCreateAccount({ name: "my-account" });
const smartAccount = await cdp.evm.getOrCreateSmartAccount({ owner });

const userOperation = await cdp.evm.sendUserOperation({
  smartAccount,
  network: "eip155:42161", // Arbitrum — CDP doesn't natively sponsor here
  calls: [
    {
      to: "0xRecipient",
      value: 0n,
      data: "0x",
    },
  ],
});

Security: why server-side storage matters

Storing the paymaster URL and secret in CDP Portal is more secure than passing them from the frontend:
CDP Portal configClient-side paymasterUrl
URL visibilityServer-side only — never in JS bundle or network trafficExposed in browser and network traffic
Secret visibilityServer-side onlyMust be passed from client if required for auth
Abuse surfaceURL + valid secret requiredLeaked URL can be abused without Portal context
Config changePortal UI or API — no code deploy neededRequires code change and redeploy
A leaked paymaster URL without the secret is useless when your paymaster validates the context secret.

Chain scope

Custom paymasters are most valuable for chains where CDP doesn’t natively sponsor gas:
ChainCDP native paymasterCustom paymaster
Base Mainnet✅ (overrides CDP’s)
Base Sepolia✅ (overrides CDP’s)
Arbitrum, Optimism, Polygon, etc.
Any ERC-7677 chain
On Base, a custom paymaster configured in Portal overrides CDP’s built-in paymaster for that project. On other chains, it’s the only path to gas sponsorship through CDP Smart Accounts. See also: Paymaster Proxy · Paymaster Security · EIP-7677

Builder Codes

Base Builder Codes attribute onchain activity back to your app for rewards and analytics. Pass dataSuffix on any user operation — no contract changes needed. First, register at base.dev and generate your suffix:
import { Attribution } from "ox/erc8021";

const DATA_SUFFIX = Attribution.toDataSuffix({
  codes: ["YOUR-BUILDER-CODE"],
});
Then pass it to sendUserOperation:
import { useSendUserOperation, useCurrentUser } from "@coinbase/cdp-hooks";

await sendUserOperation({
  evmSmartAccount: smartAccount,
  network: "base",
  calls: [{ to: "0xYourContract", value: 0n, data: "0x" }],
  dataSuffix: DATA_SUFFIX,
});

Supported networks

NetworkMainnetTestnet
Base✓ (Base Sepolia)
Arbitrum
Optimism
Zora
Polygon
BNB Chain
Avalanche
Ethereum✓ (Sepolia)

Debugging

When a user operation reverts, the revert reason is included in its receipts if it can be decoded:
const userOp = await cdp.evm.getUserOperation({
  userOpHash: result.userOpHash,
  smartAccount,
});
console.log(userOp.receipts);