self-olympics: A Country Leaderboard That Knows Who You Are, Without Knowing You

Self-Olympics uses passport-backed zero-knowledge proofs, a Farcaster Frame, and a PostgreSQL nullifier ledger to make one-human, one-vote ranking feel practical.

8 min read · sodofi/self-olympics

A passport dissolves into a sealed ledger token while a country map rises behind it. The scene explains how the project separates identity proof from identity storage, so a person can be counted without being exposed.
The project’s core move is to separate proof from profile. It keeps the nationality signal, then throws away the rest.
Key Takeaways

The hard problem here is not identity. It is repeat identity. Self-Olympics tries to answer a blunt question: how do you build a country leaderboard that counts humans once, proves nationality privately, and still feels native inside a social app?

A prototype with production instincts

The repository reads like a working prototype, but not a throwaway one. A Turborepo splits the Next.js app, the verification API, and the contract workspace, while Farcaster metadata and webhook code show the product is meant to live inside Warpcast rather than beside it.

A close-up of a hash-like seal locking a second ledger entry shut while a fresh country counter keeps moving forward. It illustrates the nullifier check that blocks repeat registrations without storing the underlying passport data.
The nullifier is the real anti-bot mechanism. It makes duplicates obvious without revealing the person behind them.

The thin line between proof and profile

The core idea is elegant. The app does not need your passport number or your full identity. It needs a zero-knowledge proof that a Self-compatible passport check succeeded, plus a nullifier that says this same human has already registered.

The app only keeps the pieces it needs: proof success, country code, and a nullifier that blocks repeat registrations.

That is why the database is not just storage. It is the gatekeeper for uniqueness. The moment the nullifier has an entry, the app stops treating a fresh request as a fresh person.

How the backend enforces one human, one vote

The heart of the app sits in apps/web/src/app/api/register/route.ts. The route verifies the proof, checks the nullifier, and only then writes a registration row tied to a country code.

const proof = await verifier.verify(request.body.proof);

const duplicate = await db.query(
  'select country_code from registrations where nullifier = $1',
  [proof.nullifier]
);

if (duplicate.rowCount > 0) {
  return Response.json({ error: 'Already registered' }, { status: 409 });
}

await db.query(
  'insert into registrations (nullifier, country_code) values ($1, $2)',
  [proof.nullifier, proof.nationality]
);

The route also requests nationality, which is enough to increment a country count without turning the system into a dossier. In other words, the app keeps the attribute it needs and discards the rest.

Why Farcaster matters more than the chain

The frame metadata, QR verification flow, and notification webhook point to a distribution strategy, not just a feature list. A user can encounter the app inside a feed, verify on mobile, and return with the result without leaving the social loop.

That makes Farcaster the real front door. The blockchain stack is there, but the user experience is built around the place where people already are, which is where adoption actually starts.

A pragmatic comparison

ApproachIdentity signalTrade-off
Self-Olympics nowZK passport proof plus nullifierStrong privacy and sybil resistance, but it still depends on backend state
Fully on-chain registryWallet or contract recordEasy to audit, harder to keep cheap and private
Simple signup formEmail or wallet onlyFast to launch, easy to game

The current architecture is a deliberate middle path. The contract folder is configured for Celo, but the visible source of truth is still PostgreSQL, which is sensible when the product is changing fast and the most important thing is getting the verification loop right.

That is the real maturity signal here. The repository knows which parts must be trust-minimized today and which parts can stay operationally simple until the rules are stable enough to move on-chain.