A cross-collection NFT battle game built on Base. Any NFT, any collection — pick your fighter and enter the arena.
🏆 Built for Base Batches 003 Student Track
The flagship feature — a real-time turn-based battle system where NFTs from different collections fight each other.
- Universal stat normalization — every NFT (Base Invaders, BaseHeads 404, BaseMoods, VoidPFPs, etc.) is converted into a universal stat format (HP, ATK, DEF, SPD, CRIT, Dodge, Lifesteal, Regen)
- Passive abilities — Ghost Step, Iron Wall, Drain, Berserker, Regen Burst — each with cooldowns and trigger conditions
- Animated combat — particle effects, floating damage numbers, crit bursts, dodge ghosts, screen shake, slash trails, cinematic round splashes
- AI opponents — challenge AI-controlled fighters with configurable win rates
- Anti-cheat snapshots — stat snapshots with SHA-256 hashing to prevent drift abuse
- Challenge board — post challenges, accept fights, collection-themed card UI
- NFT Minting — mint from curated Base collections with auto-discovery
- Bandwidth-smart OpenSea gallery — lazy display previews while browsing, original-resolution images only after opening an item, and opt-in animation playback
- Rich NFT details — OpenSea rarity rank, estimated value, ownership, traits, trust flags, and marketplace links
- Reliable mint analytics — browser-persistent outbox, local/OpenSea historical reconciliation, receipt-derived token IDs and rich live-feed cards; no dedicated or always-on server required
- Points & Gamification — streaks, status badges, global leaderboards
- Growth & Distribution — automated social sharing, replay-to-play conversion, featured battle highlights
- Analytics — retention cohorts, conversion funnels, wallet insights
- Social Sharing — Farcaster cast composing, share cards, viral loops
- Farcaster Mini App — native integration with Farcaster frames
| Layer | Technology |
|---|---|
| Frontend | Vite, Vanilla JS, Tailwind CSS |
| Web3 | Reown AppKit, Wagmi, Viem, SIWE |
| Blockchain | Base (Mainnet) |
| Backend | Vercel Serverless Functions |
| Database | Vercel KV (Redis) |
| Hashing | Web Crypto API (SHA-256) |
- Node.js v18+
- WalletConnect Project ID (Reown)
- Vercel KV (Redis) for backend features
# Install dependencies
npm install
# Run frontend only
npm run dev
# Run full app (frontend + serverless functions)
npm run dev:fullCreate .env in the project root.
Anything prefixed
VITE_is compiled into the client bundle and is public. Never put a secret behind aVITE_name — the CI build fails if a server-only key shows up indist/.
# ── Client (public — inlined into the bundle) ──
VITE_WALLETCONNECT_PROJECT_ID=your_reown_project_id
VITE_DEFAULT_CHAIN=base
VITE_BASE_RPC_URL= # must be safe to expose (domain-restricted)
VITE_ADMIN_WALLETS=0x123...,0x456... # only reveals the admin UI; the API enforces auth
# ── Server (secret) ──
JWT_SECRET=your_jwt_secret # required: auth is disabled without it
OPENSEA_API_KEY= # used by /api/nfts, never reaches the browser
RPC_URL= # server-side reads (NFT ownership verification)
ADMIN_WALLETS=0x123...,0x456... # the list the API actually trusts
GEMINI_API_KEY= # optional: AI share-post generation
# ── Storage (Upstash Redis / Vercel KV) ──
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# Legacy Vercel KV names are also accepted:
KV_REST_API_URL=
KV_REST_API_TOKEN=
# ── Optional switches ──
# ALLOWED_ORIGINS=https://preview.example.com # extra CORS origins
# STRICT_BATTLE_OWNERSHIP=true # reject unverifiable fighters
# ALLOW_INSECURE_ADMIN=true # local-only admin auth bypassThere is no dedicated or always-on server process. Shared analytics use the existing Vercel
serverless /api functions and Upstash REST storage. The browser outbox works without
OpenSea; OPENSEA_API_KEY additionally enables one-year account mint discovery.
npm test # battle integrity, analytics, KV, proxy allowlist, CSP guards
npm run build # production buildThe CI workflow lives in ci/github-actions-ci.yml
(copy it to .github/workflows/ci.yml to enable — see ci/README.md).
It runs both commands on every push and pull request, and additionally fails if
an inline script/handler or a server secret reaches the build output.
npm run analytics:migrate # dry run: reconcile battle counters in KV
npm run analytics:migrate:apply # apply it
npm run kv:cleanup # dry run: remove stale/legacy KV keyssrc/
├── components/game/ # Battle UI components
│ ├── ChallengeBoard.js # Challenge listing with collection-themed cards
│ ├── MatchPreviewModal.js # VS split-screen with stat comparison
│ └── NFTSelectorModal.js # Fighter picker with stat previews
├── lib/battle/ # Battle engine core
│ ├── balanceConfig.js # Centralized stat caps, passives, tuning
│ ├── collectionProfiles.js # Collection definitions & trait mappings
│ ├── metadataNormalizer.js # Universal stat normalization
│ └── snapshot.js # Browser-safe SHA-256 stat snapshots
├── lib/game/
│ ├── engine.js # Turn-based combat engine with passive resolution
│ ├── arenaRenderer.js # Animated battle renderer (particles, effects)
│ └── matchmaking.js # Challenge KV store (V2 schema)
├── pages/ # UI pages (home, mint, analytics, battle)
└── utils/ # Shared utilities (DOM, social, router)
| Collection | Role | Passive | Archetype |
|---|---|---|---|
| Base Invaders | Fighter | Ghost Step | Speed / Dodge |
| BaseHeads 404 | Fighter | Berserker | Aggro DPS |
| BaseMoods | Fighter | Regen Burst | Balanced / Healer |
| Void PFPs | Fighter | Ghost Step | Glass Cannon |
| Quantum Quills | Fighter | Drain | Sustain DPS |
| Base Fortunes | Fighter | Iron Wall | Tank |
| Neon Runes | Item Buff | — | V2 Modifier |
| Neon Shapes | Item Buff | — | V2 Modifier |
| ByteBeats | Item Buff | — | V2 Modifier |
| Mini Worlds | Environment | — | V2 Modifier |
Collections are auto-discovered from collections/*.js:
# Sync collection index
npm run collections:syncAuto-sync runs on npm run dev, npm run build, and npm run dev:full.
The Vercel Hobby plan allows 12 Serverless Functions per deployment, so
related endpoints are grouped behind one function using an ?action= router
(helpers live in api/_lib/, which Vercel does not count):
| Function | Actions |
|---|---|
/api/admin |
overview, user, collection, cohort, daily, retention, reconcile, csp, export, cleanup_profile |
/api/auth |
nonce, verify, logout |
/api/battle |
challenge, fight, history, replay, record |
/api/share |
share page (GET), generate-post (POST) |
/api/track, /api/leaderboard, /api/user, /api/nfts, /api/csp-report |
single purpose |
api/**/*.test.js is excluded via .vercelignore — without that, each test file
would be deployed as its own function. A test enforces the budget so the
"No more than 12 Serverless Functions" deploy error cannot come back.
| Concern | How it is handled |
|---|---|
| Arena results | Wins/points only count for a battle the server produced and stored: PvP is simulated server-side, AI battles are re-simulated from the seed and rejected on mismatch, and each battle counts once per wallet |
| Fighter stats | Every client-supplied stat is clamped to the balance envelope before simulation |
| NFT ownership | Verified on-chain (ownerOf/balanceOf) when posting a challenge, defending a fight and recording an AI battle, with an OpenSea inventory fallback |
| Mint analytics | A mint is counted only when the configured collection emitted an ERC-721/1155 mint from the zero address to the wallet; transaction hashes are idempotent, and failed browser writes remain in a local outbox for retry |
| API keys | Held server-side; the browser talks to /api/nfts instead of OpenSea directly |
| NFT media bandwidth | Cards use OpenSea display_image_url; original_image_url is fetched only after the user opens an NFT, while animation media requires an explicit play action |
| Auth | SIWE/SIWF with single-use nonces and short-lived JWTs; admin routes need an allowlisted wallet |
| XSS | Third-party NFT metadata is escaped, URLs sanitised, and a CSP without script-src 'unsafe-inline' is enforced |
Full findings and fixes: FULL_APP_AUDIT.md and
ANALYTICS_AUDIT.md.
- Live App: base-mintapp.vercel.app
- Devfolio: NFT Battle Arena
- Hackathon: Base Batches 003 Student Track
NFT Metadata → Normalizer → Universal Stats → Combat Engine → Animated Renderer
-
Normalization — Raw NFT traits (Faction, Mood, Body, etc.) are parsed through collection-specific
traitsMaprules defined incollectionProfiles.js, producing a universal stat block (HP, ATK, DEF, SPD, CRIT, Dodge, Lifesteal, Regen). -
Stat Clamping — All stats are bounded by centralized caps and floors from
balanceConfig.jsto prevent broken builds. E.g., HP max 300, CRIT max 75%, ATK min 3. -
Passive Resolution — Each fighter gets a passive ability based on their collection (with trait-based overrides). Passives fire automatically during combat with cooldown tracking.
-
Turn-Based Combat — Higher SPD goes first. Each turn: regen → passive triggers → attack roll (crit/dodge checks) → lifesteal → damage application. Max 50 rounds.
-
AI Rigging — AI battles use a simulation loop (up to 25 seeds) to find a random timeline that matches the configured win rate (default 60%), making fights feel fair while keeping AI competitive.
-
Snapshot Anti-Cheat — Fighter stats are hashed (SHA-256) when a challenge is posted. Before a fight starts, the hash is re-verified to ensure no stat drift for mutable collections.
| Passive | Trigger | Effect | Cooldown |
|---|---|---|---|
| Ghost Step | On Defend | +25% dodge for 1 turn | 2 turns |
| Iron Wall | On Defend | -30% incoming damage for 1 turn | 3 turns |
| Drain | On Attack | Leech 20% of damage dealt as HP | 2 turns |
| Berserker | Below 30% HP | +40% ATK, -10% DEF | Always active |
| Regen Burst | Turn Start | Heal 8% of max HP | 3 turns |
- Cross-collection stat normalization (12+ collections)
- Turn-based auto-combat engine
- 5 passive abilities with cooldown system
- Cinematic battle renderer (particles, damage numbers, screen shake)
- Challenge board with AI opponents
- VS screen with stat comparison
- NFT fighter selector with stat previews
- Multi-NFT loadouts (Fighter + Item + Arena modifiers)
- Wallet inventory parsing for Team Synergies
- SIWE (Sign-In with Ethereum) JWT authentication
- PvP match resolution via server-side APIs (Vercel KV)
- Deterministic seeded PRNG for shareable battle replays
- Snapshot anti-cheat system (Server verified)
- V2 Analytics tracking (
battle_loadout_built,battle_started_v2,battle_result_v2) - Auth cleanup on wallet disconnect (
clearBattleAuth) - Live balance configuration fetching via CDN (with bundled fallback)
- Spectator Mode — shareable battle replay URLs
- Strict JWT event authentication & Rate-limiting (Hardened Pipeline)
- Split Analytics Dashboard (Arena vs NFT views)
- Dedicated Battle Points Leaderboard
- Real-time PvP matchmaking with WebSocket
- Battle replays — shareable animated GIFs
- On-chain battle result logging (Base smart contract)
- Add more Base NFT collections to the battle roster
- Collection search by name and contract address
- Multi-NFT team battles (3v3 with synergy bonuses)
- More game modes (tournament brackets, seasons, wagered battles)
- Marketplace tab — browse, buy, sell, make offers via OpenSea API
- Token-gated features and rewards
- Cross-chain collection support (Ethereum → Base bridge)
- Mobile-optimized battle experience
Base Mint evolves into a full NFT gaming platform on Base — where any NFT from any collection has utility through games, trading, and social features. The battle arena is the first module; marketplace and additional game modes follow.
- Fork the repo
- Create a feature branch (
git checkout -b feature/awesome) - Commit changes (
git commit -m 'Add awesome feature') - Push and open a PR
MIT
