Search projects, hackathons, sponsors and tech

    ← All projects

    Proof of Attraction

    A dating app where real human verification is built into every interaction.

    Demo

    About

    PROOF OF ATTRACTION Detailed Project Description ETHOnline 2026 — World Track =============================================================================== THE ONE-LINE IDEA =============================================================================== A dating app where every single profile is a verified, unique, living human — no bots, no catfish, no duplicate or throwaway accounts — and where an optional human-backed AI agent can act on your behalf in a way the person you are talking to can actually trust. Human verification is not a badge bolted on at the end. It is the foundation the whole product stands on. If you cannot prove you are a real, unique person, you do not get in. =============================================================================== THE PROBLEM =============================================================================== Every mainstream dating app has the same three rotten foundations: 1. Bots and scammers. A large share of "people" on dating apps are automated accounts, romance scammers, or paid engagement farms. Users waste time, lose money, and lose trust. 2. Fake and duplicate identities. One person can spin up ten profiles. Banned users return instantly under a new email. There is no notion of "one real human = one account." 3. No trust in AI. AI is flooding these apps — auto-swipers, script-written openers, ghost-written conversations. When a message feels too smooth, you cannot tell if a human is even involved. AI is treated as something to hide. The result: matching feels like gambling against machines, and the moment AI enters the chat, trust collapses entirely. =============================================================================== THE CORE INSIGHT =============================================================================== The scarce, valuable thing in online dating is not looks or wit — it is the guarantee that there is ONE REAL, UNIQUE HUMAN on the other side. World ID can prove exactly that, anonymously, without collecting your identity documents or selling your data. So we built a dating app where that proof is the entry ticket, the trust signal on every screen, and — crucially — the thing that makes AI safe instead of scary. Instead of hiding AI, we make it accountable: an agent may act for you, but every action it takes is cryptographically tied back to a verified human via World's AgentBook. The other person sees "this was sent by a verified human's agent," not "you might be talking to a bot." =============================================================================== HOW IT WORKS FOR A USER (THE EXPERIENCE) =============================================================================== 1. PROVE YOU ARE HUMAN (onboarding) You open the app and verify with World ID. This is a real, anonymous proof of personhood — one account per unique human, enforced at the protocol level. No email/password, no phone number. The proof itself is your identity. Because World ID 4.0 nullifiers are one-time-use, we also create a World ID "session" during signup so you can log back in later with a single tap, without re-proving from scratch. On a new device you just enter your handle and approve the login in the World App. 2. LIVE SELFIE ESTIMATE (optional, clearly labelled) A quick two-frame front-camera check. It confirms you are a live person in front of the camera (not a photo of a screen) via a small liveness challenge — "turn your head", "smile" — and records an ESTIMATED gender presentation. This is shown everywhere as an estimate, never as "verified." World does not verify gender and we never claim it does. It is a third-party vision-model guess, kept honest and separate from the real human-verification badge. 3. BUILD A PROFILE Handle, photos, bio, interests, self-declared gender, who you are looking for, and location. Photos are stored privately and served through short-lived signed links, never as permanent public URLs. 4. DISCOVER REAL HUMANS ONLY You swipe through other verified humans. Each card shows a humanity badge (Orb / Selfie tier), an "Agent enabled" badge if that person authorized an agent, and a compatibility match score computed from shared interests, age, distance, verification tier, and on-chain reputation signals. A "Verified humans only" filter is always available. 5. THE AGENT ACTS FOR YOU (the AgentKit part) You can register a personal agent. It genuinely acts on your behalf: - "Ask my agent about her" — pre-screens a profile and gives you a take before you even match. - Auto-suggested opener — when you match, your agent drafts a first message in your chosen tone (warm / witty / direct). You can send via the agent, edit it, or dismiss it. - Reply drafting — mid-conversation, it drafts your next reply from the thread. The recipient always sees a disclosure: "sent by a verified human's agent." Nothing is hidden. 6. TRUST GATES ON HIGH-STAKES MOMENTS First message and event RSVP are the moments abuse actually happens, so human-verification is required there — a bot cannot send the first message or claim a limited event seat. 7. AN ACTION LOG YOU CONTROL Every action your agent takes is recorded and shown to you, each tagged with whether it was cryptographically verified through AgentBook. You can revoke any action. You always know what your agent did in your name. =============================================================================== WHY THIS IS DIFFERENT =============================================================================== - Verification is the product, not a feature. The entire funnel is gated on proof of a unique human. This is Sybil resistance applied to dating. - AI is disclosed and accountable, not hidden. Most apps pretend AI is not there. We surface it and make it provably human-backed. That flips AI from a trust-destroyer into a trust signal. - Privacy-preserving by construction. World ID proves personhood without revealing who you are. We store an anonymous nullifier, not your identity. - Gender is handled honestly. It is an ESTIMATE from a live selfie, always labelled as such, always separate from the real human badge. We refuse to launder a guess as a verified fact. =============================================================================== THE PRIZE ANGLE (ETHOnline — World Track) =============================================================================== The build targets both World prizes at once, and they reinforce each other: - AgentKit Continuity: a human-backed agent acts in chat and discovery, and every action routes through a real AgentKit-protected backend that resolves the agent to a human through AgentBook on World Chain. A durable, per-human free-trial quota (keyed to the verified human, not a device or wallet) demos real continuity, with an on-chain testnet top-up when the trial runs out. - Selfie Check / Proof of Human: human verification is used as a risk, eligibility, fairness, continuity, and abuse-prevention signal at the two highest-stakes moments — first message and event RSVP — not as a one-time checkbox. =============================================================================== ONE-SENTENCE PITCH =============================================================================== Proof of Attraction is a dating app where you can finally be certain the person you match with is a real, unique human — and where AI is allowed to help, but only when it is provably backed by one.

    How it's made

    PROOF OF ATTRACTION How It's Made — Deep Technical Description ETHOnline 2026 — World Track =============================================================================== 1. HIGH-LEVEL ARCHITECTURE =============================================================================== Three moving parts: [ React Native app ] --HTTPS--> [ Hono API on Vercel ] --> [ Neon Postgres ] (Expo) (serverless) (Drizzle ORM) | | \--> Upstash Redis (quota/nonces) | | \--> AWS S3 (private photos) | | \--> Ably (realtime chat) | | \--> Groq LLM (OpenAI-compatible) | | \--> World Chain (AgentBook, reputation) | \--> developer.world.org/api/v4/verify \--> WebView -> IDKit (World App) for World ID proofs \--> Privy embedded wallet + viem for testnet top-ups - Client: React Native via Expo SDK 57, Expo Router for navigation. Standalone app (not a World Mini App), built as an Android dev/release APK. - Backend: Hono, deployed to Vercel serverless (local dev via @hono/node-server). Stable public alias: server-two-steel-51.vercel.app. - Repo split: /app and /src (client), /server (backend), /docs (submission + feedback docs). Drizzle migrations in /server/drizzle. =============================================================================== 2. IDENTITY, AUTH, AND SESSIONS (the hard part) =============================================================================== There is no "Sign in with World ID" session product — IDKit produces a one-shot proof, not a session. So we own the session lifecycle. WORLD ID 4.0 SPECIFICS THAT SHAPED THE DESIGN - We use idkit-core 4.2.4 and verify against developer.world.org/api/v4/verify/{rp_id}. - v4 requires a signed rp_context: the backend signs a short-lived request token with an RP signing key (signRequest from @worldcoin/idkit-core/signing) before the widget can open. App ID + RP ID + signing key are the three portal values. - v4 uniqueness nullifiers are ONE-TIME per (rp, action). Re-verifying the same action returns nullifier_replayed. There is no "max verifications" toggle on a v4 action page. This is why naive "verify again to log in" does not work. THE FLOW WE BUILT - Signup: proofOfHuman uniqueness proof (Sybil check, action from EXPO_PUBLIC_WORLD_ID_ACTION) -> backend verifies -> issues our own HS256 JWT keyed to the anonymous nullifier -> upserts a users row. Immediately after, we run IDKit.createSession and store the returned session_id on the user (users.world_session_id) and in SecureStore. - Login: IDKit.proveSession(session_id) -> POST /auth/session/prove -> new JWT. No uniqueness re-proof, so no replay. On a fresh device with no stored session, the user types their handle -> GET /auth/session/lookup?handle= resolves the session_id -> proveSession. - The WebView page (server/public/verify.html) takes a mode param: uniqueness | create-session | prove-session, and drives IDKit accordingly. A native bridge (WorldIdProvider / WorldIdModal, react-native-webview) returns the proof to the app. TOKEN HANDLING - JWT (7-day) stored in expo-secure-store, attached as a Bearer token by authedFetch, which transparently retries once via POST /auth/refresh on a 401 before signing out. - The derived humanId (from the nullifier) is the exact key the AgentKit quota counts against — tying agent continuity to the verified human. =============================================================================== 3. THE AGENT — REAL AGENTKIT, NOT A COSMETIC CHATBOT =============================================================================== What makes it a genuine AgentKit integration: every in-chat/discovery agent action routes through an AgentKit-protected backend endpoint, and the server verifies the caller against AgentBook on every call. CLIENT SIDE (src/agent/agentkit.ts) - Uses @worldcoin/agentkit createAgentkitClient + declareAgentkitExtension. - CRITICAL: the extension is declared with network 'eip155:4801' (World Chain Sepolia). Without the network, supportedChains is empty and createHeader throws "Signer eip155:4801/eip191 is not supported" — the bug that silently dropped to the fallback gate. The signer is the Privy embedded wallet (eip191, signMessage). SERVER SIDE (server/src/agentkit.ts + agentGate in index.ts) - Real verify path: parseAgentkitHeader -> validateAgentkitMessage (maxAge 300s, nonce replay set) -> verifyAgentkitSignature (RPC) -> AgentBook lookupHuman. - Fallback gate: if no/invalid AgentKit header, resolve via x-agent-wallet against a homegrown registry. The response carries a "gate" field: "agentkit" or "fallback: <reason>", surfaced in the app ("Last call: agentkit"). - AgentBook is read on World Chain mainnet (eip155:480) — a free eth_call, no funds, no tx. Our agent signs on Sepolia (eip155:4801). Single deliberate mainnet touch, flagged; the fallback covers it if unavailable. AGENT ENDPOINTS (all pass through agentGate) POST /agent/context pre-screen a profile or read match context (generateTake for discovery, generateContext for a match) POST /agent/icebreaker draft a first message in a chosen tone POST /agent/reply draft the next reply from the last ~8 messages POST /agent/send send a message as the agent (viaAgent=true) GET /agent/actions action log + quota { used, freeTrial, credit, remaining } DELETE /agent/actions/:id revoke a logged action Every call records an agent_actions row (kind, detail, agentBacked, registered), shown in the Agent tab and revocable. CONTINUITY QUOTA + x402-STYLE TOP-UP - FREE_TRIAL_CALLS per verified human, stored in Upstash Redis (durable across serverless cold starts). Exhaustion returns HTTP 402 with a top-up target. - On-chain settlement (testnet only): the agent settles a native ETH transfer on World Chain Sepolia (chain id 4801) from the Privy embedded wallet via viem. Client reports the tx hash to POST /agent/topup; verifyTopUp (server/src/payments.ts) waits for the receipt and checks recipient, minimum value, and unused hash (replay protection), then credits N calls in Redis. - The wallet defaults to World Chain Sepolia (Privy supportedChains) and calls wallet_switchEthereumChain to 4801 before sending; the signer pre-checks balance to give a clean "wallet has 0 ETH" message instead of a viem stack dump. - A "Credit from transaction" fallback lets any wallet's tx hash be pasted to settle, since a keyless exchange wallet cannot sign on a custom chain. =============================================================================== 4. LIVE SELFIE GENDER ESTIMATE (vision + liveness) =============================================================================== - Client (src/verification/GenderEstimateCapture.tsx): expo-camera front camera, two frames — neutral then a challenge (turn head / smile / raise eyebrows). Capture quality is stepped down in a loop until each base64 frame is small enough to fit Vercel's ~4.5MB request cap (the FUNCTION_PAYLOAD_TOO_LARGE issue). - Server: POST /me/gender-estimate (GET .../challenge issues the challenge). llm.ts estimateGenderFromFrames calls a Groq VISION model (qwen/qwen3.6-27b; Llama-4 vision is deprecated on Groq) with JSON mode, checking live / samePerson / challengePassed plus an estimated label + confidence. - Result stored in users.gender_estimate. Frames are held in memory only — never written to S3 or the DB. Soft signal only: it never blocks, and the profile shows a private "estimate differs from what you declared" note, never exposed to other users as verified. =============================================================================== 5. DOMAIN MODEL, MATCHING, AND REALTIME =============================================================================== REPO PATTERN (server/src/repo/{types,memory,pg}.ts) - getRepo() returns PgRepo when DATABASE_URL is set, else an in-memory MemoryRepo (used for local dev and stub mode). Same interface for both. DRIZZLE SCHEMA (server/src/db/schema.ts) users (nullifier_hash, handle, tier, gender_estimate, wallet, world_session_id, likes_everyone), profiles, likes, matches, messages, events, rsvps, push_tokens, agent_actions. DOMAIN ROUTES (server/src/routes.ts, all behind requireSession) GET /me user + profile (photos presigned) PUT /me/profile requireOrb PUT /me/handle unique handle POST /me/wallet link agent wallet -> human POST /me/photo multipart upload GET /discovery ranked deck (verifiedOnly filter) POST /likes like/pass, auto-match, push GET /matches, /matches/:id/messages, POST .../messages GET /realtime/token Ably token GET /events, POST/DELETE /events/:id/rsvp (RSVP requires human tier) POST /push/register MATCHING (server/src/matching.ts) — pure weighted scorer interest Jaccard 0.34, distance (haversine) 0.14, tier 0.14, agentBacked 0.14, age 0.12, reputation 0.12; top candidates enriched with on-chain reputation signals (onchain.ts: viem publicClient — tx count, balance band, AgentBook registration; null-safe). REALTIME + MEDIA - Chat over Ably websockets (src/realtime/ably.ts) with a polling fallback and a "loaded" flag used to trigger the agent's auto-suggested opener exactly once. - Photos: private S3 objects (server/src/s3.ts) returned as 12-hour presigned GET URLs; blob.ts routes to S3 when configured, else a data-uri/Vercel Blob fallback. - Push via Expo push tokens. =============================================================================== 6. STACK SUMMARY =============================================================================== CLIENT React Native 0.86 / Expo SDK 57, Expo Router @worldcoin/agentkit 0.2.1 @privy-io/expo 0.73 + viem 2.56 (embedded wallet, testnet signing) expo-camera 57 (live selfie), react-native-webview (World ID widget) ably 2.28 (chat), expo-secure-store (JWT + session), expo-notifications BACKEND hono 4.6 on Vercel serverless @worldcoin/idkit-core 4.2.4 (v4 verify + signing) @worldcoin/agentkit 0.2.1 (AgentBook verify) drizzle-orm 0.45 + @neondatabase/serverless (Neon Postgres) @upstash/redis 1.38 (durable per-human quota + nonces) @aws-sdk/client-s3 + s3-request-presigner (private photos) ably 2.28 (chat), openai 7.15 -> Groq (LLM text + vision) viem 2.56 (World Chain reads + testnet receipt verification) CHAINS World Chain mainnet eip155:480 — AgentBook resolution (read-only eth_call) World Chain Sepolia eip155:4801 — agent signing + testnet top-up settlement =============================================================================== 7. NOTABLE ENGINEERING DECISIONS / GOTCHAS SOLVED =============================================================================== - World ID v4 replay: solved with createSession/proveSession instead of re-running uniqueness proofs; login is a session proof, not a new nullifier. - AgentKit header: must declare the extension on eip155:4801 or createHeader rejects the Sepolia signer. - Vercel payload cap: selfie frames are downscaled client-side until under the request limit; non-JSON error bodies (413) are handled instead of JSON.parse. - Node/undici picks broken IPv6 routes to Neon on some networks; db/client.ts forces IPv4 (dns ipv4first + undici Agent family:4), escape hatch NEON_FORCE_IPV4=0. - Privy provisioning failures were swallowed; useWallet now surfaces the real error (e.g. invalid_native_app_id) with a retry, deduped across call sites. - Everything on-chain is testnet only, never mainnet — the one mainnet touch is a free AgentBook read, explicitly flagged, with a fallback gate.