Skip to content

Repository files navigation

PullChain

Pull blocks. Master crypto. Own your knowledge.

PullChain is a gamified Web3 education app on Base Mainnet. Players pull blocks from a Jenga-style tower, learn real crypto topics, pass quizzes, and mint soulbound ERC-721 certificates as non-transferable on-chain proof of knowledge.

This repository is production software. The certificate contract is not formally audited unless separate audit evidence is published.


Game thresholds (source of truth)

Defined in lib/game-config.ts:

Mode Tower size Clear / certificate threshold
Easy (learning) 36 16
Medium (learning) 36 24
Hard (learning) 36 36
Elite (slang) 18 18 (leaderboard only, no cert)

Shared rules

Rule Value
Quiz questions per block 4
Quiz pass (progress / streak) 3/4 correct
Life loss Only on 0/4 correct
Starting lives 3
Starting stability 100
Stability damage by correct count 22, 14, 8, 3, 0
Last-in-row dangerous bonus +5 stability damage
Elite access Minted Easy certificate (DB record and/or on-chain hasCertificate)

Elite attempts use mode: elite and never write learning Progress rows.


Auth & security model

  • Privy verifies access tokens server-side (requireAuthenticatedUser / verifiedPrivyIdentity).
  • /api/auth/sync derives identity only from the verified Privy token — browser identity fields are not trusted.
  • Session, progress, certificate, and leaderboard mutations require ownership of the authenticated user.
  • Quiz scoring is server-authoritative: selected option text is re-checked against the word bank. Client correct flags and scores are not trusted.
  • Session lives, stability, score, completion, and failure are computed on the server in applyAttemptToSession.
  • There is no free-form PATCH for game stats — state advances via POST /api/session/[id]/attempt.
  • Progress cannot be written directly (POST /api/progress → 405).
  • Certificate mint recording verifies Base receipt, sender, contract address, CertificateMinted event, difficulty, and token ID.
  • Duplicate certs per user/difficulty and duplicate tx_hash are rejected.
  • API errors never return raw stack traces, emails, wallets, or secrets.

Known limitations

  • Quiz option presentation is still client-generated from a deterministic seed; integrity relies on matching selected text to the word bank, not on hiding the answer key from the browser.
  • Elite gate UI primarily checks the certificates API; server session create also enforces Easy cert (DB and on-chain when a wallet is present).
  • Contract is open mint for any wallet calling mint — game eligibility is enforced in the app DB layer when recording mints, not by an on-chain allowlist.
  • No formal smart-contract audit claimed.

Deterministic sessions & resume

On POST /api/session:

  1. A cryptographic seed and ordered topic_ids are stored on the session.
  2. Topic order uses seeded Fisher–Yates (lib/seeded-random.ts).
  3. If an active session exists for the same user + difficulty + mode and restart is not set, it is returned (resumed: true) — React Strict Mode cannot create duplicates.
  4. restart: true fails the prior active session and creates a new seed/topic order (replay).

Resume restores:

  • seed, topic order, pulled words_seen, score, lives, stability, status, quiz attempts

API routes

Method Path Notes
POST /api/auth/sync Create/update user from verified Privy identity
PATCH /api/profile/username Zod username validation; conflict → 409
POST /api/session Create or resume session (difficulty, mode, restart?)
GET /api/session/active Active session for user (optional difficulty/mode filters)
GET /api/session/[id] Owned session + attempts
POST /api/session/[id]/attempt Authoritative quiz attempt + state transition
GET /api/progress Owned progress (read-only)
POST /api/progress Disabled (405)
GET /api/certificate/eligibility?difficulty= Authoritative eligibility
GET /api/certificate/mint List owned certificates
POST /api/certificate/mint Record verified on-chain mint
GET/POST /api/leaderboard Read public ranks; write from owned completed session only
GET /api/nft/[tokenId] Token metadata
GET /api/nft/image/[tokenId] Dynamic cert image
POST /api/quiz/submit Deprecated (410) — use session attempt

