Mālama Labs is a DePIN (Decentralized Physical Infrastructure Network) startup building a cryptographic environmental data network. We utilize hardware-signed data from specialized sensors (ATECC608A + BME680) as the root of trust, completely removing human-in-the-loop oracles. The platform employs a dual-token model ($MALAMA and $LCO2/$VCO2) with transactions bridged seamlessly between Cardano (Aiken/CIP-68) and Base (EVM/LayerZero).
Our architecture provides a secure pipeline from data birth to multi-chain settlement. Sensor data forms a Merkle tree batch, is uploaded to IPFS (via Pinata), validated by an AI validator using Apache Kafka and OPA/Rego policies, and finally anchored on-chain every 4 hours. Every component is designed with strict invariants, and our Next.js frontend is built with the highest standards of modern UX.
+--------------------+
| Sensor Device |
| (Py + ATECC608A + |
| BME680) |
+---------+----------+
| (Hardware Signed Data)
v
+---------+----------+
| Kafka / Pipeline |
| (Merkle Batching) |
+---------+----------+
|
+-----+-----+
| |
v v
+---+---+ +---+---+
| IPFS | | OPA |
|(Pinata| | (Rego)|
+---+---+ +---+---+
| |
+-----+-----+
|
+---------+----------+
| AI Validator |
+---------+----------+
|
+---------+----------+
| Blockchain Anchor |
| (Cardano / Base) |
+--------------------+
cd apps/web
npm install
npm run devcd contracts/evm
npm install
npx hardhat testdocker-compose up -dcd contracts/cardano
aiken build
aiken checkBranch:
hex-explorer· Deployed on Vercel · Stack: Next.js 14 App Router, Mapbox GL JS, H3 geospatial, wagmi v2, MeshSDK, Vercel KV (Redis)
The Launch site is the public-facing presale and node-reservation platform for Genesis Node Licenses. It is a separate Next.js app inside the monorepo (apps/web) and is deployed independently of the core data pipeline.
Marketing hero with animated Mālama Labs branding, mission statement, and links to the presale and docs.
The primary conversion page. Embeds the GenesisMint component which guides a buyer through the full purchase wizard:
- Connect wallet — Cardano (Lace / Eternl via CIP-30) or EVM (MetaMask / injected on Base Sepolia).
- Select a hex — Opens the hex explorer; selected hex is passed back to the mint flow.
- Pay & mint — EVM path: USDC approve →
secureNode()on the Genesis ERC-721 contract. Cardano path: CIP-8 message signing → server-side treasury mint via Blockfrost. - Success — NFT image preview, links to Basescan / OpenSea (testnet), and map confirmation.
Purchase flow resilience:
- 409 resume: If MetaMask hangs and the user retries, the second
POST /api/nft/claimreturns409 { claimId, editionNumber, claimedOnChain }. Rather than blocking with "Hex Already Claimed", the flow checksclaimedOnChain === 'base'(same chain) and resumes from the USDC approve step — no data loss, no user confusion. - Lace MV3 timeout: Lace's Manifest V3 service worker restarts periodically. A 12-second
Promise.racetimeout wraps everyenable()call. On timeout: friendly "Lace is still starting up — click again in a moment" message. The second click almost always succeeds once the background reconnects. - Cross-chain conflict detection: If a hex is already claimed on the other chain, the error is surfaced immediately with a clear message rather than silently failing.
Wallet picker: When multiple Cardano wallets are detected via window.cardano, an inline picker is rendered below the button (not an absolute dropdown — avoids overflow clipping in the modal).
Global interactive map of all Genesis Node hex cells (H3 Resolution 4, ~1,770 km² each) built on Mapbox GL JS.
Three-tier color system:
| Color | Status | Meaning |
|---|---|---|
🟢 Green (#22c55e) |
available |
Open for reservation |
🔴 Red (#ef4444) |
reserved |
Claimed by another buyer |
🟡 Yellow (#eab308) |
reserved-user |
Claimed by the currently signed-in user |
Features:
- Click any hex to open the Node Details Panel (
HexPanel) — environmental zone classification, estimated water coverage, region, edition number, H3 index, and a "Reserve Node" CTA. ?hex=<h3Index>URL parameter: deep-link to a specific hex. On load the map flies to the cell and opens its details panel. Used by the dashboard "View on Map" action.- User's own hexes are overlaid in yellow by fetching
/api/useron mount and merging the result into the manifest before rendering Mapbox paint expressions. - All fill-color, fill-opacity, line-color, and line-width are driven by Mapbox GL expressions keyed on
HexStatus— no per-feature DOM manipulation.
Dedicated auth page with three sign-in methods:
| Method | Implementation |
|---|---|
POST /api/auth/email-session — creates or retrieves account, sets HTTP-only session cookie |
|
| Cardano | window.cardano[walletKey].enable() with 12s timeout, wallet picker for multi-wallet installs |
| EVM / Base | wagmi useConnect() + injected() connector, auto-redirects on evmConnected |
If the user is already authenticated (checked via GET /api/auth/session on mount), they are immediately redirected to /dashboard with router.replace.
Auth-gated page showing the user's purchased Genesis Node Licenses.
- Authentication guard: on load, fetches
/api/auth/session. If unauthenticated after a 1.5-second grace period (for wallet reconnection), redirects to/auth. - Hex inventory: each owned hex is displayed as a card with region, H3 index, edition number, and status badge.
- Inline Node Details: clicking "See Details" expands an accordion panel below the hex card (no modal overlay — no double scroll, no duplicate close buttons). The expansion loads
HexPanelvia dynamic import and constructs the fullPhase1Hexobject from the H3 index usingcellToLatLng,getResolution,classifyZone,estimateWaterCoverage, anddetectRegion. - View on Map: navigates to
/explorer?hex=<h3Index>, which triggers the map flyTo + auto-open. - Sign out: clears the session cookie and fires the
malama:authcross-component event.
The Navbar, Dashboard, and Auth page must stay in sync without a global state store. The solution is a lightweight browser event:
window.dispatchEvent(new Event('malama:auth'))Fired by: the Auth page (after sign-in), the Dashboard (after sign-in and sign-out), and any wallet connection handler.
The Navbar listens for this event and re-fetches /api/auth/session on demand, in addition to re-fetching whenever the Next.js pathname changes. This gives instant UI feedback across navigation without polling.
| Route | Method | Description |
|---|---|---|
/api/auth/session |
GET | Returns { auth: 'email' | null, email } — reads HTTP-only session cookie |
/api/auth/email-session |
POST | Creates or retrieves user account by email; sets session cookie |
/api/auth/logout |
POST | Clears session cookie |
/api/nft/claim |
POST | Atomically reserves a hex in Redis for a given chain + buyer address |
/api/nft/claim |
PATCH | Binds the on-chain txHash + tokenId to an existing claim (post-mint) |
/api/nft/claim |
GET | Returns existing claim data for a hexId |
/api/nft/mint-cardano |
POST | Server-side treasury mint via Blockfrost — pays ADA fees, sends NFT to buyer |
/api/user |
GET | Returns the authenticated user's account including owned hex inventory |
/api/nft/[tokenId]/image |
GET | Generates dynamic NFT image for a given tokenId + hexId |
Claim registry (Redis):
The hex claim registry uses SADD for atomic reservation. Each claim stores: hexId, chain, buyerAddress, claimId (UUID), editionNumber, and optionally txHash once the mint completes. The 409 resume logic checks claimedOnChain to distinguish "same-chain retry" (safe to resume) from "cross-chain conflict" (block).
| Package | Role |
|---|---|
next 14 |
App Router, RSC, API routes |
mapbox-gl |
Interactive hex map rendering |
h3-js |
H3 geospatial indexing (Resolution 4 cells) |
wagmi v2 + viem |
EVM wallet connection, contract calls, tx receipts |
@meshsdk/react |
Cardano wallet connection (MeshSDK CIP-30 wrapper) |
@vercel/kv |
Redis-backed claim registry + user accounts |
lucide-react |
Icon set throughout the UI |
All secrets live in
.env.local(gitignored). Configure on Vercel per-project.
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_MAPBOX_TOKEN |
✅ | Mapbox GL public token |
NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS |
✅ | Deployed Genesis ERC-721 address on Base |
NEXT_PUBLIC_USDC_CONTRACT_ADDRESS |
✅ | USDC contract address on Base |
NEXT_PUBLIC_APP_BASE_URL |
✅ | Public origin (e.g. https://launch.malamalabs.com) |
KV_REST_API_URL |
✅ | Vercel KV endpoint |
KV_REST_API_TOKEN |
✅ | Vercel KV auth token |
REDIS_URL |
optional | Redis URL fallback (node-redis adapter) |
BLOCKFROST_PROJECT_ID |
✅ (Cardano) | Blockfrost API key for Cardano treasury minting |
TREASURY_CARDANO_ADDRESS |
✅ (Cardano) | Treasury wallet address for ADA fee payments |
SESSION_SECRET |
✅ | Secret for signing HTTP-only session cookies |
STRIPE_SECRET_KEY |
optional | Stripe key for fiat card checkout path |
| Branch | Purpose |
|---|---|
main |
Production — auto-deploys to launch.malamalabs.com |
hex-explorer |
Active feature branch — explorer, dashboard, auth, mint flow. Do not merge to main without explicit sign-off. |
All current Launch site work lives on hex-explorer. The branch is deployed to a Vercel preview URL for QA before any production promotion.
The frontend (apps/web) deploys to Vercel. There are currently two Vercel projects linked to this repository:
malamalaunch— primary project, auto-deploys frommainlaunch-malamalabs-com— project owning thelaunch.malamalabs.comcustom domain
Required Vercel dashboard configuration for both projects:
- Root Directory:
apps/web - Framework Preset: Next.js (auto-detected)
- Build Command: (leave blank — use framework default
next build) - Output Directory: (leave blank — use framework default
.next) - Install Command: (leave blank — use framework default
npm install)
With both projects configured this way, no vercel.json is required at the repo root. Next.js auto-detection handles everything.
If either project is misconfigured with Root Directory set to repo root, the build will fail with "The Next.js output directory '.next' was not found" because the monorepo root has no Next.js app. Fix via Project Settings → General → Root Directory.
