Search projects, hackathons, sponsors and tech

    ← All projects

    PragueExplorer

    Privacy-first ZK geocaching on Scroll

    Demo

    About

    The problem it solves Prague Explorer is a privacy-first geocaching game for EthPrague attendees and Prague tourists. Players find physical QR codes hidden at six real Prague landmarks, scan them in person, answer a Czech history quiz, and claim an on-chain EAS attestation — without the contract ever learning who they are. **The privacy model is the core innovation.** Presence is proved by combining a per-cache QR secret (only obtainable on-site) with a Semaphore v4 ZK proof generated entirely in the browser (~3 seconds). The contract sees only an opaque nullifier and a display name — never a wallet address, email, or any externally-linked identity. Nullifiers are scoped per-cache (`scope = cacheId`), so claiming two different caches produces cryptographically uncorrelated nullifiers that cannot be linked on-chain. **Zero friction for users.** Privy embedded wallets let anyone sign in with email or Google — no MetaMask, no seed phrase. A Pimlico ERC-4337 paymaster on Scroll Sepolia covers gas entirely. A successful claim produces an EAS attestation the user owns permanently: a composable, portable credential, not a custodied badge. **Cultural layer.** Each cache is tied to a real Prague landmark. Cache beneficiary addresses point to Czech cultural institutions that receive optional ETH micro-donations on claim. The app ships in Czech and English.

    How it's made

    Challenges we ran into **In-browser ZK proving.** Integrating Semaphore v4 into a live Next.js frontend was non-trivial: the WASM circuit and proving key total ~6 MB. We preload the key on page mount and display a progress indicator so the ~3-second proving time feels intentional rather than broken. **ERC-4337 on Scroll Sepolia.** Wiring Pimlico's paymaster with `permissionless` v0.3.5 required careful chain-ID configuration (534351) and handling the case where the paymaster rejects sponsorship. We built a fail-open fallback: if the paymaster declines, the user's EOA pays gas directly — the claim flow never breaks. **Binding the display name to the proof.** To prevent front-running (changing the displayed name after proof generation), we embed the display name in the Semaphore message field: `message = uint256(keccak256(displayName)) >> 8`. The `>> 8` right-shift is required to fit within BN254's scalar field. Any post-generation name change produces an invalid proof, enforced at the circuit level. **Semaphore anonymity-set trade-off.** The `joinCache()` call (group membership registration) is observable on-chain — an attacker can see that *someone* is joining cache X, but cannot determine who, because the identity commitment has no link to any external identifier. We mitigate this by funding user wallets from a separate deployer account so the gas-payer address is distinct from the Semaphore identity being registered.