LazorKit LogoLazorKit
React Native SDK

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.

The portal returns signature payloads via a redirect. Your app needs to own a URL scheme.

app.json
{
  "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

app/_layout.tsx
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

app/index.tsx
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

app/send.tsx
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