Smart Wallet
How LazorKit smart wallets work — PDA structure, vault vs wallet PDA, authentication, roles, lifecycle.
A LazorKit smart wallet is a cluster of on-chain accounts controlled by the LazorKit program. The owner authenticates via WebAuthn (passkey) or Ed25519 — no seed phrase, no browser extension, no private key storage in your app.
Two PDAs you'll see everywhere
smartWallet / vault PDA — where SOL and tokens live. This is the address users see,
share, and receive funds at.
walletPda — the wallet metadata account. Stores nothing you care about directly;
it's the identity anchor used internally by the program.
In SDK v2 the hook returns smartWalletPubkey = vault and walletPdaPubkey = metadata,
matching this mental model.
Account structure
| Account | Seeds | Size | What it holds |
|---|---|---|---|
| Wallet PDA | ["wallet", user_seed] | 8 bytes | Identity anchor; bump + version only. |
| Vault PDA | ["vault", wallet] | 0 bytes | Holds all SOL and tokens. Program signs via PDA seeds. |
| Authority PDA | ["authority", wallet, credential_id_hash] | 80 (Ed25519) / 145 (Secp256r1) | Per-key auth record + replay counter. |
| Session PDA | ["session", wallet, session_key] | 80+ bytes | Ephemeral signer + immutable action buffer. |
Wallet PDA
Minimal 8-byte account. Chosen by a 32-byte user_seed at creation. You don't need to
store it client-side — look it up via findWalletsByAuthority(credentialIdHash).
Vault PDA
Zero-data account that holds SOL and tokens. The program signs for it during Execute.
No rent deposit required — the vault is itself a PDA with no allocated data.
Send funds here — not to the wallet PDA
Users should receive SOL/tokens at the vault (smartWallet / smartWalletPubkey).
Sending to the wallet PDA won't fail, but the program won't recognize those funds during
Execute — it only spends from the vault.
Authority PDA
One PDA per authorised key. Header (48 bytes) + tail:
authority_type—Ed25519(0) orSecp256r1(1)role—Owner(0) /Admin(1) /Spender(2)counter— monotonic u32 for Secp256r1 replay protection- tail — 32-byte pubkey for Ed25519;
credential_id_hash(32) +compressed_pubkey(33) +rpId_hash(32) for Secp256r1 (fixed 97 bytes)
Secp256r1 accounts are now fixed-size
Post Phase 2.1, Secp256r1 authorities store sha256(rpId) instead of the raw rpId
string. That makes every Secp256r1 authority exactly 145 bytes, eliminating one
sol_sha256 syscall per Execute. Clients still send rpId as a string when creating
a wallet or adding an authority — the program hashes it once at write time.
Because each authority has its own PDA, different authorities on the same wallet
execute in parallel — the only writable account during Execute is the caller's own
authority PDA (counter increment). Wallet and vault are read-only.
Authentication
Secp256r1 (Passkey)
WebAuthn credentials bound to the device's Secure Enclave (Face ID, Touch ID, Windows Hello, security keys). The credential never leaves the device. On-chain verification uses the native Secp256r1 precompile and enforces:
- Odometer counter — monotonic u32 per authority. Client submits
stored_counter + 1. Committed only after successful verification. - Slot freshness — auth payload slot must be within 150 slots of
Clock::get(). - CPI protection — stack height check prevents passkey auth via CPI.
- Accounts hash — signed payload binds to all account pubkeys, preventing account-swap attacks.
Ed25519
Standard Solana keypair. Verified by the Solana runtime. No counter required — Solana's tx-level duplicate detection handles replay.
Roles
| Role | Value | Permissions |
|---|---|---|
| Owner | 0 | Full control — add/remove any authority, transfer ownership, create/revoke sessions, execute |
| Admin | 1 | Add Spender authorities, create/revoke sessions, execute |
| Spender | 2 | Execute only |
See RBAC for the full permission breakdown.
Wallet lifecycle
Create
const { instructions, walletPda, vaultPda, authorityPda } = await client.createWallet({
payer: payer.publicKey,
userSeed: crypto.randomBytes(32),
owner: {
type: 'secp256r1',
credentialIdHash, // 32-byte SHA256 of WebAuthn credential ID
compressedPubkey, // 33-byte compressed Secp256r1 public key
rpId: 'your-app.com',
},
});Creates Wallet PDA, Vault PDA, and the first Authority PDA in one transaction.
Look up a returning user
No need to persist walletPda client-side:
const [wallet] = await client.findWalletsByAuthority(credentialIdHash);
// { walletPda, authorityPda, vaultPda, role, authorityType }Add authority
Adds another key. Owner can add any role; Admin can only add Spender.
Remove authority
Closes the authority PDA and refunds rent to a specified destination. Prevents self-removal and removal of the last owner.
Transfer ownership
Atomically closes the old owner PDA and creates a new owner PDA. Refunds the old owner's rent to an explicit destination.
Costs
| Operation | Cost |
|---|---|
| Wallet creation (Ed25519) | 0.002399 SOL |
| Wallet creation (Secp256r1) | 0.002713 SOL |
| Execute (per tx) | 0.000005 SOL |
| Session setup (one-time) | 0.001453 SOL |
At $150/SOL, wallet creation is ~$0.36–$0.41. Session setup is ~$0.22, with subsequent session-signed executes at ~$0.00075 each.