Mint standard NFTs using Metaplex Token Metadata with LazorKit smart wallets
This recipe demonstrates how to integrate Metaplex's Token Metadata program with LazorKit to mint traditional NFTs. While LazorKit covers gas fees, users still need SOL in their wallet for rent (~0.02 SOL per mint) since regular NFTs create on-chain accounts.
Environment: Next.js 16 + React 19. See next.config.ts for required polyfills.
- Use Metaplex Umi library with LazorKit smart wallets
- Create dummy signers for Umi instruction building
- Convert Umi instructions to Web3.js format
- Handle LazorKit's smart wallet validation requirements
- Build Metadata and Master Edition accounts for NFTs
┌─────────────────────────────────────────────────────────────────────┐
│ REGULAR NFT MINTING FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ User UI │───▶│ Store Metadata │───▶│ Get Metadata │ │
│ │ (Next.js) │ │ via API │ │ URI │ │
│ └─────────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Build NFT Instructions │ │
│ │ 1. Create Mint Account (via createAccountWithSeed) │ │
│ │ 2. Initialize Mint (0 decimals) │ │
│ │ 3. Create Associated Token Account │ │
│ │ 4. Mint 1 Token │ │
│ │ 5. Create Metadata Account (Metaplex) │ │
│ │ 6. Create Master Edition (Metaplex) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ LazorKit Paymaster │ │
│ │ - Signs with passkey (no seed phrase) │ │
│ │ - Pays gas fees (but NOT account rent) │ │
│ │ - Submits to Solana network │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
- Completed Recipe 01 and Recipe 02
- Understanding of NFT standards and Metaplex
- ~0.02 SOL in wallet for account rent
npm install @metaplex-foundation/mpl-token-metadata \
@metaplex-foundation/umi-bundle-defaults \
@metaplex-foundation/umi-web3js-adaptersUmi requires a signer, but LazorKit handles actual signing via passkey:
import { publicKey as umiPublicKey, signerIdentity, Signer } from '@metaplex-foundation/umi';
function createDummySigner(walletAddress: string): Signer {
return {
publicKey: umiPublicKey(walletAddress),
signMessage: async () => new Uint8Array(64),
signTransaction: async (tx) => tx,
signAllTransactions: async (txs) => txs,
};
}Use Umi to build metadata and master edition instructions:
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import {
createMetadataAccountV3,
createMasterEditionV3,
mplTokenMetadata,
findMetadataPda,
findMasterEditionPda,
} from '@metaplex-foundation/mpl-token-metadata';
import { toWeb3JsInstruction } from '@metaplex-foundation/umi-web3js-adapters';
async function buildMetaplexInstructions(
walletAddress: string,
mintAddress: string,
nftName: string,
metadataUri: string
): Promise<TransactionInstruction[]> {
const umi = createUmi(RPC_URL).use(mplTokenMetadata());
const dummySigner = createDummySigner(walletAddress);
umi.use(signerIdentity(dummySigner));
const mintPublicKey = umiPublicKey(mintAddress);
const metadata = findMetadataPda(umi, { mint: mintPublicKey });
const masterEdition = findMasterEditionPda(umi, { mint: mintPublicKey });
// Build CreateMetadataAccountV3
const metadataBuilder = createMetadataAccountV3(umi, {
metadata,
mint: mintPublicKey,
mintAuthority: dummySigner,
payer: dummySigner,
updateAuthority: umiPublicKey(walletAddress),
data: {
name: nftName,
symbol: 'LKCB',
uri: metadataUri,
sellerFeeBasisPoints: 0,
creators: [{ address: umiPublicKey(walletAddress), verified: false, share: 100 }],
collection: null,
uses: null,
},
isMutable: true,
collectionDetails: null,
});
// Build CreateMasterEditionV3
const masterEditionBuilder = createMasterEditionV3(umi, {
edition: masterEdition,
mint: mintPublicKey,
updateAuthority: dummySigner,
mintAuthority: dummySigner,
payer: dummySigner,
metadata,
maxSupply: 0, // True 1/1 NFT
});
// Convert Umi instructions to Web3.js format
const instructions: TransactionInstruction[] = [];
for (const ix of metadataBuilder.getInstructions()) {
instructions.push(toWeb3JsInstruction(ix));
}
for (const ix of masterEditionBuilder.getInstructions()) {
instructions.push(toWeb3JsInstruction(ix));
}
return instructions;
}LazorKit requires the smart wallet in all instruction key lists:
function addSmartWalletToInstructions(
instructions: TransactionInstruction[],
smartWalletAddress: string
): void {
const walletPubkey = new PublicKey(smartWalletAddress);
instructions.forEach((ix) => {
const hasSmartWallet = ix.keys.some(
k => k.pubkey.toBase58() === smartWalletAddress
);
if (!hasSmartWallet) {
ix.keys.push({
pubkey: walletPubkey,
isSigner: false,
isWritable: false
});
}
});
}const handleMint = async () => {
if (!wallet) return;
// 1. Store metadata via API
const mintId = generateMintId('nft');
const metadataUri = await storeNftMetadata(mintId, {
name: nftName.trim(),
description: nftDescription.trim(),
});
// 2. Generate mint address using createAccountWithSeed
const seed = `nft-${Date.now()}`;
const mintAddress = await PublicKey.createWithSeed(
new PublicKey(wallet.smartWallet),
seed,
TOKEN_PROGRAM_ID
);
// 3. Build all instructions (mint account, token account, metaplex)
const instructions = await buildAllMintInstructions(
wallet.smartWallet,
mintAddress.toBase58(),
seed,
nftName,
metadataUri
);
// 4. Add smart wallet to all instructions
addSmartWalletToInstructions(instructions, wallet.smartWallet);
// 5. Sign and send via LazorKit
const signature = await signAndSendTransaction({
instructions,
transactionOptions: { computeUnitLimit: 400_000 },
});
};Problem: LazorKit smart wallets are PDAs, which can't be used as the from account in createAccount.
Solution: Use createAccountWithSeed instead, which works with any wallet.
const mintAddress = await PublicKey.createWithSeed(walletPubkey, seed, TOKEN_PROGRAM_ID);
const createMintIx = SystemProgram.createAccountWithSeed({
fromPubkey: walletPubkey,
newAccountPubkey: mintAddress,
basePubkey: walletPubkey,
seed,
lamports: rentExemptBalance,
space: 82, // Mint account size
programId: TOKEN_PROGRAM_ID,
});Problem: Metaplex Umi uses its own instruction format.
Solution: Use @metaplex-foundation/umi-web3js-adapters to convert.
import { toWeb3JsInstruction } from '@metaplex-foundation/umi-web3js-adapters';
const web3Ix = toWeb3JsInstruction(umiIx);Problem: LazorKit paymaster covers gas fees but NOT account rent (~0.02 SOL). Solution: User needs SOL in wallet, or use Recipe 07 for truly gasless cNFT minting.
| Aspect | Regular NFT (Recipe 06) | Compressed NFT (Recipe 07) |
|---|---|---|
| Cost | ~0.02 SOL rent | Gas sponsored by paymaster |
| Accounts | 4 accounts created | 0 accounts created |
| Instructions | 6 instructions | 1 instruction |
| Viewing | Standard explorers | DAS API / Orb Explorer |
| Use Case | High-value collectibles | Mass distribution |
| Issue | Solution |
|---|---|
| "Insufficient funds for rent" | Need ~0.02 SOL in wallet for account creation |
| "Account already in use" | Seed collision - use timestamp + random string |
| "Invalid signer" | Ensure dummy signer publicKey matches wallet |
| "Metadata validation failed" | Name must be ≤32 chars, description ≤200 chars |
Try this recipe live at: https://lazorkit-cookbook.vercel.app/examples/06-nft-minting
For step-by-step tutorials and patterns explanation, see the Cookbook Documentation:
- Metaplex NFT Tutorial - Integration walkthrough
- Cookbook Patterns - Understanding
createDummySignerand other patterns - LazorKit Basics - What the SDK provides natively
- Metaplex Token Metadata Documentation
- Metaplex Umi Documentation
- LazorKit SDK Documentation
- Solana Devnet Faucet
- Explore Example 07: Gasless cNFT Minting for truly gasless NFT minting
- Check out other Solana Protocol Integrations
- Build your own NFT marketplace with LazorKit!