A gacha-style digital vending machine for graded trading cards, built on Next.js 16, Privy wallet auth, and Solana, powered by the CollectorCrypt API.
- Node.js 18+ (tested on v25.2.1)
- npm or pnpm
- A Privy account with an app configured
- A CollectorCrypt API key (request via Discord)
git clone https://github.com/gossipgirlonchain/comicbook.git
cd comicbook
npm installCreate a .env.local file in the project root:
# Client-side (exposed to browser)
NEXT_PUBLIC_PRIVY_APP_ID=<your-privy-app-id>
NEXT_PUBLIC_PRIVY_CLIENT_ID=<your-privy-client-id>
NEXT_PUBLIC_SOLANA_NETWORK=devnet
NEXT_PUBLIC_USDC_MINT_ADDRESS=Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr
# Server-side ONLY (never exposed to browser)
COLLECTOR_CRYPT_API_KEY=<your-collectorcrypt-api-key>
COLLECTOR_CRYPT_BASE_URL=https://dev-gacha.collectorcrypt.com| Variable | Public? | Description |
|---|---|---|
NEXT_PUBLIC_PRIVY_APP_ID |
Yes | Privy dashboard > Settings > API Keys > App ID |
NEXT_PUBLIC_PRIVY_CLIENT_ID |
Yes | Privy dashboard > Settings > Clients > Client ID |
NEXT_PUBLIC_SOLANA_NETWORK |
Yes | devnet for testing, mainnet-beta for production |
NEXT_PUBLIC_USDC_MINT_ADDRESS |
Yes | USDC SPL token mint address for the target network |
COLLECTOR_CRYPT_API_KEY |
No | Server-side only. Request from CollectorCrypt team |
COLLECTOR_CRYPT_BASE_URL |
No | https://dev-gacha.collectorcrypt.com (dev) or production URL |
SOLANA_RPC |
No | Optional. Custom Solana RPC URL (defaults to public devnet) |
npm run devOpens at http://localhost:3000.
npm run build
npm startThe Privy provider is configured in app/providers.tsx:
- Login methods: Google, Email, Wallet
- Embedded wallets: Auto-created for Solana on first login
- External wallets: Phantom, Solflare, etc. via
toSolanaWalletConnectors() - Theme: Dark mode with ComicBook.com brand accent (#ffb91d)
Ensure your deployment domain is added to Privy Dashboard > Settings > Domains.
All CollectorCrypt API calls are proxied through a single Next.js API route to keep the API key server-side.
Browser Next.js Server CollectorCrypt
| | |
| GET /api/gacha/status | |
|--------------------------->| |
| | GET /api/status |
| | x-api-key: <secret> |
| |--------------------------->|
| | |
| | { machineStatus: "..." } |
| |<---------------------------|
| { machineStatus: "..." } | |
|<---------------------------| |
File: app/api/gacha/[...path]/route.ts
- Supports GET and POST
- Forwards the path segments to
COLLECTOR_CRYPT_BASE_URL/api/{path} - Injects
x-api-keyheader server-side - Endpoint allowlist: Only known CollectorCrypt endpoints are permitted (see
ALLOWED_ENDPOINTSin the route file). Unknown paths return 404. - Rate limiting: 60 requests/minute per IP. Returns 429 when exceeded.
| Method | Endpoint | Description |
|---|---|---|
| GET | status |
Machine status (running/stopped) |
| GET | stock |
Pack inventory levels |
| GET | getAllWinners |
Recent pack openings |
| GET | getNfts |
All cards in the house pool (optional ?owner= filter) |
| GET | purchasedPacks |
User's purchased packs (?wallet=) |
| GET | getGifted |
User's gifted packs (?wallet=) |
| GET | buyback/check |
Poll buyback completion (?memo=) |
| POST | generatePack |
Create unsigned pack purchase transaction |
| POST | submitTransaction |
Submit a signed transaction |
| POST | openPack |
Poll for pack opening result |
| POST | generateYoloPacks |
Batch generate multiple packs |
| POST | buyback |
Initiate card buyback |
| POST | generatePurchasedPack |
Create message to sign for purchased pack |
| POST | usePurchasedPack |
Consume a purchased pack |
| POST | generateGift |
Create gift transaction |
File: app/api/getUsdcBalance/route.ts
- POST with
{ address: string } - Queries Solana RPC for USDC token account balance
- Returns
{ balance: number }
app/
layout.tsx Root layout (fonts, Privy provider)
providers.tsx Privy configuration
page.tsx Home — gacha machine with card grid
globals.css Theme, animations, custom CSS
components/
Header.tsx Nav bar with auth-gated links
PrivyConnect.tsx Login button / wallet display / USDC balance
VendingMachine.tsx Pack selection, purchase flow, stats, winners
PackReveal.tsx Video intro + card flip reveal + buyback
NftGallery.tsx Reusable card grid (used in inventory)
WinnersFeed.tsx Horizontal scrolling winners ticker
marketplace/
page.tsx Browse all cards — filter, sort, search
[id]/page.tsx Card detail — image, attributes, buy button
inventory/page.tsx User's collection, purchased packs, gifted packs
profile/page.tsx Stats, rarity breakdown, leaderboard
api/
gacha/[...path]/route.ts CollectorCrypt API proxy
getUsdcBalance/route.ts Solana USDC balance lookup
lib/
api.ts API client (gachaApi object with all endpoints)
types.ts TypeScript types, pack config, rarity colors
solana.ts Transaction signing, message signing, image URL helper
public/
gacha-video.webm Pack opening intro video
cb-logo-*.png ComicBook.com brand logos
cb-bug-*.png ComicBook.com bug mascot
- User selects pack type (Standard $50 / Legendary $250)
- Client calls
generatePack(walletAddress, packType)→ receives unsigned base64 transaction - Transaction is deserialized and signed via Privy wallet
- Signed transaction submitted via
submitTransaction() - Client polls
openPack(memo)every 500ms (30s timeout) - Result returned with card metadata, rarity, points
- Pack reveal video plays → card flip animation → card displayed
Same as above but generateYoloPacks() returns an array of transactions. All are signed sequentially, submitted, then results polled and displayed in a summary grid.
- Available for 72 hours after opening (per CollectorCrypt API)
- Returns 85% of insured value
- Flow:
buyback()→ sign tx →submitTransaction()→pollBuybackCheck(memo) - Polling timeout is 90s; if it expires, the sale is treated as optimistically successful (the on-chain tx was already submitted) and the card is removed from the user's collection while the CC webhook catches up
- Receiving: Visible in
/inventoryGifted Packs tab - Opening:
generatePurchasedPack()→ sign message →usePurchasedPack()→openPack()
- API key isolation:
COLLECTOR_CRYPT_API_KEYis server-side only, injected by the proxy - Endpoint allowlist: Only known CollectorCrypt endpoints are forwarded
- Rate limiting: 60 req/min per IP on all proxy routes
- Security headers: X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
- No secrets in client bundle: All
NEXT_PUBLIC_vars are safe to expose - Wallet signing: All transaction signing happens client-side via Privy — no private keys on server
- Connect the GitHub repo to Vercel
- Set all environment variables in Vercel dashboard (Settings > Environment Variables)
- Deploy
- Set
NEXT_PUBLIC_SOLANA_NETWORK=mainnet-beta - Set
NEXT_PUBLIC_USDC_MINT_ADDRESSto mainnet USDC mint (EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v) - Set
COLLECTOR_CRYPT_BASE_URLto production URL - Rotate
COLLECTOR_CRYPT_API_KEYfor production - Set
NEXT_PUBLIC_PRIVY_APP_IDandNEXT_PUBLIC_PRIVY_CLIENT_IDfor production Privy app - Add production domain to Privy allowed domains
- Set
SOLANA_RPCto a paid RPC provider (Helius, QuickNode, etc.)
Push to main branch on gossipgirlonchain/comicbook — Vercel auto-deploys.
Get test USDC at spl-token-faucet.com using mint Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr.
Full API documentation: docs.collectorcrypt.com/gacha/api