Skip to main content
Expose a modular wallet through the EIP-1193 provider interface so libraries such as viem and ethers can use it as a wallet without knowing about Circle’s bundler or passkey signer. Use this when your app already has Web3 plumbing in place (a viem or ethers code path, a wallet connector, or an existing signing pipeline) and you want the modular wallet to slot in as a drop-in signer. This is a web-only flow.

Before you begin

Before you begin, ensure that you’ve:
  • Created a modular wallet and have access to the smartAccount, bundlerClient, and modularTransport client from that tutorial.
  • Installed viem. The examples below use viem. Any library that accepts an EIP-1193 provider works the same way.

Steps

1

Create a read-only public client

Keep the modularTransport client from the Create a modular wallet quickstart for account creation and the bundler. EIP1193Provider also needs a separate read-only PublicClient on a standard chain RPC (not modularTransport) for receipt lookups.
Web SDK (TypeScript)
import { createPublicClient, http } from "viem";
import { arcTestnet } from "viem/chains";

const readClient = createPublicClient({
  chain: arcTestnet,
  transport: http(), // or http("https://your-rpc-url")
});
2

Wrap the clients in an EIP-1193 provider

EIP1193Provider is exported from @circle-fin/modular-wallets-core. Construct it with the bundler client and the read-only public client.
Web SDK (TypeScript)
import { EIP1193Provider } from "@circle-fin/modular-wallets-core";

const eip1193Provider = new EIP1193Provider(bundlerClient, readClient);
The bundler client must include the smart account. The constructor throws if bundlerClient.account is missing.
3

Create a viem wallet client

Pass the provider to viem through a custom transport. EIP1193Provider.request returns a full JSON-RPC response, so unwrap result before returning it to viem.
Web SDK (TypeScript)
import { createWalletClient, custom } from "viem";
import { arcTestnet } from "viem/chains";

const walletClient = createWalletClient({
  account: smartAccount.address,
  chain: arcTestnet,
  transport: custom({
    request: async ({ method, params }) => {
      const response = await eip1193Provider.request({
        jsonrpc: "2.0",
        id: 1,
        method,
        params,
      });
      return (response as { result: unknown }).result;
    },
  }),
});
4

Sign and read accounts through the provider

Use the wallet client for account discovery and signing. The address passed to signing methods must match the smart account address.
Web SDK (TypeScript)
const [account] = await walletClient.getAddresses();
// account === smartAccount.address

const signature = await walletClient.signMessage({
  account,
  message: "Hello, world",
});

const typedDataSignature = await walletClient.signTypedData({
  account,
  domain: {
    name: "MyApp",
    version: "1",
    chainId: arcTestnet.id,
    verifyingContract: account,
  },
  types: {
    Order: [
      { name: "maker", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "expiry", type: "uint256" },
    ],
  },
  primaryType: "Order",
  message: {
    maker: account,
    amount: 1_000_000n,
    expiry: BigInt(Math.floor(Date.now() / 1000) + 3600),
  },
});
signMessage and signTypedData return a hex signature string. Verify it against the smart account contract using EIP-1271, as shown in Sign and verify messages.Common methods routed through the provider:
  • eth_accounts, eth_requestAccounts: return the smart account address.
  • personal_sign, eth_signTypedData_v4: sign through the passkey. The address parameter must match the smart account.
  • eth_getTransactionReceipt: reads the receipt through the read-only public client.
  • eth_sendTransaction: wraps the call in a user operation, submits it through the bundler, and returns the resulting transaction hash. It does not enable paymaster sponsorship. For gasless token transfers, send user operations through bundlerClient as shown in Transfer tokens.