Skip to main content
Most applications create wallets server-side for security and control. However, to enable frictionless client-side signing with a device signer, it must be registered at wallet creation time — otherwise, adding it later requires an OTP verification step. This guide walks through the recommended pattern: generate the device signer on the client, send it to your server, create the wallet with both the device signer and an email recovery signer, then return the wallet to the client.

Prerequisites

  • A server API key with wallets.create and wallets.read scopes
  • A client API key for the React or React Native SDK
  • @crossmint/client-sdk-react-ui installed on the client (React), or @crossmint/client-sdk-react-native-ui (React Native)

Architecture

Provider Setup

Wrap your app with the Crossmint providers. During the initial wallet creation flow, omit createOnLogin — the wallet will be created server-side in Step 2.
import {
    CrossmintProvider,
    CrossmintAuthProvider,
    CrossmintWalletProvider,
} from "@crossmint/client-sdk-react-ui";

function App({ children }) {
    return (
        <CrossmintProvider apiKey="YOUR_CLIENT_API_KEY">
            <CrossmintAuthProvider loginMethods={["email", "google"]}>
                <CrossmintWalletProvider>
                    {children}
                </CrossmintWalletProvider>
            </CrossmintAuthProvider>
        </CrossmintProvider>
    );
}

Step 1: Create the Device Signer (Client)

Use createDeviceSigner() from the useWallet hook to generate a hardware-backed P256 keypair on the user’s device. This happens before any wallet exists. The returned descriptor contains the public key coordinates and a locator — it looks like this:
{
    "type": "device",
    "publicKey": { "x": "0x...", "y": "0x..." },
    "locator": "device:<base64-public-key>",
    "name": "Chrome on Mac"
}
import { useWallet } from "@crossmint/client-sdk-react-ui";
import { useState } from "react";

function CreateWalletFlow() {
    const { createDeviceSigner } = useWallet();
    const [walletAddress, setWalletAddress] = useState<string | null>(null);

    const handleCreateWallet = async () => {
        const deviceSigner = await createDeviceSigner();

        // Send the device signer to your server to create the wallet
        const response = await fetch("/api/wallet", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
                email: "user@example.com",
                deviceSigner,
            }),
        });
        const { address } = await response.json();
        setWalletAddress(address);
    };

    return (
        <div>
            {walletAddress ? (
                <p>Wallet: {walletAddress}</p>
            ) : (
                <button onClick={handleCreateWallet}>Create Wallet</button>
            )}
        </div>
    );
}
The createDeviceSigner() function generates and stores the private key in the browser’s secure storage (hidden iframe at crossmint-signer.io). The private key never leaves the device — only the public key descriptor is sent to the server.

Step 2: Create the Wallet (Server)

On your server, use the device signer descriptor received from the client to create a wallet with email recovery and the device signer pre-registered.
import { createCrossmint, CrossmintWallets } from "@crossmint/wallets-sdk";

const crossmint = createCrossmint({
    apiKey: "YOUR_SERVER_API_KEY",
});

const crossmintWallets = CrossmintWallets.from(crossmint);

// deviceSigner is the object received from the client
const wallet = await crossmintWallets.createWallet({
    chain: "base-sepolia",
    owner: "email:user@example.com",
    recovery: {
        type: "email",
        email: "user@example.com",
    },
    signers: [deviceSigner],
});

console.log("Wallet created:", wallet.address);
// Return wallet.address to the client

Step 3: Use the Wallet (Client)

Once the wallet is created server-side, the client needs to retrieve it. Choose the approach that fits your app:
Enable createOnLogin on CrossmintWalletProvider so the wallet is automatically retrieved whenever the user logs in — no explicit getWallet() call needed. This is the recommended approach for most apps.
import {
    CrossmintProvider,
    CrossmintAuthProvider,
    CrossmintWalletProvider,
} from "@crossmint/client-sdk-react-ui";

function App({ children }) {
    return (
        <CrossmintProvider apiKey="YOUR_CLIENT_API_KEY">
            <CrossmintAuthProvider loginMethods={["email", "google"]}>
                <CrossmintWalletProvider
                    createOnLogin={{
                        chain: "base-sepolia",
                        recovery: { type: "email" },
                    }}
                >
                    {children}
                </CrossmintWalletProvider>
            </CrossmintAuthProvider>
        </CrossmintProvider>
    );
}
With createOnLogin enabled, any component using useWallet() will automatically have access to the wallet after the user logs in:
import { useWallet } from "@crossmint/client-sdk-react-ui";

function WalletActions() {
    const { wallet, status } = useWallet();

    if (status === "in-progress") return <p>Loading...</p>;
    if (!wallet) return <p>No wallet</p>;

    const handleSend = async () => {
        const { hash, explorerLink } = await wallet.send(
            "0xYOUR_RECIPIENT_ADDRESS",
            "usdc",
            "10"
        );
        console.log("Transaction:", explorerLink);
    };

    return (
        <div>
            <p>Wallet: {wallet.address}</p>
            <button onClick={handleSend}>Send USDC</button>
        </div>
    );
}
createOnLogin will retrieve an existing wallet if one is found for the logged-in user, or create a new one if none exists. Since the wallet was already created server-side, it will be retrieved automatically.

New Device Recovery

When the user accesses their wallet from a new device, there is no local device signer. The SDK handles this automatically:
  1. wallet.needsRecovery() returns true
  2. On the first transaction (or explicit recover() call), the recovery signer (email OTP) authorizes a new device signer
  3. After recovery, all subsequent transactions are frictionless again
See Device Signer — New Device Recovery for details.

Next Steps

Device Signer

Understand how hardware-backed device signers work

Configure Recovery

Explore other recovery signer options

Transfer Tokens

Send tokens from your wallet