A Web3 dApp on Base L2 where users stake NFT mining devices to earn
$HASH tokens, trade gem launchpad tokens, and register on-chain names.
Live at https://hashcoin.farm.
┌─────────────────────┐ HTTPS ┌──────────────────────┐
│ React SPA (Vite) │ ───────────────> │ Fastify backend │
│ Vercel │ │ Back4app Containers │
│ hashcoin.farm │ │ api.hashcoin.farm │
└─────────────────────┘ └──────────┬───────────┘
│ │
│ wagmi + viem (read/write) │ viem (read)
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Base L2 (Ankr RPC) │
│ Mining / Staking / Name / $HASH / Gem Launchpad contracts │
└─────────────────────────────────────────────────────────────────┘
▲
│ indexed events,
│ cached state
▼
┌──────────────────┐
│ Neon Postgres │
│ (Frankfurt) │
└──────────────────┘
Frontend (/, src/)
- React 18 + TypeScript + Vite, Mantine UI, RainbowKit + wagmi + viem for wallet/web3, react-query for fetching, react-router for routing.
- Reads on-chain data directly via Ankr RPC (browser-side viem).
- Reads aggregated/indexed data from the backend over HTTP.
- Deployed as a static bundle to Vercel.
Backend (backend/)
- Fastify + drizzle-orm + postgres-js + viem.
- Two responsibilities: HTTP API (
/api/*) and chain indexer that follows Base head, persists token/trade/holdings state into Postgres. - IPFS upload proxy to Pinata with rate-limit + Origin allowlist.
- Single-instance only — guarded by a Postgres session-level advisory lock so two replicas can't double-index.
Database: Neon Postgres 16, 9 tables, 6 indexes. Schema in
backend/src/db/schema.ts, migration in backend/src/db/migrations/.
External services: Base via Ankr (RPC), Pinata (IPFS pinning), Back4app (container hosting), Vercel (static hosting), Neon (managed Postgres).
.
├── src/ React SPA
│ ├── api/ react-query hooks + HTTP client
│ ├── components/ UI components
│ ├── views/ route-level pages
│ ├── hooks/ wallet/account hooks
│ └── source/ contract addresses, ABIs, constants
├── public/ static assets + SPA fallback configs
│ (_redirects, .htaccess, web.config)
├── backend/
│ ├── src/
│ │ ├── routes/ Fastify route handlers
│ │ ├── chain/ viem client + contract reads
│ │ ├── indexer/ chain head follower (gem launchpad, etc.)
│ │ ├── refreshers/ periodic state refreshers + LRU TTL cache
│ │ ├── db/ drizzle schema + migrations + client
│ │ └── config.ts zod-validated env schema
│ └── Dockerfile multi-stage build for Back4app
├── DEPLOY_BACK4APP.md full deploy walkthrough
├── HANDOFF.md owner takeover playbook
└── MIGRATION.md router between the two
- Node.js 22+ (matches
Dockerfileruntime) - npm 10+
- Postgres 16 (local, or use a free Neon project)
- Ankr Base RPC key (free at ankr.com)
npm ci
cp .env.example .env # then fill VITE_* vars
npm run dev # http://localhost:3000cd backend
npm ci
cp .env.example .env # fill DATABASE_URL, ANKR_RPC_URL, PINATA_JWT
npm run db:push # apply schema (only needed once per DB)
npm run dev # http://localhost:3001The frontend's VITE_API_URL points at http://localhost:3001 by
default in .env.example.
-
VITE_*env vars are baked into the JS bundle at build time — they are not secrets. The frontend's Ankr RPC URL is exposed to anyone who opens DevTools; that's fine because Ankr gates by key with rate limits, not by secrecy. BackendPINATA_JWTandDATABASE_URLare server-only and never reach the bundle. -
Backend env vars are read at startup, validated by zod in
backend/src/config.ts. InNODE_ENV=productionthe process refuses to boot unlessCORS_ORIGINSandPINATA_JWTare set. -
Use Neon's direct (non-pooled) endpoint for
DATABASE_URL. The indexer relies on Postgres session-level advisory locks to prevent double-indexing. Neon's pooled endpoint runs pgbouncer in transaction mode, which silently breaks session-level locks. With the direct endpoint and a single backend instance, the lock works correctly. SeeDEPLOY_BACK4APP.md §7for details. -
Backend is single-instance. Indexer state lives in Postgres (
indexer_state.cursor) but the in-process advisory lock means only one replica may run. Horizontal scaling requires either splitting the indexer into a separate worker process or moving the lock to a different mechanism. -
CORS allowlist is exact-match, comma-separated origins via
CORS_ORIGINS. Wildcards/regex not supported. Each new public frontend URL must be added explicitly. -
Smart contract addresses live in
src/source/contracts.ts(frontend) and are duplicated where the backend needs them (e.g.backend/src/refreshers/balances.ts). Treat the frontend file as the canonical source. -
Indexer health: monitor
/api/health/ready— returns 503 ifindexer.stale=trueor DB ping fails.indexer.behindreports how many blocks behind chain head. Healthy steady state isbehind < 5.
Public HTTP API exposed under /api/*:
GET /api/health— liveness (no DB touch).GET /api/health/ready— readiness (DB ping + indexer status).GET /api/hash/supply— $HASH total + circulating supply + per-wallet balances.GET /api/wallet/:addr/{balances,name,role,galxe}— wallet aggregates.GET /api/shop/tools— NFT mining device catalog.GET /api/shop/user/:addr— user's owned tools.GET /api/gems— paginated gem launchpad tokens.GET /api/gems/:addr+/api/gems/:addr/user/:user— token details + user position.GET /api/gems/rates,/api/gems/rate/:nftId— exchange rates.GET /api/gems/mining/:user— user mining state.GET /api/launch/allowance/:addr— launchpad allowance.POST /api/ipfs/upload— IPFS pin via Pinata (Origin-allowlisted, rate-limited).
DEPLOY_BACK4APP.md— full walkthrough to deploy backend to Back4app Containers + Neon, plus frontend rebuild.HANDOFF.md— playbook for transferring the live deployment to a new owner's accounts (GitHub, Neon, Pinata, Ankr, Back4app, Vercel, custom domains).MIGRATION.md— entry point pointing at the two above.
Originally developed by LookHook.info.