Skip to content

steffenpharai/Landmark

Repository files navigation

Landmark ($LMRK)

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.


🎯 Features

  • 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

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        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    β”‚   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”˜

πŸš€ Quick Start

Prerequisites

  • Node.js β‰₯ 20
  • pnpm β‰₯ 9
  • Foundry (for smart contracts)
  • Base RPC URL (public or Alchemy/Infura)
  • Supabase account (free tier)

1. Clone & Setup

git clone https://github.com/yourusername/landmark.git
cd landmark

# Initialize submodules
git submodule update --init --recursive

# Install dependencies
pnpm install

2. Environment Variables

Copy .env.example to .env.local:

Copy-Item .env.example .env.local

Fill 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=true

3. Deploy Smart Contracts

cd 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 --verify

Copy contract addresses to .env.local

4. Setup Supabase

Run the schema migration:

-- See docs/supabase-schema.sql
-- Create tables: landmarks, leaderboard
-- Create storage bucket: landmarks

5. Run Development Server

pnpm dev

Open http://localhost:3000

6. Run Indexer (Separate Process)

pnpm indexer:dev

The indexer subscribes to Base RPC events and populates Supabase.


πŸ“ Project Structure

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

🎨 Design System

All design tokens follow the Base Brand Kit.

Color Palette

--brand: #0052FF;      /* Base blue accent */
--bg: #0A0B0D;         /* Dark canvas */
--surface: #121418;    /* Elevated surfaces */
--text: #E7ECF2;       /* Primary text */
--token: #73F3C4;      /* $LMRK accent */

Typography

  • Font: Inter (400, 500, 600, 700, 800)
  • Sizes: 12/14/16/20/24/32 px
  • Line Height: 1.2 (tight), 1.35 (normal)

Components

See src/components/ for implementation. All components:

  • WCAG AA+ contrast
  • Keyboard accessible
  • Mobile-first (touch targets β‰₯ 44px)
  • Reduced motion support

πŸ” Smart Contracts

LandmarkRegistry

Core Functions:

  • postPin(geohash6, region3, contentCID) β†’ Stakes 0.1 $LMRK, emits LandmarkPosted
  • upvote(pinId, amount) β†’ Splits 90% to author, 10% to treasury
  • withdrawStake(pinId) β†’ After 7-day cooldown, 1% burn fee
  • settleRegion(region3, rewardAmount, topPins, shares) β†’ Distributes daily rewards

Constants:

  • STAKE_AMOUNT: 0.1 $LMRK
  • COOLDOWN_PERIOD: 7 days
  • WITHDRAW_BURN_BPS: 100 (1%)
  • UPVOTE_AUTHOR_BPS: 9000 (90%)

LandmarkNFT

Core Functions:

  • mintFromPost(landmarkId, tokenURI) β†’ Burns 10 $LMRK, mints ERC-721

Note: Each landmark can only be minted once.


πŸ“Š Indexer

The indexer:

  1. Subscribes to Base RPC for LandmarkPosted, LandmarkVoted, RegionSettled events
  2. Decodes geohashes to lat/lng
  3. Stores in Supabase (landmarks, leaderboard tables)
  4. Computes daily top-20 per region3

Run Indexer:

pnpm indexer:dev

For production, deploy as a serverless function (Vercel Cron, Railway, etc.).


🌐 API Endpoints

GET /api/landmarks

Fetch landmarks in viewport.

Query Params:

  • minLat, minLng, maxLat, maxLng: Bounding box
  • limit: Max results (default 500)

POST /api/upload

Upload image to Supabase Storage.

Body: multipart/form-data with file field

Response:

{
  "cid": "path/to/file",
  "url": "https://xxx.supabase.co/storage/v1/..."
}

POST /api/nft/metadata

Generate NFT metadata JSON.

Body:

{
  "landmarkId": "123",
  "imageUrl": "https://...",
  "caption": "Amazing view!"
}

Response:

{
  "tokenURI": "https://xxx.supabase.co/storage/v1/.../metadata/123.json"
}

🎯 User Flows

Drop a Landmark (≀60s)

  1. Tap [+] FAB
  2. Upload photo β†’ Auto-strips EXIF
  3. Write caption (≀200 chars)
  4. Geohash auto-fills from map center
  5. Tap Post β†’ Approve $LMRK β†’ Confirm stake
  6. Success toast: "Posted β€’ Stake locked"

Upvote

  1. Tap marker on map β†’ Detail sheet opens
  2. Tap Upvote (0.01 $LMRK)
  3. Approve β†’ Confirm β†’ "+1 upvote sent"

Mint Placemark NFT

  1. In landmark detail β†’ Tap Mint NFT
  2. Modal shows burn amount (10 $LMRK)
  3. Progress: Approve β†’ Generate metadata β†’ Mint
  4. Success: "Placemark minted"

πŸ”§ Configuration

Feature Flags

Set in .env.local:

  • NEXT_PUBLIC_FF_SHARE_CARDS: Enable share card generation
  • NEXT_PUBLIC_FF_PWA: Enable PWA features
  • NEXT_PUBLIC_FF_A11Y_HI_CONTRAST: High-contrast mode
  • NEXT_PUBLIC_FF_OFFLINE_QUEUE: Queue posts/votes offline

Bundle Optimization

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

πŸ§ͺ Testing

# Type check
pnpm type-check

# Lint
pnpm lint

# Smart contract tests
cd contracts
forge test

🚒 Deployment

Frontend (Vercel)

  1. Connect repo to Vercel
  2. Set environment variables
  3. Deploy: vercel --prod

Contracts (Base Mainnet)

cd contracts
forge script script/Deploy.s.sol:DeployScript --rpc-url $BASE_RPC_URL --broadcast --verify

Indexer (Railway / Render)

Deploy 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"]

πŸ“ˆ Performance Targets

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+

β™Ώ Accessibility

  • 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

πŸ”’ Security

  • 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

πŸ“– References


🀝 Contributing

Contributions welcome! Please:

  1. Fork the repo
  2. Create a feature branch: git checkout -b feature/amazing
  3. Commit changes: git commit -m 'Add amazing feature'
  4. Push: git push origin feature/amazing
  5. Open a Pull Request

πŸ“„ License

MIT License - see LICENSE for details.


πŸ™ Acknowledgments

  • Base for the incredible L2 infrastructure
  • Coinbase for OnchainKit and MiniKit
  • Farcaster for the MiniApps framework
  • MapLibre for the open-source map renderer

πŸ“ž Support


Built with ❀️ on Base

About

Landmark NFT project

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published