🚧 Status: Active Development (Architecture & Infrastructure Proof of Concept)
A full-stack play-to-earn math game with a real on-chain settlement layer deployed on Base Sepolia testnet. The MATH token is a deliberately valueless ERC-20 used to demonstrate the full architecture: off-chain gameplay → Merkle distribution → on-chain claiming via a cumulative Merkle distributor.
Testnet only. No mainnet deployment exists or is planned. MATH has no monetary value and is never intended to acquire any.
The core game (auth + gameplay + in-app wallet) runs locally with two external dependencies: Supabase (for email/Google login + Postgres) and Node.js + Rust.
- Docker + Docker Compose
- Node.js >= 18 and npm
- Rust (stable) +
cargo - sqlx-cli:
cargo install sqlx-cli --no-default-features --features native-tls,postgres - Supabase CLI:
npm i -g supabase(or official installer) - (Optional) Foundry (
forge,cast) — only needed for the on-chain layer
git clone https://github.com/debbsgomes/mathcoin.git
cd mathcoin
bash scripts/setup.sh1. Start local Supabase (Auth + Postgres)
supabase startThis boots a full local Supabase stack in Docker. Run supabase status to see the
local URLs and keys. Copy the output values into your .env file.
2. Configure environment
cp .env.example .env # root .env is the single source of truth
cp api/.env.example api/.env # mirror for running the API standaloneThe root .env.example is the single source of truth. api/.env.example is a
subset mirror for when you run cd api && cargo run directly (the API binary loads
api/.env from its working directory).
Fill in .env with the values from supabase status:
DATABASE_URL← DB URL fromsupabase statusJWKS_URL←http://127.0.0.1:54321/auth/v1/.well-known/jwks.jsonJWT_ISS←http://127.0.0.1:54321/auth/v1- Leave the on-chain vars empty (on-chain is disabled by default).
Also configure the frontend (web/.env):
VITE_SUPABASE_URL← API URL fromsupabase statusVITE_SUPABASE_ANON_KEY← anon key fromsupabase statusVITE_API_URL=http://127.0.0.1:3000
3. Run database migrations
cd api && sqlx migrate run && cd ..4. Start the API
cd api && cargo runThe API starts on http://127.0.0.1:3000. Verify with curl http://127.0.0.1:3000/api/health.
5. Start the frontend
cd web && npm install && npm run devOpen http://localhost:5173. Sign in with email/password or Google.
The core game works entirely off-chain — no wallet, no gas, no RPC needed. The on-chain layer is optional. See On-Chain Layer (Optional) below.
┌──────────┐ ┌───────────┐ ┌────────────┐ ┌───────────────┐
│ Web UI │────▶│ Rust API │────▶│ Publisher │────▶│ Base Sepolia │
│ (Vue 3) │ │ (Axum) │ │ (TS) │ │ (MathCoin.sol)│
└──────────┘ └───────────┘ └────────────┘ └───────────────┘
│ │
▼ ▼
┌──────────┐ ┌───────────┐
│PostgreSQL│ │ Chain │
│(earnings)│ │ Indexer │
└──────────┘ │ (Rust) │
└───────────┘
- Web UI (Vue 3 + TypeScript) — Math challenge game, Supabase auth (email/Google), in-app wallet
- Rust API (Axum + sqlx) — Generates challenges at adaptive difficulty, validates answers server-side, awards off-chain coins in PostgreSQL
- Publisher (TypeScript) — Snapshots cumulative earnings, builds OpenZeppelin Merkle trees, persists proofs as JSONB
- MathCoin.sol (Solidity, Foundry) — ERC-20 token with cumulative Merkle distributor (
updateRoot+claim). Deployed on Base Sepolia testnet - Rust On-Chain Adapter — TxSubmitter (nonce management + gap recovery), event indexer (cursor + idempotent replay), claim relay
The on-chain layer is a real, deployed ERC-20 on Base Sepolia. It is a demonstration, not a financial product. It is NOT required to run or evaluate the project.
- Deploy the contract to Base Sepolia:
cd contracts
forge install # install OZ + forge-std dependencies
source .env # sets PRIVATE_KEY, BASESCAN_API_KEY
forge script script/Deploy.s.sol --rpc-url base_sepolia --broadcast --verify- Set the on-chain env vars in
.env:
CONTRACT_ADDRESS=0x... # the deployed contract address
CHAIN_NAME=base_sepolia
CHAIN_ID=84532
EXPLORER_URL=https://sepolia.basescan.org
BASE_RPC_URL=https://sepolia.base.org
RELAYER_PRIVATE_KEY=0x... # testnet-only key
- Restart the API. On-chain endpoints are now available.
POST /api/claim-address— Opt in by setting a destination address (works even without on-chain config)GET /api/claim-data— Returns Merkle proof for on-chain claimingGET /api/claim-relay— Submits claim transaction (relayed, gasless for user)GET /api/audit— Public proof-of-reserves view
cd publisher && npm install
DATABASE_URL=postgres://... npx tsx src/cli.tsThe publisher snapshots earnings, builds the Merkle tree, and persists the distribution.
The Rust adapter handles the on-chain updateRoot transaction.
The project supports two ways to run Postgres locally. Use ONE or the other — never both at once (they conflict on port 5432).
This is the default path for clone-and-run. It provides both Postgres and Auth in one command — everything the app needs to function.
supabase start # boots Postgres + Auth + API in Docker
supabase status # prints URLs and keys → copy into .envThis is what the Getting Started steps above assume.
For CI pipelines, or if you already use Supabase cloud (managed) and only
need a local Postgres for development. This starts ONLY Postgres — you must
point DATABASE_URL at it and configure JWKS_URL/JWT_ISS to Supabase cloud.
cp .env.example .env # edit DATABASE_URL to point at the compose DB
docker compose up api # starts API + PostgresDo not run both
supabase startanddocker compose upat the same time — they both bind port 5432 and will conflict.
The Dockerfile uses SQLX_OFFLINE=true for builds without a live database.
The .sqlx/ directory is intentionally committed (currently empty — this project uses
runtime-checked sqlx queries, not query! macros). If you adopt query! macros later,
run cargo sqlx prepare to populate the cache so the offline Docker build keeps working.
| Suite | Framework | Command | Tests |
|---|---|---|---|
| Rust handler tests | cargo test | cargo test --test handler_tests |
28 |
| Rust chain/adapter tests | cargo test | cargo test --test chain_tests |
8 |
| Rust auth verifier tests | cargo test | cargo test --test auth_verifier_tests |
9 |
| Rust challenge tests | cargo test | cargo test --test challenge_tests |
9 |
| Rust concurrency tests | cargo test | cargo test --test concurrency_tests |
7 |
| Foundry contract tests | forge test | forge test |
11 (incl. 2 fuzz) |
| Publisher tests | vitest | npm test |
10 |
| Frontend tests | vitest | npm test |
2 |
The concurrency tests (100 racers) can exhaust the connection pool. Run them with --test-threads=1.
The default bind is 127.0.0.1:3000. In Docker/cloud hosts, set BIND_ADDRESS=0.0.0.0:3000 or PORT=3000.
Ensure FRONTEND_ORIGIN matches exactly the frontend URL (including protocol and port).
For local dev: FRONTEND_ORIGIN=http://localhost:5173.
Run supabase start. If it fails, check Docker is running. The supabase status command
prints all the URLs and keys you need.
The API fails fast at startup if DATABASE_URL, JWT_ISS, JWT_AUD, or JWKS_URL are missing.
On-chain vars (CONTRACT_ADDRESS, etc.) are optional — the core runs without them.
CONTRACT_ADDRESS is not set in .env. This is expected for local dev. Set it to enable.
The Dockerfile sets SQLX_OFFLINE=true. The .sqlx/ cache directory is committed and
intentionally kept (with a .gitkeep). It is currently empty because the project uses
runtime-checked sqlx::query/sqlx::query_as function variants. If you adopt the
sqlx::query! / sqlx::query_as! compile-time-checked macros, run cargo sqlx prepare
to populate the cache so the Docker build keeps working.
If you see "port 5432 already in use", you have both supabase start and
docker compose up running. Stop one: supabase stop or docker compose down.
In Supabase dashboard → Authentication → URL Configuration, set Site URL to your frontend URL
and add it to Redirect URLs. For local dev: http://localhost:5173.
See docs/SECURITY.md for the OWASP Top 10:2025 compliance matrix, secrets management table, and deferred security items.
MIT — see LICENSE.