Documentation

FriendlyMinter docs

Build against the live Solana devnet. Connect a wallet, sign a request, and the deployer creates the on-chain Merkle tree for you.

Introduction

FriendlyMinter is a web app for minting compressed NFTs on Solana. It wraps the Bubblegum standard in a UI so you don't have to manage Merkle trees by hand.

The current deployment is a fully clickable frontend. All data flows through lib/mock-api.ts, which simulates network latency and a couple of failure modes. The real on-chain adapters sit behind the same interface, so swapping the data layer is a one-flag change.

Quick start

Run it locally in three commands:

bashbash
git clone https://github.com/404Piyush/FriendlyMinter.git
cd FriendlyMinter
npm install && npm run dev

Open http://localhost:3000. Connect Phantom (or any wallet) and switch it to Devnet. You can mint to your own wallet or just play with the UI in mock mode.

Environment variables

All vars are public (browser-readable). Copy .env.example to .env.local:

bashbash
# .env.local
NEXT_PUBLIC_SOLANA_NETWORK=devnet
NEXT_PUBLIC_SOLANA_RPC_URL=
NEXT_PUBLIC_USE_MOCK_API=true

Core concepts

Compressed NFTs (cNFTs)

A cNFT is a 32-byte hash stored as a leaf inside a Merkle tree on-chain. The tree root is published in a single account. The actual metadata and image live off-chain in a ledger maintained by Solana validators.

Cost per mint is roughly an order of magnitude lower than a regular NFT, because you're not paying for a new account per mint. You are paying for tree rent up front.

Bubblegum

Bubblegum is the Metaplex program that defines the cNFT standard. FriendlyMinter uses it for tree creation, minting, and transfers. Read the official spec for protocol-level details.

Merkle tree config

Each collection is backed by one concurrentMerkle tree. The tree's parameters set its capacity and write throughput.

PresetDepthCapacityUse case
Tiny3~8Test drops
Small10~1KSmall collections
Medium14~16KPFP projects
Large17~131KGenerative art
XL20~1MEvent tickets, POAPs

maxBufferSize sets how many concurrent writes the tree can absorb. Higher buffer = more parallel throughput but more rent. canopyDepth stores the upper levels of the tree in the transaction itself, which dramatically reduces the size of subsequent mint proofs.

CSV format

For bulk mints, drop a CSV with the schema below. The first column is required; everything else is optional. Column names match the standard Metaplex metadata spec.

csvcsv
name,description,image,attributes,external_url,animation_url
"#1 Welcome Mat","First in the series",https://example.com/1.png,"bg:purple;rarity:common",,
"#2 Welcome Mat","Second in the series",https://example.com/2.png,"bg:blue;rarity:rare",https://example.com,https://example.com/2.mp4
"#3 Welcome Mat","Third in the series",ipfs://QmXxx...,"bg:green;rarity:epic",,

Attributes: use trait:value pairs separated by ;. For example bg:purple;rarity:epic produces two trait rows in the on-chain metadata.

Image URLs: FriendlyMinter auto-detects ipfs://, ar://, and https:// schemes. Pinata, Arweave, and most CDNs work out of the box.

API reference

The public API lives in two modules. Both are accessible from any client component:

lib/solana.ts

Connection management, network selection, and explorer URL helpers.

tsts
import {
  getConnection,
  getCurrentNetwork,
  getExplorerUrl,
  testSolanaConnection,
} from '@/lib/solana';

const conn = getConnection();          // singleton Connection for the current network
const network = getCurrentNetwork();   // 'devnet' | 'testnet' | 'mainnet-beta'

const ok = await testSolanaConnection();
const url = getExplorerUrl(signature); // explorer.solana.com/tx/<sig>?cluster=devnet

lib/mock-api.ts

Mock data layer. Each method returns a promise that resolves after a configurable delay (NEXT_PUBLIC_MOCK_DELAY, default 1000ms).

tsts
import { mockAPI } from '@/lib/mock-api';

// Collections
const collections = await mockAPI.getCollections(userId);
const collection  = await mockAPI.getCollection(id);
const created     = await mockAPI.createCollection({ name, symbol, ... });

// Mint jobs
const job         = await mockAPI.startMintJob(jobId);
await mockAPI.pauseMintJob(jobId);
await mockAPI.resumeMintJob(jobId);

// File uploads
const { url, cid } = await mockAPI.uploadFile(file);

components/wallet

Drop-in wallet adapter wired to the configured network.

tsxtsx
import { WalletButton } from '@/components/wallet/WalletButton';
import { useWallet } from '@solana/wallet-adapter-react';

export function MyComponent() {
  const { connected, publicKey, connect, disconnect, signTransaction } = useWallet();
  return <WalletButton />;
}

Tech stack

Framework

Next.js 15 (App Router) + React 19

Language

TypeScript

Type checking

strict mode (tsconfig.json)

Styling

Tailwind CSS v4

Solana

@solana/wallet-adapter, @solana/web3.js

State

Zustand (client), TanStack Query (server)

Forms

React Hook Form + Zod

Toasts

Sonner

UI primitives

Custom shadcn-style components in src/components/ui

Ready to mint?

Connect a wallet and create your first collection. Test SOL on devnet.

Start a collection →