This file is for AI assistants and human contributors. It documents the project layout, contract interface, and key conventions so you can orient quickly without reading every file.
Genjury is a platform of on-chain mini-games built on GenLayer. Every game outcome is settled by an Intelligent Contract — a Python smart contract that can call an LLM and have multiple validator nodes independently verify the result before it finalises.
Mistrial is the first playable game. Two truths and a lie. The AI Judge decides which statement is false.
genjury/
├── contracts/
│ └── mistrial.py # The Intelligent Contract — all game logic lives here
├── frontend/ # React 18 + Vite app (workspace: @workspace/genjury)
│ ├── src/
│ │ ├── pages/ # One file per game phase (Lobby, Writing, Voting, …)
│ │ ├── components/ # Shared UI components
│ │ └── lib/
│ │ ├── genlayer.js # GenLayer SDK wrapper, wallet connect, contract reads/writes
│ │ ├── store.js # Zustand game store — polls contract, shapes state for UI
│ │ ├── profile.js # Local profile cache (localStorage)
│ │ ├── joinedRooms.js # Joined-rooms registry (localStorage)
│ │ └── ens.js # ENS name resolution cache
│ ├── .env.example # All required env vars documented here
│ └── vite.config.ts
├── api/ # Vercel serverless functions (Express-style)
│ ├── chat.js # Socket.io chat proxy
│ ├── username.js # Username claim/check
│ └── profile/ # Profile CRUD + ENS + avatar endpoints
├── lib/
│ └── db/ # Drizzle ORM schema + Neon Postgres client
├── vercel.json # Vercel build config (outputDirectory, rewrites)
└── pnpm-workspace.yaml # Monorepo workspace — includes frontend, lib/*, artifacts/*
The contract is a singleton — deployed once by the platform owner, reused by every player. All rooms live inside one contract instance.
class Genjury(gl.Contract):
rooms: dict[str, Room] # roomCode → Room
player_xp: dict[str, int] # address → cumulative XP
prize_pool: dict[str, int] # roomCode → wei balance| Method | Who calls it | What it does |
|---|---|---|
create_room(rounds, entry_fee) |
Host | Creates a new room, returns 6-char room code |
join_room(room_code) |
Player | Joins an open room, locks in entry fee |
submit_statement(room_code, s1, s2, s3, lie_index) |
Current writer | Submits three statements + declares which is the lie |
submit_vote(room_code, vote_index, confidence) |
Voters | Votes on which statement is the lie |
run_ai_judge(room_code) |
Host (after voting) | Calls the LLM via gl.eq_principle.strict_eq, returns AI verdict |
call_objection(room_code) |
Any player | Opens an objection vote window |
submit_objection_vote(room_code, sustain) |
Players | Votes to sustain or overrule the AI verdict |
finalize_round(room_code) |
Host | Distributes XP, advances to next round or ends game |
get_room_state(room_code) |
Frontend poll | Returns full room state for UI rendering |
get_leaderboard() |
Frontend | Returns top players by cumulative XP |
create_room → join_room (×N players) → [for each round:]
submit_statement → submit_vote (×N) → run_ai_judge
→ [optional: call_objection → submit_objection_vote (×N)]
→ finalize_round → [next round or end]
The contract calls gl.eq_principle.strict_eq with a structured prompt that includes all three statements. GenLayer's validator network runs the LLM call independently on each node and reaches consensus before the result is committed on-chain. There is no off-chain oracle.
The deployed contract address is set via VITE_MISTRIAL_CONTRACT. The frontend never asks users to deploy anything — it reads and writes to this single address.
genlayer.js exports two main helpers:
readContract(method, args)— read-only call, no wallet neededcallContract(method, args, value)— write call, requires connected wallet
store.js is a Zustand store that:
- Polls
get_room_state(roomCode)every ~1.5 seconds - Normalises the contract response into UI-friendly state
- Exposes actions that call contract write methods
Multi-wallet support via EIP-6963 (Rabby, MetaMask, Coinbase Wallet, etc). Wallet state is managed in genlayer.js. The chosen provider is stored in memory; the address is persisted in localStorage as genjury_injected_address.
All variables go in frontend/.env (copy from frontend/.env.example).
| Variable | Required | Description |
|---|---|---|
VITE_MISTRIAL_CONTRACT |
Yes | Deployed Mistrial contract address (0x…) |
VITE_GENLAYER_NETWORK |
Yes | Target network: studionet / asimov / bradbury |
DATABASE_URL |
Yes | Neon Postgres connection string (profiles + ENS cache) |
SESSION_SECRET |
Yes | Random string for session signing |
VITE_GENLAYER_RPC |
No | Override RPC URL (defaults to network's public RPC) |
# Install GenLayer CLI
pip install genlayer
# Deploy to Bradbury testnet
genlayer deploy contracts/mistrial.py --network bradbury --args 3 0
# args: default_rounds(3), entry_fee_wei(0)
# Paste the returned address into frontend/.env
VITE_MISTRIAL_CONTRACT=0x…git clone https://github.com/Jeephoenix/genjury.git
cd genjury
pnpm install
cp frontend/.env.example frontend/.env
# fill in VITE_MISTRIAL_CONTRACT and VITE_GENLAYER_NETWORK
pnpm --filter @workspace/genjury run dev
# → http://localhost:5173- Never add game logic to the frontend. All rules, XP math, and verdicts live in
contracts/mistrial.py. The frontend only displays state and collects input. [genjury]log prefix — used inconsole.warnthroughout the frontend. This is the platform name, not game-specific.- localStorage key prefixes —
genjury_*keys are platform-level (wallet, network, profile, rooms).mistrial_*keys are game-specific (contract address override). - Room codes — 6-char uppercase alphanumeric (e.g.
TRIAL9). Generated by the contract, validated byisValidRoomCode()ingenlayer.js. - pnpm only — the preinstall script blocks npm and yarn. Always use
pnpm.