Documentation Index Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
Use this file to discover all available pages before exploring further.
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 >
);
}
import { useEffect } from "react" ;
import {
CrossmintProvider ,
CrossmintWalletProvider ,
useCrossmint ,
} from "@crossmint/client-sdk-react-native-ui" ;
// Replace with your auth provider
// See: https://docs.crossmint.com/wallets/guides/bring-your-own-auth
import { YourAuthProvider , useYourAuth } from "@your-auth-provider" ;
function App ({ children }) {
return (
< YourAuthProvider >
< CrossmintProvider apiKey = "YOUR_CLIENT_API_KEY" >
< CrossmintWalletProvider >
< JwtSync />
{ children }
</ CrossmintWalletProvider >
</ CrossmintProvider >
</ YourAuthProvider >
);
}
function JwtSync () {
const { jwt } = useYourAuth ();
const { setJwt } = useCrossmint ();
useEffect (() => {
setJwt ( jwt );
}, [ jwt ]);
return null ;
}
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.
import { useWallet } from "@crossmint/client-sdk-react-native-ui" ;
import { useState } from "react" ;
import { View , Text , TouchableOpacity } from "react-native" ;
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 ( "https://YOUR_SERVER/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 (
< View >
{ walletAddress ? (
< Text > Wallet: { walletAddress } </ Text >
) : (
< TouchableOpacity onPress = { handleCreateWallet } >
< Text > Create Wallet </ Text >
</ TouchableOpacity >
) }
</ View >
);
}
The createDeviceSigner() function generates and stores the private key in the device’s native secure storage (iOS Secure Enclave / Android Keystore). 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:
Get Wallet on Login
Get Wallet on Demand
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 >
);
}
import { useEffect } from "react" ;
import {
CrossmintProvider ,
CrossmintWalletProvider ,
useCrossmint ,
} from "@crossmint/client-sdk-react-native-ui" ;
// Replace with your auth provider
import { YourAuthProvider , useYourAuth } from "@your-auth-provider" ;
function App ({ children }) {
return (
< YourAuthProvider >
< CrossmintProvider apiKey = "YOUR_CLIENT_API_KEY" >
< CrossmintWalletProvider
createOnLogin = { {
chain: "base-sepolia" ,
recovery: { type: "email" },
} }
>
< JwtSync />
{ children }
</ CrossmintWalletProvider >
</ CrossmintProvider >
</ YourAuthProvider >
);
}
function JwtSync () {
const { jwt } = useYourAuth ();
const { setJwt } = useCrossmint ();
useEffect (() => {
setJwt ( jwt );
}, [ jwt ]);
return null ;
}
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-native-ui" ;
import { View , Text , TouchableOpacity } from "react-native" ;
function WalletActions () {
const { wallet , status } = useWallet ();
if ( status === "in-progress" ) return < Text > Loading... </ Text > ;
if ( ! wallet ) return < Text > No wallet </ Text > ;
const handleSend = async () => {
const { hash , explorerLink } = await wallet . send (
"0xYOUR_RECIPIENT_ADDRESS" ,
"usdc" ,
"10"
);
console . log ( "Transaction:" , explorerLink );
};
return (
< View >
< Text > Wallet: { wallet . address } </ Text >
< TouchableOpacity onPress = { handleSend } >
< Text > Send USDC </ Text >
</ TouchableOpacity >
</ View >
);
}
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.
Keep the provider setup as-is (without createOnLogin) and call getWallet() explicitly whenever you need the wallet. Use this approach when you want full control over when the wallet is fetched. import { useWallet } from "@crossmint/client-sdk-react-ui" ;
import { useEffect } from "react" ;
function WalletActions () {
const { wallet , status , getWallet } = useWallet ();
useEffect (() => {
async function fetchWallet () {
await getWallet ({ chain: "base-sepolia" });
}
fetchWallet ();
}, []);
if ( status === "in-progress" ) return < p > Loading... </ p > ;
if ( ! wallet ) return < p > No wallet </ p > ;
return (
< div >
< p > Wallet: { wallet . address } </ p >
</ div >
);
}
import { useWallet } from "@crossmint/client-sdk-react-native-ui" ;
import { useEffect } from "react" ;
import { View , Text } from "react-native" ;
function WalletActions () {
const { wallet , status , getWallet } = useWallet ();
useEffect (() => {
async function fetchWallet () {
await getWallet ({ chain: "base-sepolia" });
}
fetchWallet ();
}, []);
if ( status === "in-progress" ) return < Text > Loading... </ Text > ;
if ( ! wallet ) return < Text > No wallet </ Text > ;
return (
< View >
< Text > Wallet: { wallet . address } </ Text >
</ View >
);
}
New Device Recovery
When the user accesses their wallet from a new device, there is no local device signer. The SDK handles this automatically:
wallet.needsRecovery() returns true
On the first transaction (or explicit recover() call), the recovery signer (email OTP) authorizes a new device signer
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