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.
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:
'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:
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
'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
'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
'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
Session keys
One prompt → many prompt-free sends. Built-in SpendingLimits wrapper.
Deferred execution
Authorize once, execute payloads too big for a single tx.
Sending transactions
Confirmation, isSigning, sequencing and the errors a send can reject with.
Sign-in with a message
Sign a message, then check on your server which wallet signed it.
Troubleshooting
SSR errors, NotAllowedError, 0xbd0, 3006.