Citizen-science platform for reporting shark sightings around Cozumel, Mexico. Every verified sighting is anchored to the Hedera network and rewarded with SHARK tokens (HTS), turning divers and locals into a distributed sensor network for marine conservation.
Built at ETHGlobal Cannes for Mar Sustentable and marine biologist Nadia Rubio.
Distributed Ledger
- Hedera Hashgraph (Testnet) — Hedera Consensus Service (HCS) for the immutable sighting log, Hedera Token Service (HTS) for SHARK rewards, Hedera Mirror Node REST API for historical topic reads
@hashgraph/sdk— transaction signing, topic submit, token transfer@hashgraph/hedera-wallet-connect+ WalletConnect v2 — in-browser wallet pairing (HashPack, Blade, Kabila)
Identity
- Worldcoin World ID (
@worldcoin/idkit) — Sybil-resistant proof-of-personhood, one verified human per SHARK reward stream
Backend
- Node.js 20 + TypeScript 5.9
- Fastify 5 (
@fastify/jwt,@fastify/multipart,@fastify/cors,@fastify/rate-limit,@fastify/swagger) - Prisma 7 ORM + PostgreSQL 16
- Pinata SDK — IPFS pinning for sighting photos
Indexer
- Node.js 20 + TypeScript
- GraphQL Yoga 5 — public read API at
/graphql - Hedera Mirror Node polling — materialises HCS topic messages into an in-memory store
Frontend
- React 18 + TypeScript, Vite 6
- Tailwind CSS 3
- React Router 7, TanStack Query 5, React Hook Form + Zod
- Leaflet 1.9 — real-time sighting map
- graphql-request — indexer client
Infrastructure
- Docker + Docker Compose
- Nginx (reverse proxy, TLS termination via Let's Encrypt)
- Hetzner Cloud (Ubuntu VPS)
- GitHub Actions CI/CD — build on
main, SSH deploy viaappleboy/ssh-action
+----------------------+
| React PWA (Vite) |
| app.oceanwatch.xyz |
+----------+-----------+
|
REST (JWT) | GraphQL (public)
v
+------------------+------------------+
| |
+-------+--------+ +---------+---------+
| Fastify API | | GraphQL Indexer |
| (backend) | | (indexer) |
+---+------+-----+ +---------+---------+
| | ^
| | | polls
| v |
| PostgreSQL Hedera Mirror Node
| (users, rewards, (testnet.mirrornode
| auth_challenges) .hedera.com)
|
v
Hedera Testnet
- HCS topic (sightings)
- HTS SHARK token (rewards)
- Pinata / IPFS (media CIDs referenced in HCS messages)
- User authenticates a Hedera account via a wallet-signed challenge (
auth_challenges) and proves humanness with World ID (nullifier stored inusers.worldid_nullifier). - Frontend uploads the photo to the backend (
POST /upload); backend pins it to IPFS via Pinata and returns a CID. - Frontend submits the sighting payload (
POST /sightings). Backend:- Validates the schema.
- Submits a JSON message to the Hedera Consensus Service topic (
HEDERA_TOPIC_ID). - Transfers a fixed amount of SHARK (HTS token
HEDERA_TOKEN_ID) from the treasury to the reporter's account. - Records the reward in the
rewardstable keyed by consensus sequence number.
- The indexer polls the Hedera Mirror Node, decodes topic messages, and exposes them through the GraphQL API consumed by the map and fieldbook views.
ocean-watch/
backend/ Fastify REST API, Prisma schema, Hedera + IPFS integration
indexer/ GraphQL Yoga server backed by Hedera Mirror Node
frontend/ React + Vite PWA
landing/ Static marketing page (oceanwatch.xyz)
nginx/ Reverse proxy config (TLS, vhost routing)
docker-compose.yml
.github/workflows/ CI/CD pipelines
Each service has its own README with details.
Prerequisites: Node.js 20+, Docker, a Hedera testnet account, a WalletConnect project ID, a Worldcoin app ID, and a Pinata JWT.
cp .env.example .env
# fill in HEDERA_*, JWT_SECRET, PINATA_JWT, WORLDID_*, VITE_* values
docker compose up -d postgres
cd backend && npm install && npx prisma migrate deploy && npm run dev
cd indexer && npm install && npm run dev
cd frontend && npm install && npm run devFull stack:
docker compose up -d --buildThe main branch is continuously deployed to the Hetzner host behind *.oceanwatch.xyz:
oceanwatch.xyz— landing page (Nginx static)app.oceanwatch.xyz— React PWAapi.oceanwatch.xyz— Fastify backendindexer.oceanwatch.xyz/graphql— GraphQL indexer
CI runs type-checks and Docker builds on every push; on success, a second job SSHs into the server, pulls the latest commit, and runs docker compose up -d --build.
MIT