Schema summary (Prisma)

  • Userprivy_id, optional wallet/email, unique username
  • Session — difficulty, mode (learning|elite), seed, topic_ids[], lives, stability, score, streaks, words_seen[], status
  • QuizAttempt — owned via session; structured answers JSON; server score/passed
  • Progress — learning only; unique (user_id, topic_id, difficulty)
  • Certificate — unique (user_id, difficulty); unique tx_hash when set
  • LeaderboardEntry — difficulty + mode; score/time from completed sessions

Migrations

# Review SQL under prisma/migrations/, then on the target DB:
pnpm exec prisma migrate deploy

Do not use prisma db push against production.

Additive migrations:

  1. 20260803000000_add_session_game_modesessions.mode
  2. 20260803120000_session_seed_topic_idssessions.seed, sessions.topic_ids, unique certificates.tx_hash

Smart contract boundary (contracts/src/PullChainCert.sol)

Soulbound ERC-721 on Base:

Behavior Implementation
One cert per wallet per difficulty hasCertificate[wallet][diff]; AlreadyMinted()
Invalid difficulty difficultyLevel > 2InvalidDifficulty()
Non-transferable All transfer/approve paths revert Soulbound()
Token lookup tokenIdOf, getCertificate, ownerOf
Metadata tokenURI = base URI + tokenId + .json; owner may setMetadataURI
Mint event CertificateMinted(holder, tokenId, difficulty, score, issuedAt)

Difficulty encoding: 0 = Easy, 1 = Medium, 2 = Hard.

Do not redeploy or upgrade this contract from app changes. App verification lives in lib/chain.ts.


Environment

Variable Required Purpose
DATABASE_URL yes Postgres (Prisma)
NEXT_PUBLIC_PRIVY_APP_ID yes Privy client
PRIVY_APP_SECRET yes Privy server verification
NEXT_PUBLIC_CONTRACT_ADDRESS yes PullChainCert address
NEXT_PUBLIC_BASE_RPC optional Base RPC (default public)
RESEND_API_KEY optional Welcome email (lazy init; missing key must not break build)
EMAIL_FROM optional Resend from-address

Never commit secrets. CI may inject build-time public/service placeholders only.


Local development

pnpm install
# configure .env
pnpm exec prisma migrate deploy
pnpm dev

Checks

pnpm lint
pnpm typecheck
pnpm test
pnpm build
  • Node 22 and pnpm 11 match GitHub Actions.
  • Tests: Vitest unit suite under lib/*.test.ts (validation, shuffle, resume state, quiz re-score, thresholds).

Pages

Path Role
/ Landing; unauthenticated users go to login before learning
/login Privy auth
/dashboard Stats / resume entry
/learn Difficulty selector
/learn/[difficulty] Learning Jenga (easy | medium | hard)
/learn/elite Elite slang mode (Easy cert gated)
/certificate, /certificate/mint View / mint certs
/leaderboard Rankings
/about, /privacy, /terms Marketing / legal

Word banks

File Count (approx) Use
lib/words/easy.ts 500 Easy learning
lib/words/medium.ts 500 Medium learning
lib/words/hard.ts 500 Hard learning
lib/words/slang.ts 150 Elite only

Tech stack

  • Next.js (App Router) + TypeScript + Framer Motion
  • Privy + wagmi/viem (Base)
  • Prisma 7 + PostgreSQL
  • Zod trust boundaries
  • Resend (optional email)
  • Vitest + ESLint + GitHub Actions CI
  • Vercel deployment

Builder

Mojeeb Titilayo — Full-stack Web3 and AI Product Engineer. Founder of BlindspotLab. Designs and ships production applications across smart contracts, embedded wallets, authentication, backend systems and user-facing products.

About

Gamified Web3 education on Base — learn crypto concepts, mint soulbound certificates, climb the leaderboard.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages