Skip to content

Latest commit

 

History

History
932 lines (728 loc) · 29.5 KB

File metadata and controls

932 lines (728 loc) · 29.5 KB

Provit

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.


Table of Contents


What is Provit

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.


How it Works

For stakers

  1. Pick a tribe (Music, Sports, Film, Relationships, Finance)
  2. Post a debate topic and choose your side — e.g. "Burna Boy has more global impact than Wizkid"
  3. Stake between $0.50 and $10 USDC on your position
  4. An opponent stakes the same amount on the opposite side
  5. The 24-hour voting window opens to the public
  6. If 65%+ of crowd votes agree with you → you win the pool (minus 5% platform fee)
  7. If the vote is too close (under 65% either way) → draw, both stakers refunded minus 1%
  8. Claim your USDC. Your reputation updates automatically.

For voters

  1. Log in with Google (no wallet setup needed)
  2. Browse debate feeds by tribe
  3. Vote on any active debate — completely free, gas sponsored
  4. One vote per debate per account
  5. Correct votes earn +5 reputation in that tribe

Settlement

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.


The 5 Tribes

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

Tech Stack

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

Architecture

System overview

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

What lives onchain vs offchain

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.


Smart Contracts

Three contracts. Deployed in this order: FeeVault → VoteRegistry → DebateEscrow.

DebateEscrow.cairo

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.

VoteRegistry.cairo

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

FeeVault.cairo

Receives platform fees from every settlement.

Functions:
  receive_fee(amount)        // called by DebateEscrow
  withdraw(to)               // owner only
  get_balance() → u256

Gas Strategy

All user transactions are gas-sponsored. Users never see or manage STRK.

Plan A — AVNU Paymaster (default)

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.

Plan B — Server wallet (fallback)

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.

Settlement transactions

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.

Cost math

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

Sybil Resistance

Different mechanisms for stakers and voters. No circular dependencies.

Stakers

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')
  }
}

Voters — 3 layers

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 System

Reputation is the product. Staking is the mechanic.

Formula (stake-weighted)

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.

Reputation surfaces

  • 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

DB schema for reputation

-- 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'));

Revenue Streams

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

Featured slot flow

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

Revenue projection

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

Project Structure

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

Environment Variables

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

Getting Started

Prerequisites

  • Node.js 18+
  • Rust (for Scarb/snforge)
  • Scarb (Cairo package manager)
  • Starknet Foundry (snforge)

1. Clone and install

git clone https://github.com/yourhandle/provit.git
cd provit

# Install frontend
cd apps/web && npm install

# Install backend
cd ../server && npm install

2. Set up environment variables

Copy the variables from the section above into your .env files.

3. Set up Supabase

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()
);

4. Generate VAPID keys for push notifications

npx web-push generate-vapid-keys

Copy the output into your .env files.

5. Start development servers

# Terminal 1 — frontend
cd apps/web && npm run dev

# Terminal 2 — backend
cd apps/server && npm run dev

Contract Deployment

Deploy 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_ADDRESS

Update all three addresses in your .env files. Verify all contracts on Starkscan.


Fixing snforge USC Error on Windows

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:

Option 1 — Use WSL (recommended for Windows)

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 --version

Universal-Sierra-Compiler will be automatically installed if you use snfoundryup. No separate USC step needed.

Option 2 — Download USC binary manually (Windows without WSL)

  1. Go to https://github.com/software-mansion/universal-sierra-compiler/releases
  2. Download the latest universal-sierra-compiler-x86_64-pc-windows-msvc.zip
  3. Extract it — you will get a universal-sierra-compiler.exe file
  4. Move the .exe to a folder on your PATH, for example C:\Users\hp\.cargo\bin\
  5. Open a new terminal and verify:
universal-sierra-compiler --version
  1. Run snforge test again — the error will be gone.

Option 3 — Set the env var instead of PATH

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 test

To make it permanent, add UNIVERSAL_SIERRA_COMPILER to your Windows system environment variables via System Properties → Advanced → Environment Variables.


Running Tests

cd contracts

# Run all tests
snforge test

# Run specific test file
snforge test --filter debate_test

# Run with verbose output
snforge test -v

Test coverage required (all must pass before deployment)

DebateEscrow — 7 tests:

  1. Happy path win — create → join → votes reach 65% → settle → winner withdraws
  2. Draw path — votes 55/45 → settle → both refunded
  3. Expire path — no opponent → expire → creator refunded
  4. Duplicate vote reverts
  5. Wrong stake amount on join reverts
  6. Settle before vote deadline reverts
  7. Non-owner settle attempt reverts

VoteRegistry — 4 tests:

  1. Cast vote records correctly
  2. Duplicate vote from same wallet reverts
  3. Vote after deadline reverts
  4. Tally returns accurate counts

VoteRegistry Plan B — 2 tests:

  1. cast_vote_for by owner records voter address correctly
  2. cast_vote_for by non-owner reverts

Roadmap

Phase 0 — Research (Days 1–5)

  • 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)

Week 1 — Contracts + Anti-Sybil

  • 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

Week 2 — Backend API

  • 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

Week 3 — Frontend

  • 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

Week 4 — Reputation + Live Fire

  • 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

Week 5 — Public Launch

  • Twitter launch thread with settlement ceremony recording
  • Starknet ecosystem posts
  • First featured slot sold
  • 50+ signups in first 48 hours

Push Notifications

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."

Contributing

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.


License

MIT