Getting Started
Wire up LazorKit in your React Native / Expo app in 5 minutes.
A complete working integration takes four steps: install dependencies, add polyfills, mount the provider, call the hook.
Zero-config defaults
Every provider prop is optional. If you leave them blank the SDK uses Devnet defaults
(api.devnet.solana.com, portal.lazor.sh, kora.devnet.lazorkit.com). Ship to production
by overriding rpcUrl + configPaymaster.
Prerequisites
- React Native ≥ 0.70 or Expo SDK ≥ 50
- A physical device or a simulator with biometrics enrolled (see Troubleshooting if you hit
NotAllowedError) - A deep-link scheme for your app (e.g.
myapp://)
Install
Install the SDK and peers
npm install @lazorkit/wallet-mobile-adapter @solana/web3.js \
@react-native-async-storage/async-storage expo-web-browser \
react-native-get-random-values react-native-url-polyfill buffer@solana/web3.js is a peer dependency. Install it explicitly so bundlers pick one
version instead of duplicating.
Add polyfills
React Native is missing some Node globals that Solana libraries expect. Add these imports
at the very top of your entry file (app/_layout.tsx, index.js, or App.tsx):
import 'react-native-get-random-values';
import 'react-native-url-polyfill/auto';
import { Buffer } from 'buffer';
global.Buffer = global.Buffer || Buffer;The provider itself patches Buffer.prototype.subarray once mounted.
Register your deep-link scheme
The portal returns signature payloads via a redirect. Your app needs to own a URL scheme.
{
"expo": {
"scheme": "myapp"
}
}In dev with Expo Go you can use exp://localhost:8081 (or your tunnel URL).
Add to ios/<YourApp>/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>Add to android/app/src/main/AndroidManifest.xml:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>Wrap your app with the provider
import { LazorKitProvider } from '@lazorkit/wallet-mobile-adapter';
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<LazorKitProvider isDebug={__DEV__}>
<Stack />
</LazorKitProvider>
);
}That's the minimum. To override defaults:
<LazorKitProvider
rpcUrl={process.env.EXPO_PUBLIC_RPC_URL}
portalUrl={process.env.EXPO_PUBLIC_PORTAL_URL}
configPaymaster={{
paymasterUrl: process.env.EXPO_PUBLIC_PAYMASTER_URL!,
apiKey: process.env.EXPO_PUBLIC_PAYMASTER_KEY,
}}
>
<Stack />
</LazorKitProvider>First flow: connect + send
Connect
import { useWallet } from '@lazorkit/wallet-mobile-adapter';
import { Button, Text } from 'react-native';
export default function HomeScreen() {
const { connect, isConnected, isConnecting, smartWalletPubkey } = useWallet();
if (isConnected) {
return <Text>Connected: {smartWalletPubkey!.toBase58()}</Text>;
}
return (
<Button
title={isConnecting ? 'Connecting…' : 'Connect with Passkey'}
onPress={() => connect({ redirectUrl: 'myapp://home' })}
/>
);
}smartWalletPubkey = vault
smartWalletPubkey is the vault PDA — the address that receives and sends funds.
Use it for connection.getBalance, for QR codes, and wherever you show the user their
wallet address.
Send a transaction
import { useWallet } from '@lazorkit/wallet-mobile-adapter';
import { LAMPORTS_PER_SOL, PublicKey, SystemProgram } from '@solana/web3.js';
import { Button } from 'react-native';
export default function SendScreen() {
const { smartWalletPubkey, signAndSendTransaction } = useWallet();
const send = async () => {
if (!smartWalletPubkey) return;
const ix = SystemProgram.transfer({
fromPubkey: smartWalletPubkey, // vault
toPubkey: new PublicKey('RECIPIENT_ADDRESS'),
lamports: 0.01 * LAMPORTS_PER_SOL,
});
const sig = await signAndSendTransaction(
{ instructions: [ix] },
{ redirectUrl: 'myapp://home' },
);
console.log('tx', sig);
};
return <Button title="Send 0.01 SOL" onPress={send} />;
}(Optional) Use the vault balance
const { smartWalletPubkey, connection } = useWallet();
useEffect(() => {
if (!smartWalletPubkey) return;
connection.getBalance(smartWalletPubkey).then((l) =>
console.log('balance (SOL):', l / LAMPORTS_PER_SOL),
);
}, [smartWalletPubkey?.toBase58()]);8. Deferred execution — split TX1 / TX2
For flows where TX1 and TX2 don't run in the same process — a relayer on your server,
a scheduled job, a secondary device — use the split API instead of authorizeAndExecute:
import {
useWallet,
serializeDeferredPayload,
deserializeDeferredPayload,
} from '@lazorkit/wallet-mobile-adapter';
// Device A — passkey-signed TX1 only, returns payload
const { signature: tx1Sig, deferredPayload } = await authorizeDeferred(
{ instructions: jupiterSwapIxs, expiryOffset: 600 },
{ redirectUrl: 'myapp://cb' },
);
// Send the payload to your relayer as a JSON-safe string
await fetch('/api/relayer', {
method: 'POST',
body: serializeDeferredPayload(deferredPayload),
});
// Relayer / server / other device — no passkey required
const payload = deserializeDeferredPayload(await req.text());
const tx2Sig = await executeDeferred({ deferredPayload: payload });If the authorization expires before TX2 is submitted, reclaim the rent:
await reclaimDeferred({
deferredExecPda, // from authorizeDeferred result
refundDestination: smartWalletPubkey, // refund to vault
});See useWallet → Deferred execution
for the full API.
Next level
Session keys
One passkey prompt → many prompt-free sends. Optional on-chain spending limits.
Deferred execution
Authorize once, execute payloads too big for a single tx (Jupiter swaps, batches).
Authorities
Add Ed25519 device keys as admin or spender on the wallet.
Troubleshooting
Common errors: NotAllowedError, Network request failed, 0x2, 0xbd0, deep-link mismatches.