Stake to Mark, Vote to Earn
A Base Mini-App for posting geolocated landmarks. Users stake $LMRK to post map "Landmarks" (photos, facts, memes) at a geohash6 location; others upvote by spending $LMRK; daily per-region rewards distribute from a pool.
Built with OnchainKit, Base RPC, MapLibre GL, and Supabase. No Neynar or paid APIs required.
- Post Landmarks: Stake 0.1 $LMRK to drop a landmark at any geohash6 location
- Vote & Earn: Upvote great content; 90% goes to creators, 10% to treasury
- Mint NFTs: Burn 10 $LMRK to mint commemorative "Placemark" NFTs
- Regional Leaderboards: Daily rewards distributed to top landmarks per region
- Privacy-First: Geohash6 (~1.2km grid), EXIF stripping, no GPS tracking
- PWA Ready: Installable, offline-capable, mobile-optimized
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Frontend (Next.js) β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β
β β Map β β Post β β Vote β β Mint β β
β β (MapGL) β β Sheet β β Button β β Button β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β
β β β β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β OnchainKit + Wagmi (Base RPC) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββ
β Smart Contracts (Base) β β Indexer (Serverless)β
β ββββββββββββββββββββββββββββ β β ββββββββββββββββ β
β β LandmarkRegistry.sol β β β β Event Indexerβ β
β β - postPin() β ββββ€ β - Posted β β
β β - upvote() β β β β - Voted β β
β β - withdrawStake() β β β β - Settled β β
β ββββββββββββββββββββββββββββ β β ββββββββββββββββ β
β ββββββββββββββββββββββββββββ β β β β
β β LandmarkNFT.sol β β β ββββββββββββββββ β
β β - mintFromPost() β β β β Supabase DB β β
β ββββββββββββββββββββββββββββ β β β + Storage β β
βββββββββββββββββββββββββββββββββββ ββββ΄βββββββββββββββ΄ββββ
- Node.js β₯ 20
- pnpm β₯ 9
- Foundry (for smart contracts)
- Base RPC URL (public or Alchemy/Infura)
- Supabase account (free tier)
git clone https://github.com/yourusername/landmark.git
cd landmark
# Initialize submodules
git submodule update --init --recursive
# Install dependencies
pnpm installCopy .env.example to .env.local:
Copy-Item .env.example .env.localFill in required values:
# Base Network
NEXT_PUBLIC_BASE_RPC_URL=https://mainnet.base.org
NEXT_PUBLIC_CHAIN_ID=8453
# Contracts (populate after deployment)
NEXT_PUBLIC_LMRK_TOKEN_ADDRESS=0x...
NEXT_PUBLIC_LANDMARK_REGISTRY_ADDRESS=0x...
NEXT_PUBLIC_LANDMARK_NFT_ADDRESS=0x...
NEXT_PUBLIC_TREASURY_ADDRESS=0x...
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
# Feature Flags
NEXT_PUBLIC_FF_SHARE_CARDS=true
NEXT_PUBLIC_FF_PWA=true
NEXT_PUBLIC_FF_OFFLINE_QUEUE=truecd contracts
# Set deployment env vars
$env:BASE_RPC_URL="https://mainnet.base.org"
$env:PRIVATE_KEY="0x..."
$env:LMRK_TOKEN_ADDRESS="0x..." # Your $LMRK token
$env:TREASURY_ADDRESS="0x..." # Treasury wallet
# Compile
forge build
# Deploy to Base
forge script script/Deploy.s.sol:DeployScript --rpc-url $env:BASE_RPC_URL --broadcast --verifyCopy contract addresses to .env.local
Run the schema migration:
-- See docs/supabase-schema.sql
-- Create tables: landmarks, leaderboard
-- Create storage bucket: landmarkspnpm devpnpm indexer:devThe indexer subscribes to Base RPC events and populates Supabase.
landmark/
βββ contracts/ # Smart contracts (Foundry)
β βββ src/
β β βββ LandmarkRegistry.sol
β β βββ LandmarkNFT.sol
β β βββ interfaces/
β β βββ utils/
β βββ script/Deploy.s.sol
β βββ foundry.toml
βββ src/
β βββ app/ # Next.js App Router
β β βββ api/ # API routes (upload, metadata)
β β βββ layout.tsx
β β βββ page.tsx
β β βββ providers.tsx
β βββ components/ # React components
β β βββ layout/ # AppShell, TopBar, BottomDock
β β βββ map/ # MapView (MapLibre GL)
β β βββ landmark/ # Detail, VoteButton, MintButton
β β βββ post/ # PostSheet
β β βββ wallet/ # ConnectButton
β β βββ ui/ # Toast, Dialog, etc.
β β βββ icons/
β βββ lib/ # Utilities & logic
β β βββ api.ts # API client
β β βββ contracts.ts # ABIs & addresses
β β βββ geohash.ts # Geohash utilities
β β βββ image.ts # EXIF stripping, validation
β β βββ sharecard.ts # Share card generation
β β βββ store.ts # Zustand state
β β βββ types.ts # TypeScript types
β β βββ utils.ts # Helpers
β β βββ analytics.ts # Privacy-friendly tracking
β βββ indexer/ # Event indexer
β β βββ index.ts
β βββ styles/
β βββ globals.css # Tailwind + design tokens
βββ public/
β βββ manifest.json # PWA manifest
β βββ sw.js # Service worker
β βββ offline.html # Offline fallback
βββ design/
β βββ tokens.json # Design tokens (Figma-ready)
βββ docs/
β βββ third_party.md # Submodule SHAs
β βββ supabase-schema.sql # Database schema
βββ vendor/ # Git submodules
β βββ onchainkit/
β βββ miniapps/
β βββ ...
βββ .env.example
βββ package.json
βββ tailwind.config.js
βββ tsconfig.json
βββ README.md
All design tokens follow the Base Brand Kit.
--brand: #0052FF; /* Base blue accent */
--bg: #0A0B0D; /* Dark canvas */
--surface: #121418; /* Elevated surfaces */
--text: #E7ECF2; /* Primary text */
--token: #73F3C4; /* $LMRK accent */- Font: Inter (400, 500, 600, 700, 800)
- Sizes: 12/14/16/20/24/32 px
- Line Height: 1.2 (tight), 1.35 (normal)
See src/components/ for implementation. All components:
- WCAG AA+ contrast
- Keyboard accessible
- Mobile-first (touch targets β₯ 44px)
- Reduced motion support
Core Functions:
postPin(geohash6, region3, contentCID)β Stakes 0.1 $LMRK, emitsLandmarkPostedupvote(pinId, amount)β Splits 90% to author, 10% to treasurywithdrawStake(pinId)β After 7-day cooldown, 1% burn feesettleRegion(region3, rewardAmount, topPins, shares)β Distributes daily rewards
Constants:
STAKE_AMOUNT: 0.1 $LMRKCOOLDOWN_PERIOD: 7 daysWITHDRAW_BURN_BPS: 100 (1%)UPVOTE_AUTHOR_BPS: 9000 (90%)
Core Functions:
mintFromPost(landmarkId, tokenURI)β Burns 10 $LMRK, mints ERC-721
Note: Each landmark can only be minted once.
The indexer:
- Subscribes to Base RPC for
LandmarkPosted,LandmarkVoted,RegionSettledevents - Decodes geohashes to lat/lng
- Stores in Supabase (
landmarks,leaderboardtables) - Computes daily top-20 per region3
Run Indexer:
pnpm indexer:devFor production, deploy as a serverless function (Vercel Cron, Railway, etc.).
Fetch landmarks in viewport.
Query Params:
minLat,minLng,maxLat,maxLng: Bounding boxlimit: Max results (default 500)
Upload image to Supabase Storage.
Body: multipart/form-data with file field
Response:
{
"cid": "path/to/file",
"url": "https://xxx.supabase.co/storage/v1/..."
}Generate NFT metadata JSON.
Body:
{
"landmarkId": "123",
"imageUrl": "https://...",
"caption": "Amazing view!"
}Response:
{
"tokenURI": "https://xxx.supabase.co/storage/v1/.../metadata/123.json"
}- Tap [+] FAB
- Upload photo β Auto-strips EXIF
- Write caption (β€200 chars)
- Geohash auto-fills from map center
- Tap Post β Approve $LMRK β Confirm stake
- Success toast: "Posted β’ Stake locked"
- Tap marker on map β Detail sheet opens
- Tap Upvote (0.01 $LMRK)
- Approve β Confirm β "+1 upvote sent"
- In landmark detail β Tap Mint NFT
- Modal shows burn amount (10 $LMRK)
- Progress: Approve β Generate metadata β Mint
- Success: "Placemark minted"
Set in .env.local:
NEXT_PUBLIC_FF_SHARE_CARDS: Enable share card generationNEXT_PUBLIC_FF_PWA: Enable PWA featuresNEXT_PUBLIC_FF_A11Y_HI_CONTRAST: High-contrast modeNEXT_PUBLIC_FF_OFFLINE_QUEUE: Queue posts/votes offline
Target: <180KB initial JS
- Code splitting: Map, post sheet lazy-loaded
- Tree shaking: OnchainKit, MapLibre modular imports
- Image optimization: Next.js
<Image>with AVIF/WebP
# Type check
pnpm type-check
# Lint
pnpm lint
# Smart contract tests
cd contracts
forge test- Connect repo to Vercel
- Set environment variables
- Deploy:
vercel --prod
cd contracts
forge script script/Deploy.s.sol:DeployScript --rpc-url $BASE_RPC_URL --broadcast --verifyDeploy src/indexer/index.ts as a long-running process.
Dockerfile Example:
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
CMD ["pnpm", "indexer:dev"]| Metric | Target | Notes |
|---|---|---|
| Initial Load | <1.5s | 4G network |
| TTI | <2.5s | Time to Interactive |
| Map FPS | β₯45 | With 500 markers |
| Bundle Size | <180KB | Initial JS |
| Lighthouse PWA | β₯90 | |
| Lighthouse A11y | β₯95 | WCAG AA+ |
- Keyboard Navigation: All controls operable without mouse
- Screen Reader: ARIA labels, live regions for tx updates
- Contrast: WCAG AA+ (4.5:1 minimum)
- Focus Visible: 2px high-contrast rings
- Reduced Motion: Respects
prefers-reduced-motion
- No Private Keys: MiniKit/wallet handles signing
- EXIF Stripping: Client-side before upload
- Geohash Privacy: 6-char (~1.2km), no raw GPS
- File Validation: Type/size checks, 2MB max
- Rate Limiting: Consider Cloudflare/Vercel protection
Contributions welcome! Please:
- Fork the repo
- Create a feature branch:
git checkout -b feature/amazing - Commit changes:
git commit -m 'Add amazing feature' - Push:
git push origin feature/amazing - Open a Pull Request
MIT License - see LICENSE for details.
- Base for the incredible L2 infrastructure
- Coinbase for OnchainKit and MiniKit
- Farcaster for the MiniApps framework
- MapLibre for the open-source map renderer
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Twitter: @LandmarkApp
Built with β€οΈ on Base