Skip to main content
Modular wallets sign with a passkey on the user’s device and execute through a Modular Smart Contract Account (MSCA). Register the user’s passkey and create their MSCA to give them a passkey-backed modular wallet ready to transact. To use a non-passkey signer instead, see Use Dynamic as a signer.
For a complete working app, see the Circle Smart Account example in the modular wallets web SDK repository.

Prerequisites

Before you begin this tutorial, ensure that you’ve:
  • Created a Circle Console account and configured:
    • A client key for the modular wallets SDK: Console → Keys → Create a key → Client Key
    • A passkey domain matching the client key’s web domain: Console → Wallets → Modular Wallets → Passkey
  • Installed Node.js 22+ (web), Xcode (iOS), or Android Studio (Android).
  • Installed the modular wallets SDK for your platform: Web, iOS, or Android.
    For iOS and Android apps, passkeys are bound to a web domain. Publish an Apple associated domains file or Android Digital Asset Links file at your domain root so your native app can use the passkey.

Step 1. Configure your client key and backend URL

Make your client key and the modular wallets backend URL available to your app. The backend URL is the same for every app: https://modular-sdk.circle.com/v1/rpc/w3s/buidl.
# .env
VITE_CLIENT_KEY=YOUR_CLIENT_KEY
VITE_CLIENT_URL=https://modular-sdk.circle.com/v1/rpc/w3s/buidl
let CLIENT_KEY = "YOUR_CLIENT_KEY"
let clientUrl = "https://modular-sdk.circle.com/v1/rpc/w3s/buidl"
const val CLIENT_KEY = "YOUR_CLIENT_KEY"
const val clientUrl = "https://modular-sdk.circle.com/v1/rpc/w3s/buidl"
public static final String CLIENT_KEY = "YOUR_CLIENT_KEY";
public static final String CLIENT_URL = "https://modular-sdk.circle.com/v1/rpc/w3s/buidl";

Step 2. Register a passkey for the user

Create a passkey transport, then register a new passkey credential. The credential becomes the wallet’s signer. To sign a returning user back in, pass WebAuthnMode.Login instead of Register.
import {
  toPasskeyTransport,
  toWebAuthnCredential,
  WebAuthnMode,
} from "@circle-fin/modular-wallets-core";

const clientKey = import.meta.env.VITE_CLIENT_KEY as string;
const clientUrl = import.meta.env.VITE_CLIENT_URL as string;

const passkeyTransport = toPasskeyTransport(clientUrl, clientKey);

const credential = await toWebAuthnCredential({
  transport: passkeyTransport,
  mode: WebAuthnMode.Register,
  username: "your-username",
});
import CircleModularWalletsCore

Task {
    do {
        let transport = toPasskeyTransport(clientKey: CLIENT_KEY)

        let credential = try await toWebAuthnCredential(
            transport: transport,
            userName: "your-username",
            mode: WebAuthnMode.register
        )

        // Create a WebAuthn owner account from the credential.
        let webAuthnAccount = toWebAuthnAccount(credential)
    } catch {
        print(error)
    }
}
CoroutineScope(Dispatchers.IO).launch {
    try {
        val transport = toPasskeyTransport(context, CLIENT_KEY, clientUrl)

        val credential = toWebAuthnCredential(
            context,
            transport,
            "your-username",
            WebAuthnMode.Register,
        )

        // Create a WebAuthn owner account from the credential.
        val webAuthnAccount = toWebAuthnAccount(credential)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
try {
    HttpTransport transport = toPasskeyTransport(
        context,
        CLIENT_KEY,
        clientUrl
    );

    CompletableFuture<WebAuthnCredential> credentialFuture = new CompletableFuture<>();
    toWebAuthnCredential(
        context,
        transport,
        "your-username",
        WebAuthnMode.Register,
        new CustomContinuation<>(credentialFuture)
    );
    WebAuthnCredential credential = credentialFuture.join();

    // Create a WebAuthn owner account from the credential.
    WebAuthnAccount webAuthnAccount = toWebAuthnAccount(credential);
} catch (Exception e) {
    e.printStackTrace();
}

Step 3. Create a transport

The modular transport routes user operations through Circle’s bundler and paymaster for the blockchain you target. The example below uses Arc Testnet. For other options, see supported blockchains and the URL format documented in toModularTransport.
import { toModularTransport } from "@circle-fin/modular-wallets-core";
import { arcTestnet } from "viem/chains";

const modularTransport = toModularTransport(
  `${clientUrl}/arcTestnet`,
  clientKey,
);
        let modularTransport = toModularTransport(
            clientKey: CLIENT_KEY,
            url: "\(clientUrl)/arcTestnet"
        )
        val modularTransport = toModularTransport(
            context,
            CLIENT_KEY,
            "$clientUrl/arcTestnet",
        )
    Transport modularTransport = toModularTransport(
        context,
        CLIENT_KEY,
        clientUrl + "/arcTestnet"
    );

Step 4. Create the bundler client and smart account

The bundler client submits user operations through Circle’s bundler. The smart account is the user’s MSCA, with the passkey credential as its signer. Its address is deterministic, so you can read it before the account is deployed onchain.
import { toCircleSmartAccount } from "@circle-fin/modular-wallets-core";
import { createPublicClient } from "viem";
import {
  createBundlerClient,
  toWebAuthnAccount,
} from "viem/account-abstraction";
import { arcTestnet } from "viem/chains";

const client = createPublicClient({
  chain: arcTestnet,
  transport: modularTransport,
});

const smartAccount = await toCircleSmartAccount({
  client,
  owner: toWebAuthnAccount({ credential }),
});

const bundlerClient = createBundlerClient({
  account: smartAccount,
  chain: arcTestnet,
  transport: modularTransport,
});

console.log("Smart account address:", smartAccount.address);
        let bundlerClient = BundlerClient(
            chain: ArcTestnet,
            transport: modularTransport
        )

        let smartAccount = try await toCircleSmartAccount(
            client: bundlerClient,
            owner: webAuthnAccount
        )

        print("Smart account address: \(smartAccount.address)")
        val bundlerClient = BundlerClient(
            ArcTestnet,
            modularTransport,
        )

        val smartAccount = toCircleSmartAccount(
            bundlerClient,
            webAuthnAccount,
        )

        println("Smart account address: ${smartAccount.address}")
    BundlerClient bundlerClient = new BundlerClient(
        ArcTestnet.INSTANCE,
        modularTransport
    );

    CompletableFuture<CircleSmartAccount> smartAccountFuture = new CompletableFuture<>();
    toCircleSmartAccount(
        bundlerClient,
        webAuthnAccount,
        new CustomContinuation<>(smartAccountFuture)
    );
    CircleSmartAccount smartAccount = smartAccountFuture.join();

    System.out.println("Smart account address: " + smartAccount.getAddress());
The user now has a modular wallet, signed by their passkey, ready to transact.

Next steps