LazorKit LogoLazorKit
React SDK

Getting Started

Ship a working LazorKit integration in under 10 minutes.

Three files total: a provider, a connect button, and a transaction handler. All three use the same useWallet hook.

Zero-config defaults

All provider props are optional. With nothing set, the SDK targets Solana devnet through portal.lazor.sh and LazorKit's devnet Kora paymaster, which is set up for protocol v2 — perfect for demos and local dev.

Mainnet

Mainnet still runs protocol v1, and the v2 mainnet program is not deployed yet. With @lazorkit/wallet 3.x, existing v1 wallets work on mainnet (through a relayer that sponsors v1: v1PaymasterConfig), but a new user cannot get a wallet there until v2 is deployed. See Networks & versions.

Prerequisites

  • React 18.3+ or 19
  • Browser with WebAuthn support (all modern browsers)
  • Client-side context — passkey APIs aren't available during SSR

Install

Install the SDK and peer

npm install @lazorkit/wallet @solana/web3.js@^1.99.0

@solana/web3.js is a peer dependency and must be 1.99 or later: an app pinned to 1.98.x gets npm ERESOLVE. Install it explicitly so your bundler resolves a single version. npm installs the other peers (zustand, @solana/wallet-adapter-base, the @wallet-standard packages) for you; with yarn or pnpm settings that do not, add them. 3.3.1 and later do not declare @solana/kit, so an app on kit 6, 7 or 8 installs the wallet without --legacy-peer-deps (3.3.0 and earlier failed with ERESOLVE there).

Add a Buffer polyfill (only if other code needs it)

@lazorkit/wallet 3.0.2 and later need no global Buffer. Other Solana libraries in your app may: add a polyfill only if you see Buffer is not defined.

vite.config.ts
import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills';

export default defineConfig({
  plugins: [nodePolyfills()],
});

Mark any file that uses wallet logic with "use client". For the Buffer global, add a one-time shim in your providers file:

app/providers.tsx
'use client';

import { Buffer } from 'buffer';
if (typeof window !== 'undefined') {
  (window as any).Buffer ??= Buffer;
}

CRA handles Buffer automatically for most versions. If you hit errors, install buffer and add:

src/polyfills.ts
import { Buffer } from 'buffer';
(window as any).Buffer = (window as any).Buffer ?? Buffer;

Import ./polyfills before anything else in src/index.tsx.

Mount the provider

app/providers.tsx
'use client';

import { LazorkitProvider } from '@lazorkit/wallet';

export function Providers({ children }: { children: React.ReactNode }) {
  return <LazorkitProvider>{children}</LazorkitProvider>;
}

That's the minimum. Override defaults via props:

<LazorkitProvider
  rpcUrl={process.env.NEXT_PUBLIC_RPC_URL}
  cluster="devnet"                       // needed when the RPC URL does not say which cluster
  portalUrl="https://portal.lazor.sh"
  paymasterConfig={{
    paymasterUrl: process.env.NEXT_PUBLIC_PAYMASTER_URL!,
    apiKey: process.env.NEXT_PUBLIC_PAYMASTER_KEY, // ships in the bundle: not a secret
  }}
>
  {children}
</LazorkitProvider>

If your app already has users on LazorKit v1, also set v1PaymasterConfig; see Migrating from v1.


First flow: connect + send

Connect button

components/ConnectButton.tsx
'use client';
import { useWallet } from '@lazorkit/wallet';

export function ConnectButton() {
  const { connect, disconnect, isConnected, isConnecting, vaultPubkey } = useWallet();

  if (isConnected && vaultPubkey) {
    const vault = vaultPubkey.toBase58();
    return (
      <button onClick={() => disconnect()}>
        Disconnect ({vault.slice(0, 6)}…)
      </button>
    );
  }
  return (
    <button onClick={() => connect()} disabled={isConnecting}>
      {isConnecting ? 'Connecting…' : 'Connect Wallet'}
    </button>
  );
}

vaultPubkey is the user-facing address

vaultPubkey (the same address as wallet.vaultPda) is the SOL-holding account — show it to users, fund it, and use it as fromPubkey. smartWalletPubkey / wallet.smartWallet is the internal wallet PDA: funds sent there cannot be spent. Do not derive the vault with findVaultPda: it derives a v2 mainnet address, wrong for v1 wallets and for devnet. Records saved by older releases get vaultPda on the next connect.

The first connect opens the portal. For a returning user it may ask them to confirm which wallet is theirs, and for an existing passkey that has no wallet yet it asks for one more signature to recover the passkey's key. See Wallet Confirmation.

Transfer SOL

components/TransferButton.tsx
'use client';
import { useWallet } from '@lazorkit/wallet';
import { LAMPORTS_PER_SOL, PublicKey, SystemProgram } from '@solana/web3.js';

export function TransferButton() {
  const { vaultPubkey, isSigning, signAndSendTransaction } = useWallet();

  const send = async () => {
    if (!vaultPubkey) return;
    const ix = SystemProgram.transfer({
      fromPubkey: vaultPubkey,                 // the vault: funds live and are signed for here
      toPubkey: new PublicKey('RECIPIENT_ADDRESS'),
      lamports: 0.01 * LAMPORTS_PER_SOL,
    });
    // Resolves once the transaction is confirmed; rejects if it failed on chain.
    const sig = await signAndSendTransaction({ instructions: [ix] });
    console.log('tx', sig);
  };

  return <button onClick={send} disabled={isSigning}>Send 0.01 SOL</button>;
}

signAndSendTransaction resolves only once the transaction is confirmed, and isSigning stays true until then; a second call meanwhile is refused with "Already signing". What it can reject with, and how to handle it: Sending transactions.

(Optional) Display balance

'use client';
import { useEffect, useMemo, useState } from 'react';
import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
import { useWallet } from '@lazorkit/wallet';

export function Balance() {
  const { wallet } = useWallet();
  const vault = useMemo(
    () => (wallet?.vaultPda ? new PublicKey(wallet.vaultPda) : null),
    [wallet?.vaultPda],
  );

  const [sol, setSol] = useState(0);

  useEffect(() => {
    if (!vault) return;
    const connection = new Connection(process.env.NEXT_PUBLIC_RPC_URL!, 'confirmed');
    connection.getBalance(vault).then((l) => setSol(l / LAMPORTS_PER_SOL));
  }, [vault?.toBase58()]);

  return <span>{sol.toFixed(4)} SOL</span>;
}

Next level