A complete Next.js demo application showcasing the LazorKit SDK for building passkey-authenticated, gasless payment experiences on Solana.
π Live Demo | π LazorKit Docs
- π Passkey Authentication - Sign in with Face ID, Touch ID, or Windows Hello
- β‘ Gasless Transactions - Send SOL without worrying about gas fees
- π± QR Payment Requests - Generate QR codes to receive payments
- π¨ Embeddable Widget - Drop-in payment component for any app
- π Transaction History - Track all your payments in real-time
- Node.js 18+
- A WebAuthn-capable browser (Chrome, Safari, Edge, Firefox)
- A device with biometric authentication (fingerprint/face) or security key
# Clone the repository
git clone https://github.com/YOUR_USERNAME/lazorpay.git
cd lazorpay
# Install dependencies
npm install
# Start development server
npm run devOpen http://localhost:3000 to see the app.
This tutorial shows how to integrate LazorKit passkey authentication into any React/Next.js application.
npm install @lazorkit/wallet @coral-xyz/anchor @solana/web3.js bufferCreate src/polyfills.ts:
import { Buffer } from 'buffer';
if (typeof window !== 'undefined') {
window.Buffer = Buffer;
}
export {};Import this in your app's entry point (e.g., layout.tsx).
Create src/context/LazorPayProvider.tsx:
"use client";
import { LazorkitProvider, useWallet } from "@lazorkit/wallet";
import { createContext, useContext, ReactNode } from "react";
const CONFIG = {
rpcUrl: "https://api.devnet.solana.com",
portalUrl: "https://portal.lazor.sh",
paymasterUrl: "https://lazorkit-paymaster.onrender.com",
};
// Create your custom context wrapping LazorKit
const WalletContext = createContext<ReturnType<typeof useWallet> | null>(null);
function WalletInner({ children }: { children: ReactNode }) {
const wallet = useWallet();
return (
<WalletContext.Provider value={wallet}>
{children}
</WalletContext.Provider>
);
}
export function LazorPayProvider({ children }: { children: ReactNode }) {
return (
<LazorkitProvider
rpcUrl={CONFIG.rpcUrl}
ipfsUrl={CONFIG.portalUrl}
paymasterUrl={CONFIG.paymasterUrl}
>
<WalletInner>{children}</WalletInner>
</LazorkitProvider>
);
}
export function useWalletContext() {
const context = useContext(WalletContext);
if (!context) throw new Error("Must be within LazorPayProvider");
return context;
}In layout.tsx:
import { LazorPayProvider } from "@/context/LazorPayProvider";
import "@/polyfills";
export default function RootLayout({ children }) {
return (
<html>
<body>
<LazorPayProvider>{children}</LazorPayProvider>
</body>
</html>
);
}"use client";
import { useWalletContext } from "@/context/LazorPayProvider";
export function ConnectButton() {
const { isConnected, connect, disconnect, smartWalletPubkey } = useWalletContext();
if (isConnected) {
return (
<div>
<p>Connected: {smartWalletPubkey?.toBase58()}</p>
<button onClick={disconnect}>Disconnect</button>
</div>
);
}
return <button onClick={connect}>Connect with Passkey</button>;
}Learn how to send SOL using LazorKit's gasless transaction feature.
import { SystemProgram, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
async function sendSol(
wallet: ReturnType<typeof useWallet>,
recipient: string,
amountSol: number
) {
if (!wallet.smartWalletPubkey || !wallet.signAndSendTransaction) {
throw new Error("Wallet not connected");
}
// Create transfer instruction
const instruction = SystemProgram.transfer({
fromPubkey: wallet.smartWalletPubkey,
toPubkey: new PublicKey(recipient),
lamports: Math.floor(amountSol * LAMPORTS_PER_SOL),
});
// Sign and send - gas is covered by LazorKit paymaster!
const signature = await wallet.signAndSendTransaction(instruction);
return signature;
}"use client";
import { useState } from "react";
import { useWalletContext } from "@/context/LazorPayProvider";
export function SendForm() {
const wallet = useWalletContext();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");
const handleSend = async () => {
setStatus("sending");
try {
const sig = await sendSol(wallet, recipient, parseFloat(amount));
console.log("Transaction:", sig);
setStatus("success");
} catch (err) {
console.error(err);
setStatus("error");
}
};
return (
<div>
<input
placeholder="Recipient address"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
/>
<input
type="number"
placeholder="Amount (SOL)"
value={amount}
onChange={(e) => setAmount(e.target.value)}
/>
<button onClick={handleSend} disabled={status === "sending"}>
{status === "sending" ? "Sending..." : "Send SOL"}
</button>
{status === "success" && <p>β
Transaction sent!</p>}
{status === "error" && <p>β Transaction failed</p>}
</div>
);
}Build a reusable payment component that can be dropped into any app.
interface PaymentWidgetProps {
recipient: string; // Wallet to receive payment
amount: number; // Amount in SOL
memo?: string; // Optional payment description
onSuccess?: (sig: string) => void;
onError?: (err: Error) => void;
}
export function PaymentWidget({
recipient,
amount,
memo,
onSuccess,
onError,
}: PaymentWidgetProps) {
const wallet = useWalletContext();
const [status, setStatus] = useState<"idle" | "paying" | "success" | "error">("idle");
const handlePay = async () => {
// Connect if not connected
if (!wallet.isConnected) {
await wallet.connect();
}
setStatus("paying");
try {
const sig = await sendSol(wallet, recipient, amount);
setStatus("success");
onSuccess?.(sig);
} catch (err) {
setStatus("error");
onError?.(err as Error);
}
};
return (
<div className="payment-widget">
<h3>{amount} SOL</h3>
<p>To: {recipient.slice(0, 8)}...</p>
{memo && <p>"{memo}"</p>}
<button onClick={handlePay} disabled={status === "paying"}>
{wallet.isConnected ? `Pay ${amount} SOL` : "Connect & Pay"}
</button>
{status === "success" && <p>Payment successful! π</p>}
</div>
);
}// Checkout page
<PaymentWidget
recipient="7BeWr6tVa1pYgrEddekYTnQENU22bBw9H8HYJUkbrN71"
amount={0.5}
memo="Order #12345"
onSuccess={(sig) => {
router.push(`/order-complete?tx=${sig}`);
}}
/>lazorpay/
βββ src/
β βββ app/
β β βββ layout.tsx # App layout with providers
β β βββ page.tsx # Main page
β β βββ globals.css # Global styles
β βββ components/
β β βββ ui/ # Base UI components
β β βββ ConnectButton.tsx
β β βββ SendPayment.tsx
β β βββ PaymentRequest.tsx
β β βββ PaymentWidget.tsx
β β βββ TransactionHistory.tsx
β βββ context/
β β βββ LazorPayProvider.tsx
β βββ lib/
β β βββ utils.ts
β βββ polyfills.ts
βββ public/
βββ package.json
βββ README.md
For production, you may want to configure:
NEXT_PUBLIC_RPC_URL=https://api.devnet.solana.com
NEXT_PUBLIC_PORTAL_URL=https://portal.lazor.sh
NEXT_PUBLIC_PAYMASTER_URL=https://lazorkit-paymaster.onrender.comUpdate the config in LazorPayProvider.tsx:
const CONFIG = {
rpcUrl: "https://api.mainnet-beta.solana.com",
// ... other mainnet configs from LazorKit
};
β οΈ Note: LazorKit is currently in beta and supports Devnet only. Do not use in production!
- Framework: Next.js 15 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS
- Wallet: @lazorkit/wallet
- Blockchain: Solana (Devnet)
- Icons: Lucide React
- QR Codes: qrcode.react
{
"@lazorkit/wallet": "^1.4.x",
"@coral-xyz/anchor": "^0.30.x",
"@solana/web3.js": "^1.95.x",
"buffer": "^6.0.x",
"qrcode.react": "^4.x",
"lucide-react": "^0.x"
}# Install Vercel CLI
npm i -g vercel
# Deploy
vercelThis is a standard Next.js app and can be deployed to:
- Netlify
- AWS Amplify
- Railway
- Any Node.js hosting
MIT License - feel free to use this code in your own projects!
Built with LazorKit - The open-source passkey wallet infrastructure for Solana.