Next.js launchpad for minting SPL Token-2022 tokens with on-chain metadata via Pinata IPFS
Problem
Creating Solana SPL tokens with metadata normally requires CLI tooling and manual handling of metadata pointers, rent-exempt account sizing, and IPFS uploads.
What it does
A single-page Next.js app where a user connects a Solana wallet, picks devnet/mainnet, fills a validated form (name, symbol, supply, decimals, optional image), and the app uploads the image plus metadata JSON to Pinata, then builds and sends a single Token-2022 transaction that creates the mint, initializes metadata pointer and metadata, creates a token account, and mints the initial supply. Created tokens are listed in a grid fetched from the owner's parsed token accounts with explorer links and copyable mint addresses (components/tokenGrid.tsx).
Architecture
[ui] Next.js App Router client (wallet adapter, token form, grid) (client) [getUri] /api/getUri route handler (api) [pinata] Pinata SDK (image + metadata JSON upload) (external) [alchemy] Alchemy Solana RPC (devnet/mainnet) (external) [wallet] Solana wallet (signs transaction client-side) (external) [spl] Solana Token-2022 program (mint + metadata on-chain) (external) ui ──▶ getUri · FormData with image + metadata JSON getUri ──▶ pinata · upload file, then JSON; returns gateway URI ui ──▶ wallet · partialSign mint/token account keypairs, sendTransaction ui ──▶ spl · createAccount, metadata pointer, initialize mint/metadata, mintTo ui ──▶ alchemy · Connection RPC calls (rent, blockhash, parsed token accounts) tokenGrid ──▶ pinata · fetch tokenMetadata.uri for image
Simplified — box-and-arrow sketch of the repo structure, not a system diagram.
Decisions
01
Token-2022 with on-chain metadata embedded in the mint
Uses MetadataPointer extension and createInitializeInstruction so metadata lives at the mint itself, then packs the metadata to compute exact rent-exempt lamports (components/tokenDialog.tsx:84-128).
Evidence: components/tokenDialog.tsx
02
Server-side Pinata uploads behind a single API route
PINATA_JWT is read only in utils/config.ts marked "server only", keeping the JWT out of the browser bundle while the client posts multipart FormData (app/api/getUri/route.ts, utils/config.ts).
Evidence: utils/config.ts
03
Atomic single transaction for the whole mint flow
Mint account creation, metadata pointer, mint init, metadata init, token account creation, and mintTo are batched into one Transaction with client-side partialSign of the generated keypairs (components/tokenDialog.tsx:158-173).
Evidence: components/tokenDialog.tsx
04
Zod-validated form with coerced numeric inputs
TokenCreationSchema bounds tokenName (max 32), symbol (max 10), decimals 0-9 and initialSupply up to 10M, resolving client-side via react-hook-form (lib/zod.ts).
Evidence: lib/zod.ts
05
Alchemy endpoints via NEXT_PUBLIC env keys
Devnet/mainnet RPC URLs are built inline from NEXT_PUBLIC_ALCHEMY_API_KEY with a runtime network switch rather than a fixed cluster (app/page.tsx:27-33).
Evidence: app/page.tsx
The hard parts
- Correct Token-2022 space/rent calculation combining metadata extension, pointer extension and packed metadata length (components/tokenDialog.tsx:84-101)
- Coordinating wallet signing of two ephemeral keypairs (mint, token account) with the user's wallet in one transaction (components/tokenDialog.tsx:171-173)
- Enumerating owned Token-2022 accounts and fetching off-chain metadata URIs per token to render the grid (components/tokenGrid.tsx:33-55)
Known limitations
- No tests, no CI, no Dockerfile, no license file in the repo
- No .env.example; required env vars (NEXT_PUBLIC_ALCHEMY_API_KEY, PINATA_JWT, NEXT_PUBLIC_GATEWAY_URL) are only discoverable from code (app/page.tsx, utils/config.ts)
- Gross error handling: every mint failure collapses to one generic toast, and upload failures just toast 'unable to fetch uri' (components/tokenDialog.tsx:70-72,178-180)
- Token grid has no loading/empty states and races setState per account in an unawaited map; description column is never populated from the fetched URI (components/tokenGrid.tsx:35-55)
- Initial supply entered as a raw number is minted without decimal scaling, so UI supply can mismatch expectations for decimals>0 (components/tokenDialog.tsx:149-156)
- No deployment evidence: no homepage/vercel URL in README or manifest; package name is still 'my-app' (package.json)