A referral tracking system for Loot Survivor on Starknet. Track which players brought in new users who actually played the game.
- π Referral Link Capture: Automatically captures referral addresses from URL parameters
- πΎ LocalStorage Persistence: Saves referral data until wallet connection
- π Wallet Integration: Cartridge Controller (Starknet) wallet connection
- β On-Chain Verification: Verifies actual gameplay via Starknet RPC
- π Leaderboard: Real-time leaderboard showing top referrers
- π¨ Dark Theme UI: Modern, clean interface with Tailwind CSS
- Frontend: Next.js 14 (App Router), Tailwind CSS, Lucide-react icons
- Backend/Database: Supabase (PostgreSQL)
- Web3: Cartridge Controller (Starknet)
- Verification: Starkscan API
npm installThe repo uses starknet@9 and @cartridge/connector / @cartridge/controller latest. Because @starknet-react/core@5 declares a peer dependency on starknet@^8, .npmrc sets legacy-peer-deps=true so npm install succeeds. You can update all of these with npm update (or bump versions in package.json and run npm install again).
Create a .env.local file in the root directory:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
VERIFY_API_KEY=your_verify_endpoint_secret
NEXT_PUBLIC_LOOT_SURVIVOR_CONTRACT=0x0100000000000000000000000000000000000000000000000000000000000000
NEXT_PUBLIC_STARKNET_CHAIN=mainnet
NEXT_PUBLIC_STARKNET_RPC=your_starknet_rpc_url
# Invite Code Ticket Claimer (server-only; never expose admin key or real codes)
STARKNET_RPC=your_starknet_rpc_url
ADMIN_ADDRESS=your_admin_starknet_account_address
ADMIN_PRIVATE_KEY=your_admin_private_key
INVITE_CODES=code1,code2,code3
# Invite claim state (required for ticket claimer): Supabase table ticket_claims
NEXT_PUBLIC_SUPABASE_URL_INVITE=https://xxxxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY_INVITE=your_service_role_keyFor the Invite Ticket Claimer, claim state is stored only in Supabase (no JSON file). Create the table in your invite Supabase project:
- Open the project (e.g. the one used for
NEXT_PUBLIC_SUPABASE_URL_INVITE) β SQL Editor β New query β paste and run the contents ofsupabase/ticket_claims.sql.
Set NEXT_PUBLIC_SUPABASE_URL_INVITE and SUPABASE_SERVICE_ROLE_KEY_INVITE so the claim flow can persist and enforce one claim per address and per-code limits.
HTTPS (recommended for dev, e.g. wallet connections):
- Install mkcert and run
mkcert -installonce. - Generate certs:
npm run certs - Start the server:
npm run dev
β https://localhost:3000
HTTP only: npm run dev:http β http://localhost:3000
Open the URL in your browser.
- User visits with referral link:
https://yoursite.com?ref=0x... - Referral captured: The
refparameter is saved to localStorage - Wallet connection: When user connects their Cartridge Controller wallet
- Referral mapping created: POST request to
/api/referralscreates the mapping - On-chain verification: Server checks Starkscan API for actual gameplay
- Leaderboard update: Verified referrals appear on the leaderboard
The verification system checks if a referee address has interacted with the Loot Survivor contract:
- Fetches all referrals where
has_played = false - For each address, queries Starkscan API for transactions
- Updates
has_played = trueif transactions are found
You can trigger verification manually:
curl -X POST http://localhost:3000/api/verify -H "x-verify-key: $VERIFY_API_KEY"Or set up a cron job to run periodically:
# Example cron job (runs every hour)
0 * * * * curl -X POST https://your-domain.com/api/verify -H "x-verify-key: $VERIFY_API_KEY"For production, consider using:
- Vercel Cron Jobs
- GitHub Actions
- A dedicated cron service
Create a new referral mapping.
Request:
{
"referee_address": "0x...",
"referrer_address": "0x..."
}Response:
{
"success": true,
"data": { ... }
}Get leaderboard data.
Response:
{
"data": [
{
"rank": 1,
"referrer_address": "0x...",
"total_points": 10
}
]
}Trigger on-chain verification for all unverified referrals.
Response:
{
"message": "Verified 5 out of 10 referrals",
"verified": 5,
"total": 10
}Get verification statistics.
Response:
{
"total": 100,
"verified": 75,
"unverified": 25
}Claim 1 Dungeon Ticket using an invite code (Admin Push: gas paid by server).
Request:
{
"inviteCode": "YOUR_INVITE_CODE",
"address": "0x_your_starknet_address"
}Response (success):
{
"success": true,
"transactionHash": "0x...",
"message": "1 Dungeon Ticket has been sent to your address."
}Security: Admin private key is server-only. Starknet address format is validated before sending the transaction. Each address can claim only once; each code is limited to 25 uses. Invite codes must not be public: set them in the INVITE_CODES environment variable (comma-separated, server-only); do not commit real codes.
The app includes an Invite Code Ticket Claimer that lets users enter a text invite code and their Starknet address to receive 1 Dungeon Ticket. The transfer is executed by the Admin Wallet (gas paid server-side).
- Valid codes: Set via
INVITE_CODESenv var (comma-separated, e.g.INVITE_CODES=CODE1,CODE2,CODE3). Never commit real codes; optional fallback filelib/invite-codes.jsonis gitignored. - Max uses per code: 25.
- One claim per address: Each Starknet address can only claim once.
- Constants: Ticket contract
0x0452810188C4Cb3AEbD63711a3b445755BC0D6C4f27B923fDd99B1A118858136.
Required env vars: STARKNET_RPC, ADMIN_ADDRESS, ADMIN_PRIVATE_KEY. Important: (1) ADMIN_ADDRESS must be a deployed Starknet account on the same network as STARKNET_RPC. (2) The admin account must hold Dungeon Ticket tokens at the ticket contract address β each claim transfers 1 ticket from the admin to the user, so "ERC20: INSUFFICIENT BALANCE" means the admin wallet needs to be funded with tickets. Claim state is stored only in Supabase: set NEXT_PUBLIC_SUPABASE_URL_INVITE and SUPABASE_SERVICE_ROLE_KEY_INVITE and run supabase/ticket_claims.sql in that project.
βββ app/
β βββ api/
β β βββ claim-ticket/
β β β βββ route.ts # Invite code ticket claim (admin push)
β β βββ referrals/
β β β βββ route.ts # Referral CRUD endpoints
β β βββ verify/
β β βββ route.ts # On-chain verification
β βββ globals.css # Global styles
β βββ layout.tsx # Root layout
β βββ page.tsx # Main page
βββ components/
β βββ InviteCodeTicketClaimer.tsx # Invite code ticket claim UI
β βββ ReferralLeaderboard.tsx # Leaderboard component
β βββ ReferralShare.tsx # Share referral link component
β βββ WalletConnection.tsx # Wallet connection component
βββ hooks/
β βββ useReferral.ts # Referral capture hook
βββ lib/
β βββ claim-state.ts # Claim state (Supabase ticket_claims only)
β βββ invite-codes.example.json # Example format only; real codes via INVITE_CODES env
β βββ referral.ts # Referral utilities
β βββ supabase.ts # Supabase client
βββ supabase/
β βββ migrations/
β βββ 001_create_referrals_table.sql
βββ package.json
The app expects Cartridge Controller to be available at window.cartridge.controller. Make sure users have the Cartridge extension installed.
The wallet connection component:
- Checks for Cartridge Controller on mount
- Polls connection status
- Automatically submits referral when wallet connects
- Handles connect/disconnect events
When you click βConnect Walletβ, the Cartridge Controller opens in an iframe loaded from api.cartridge.gg. The errors you see (Google Fonts blocked, Stripe blocked, WASM βunreachableβ) come from that iframeβs Content-Security-Policy β i.e. from Cartridgeβs server, not from this app. We canβt change their headers.
Workarounds that often fix Connect Wallet in dev:
-
Use HTTPS
Runnpm run certsthennpm run devand open https://localhost:3000 (nothttp://). -
Temporarily relax CSP for localhost (Chrome)
- Install an extension that disables CSP for the current site, e.g. βDisable Content-Security-Policyβ or βCSP Unblockβ.
- Enable it only for
https://localhost:3000(or your dev URL). - Reload and try βConnect Walletβ again.
Use only for local development; leave it off on production sites.
-
Try another browser
Sometimes one browser (e.g. Chrome vs Firefox) works when the other doesnβt. -
Report upstream
If it still fails, open an issue with Cartridge (controller) and mention that their iframe CSP blockshttps://fonts.googleapis.comandhttps://js.stripe.com, which breaks the connect flow. They need to add those tostyle-src/script-src(or equivalent) for their Controller UI.
- API Protection: Consider adding authentication to
/api/verifyendpoint - Rate Limiting: Implement rate limiting on referral creation
- Input Validation: Address format validation is already implemented
- Self-Referral Prevention: The system prevents users from referring themselves
MIT