Stake your opinion. The crowd decides. The loser pays the winner.
Provit is a social debate staking platform built on Starknet. Users stake USDC on their opinion, the crowd votes, and a supermajority settlement determines who wins the pool. No oracles. No bookmakers. The crowd is the truth machine.
Built Africa-first. Powered by Cairo smart contracts. Gas-free for all users via AVNU paymaster.
- What is Provit
- How it works
- The 5 Tribes
- Tech Stack
- Architecture
- Smart Contracts
- Gas Strategy
- Sybil Resistance
- Reputation System
- Revenue Streams
- Project Structure
- Environment Variables
- Getting Started
- Contract Deployment
- Fixing snforge USC Error on Windows
- Running Tests
- Roadmap
Every day, millions of people argue on Twitter, WhatsApp, and in barbershops — about music, sports, relationships, money. Their opinions are never rewarded for being right.
Provit changes that.
Post a debate. Stake USDC on your position. The crowd votes. If 65% or more agree with you — you win the pool. Your reputation grows onchain. Your judgment becomes a public, permanent record.
Not a casino. Not a prediction market. A crowd-settled opinion market.
- Pick a tribe (Music, Sports, Film, Relationships, Finance)
- Post a debate topic and choose your side — e.g. "Burna Boy has more global impact than Wizkid"
- Stake between $0.50 and $10 USDC on your position
- An opponent stakes the same amount on the opposite side
- The 24-hour voting window opens to the public
- If 65%+ of crowd votes agree with you → you win the pool (minus 5% platform fee)
- If the vote is too close (under 65% either way) → draw, both stakers refunded minus 1%
- Claim your USDC. Your reputation updates automatically.
- Log in with Google (no wallet setup needed)
- Browse debate feeds by tribe
- Vote on any active debate — completely free, gas sponsored
- One vote per debate per account
- Correct votes earn +5 reputation in that tribe
Settlement is fully automatic. A cron job runs every 5 minutes, reads the vote tally from the VoteRegistry contract, applies the 65% supermajority rule, and calls settle_debate() using the server wallet. No manual intervention ever needed.
Each tribe has its own feed, leaderboard, and featured debate slots. Users pick a tribe before posting a debate.
| Tribe | Focus |
|---|---|
| 🎵 Music | Afrobeats, Afropop, Hip-hop, global artists |
| ⚽ Sports | AFCON, NPFL, EPL, football debates |
| 🎬 Film | Nollywood, Hollywood, streaming content |
| 💔 Relationships | Dating, marriage, gender dynamics |
| 📈 Finance | Crypto, naira, economy, investment |
| Layer | Technology |
|---|---|
| Frontend | Next.js 14, TypeScript, Tailwind CSS, PWA |
| Auth | Privy (Google + email login, embedded wallets) |
| Blockchain | Starknet (Cairo smart contracts) |
| Gas sponsorship | AVNU Gasless SDK (Plan A) / Server wallet (Plan B) |
| USDC on-ramp | Transak (card + Nigerian bank transfer) |
| Backend | Node.js, Express |
| Database | Supabase (Postgres) |
| Hosting | Vercel (frontend), Railway (backend + cron) |
| Push notifications | Web Push (VAPID) |
| RPC | Alchemy (Starknet Sepolia + Mainnet) |
| OG images | Vercel OG / Satori |
User (Google login via Privy)
│
▼
Next.js PWA (Vercel)
│
├── Debate feed (tribes)
├── Create debate → stakerCheck → AVNU sponsored tx
├── Vote → voterCheck (Privy user_id) → AVNU sponsored tx
├── Claim winnings → AVNU sponsored tx
└── Top-up modal → Transak on-ramp
│
▼
Node.js API (Railway)
│
├── POST /api/debate/create
├── POST /api/debate/join
├── POST /api/vote ← Sybil check + paymaster
├── POST /api/claim
├── POST /api/featured/create
├── GET /api/debates?tribe=music
├── GET /api/leaderboard/:tribe
└── GET /api/og/debate/:id
│
├── Supabase (Postgres)
│ ├── users
│ ├── debates
│ ├── voter_records ← unique(privy_user_id, debate_id)
│ ├── rep_history
│ ├── ip_rate_limits
│ ├── featured_slots
│ └── push_subscriptions
│
└── Settlement cron (every 5 min)
│
├── Reads VoteRegistry.get_tally()
├── Applies 65% supermajority
└── Calls DebateEscrow.settle_debate()
│
▼
Starknet Mainnet
├── DebateEscrow.cairo ← escrow + settlement
├── VoteRegistry.cairo ← vote tallies
└── FeeVault.cairo ← platform revenue
| Onchain | Offchain (Supabase) |
|---|---|
| USDC escrow amounts | Debate topic text |
| Vote tallies (A vs B count) | Category / tribe |
| Settlement outcome | User display names |
| Payout transfers | Reputation scores |
| Platform fee extraction | Vote history per user |
| Topic hash (keccak) | Feed ordering |
Only the keccak hash of the debate topic goes onchain. Full topic text lives in Supabase, allowing content moderation without touching contracts.
Three contracts. Deployed in this order: FeeVault → VoteRegistry → DebateEscrow.
Handles all money movement. The only contract that touches USDC.
Structs:
Debate {
id: felt252
creator: ContractAddress
opponent: ContractAddress
stake_amount: u256
creator_side: u8 // 0 = Side A, 1 = Side B
status: DebateStatus
created_at: u64
vote_deadline: u64 // created_at + 86400s (24 hours)
join_deadline: u64 // created_at + 172800s (48 hours)
votes_a: u32
votes_b: u32
winner: ContractAddress
tribe: felt252 // hash of tribe name
}
DebateStatus: Open | Active | Settled | Draw | Expired
Functions:
create_debate(topic_hash, stake, tribe) → debate_id
join_debate(debate_id) // opponent joins opposite side
settle_debate(debate_id) // callable after vote_deadline
expire_debate(debate_id) // callable after join_deadline if no opponent
withdraw_winnings(debate_id) // pull pattern — winner claims
Settlement logic inside settle_debate():
total = votes_a + votes_b
if (votes_a * 100 / total) >= 65 → Side A wins
if (votes_b * 100 / total) >= 65 → Side B wins
else → Draw: refund both minus 1% fee to FeeVault
Access control: only the contract owner (server wallet) can call settle_debate(). Winners use withdraw_winnings() themselves — the contract never pushes funds.
Handles vote counting. Completely separate from money — can be upgraded without touching escrow.
Storage:
debate_votes: LegacyMap<(felt252, ContractAddress), bool>
debate_tallies: LegacyMap<felt252, (u32, u32)> // (votes_a, votes_b)
Functions:
cast_vote(debate_id, side) // user pays own gas via paymaster
cast_vote_for(debate_id, side, voter) // Plan B: server pays gas on voter's behalf
get_tally(debate_id) → (u32, u32) // read by DebateEscrow at settlement
Rules:
- One vote per wallet address per debate (enforced onchain)
- Votes only accepted during active vote window
- cast_vote_for() is owner-only — only server wallet can call it
Receives platform fees from every settlement.
Functions:
receive_fee(amount) // called by DebateEscrow
withdraw(to) // owner only
get_balance() → u256
All user transactions are gas-sponsored. Users never see or manage STRK.
Every transaction is sponsored by AVNU's gasless SDK: debate creation, joining, voting, and claiming winnings.
// gasStrategy.ts — controlled by one env var
GAS_STRATEGY=paymaster // use AVNU
async function sponsorTx(calls, walletAddress, privyWallet) {
if (process.env.GAS_STRATEGY === 'paymaster') {
return avnuSponsor(calls, walletAddress, privyWallet)
}
return selfHostedSponsor(calls, walletAddress)
}Apply for AVNU paymaster approval at avnu.fi → Developer → Gasless SDK. Takes 3–7 days. Apply before writing any frontend code.
If AVNU approval is pending or rejected:
- Voters: server wallet pays gas for
cast_vote_for()calls - Stakers: lazy prefund — server sends 0.003 STRK to staker wallet only on first stake attempt, only if their STRK balance is insufficient. Returning stakers with existing STRK are never prefunded again.
// lazyPrefund.ts
async function lazyPrefund(walletAddress: string) {
const strk = await getSTRKBalance(walletAddress)
if (strk < MIN_GAS_THRESHOLD) {
await serverAccount.execute([{
contractAddress: STRK_ADDRESS,
entrypoint: 'transfer',
calldata: [walletAddress, PREFUND_AMOUNT]
}])
}
// else: skip — wallet already has enough gas
}Switch between plans with one env var change. No code changes needed.
The settlement cron always uses the server wallet with its own STRK balance — not paymaster, not prefund. Fund the server wallet with 10 STRK; it covers ~5,000 settlement calls.
| Transaction | Gas cost (Starknet) |
|---|---|
| create_debate | ~$0.002 |
| join_debate | ~$0.002 |
| cast_vote | ~$0.001 |
| withdraw_winnings | ~$0.002 |
| settle_debate (server) | ~$0.002 |
| Full debate lifecycle (4 txs) | ~$0.008 |
| 5% rake on $10 pool | $0.50 |
| Rake covers gas cost by | 62x |
Different mechanisms for stakers and voters. No circular dependencies.
One check: USDC balance ≥ stake amount.
Stakers must hold real USDC to participate. Gas being free changes nothing — the stake itself is the economic barrier. A fake account with no USDC cannot stake.
async function stakerCheck(walletAddress: string, stakeAmount: bigint) {
const usdcBalance = await getUSDCBalance(walletAddress)
if (usdcBalance < stakeAmount) {
throw new Error('Insufficient USDC balance')
}
}Layer 1 — Privy user_id uniqueness (backend)
One Google account = one vote per debate. Enforced via a unique constraint on (privy_user_id, debate_id) in Supabase. Google's anti-abuse systems make creating thousands of fake Google accounts genuinely hard.
async function voterCheck(privyUserId: string, debateId: string) {
const { error } = await supabase
.from('voter_records')
.insert({ privy_user_id: privyUserId, debate_id: debateId })
if (error?.code === '23505') { // unique violation
throw new Error('Already voted on this debate')
}
}Layer 2 — IP rate limit (backend)
Max 3 sponsored vote transactions per IP address per debate per 24 hours. Catches multi-account creation from the same device/network.
Layer 3 — Contract duplicate wallet check (onchain)
The VoteRegistry mapping rejects any wallet that has already voted on a debate. This is the non-bypassable floor — it holds even if someone calls the contract directly without going through the backend.
Reputation is the product. Staking is the mechanic.
Win:
base_points = 100
stake_multiplier = log10(stake_usdc * 100) + 1
rep_delta = base_points * stake_multiplier
Examples:
$1 stake win → multiplier 1.0 → +100 rep
$5 stake win → multiplier 1.7 → +170 rep
$10 stake win → multiplier 2.0 → +200 rep
Loss:
rep_delta = base_points * multiplier * 0.3 (participation credit)
$5 stake loss → +51 rep
Correct vote:
rep_delta = +5 (flat, no stake multiplier)
Rep never decreases. Accuracy % = wins / (wins + losses) shown separately.
- Debate card — creator's rep + accuracy shown under their name
- Tribe leaderboards — top 20 predictors per tribe, refreshed weekly
- Public profile — rep radar across all 5 tribes, debate history, total earned
- Settlement ceremony — rep delta shown immediately after result
-- Per-tribe rep on users table
ALTER TABLE users
ADD COLUMN rep_music INTEGER DEFAULT 0,
ADD COLUMN rep_sports INTEGER DEFAULT 0,
ADD COLUMN rep_film INTEGER DEFAULT 0,
ADD COLUMN rep_relationships INTEGER DEFAULT 0,
ADD COLUMN rep_finance INTEGER DEFAULT 0,
ADD COLUMN accuracy_rate DECIMAL(5,2) DEFAULT 0,
ADD COLUMN total_earned DECIMAL(10,2) DEFAULT 0;
-- Rep history with stake weighting
ALTER TABLE rep_history
ADD COLUMN stake_amount DECIMAL(10,2) DEFAULT 0,
ADD COLUMN multiplier DECIMAL(4,2) DEFAULT 1.0,
ADD COLUMN tribe TEXT,
ADD COLUMN outcome TEXT CHECK (outcome IN ('win','loss','vote_correct','vote_wrong'));| Stream | Rate | Scales with |
|---|---|---|
| Protocol rake — settled debates | 5% of pool | Stake volume |
| Protocol rake — draw refunds | 1% of pool | Debate volume |
| Featured tribe slot | $5 USDC / debate / 24hr | Audience size |
| Global featured slot (all tribes) | $15 USDC / debate / 24hr | Audience size |
| Rep Pass (V2) | $5/month | User count |
User or brand pays 5 USDC to FeeVault
→ Backend records: featured_slots(debate_id, tribe, expires_at)
→ Feed API returns featured debates first
→ Debate shows "Featured" badge in tribe feed
→ Auto-expires after 24 hours
At 50 debates/day, $5 avg stake:
Rake = 50 × $10 pool × 5% = $25/day = $750/mo
Featured = 3 slots/day × $5 = $15/day = $450/mo
Total = ~$1,200/mo
At 200 debates/day + 10 featured slots/day:
Rake = $500/day
Featured = $50/day
Total = ~$16,500/mo
provit/
├── apps/
│ ├── web/ # Next.js 14 PWA
│ │ ├── pages/
│ │ │ ├── index.tsx # Home feed (tribe tabs)
│ │ │ ├── tribe/[name].tsx # Tribe feed + leaderboard
│ │ │ ├── debate/[id]/
│ │ │ │ ├── index.tsx # Single debate view
│ │ │ │ └── result.tsx # Settlement ceremony
│ │ │ ├── create.tsx # Post a debate
│ │ │ ├── profile/[address].tsx # Public profile + rep radar
│ │ │ └── leaderboard.tsx # Global + tribe leaderboards
│ │ ├── components/
│ │ │ ├── DebateCard.tsx
│ │ │ ├── SettlementCeremony.tsx
│ │ │ ├── VoteRevealBar.tsx
│ │ │ ├── ShareResultCard.tsx
│ │ │ ├── TribePicker.tsx
│ │ │ ├── TribeFeed.tsx
│ │ │ ├── TribeLeaderboard.tsx
│ │ │ ├── RepRadar.tsx
│ │ │ ├── RepDelta.tsx
│ │ │ ├── FeaturedBadge.tsx
│ │ │ ├── StakeModal.tsx
│ │ │ ├── TopUpModal.tsx
│ │ │ ├── BalanceGate.tsx
│ │ │ └── VoteBar.tsx
│ │ ├── hooks/
│ │ │ ├── useAuth.ts # Privy session
│ │ │ ├── useWallet.ts # USDC balance + canStake()
│ │ │ ├── useDebate.ts # Debate state
│ │ │ ├── useVote.ts # Vote + paymaster flow
│ │ │ └── useTopUp.ts # Transak integration
│ │ ├── lib/
│ │ │ ├── starknet.ts # Provider config
│ │ │ ├── avnu.ts # Paymaster helper
│ │ │ └── transak.ts # On-ramp URL builder
│ │ ├── public/
│ │ │ ├── manifest.json # PWA manifest
│ │ │ ├── icon-192.png
│ │ │ └── icon-512.png
│ │ └── next.config.js # next-pwa config
│ │
│ └── server/ # Node.js API
│ ├── routes/
│ │ ├── debate.ts # create, join, feed
│ │ ├── vote.ts # vote with Sybil check
│ │ ├── claim.ts # withdraw winnings
│ │ ├── featured.ts # featured slot purchase
│ │ ├── leaderboard.ts # tribe leaderboards
│ │ └── og.ts # result card images
│ ├── services/
│ │ ├── gasStrategy.ts # Plan A/B switch
│ │ ├── avnuSponsor.ts # Plan A: AVNU paymaster
│ │ ├── selfHostedSponsor.ts # Plan B: server wallet
│ │ ├── lazyPrefund.ts # Plan B: staker STRK prefund
│ │ ├── stakerCheck.ts # USDC balance validation
│ │ ├── voterCheck.ts # Privy user_id uniqueness
│ │ ├── ipRateCheck.ts # IP rate limiter
│ │ ├── settlementCron.ts # 5-min settlement runner
│ │ ├── eventIndexer.ts # Starknet event sync
│ │ ├── reputationCalc.ts # Stake-weighted rep formula
│ │ ├── reputationUpdater.ts # Post-settlement rep writes
│ │ └── pushNotifier.ts # Web push sender
│ └── lib/
│ ├── supabase.ts
│ └── starknet.ts
│
└── contracts/ # Cairo contracts
├── src/
│ ├── DebateEscrow.cairo
│ ├── VoteRegistry.cairo
│ └── FeeVault.cairo
├── tests/
│ ├── debate_test.cairo # 7 tests
│ ├── vote_test.cairo # 4 tests
│ └── vote_for_test.cairo # 2 tests (Plan B)
├── scripts/
│ └── deploy.ts # Deploy order: FeeVault → VoteRegistry → DebateEscrow
└── Scarb.toml
Create .env.local in apps/web and .env in apps/server. Never commit these files.
# Auth
PRIVY_APP_ID=
PRIVY_APP_SECRET=
NEXT_PUBLIC_PRIVY_APP_ID=
# Starknet
STARKNET_RPC_URL= # Alchemy Sepolia or Mainnet
SERVER_WALLET_ADDRESS= # Burner wallet for cron + Plan B
SERVER_WALLET_PK= # Keep this secret
# Contracts (fill after deployment)
DEBATE_ESCROW_ADDRESS=
VOTE_REGISTRY_ADDRESS=
FEE_VAULT_ADDRESS=
USDC_CONTRACT_ADDRESS=
# Gas strategy
GAS_STRATEGY=selfhosted # Change to: paymaster after AVNU approval
AVNU_API_KEY= # From AVNU developer portal
# Database
DATABASE_URL= # Supabase connection string
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
# On-ramp
TRANSAK_API_KEY=
TRANSAK_ENV=staging # Change to: production at launch
# Push notifications
VAPID_PUBLIC_KEY= # Generate: npx web-push generate-vapid-keys
VAPID_PRIVATE_KEY=
NEXT_PUBLIC_VAPID_PUBLIC_KEY= # Same public key, exposed to frontend- Node.js 18+
- Rust (for Scarb/snforge)
- Scarb (Cairo package manager)
- Starknet Foundry (snforge)
git clone https://github.com/yourhandle/provit.git
cd provit
# Install frontend
cd apps/web && npm install
# Install backend
cd ../server && npm installCopy the variables from the section above into your .env files.
Run these migrations in your Supabase SQL editor:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wallet_address TEXT UNIQUE NOT NULL,
privy_id TEXT UNIQUE NOT NULL,
username TEXT,
rep_music INTEGER DEFAULT 0,
rep_sports INTEGER DEFAULT 0,
rep_film INTEGER DEFAULT 0,
rep_relationships INTEGER DEFAULT 0,
rep_finance INTEGER DEFAULT 0,
accuracy_rate DECIMAL(5,2) DEFAULT 0,
total_earned DECIMAL(10,2) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE debates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
onchain_id TEXT UNIQUE NOT NULL,
topic TEXT NOT NULL,
tribe TEXT NOT NULL CHECK (tribe IN ('music','sports','film','relationships','finance')),
side_a_label TEXT NOT NULL,
side_b_label TEXT NOT NULL,
creator TEXT NOT NULL,
opponent TEXT,
stake_usdc DECIMAL(10,2) NOT NULL,
status TEXT DEFAULT 'Open' CHECK (status IN ('Open','Active','Settled','Draw','Expired')),
votes_a INTEGER DEFAULT 0,
votes_b INTEGER DEFAULT 0,
winner TEXT,
is_featured BOOLEAN DEFAULT FALSE,
featured_until TIMESTAMPTZ,
vote_deadline TIMESTAMPTZ,
join_deadline TIMESTAMPTZ,
tx_hash TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE voter_records (
privy_user_id TEXT NOT NULL,
debate_id UUID NOT NULL REFERENCES debates(id),
side INTEGER NOT NULL,
tx_hash TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(privy_user_id, debate_id)
);
CREATE TABLE rep_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_wallet TEXT NOT NULL,
debate_id UUID REFERENCES debates(id),
delta INTEGER NOT NULL,
stake_amount DECIMAL(10,2) DEFAULT 0,
multiplier DECIMAL(4,2) DEFAULT 1.0,
tribe TEXT,
outcome TEXT CHECK (outcome IN ('win','loss','vote_correct','vote_wrong')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE ip_rate_limits (
ip TEXT NOT NULL,
debate_id UUID NOT NULL,
count INTEGER DEFAULT 1,
window_start TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (ip, debate_id)
);
CREATE TABLE featured_slots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
debate_id UUID NOT NULL REFERENCES debates(id),
tribe TEXT NOT NULL,
paid_by TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
tx_hash TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE push_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wallet_address TEXT NOT NULL,
subscription_json JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);npx web-push generate-vapid-keysCopy the output into your .env files.
# Terminal 1 — frontend
cd apps/web && npm run dev
# Terminal 2 — backend
cd apps/server && npm run devDeploy contracts in this exact order. Each subsequent contract needs the address of the previous one.
cd contracts
# 1. Build all contracts first
scarb build
# 2. Deploy FeeVault (no constructor args)
sncast deploy \
--contract-name FeeVault \
--rpc-url $STARKNET_RPC_URL \
--account $SERVER_WALLET_ADDRESS
# Save output address as FEE_VAULT_ADDRESS
# 3. Deploy VoteRegistry (no constructor args)
sncast deploy \
--contract-name VoteRegistry \
--rpc-url $STARKNET_RPC_URL \
--account $SERVER_WALLET_ADDRESS
# Save output address as VOTE_REGISTRY_ADDRESS
# 4. Deploy DebateEscrow (needs VoteRegistry + FeeVault + USDC addresses)
sncast deploy \
--contract-name DebateEscrow \
--constructor-calldata $VOTE_REGISTRY_ADDRESS $FEE_VAULT_ADDRESS $USDC_CONTRACT_ADDRESS \
--rpc-url $STARKNET_RPC_URL \
--account $SERVER_WALLET_ADDRESS
# Save output address as DEBATE_ESCROW_ADDRESSUpdate all three addresses in your .env files. Verify all contracts on Starkscan.
If you see this error when running snforge test:
[ERROR] Cannot find `universal-sierra-compiler` binary.
Make sure you have USC installed and added to PATH
The root cause: USC (Universal Sierra Compiler) is a required dependency of snforge that was not installed or was not added to your Windows PATH.
The fix — choose one of the three options below:
When using starkup on Windows, use WSL, as the standard install script only works on macOS and Linux.
# Inside WSL terminal:
curl --proto '=https' --tlsv1.2 -sSf https://sh.starkup.sh | sh
# Verify installation
snforge --versionUniversal-Sierra-Compiler will be automatically installed if you use snfoundryup. No separate USC step needed.
- Go to
https://github.com/software-mansion/universal-sierra-compiler/releases - Download the latest
universal-sierra-compiler-x86_64-pc-windows-msvc.zip - Extract it — you will get a
universal-sierra-compiler.exefile - Move the
.exeto a folder on your PATH, for exampleC:\Users\hp\.cargo\bin\ - Open a new terminal and verify:
universal-sierra-compiler --version- Run
snforge testagain — the error will be gone.
If you cannot modify your PATH, point snforge directly at the binary:
# Windows CMD
set UNIVERSAL_SIERRA_COMPILER=C:\path\to\universal-sierra-compiler.exe
snforge test
# Windows PowerShell
$env:UNIVERSAL_SIERRA_COMPILER="C:\path\to\universal-sierra-compiler.exe"
snforge testTo make it permanent, add UNIVERSAL_SIERRA_COMPILER to your Windows system environment variables via System Properties → Advanced → Environment Variables.
cd contracts
# Run all tests
snforge test
# Run specific test file
snforge test --filter debate_test
# Run with verbose output
snforge test -vDebateEscrow — 7 tests:
- Happy path win — create → join → votes reach 65% → settle → winner withdraws
- Draw path — votes 55/45 → settle → both refunded
- Expire path — no opponent → expire → creator refunded
- Duplicate vote reverts
- Wrong stake amount on join reverts
- Settle before vote deadline reverts
- Non-owner settle attempt reverts
VoteRegistry — 4 tests:
- Cast vote records correctly
- Duplicate vote from same wallet reverts
- Vote after deadline reverts
- Tally returns accurate counts
VoteRegistry Plan B — 2 tests:
- cast_vote_for by owner records voter address correctly
- cast_vote_for by non-owner reverts
- Competitive landscape analysis (Polymarket, Kalshi, Opinion.Trade)
- Architecture design
- Anti-Sybil mechanism design
- AVNU paymaster application submitted
- Name and domain registered
- Twitter poll — top debate category validated
- 30 seed debates written (6 per tribe)
- VoteRegistry.cairo — all functions + tests
- DebateEscrow.cairo — all functions + tests
- FeeVault.cairo
- All 13 tests passing
- Deployed to Starknet Sepolia
- sybilCheck (voterCheck) service built
- IP rate limiter built
- Supabase schema migrated
- All API routes (debate, vote, claim, featured, leaderboard, OG)
- gasStrategy.ts with Plan A/B switch
- Settlement cron job
- Event indexer
- reputationCalc.ts with stake-weighted formula
- Push notification service
- All routes tested via Postman against Sepolia
- Privy auth + useWallet hook
- TribePicker + TribeFeed
- DebateCard with rep display
- Settlement ceremony (build first)
- Create debate flow + BalanceGate
- Vote flow with paymaster
- TopUpModal + Transak integration
- RepRadar profile page
- Tribe leaderboard page
- PWA manifest + service worker
- Push notification permission flow
- Stake-weighted reputation system end-to-end
- Seed 30 debates across all tribes
- Security audit (re-entrancy, integer overflow, access control)
- Deploy to Starknet Mainnet
- Live fire test — 10 real users, real USDC
- Pass conditions: 5+ debates settled, 2+ users stake second debate unprompted
- Twitter launch thread with settlement ceremony recording
- Starknet ecosystem posts
- First featured slot sold
- 50+ signups in first 48 hours
Five notification types, triggered automatically:
| Notification | Trigger | Body |
|---|---|---|
| You won | Debate settled, user is winner | "You won +$X.XX on '[topic]' — tap to claim" |
| Debate is live | Opponent joins your debate | "Someone took the other side. Voting opens now — 24 hours left." |
| Your side is losing | 2 hours before deadline, user's side under 40% | "Your side is at 38% with 2 hours left. Share to get more votes." |
| Draw — refund | Draw settlement | "Too close to call — your stake is being refunded minus a small fee." |
| Debate expiring | 24hrs left, no opponent | "Your debate hasn't found an opponent yet. Share it or it expires." |
This is a solo build in active development. Architecture decisions and design rationale are documented throughout this README. If you're building on Starknet and want to collaborate, reach out on Twitter.
MIT