Turn every event into an on-chain experience.
Free events have 30-40% no-show rates. BeThere fixes this with USDC deposit commitments — attendees get their money back when they show up, forfeit if they don't. Built on Solana for $0.001 NFT badges, $0.00087 on-chain costs, and < 500ms check-in at the edge.
| Problem | BeThere Solution |
|---|---|
| 30-40% no-show rates for free events | USDC deposit commitment — skin in the game |
| No on-chain proof of attendance | Compressed NFT badges (cNFT) — 990x cheaper than POAP |
| Web2-only event tools | Solana-native: deposits, refunds, NFTs all on-chain |
| Expensive NFT minting ($0.50/ea) | cNFT on Solana: $0.001 per badge |
| ETH gas fees too high | Solana: $0.00087 per transaction |
Rust → Solana (Quasar) → Cloudflare Workers → Leptos WASM → Google Sheets
100% Rust codebase — shared types from on-chain program → edge worker → WASM frontend. Zero serialization bugs.
| Metric | Value |
|---|---|
| On-chain program | 63 KB (optimized) |
| NFT mint cost | $0.001 per badge |
| Transaction cost | $0.00087 (at $172/SOL) |
| Check-in latency | < 500ms (edge worker) |
| Tests | 68 passing (39 worker + 29 on-chain) |
| Program ID (devnet) | C6HDeZES9aPpNwe3UvS9ecmfcRhH1XeJb8PGJmLG3z3T |
1. 📋 Organizer creates event → sets $5 USDC deposit
2. 📝 Attendee clicks "Reserve Spot" → auto-redirect to deposit page
3. 🪙 Attendee deposits USDC via Phantom wallet (Solana Pay QR)
→ Or uploads THB slip → auto-redirect to ticket/QR page
4. 📱 Staff scans QR at door → on-chain check-in
5. 💰 Attendee gets refund + compressed NFT badge
6. ❌ No-show? → Organizer claims forfeited deposit
# 1. Install prerequisites
cargo install wasm-bindgen-cli --version 0.2.100
cd worker && npm install && cd ..
# 2. Build frontend
cd frontend-leptos && trunk build && cd ..
# 3. Configure secrets (first time only)
cd worker
npx wrangler secret put JWT_SECRET
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
npx wrangler secret put GOOGLE_REDIRECT_URI
npx wrangler secret put GOOGLE_SERVICE_ACCOUNT_EMAIL
npx wrangler secret put GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY
npx wrangler secret put GOOGLE_SERVICE_ACCOUNT_TOKEN_URI
npx wrangler secret put GOOGLE_SHEET_ID
npx wrangler secret put STAFF_EMAILS
npx wrangler secret put SUPER_ADMIN_EMAILS
# Dev mode (optional — for local E2E testing without Google OAuth)
# ⚠️ NEVER enable in production!
echo 'DEV_MODE = "1"' >> worker/.dev.vars
echo 'DEV_EMAIL = "your-email@example.com"' >> worker/.dev.vars
# 4. Create KV namespaces (first time only)
npx wrangler kv namespace create EVENTS
npx wrangler kv namespace create EVENTS --preview
# Update wrangler.toml with returned IDs
# 5. Run locally
cd worker && ./deploy.sh dev
# 6. Seed first event (after server is running)
curl -X POST http://localhost:8787/api/events/seed -H "Cookie: session=<jwt>"Open http://localhost:8787.
Note:
deploy.shautomatically handles the Yarn PnP (~/.pnp.cjs) conflict with wrangler's esbuild bundler — no manualmvneeded.
event-checkin/
├── domain/ — Shared types & logic (compiles x86_64 + wasm32)
├── worker/ — Cloudflare Worker (wasm32-unknown-unknown)
├── frontend-leptos/ — Leptos WASM frontend (standalone trunk build)
├── Cargo.toml — Workspace root (members: domain, worker)
└── README.md
The domain/ crate contains shared types (Attendee, Claims, AppConfig), QR generation, and sheet row parsing. The worker/ crate consumes it, replacing reqwest with worker::Fetch and rsa/jsonwebtoken with V8 SubtleCrypto via wasm-bindgen.
The attendee sheet (tab name configurable via GOOGLE_SHEET_NAME, default "Attendees"):
| Column | Index | Field | Notes |
|---|---|---|---|
| A | 0 | api_id |
Unique ID (e.g. gst-abc123) |
| B | 1 | name |
Full name |
| C | 2 | first_name |
First name |
| D | 3 | last_name |
Last name |
| E | 4 | email |
Attendee email |
| F | 5 | ticket_name |
Ticket type |
| G | 6 | registration_date |
ISO 8601 registration date |
| H | 7 | approval_status |
Approval state |
| I | 8 | participation_type |
In-Person / Online |
| J | 9 | phone |
Phone number |
| K | 10 | contact_channel |
Telegram / Discord / etc. |
| L | 11 | contact_handle |
Contact username |
| M | 12 | deposit_agreed |
Yes/No |
| N | 13 | deposit_method |
USDC / THB / etc. |
| O | 14 | deposit_amount |
Deposit amount |
| P | 15 | deposit_tx_signature |
Transaction signature |
| Q | 16 | deposit_verified |
Deposit verification status |
| R | 17 | checked_in_at |
ISO 8601 timestamp |
| S | 18 | checked_in_by |
Staff email who checked in |
| T | 19 | solana_address |
Filled at claim time (attendee wallet) |
| U | 20 | qr_code_url |
QR code link |
| V | 21 | claim_token |
UUID generated at check-in (for NFT claim) |
| W | 22 | claimed_at |
Timestamp when NFT + refund claimed |
A separate "staff" sheet tab (configurable via GOOGLE_STAFF_SHEET_NAME) holds authorized staff emails in column A (header in row 1, emails from row 2). This is unioned with the STAFF_EMAILS secret — a user is staff if their email appears in either source.
# Build frontend (if changed)
cd frontend-leptos && trunk build && cd ..
# Deploy to Cloudflare Workers
cd worker && ./deploy.shThe deploy.sh script handles the Yarn PnP conflict automatically. Alternatively, you can run npx wrangler deploy directly if you don't have ~/.pnp.cjs.
Non-secret vars are in worker/wrangler.toml [vars]:
| Var | Default | Purpose |
|---|---|---|
SERVER_URL |
https://event-checkin.workers.dev |
Public URL for OAuth redirect |
GOOGLE_SHEET_NAME |
Attendees |
Attendee sheet tab name |
GOOGLE_STAFF_SHEET_NAME |
staff |
Staff sheet tab name |
EVENT_NAME |
(none) | Default event name for seeding |
ORGANIZER_EMAILS |
(none) | Comma-separated organizer emails for seeding |
SUPER_ADMIN_EMAILS |
(secret) | Global admins who can create/manage all events |
The frontend is served from frontend-leptos/dist/ via Workers Assets with SPA fallback.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/health |
No | Health check |
| GET | /api/auth/url |
No | Google OAuth URL (optional ?redirect= param) |
| GET | /api/auth/callback |
No | OAuth callback, sets HttpOnly cookie, redirects based on role |
| POST | /api/auth/logout |
No | Clear session cookie |
| GET | /api/auth/me |
Cookie | Current user info + role (super_admin/organizer/staff/attendee) |
| GET | /api/my-registration/{slug} |
Cookie | Get signed-in user's registration for a specific event |
| GET | /api/my-registrations |
Cookie | List all registrations for the signed-in user |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/public/events |
No | List upcoming active events (nearest first) |
| GET | /api/public/event/{slug} |
No | Get public event details (no auth required) |
| POST | /api/public/register |
Cookie | Register for event (email from JWT, not body) |
| GET | /api/public/ticket/{id} |
No | Get attendee ticket/QR slip details |
| GET | /api/badge.svg |
No | NFT badge SVG (by claim token) |
| GET | /api/badge-hd.svg |
No | NFT badge HD SVG (by claim token) |
| POST | /api/waitlist |
No | Join waitlist (email + use case) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/events |
Cookie | List all events |
| POST | /api/events |
Cookie + SuperAdmin | Create new event |
| GET | /api/events/{id} |
Cookie | Get event config |
| PUT | /api/events/{id} |
Cookie + SuperAdmin | Update event config |
| DELETE | /api/events/{id} |
Cookie + SuperAdmin | Archive event |
| POST | /api/events/{id}/restore |
Cookie + SuperAdmin | Restore archived event to Draft |
| DELETE | /api/events/{id}/delete |
Cookie + SuperAdmin | Permanently delete event (?force=true for devnet cleanup) |
| POST | /api/events/seed |
Cookie + SuperAdmin | Seed event from env vars |
| POST | /api/events/migrate |
Cookie + SuperAdmin | Migrate quiz KV → event KV |
| GET | /api/events/{id}/audit |
Cookie + Organizer | Get audit trail for event |
| GET | /api/audit/global |
Cookie + SuperAdmin | Get system-wide audit trail |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/attendees |
Cookie + Event Staff | List all attendees + stats |
| GET | /api/attendee/{id} |
Cookie + Event Staff | Single attendee details |
| POST | /api/checkin/{id} |
Cookie + Event Staff | Check in attendee |
| POST | /api/attendee/{id}/undo-checkin |
Cookie + Event Staff | Undo check-in |
| POST | /api/generate-qrs |
Cookie + Event Staff | Generate QR codes |
| POST | /api/admin/flush-cache |
Cookie + Staff | Flush server-side caches |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/walkin/register |
Cookie + Staff | Register walk-in attendee (on-the-spot) |
| GET | /api/walkin/list |
Cookie + Staff | List walk-in attendees for event |
| GET | /api/walkin/export |
Cookie + Staff | Export walk-in attendees as CSV |
| POST | /api/walkin/sync |
Cookie + Staff | Sync walk-in attendees to Google Sheet |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/quiz |
No | Get quiz questions (public) |
| POST | /api/quiz/{token}/submit |
No | Submit quiz answers |
| GET | /api/quiz/{token}/status |
No | Get quiz progress |
| PUT | /api/admin/quiz |
Cookie + Staff | Create/update quiz config |
| GET | /api/adventure/{token}/status |
No | Get adventure progress |
| POST | /api/adventure/{token}/save |
No | Save adventure progress |
| GET | /api/admin/adventure |
Cookie + Staff | Get adventure config |
| PUT | /api/admin/adventure |
Cookie + Staff | Update adventure config |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/claim/{token} |
No | Get claim info |
| POST | /api/claim/{token} |
No | Claim NFT badge + refund |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/deposit/status/{attendee_id} |
No | Check deposit status for attendee |
| POST | /api/deposit/usdc |
No | Build Solana Pay deposit TX (USDC) |
| GET | /api/deposit/usdc/tx |
No | Solana Pay TX callback (wallet fetches serialized TX) |
| GET | /api/deposit/usdc/confirm |
No | Poll deposit TX confirmation via Solana RPC |
| POST | /api/deposit/usdc/webhook |
No | Record TX signature, verify on-chain |
| POST | /api/deposit/thb/upload |
No | Upload PromptPay slip URL (THB) |
| GET | /api/deposit/thb/pending |
Cookie + Staff | List pending THB slips |
| POST | /api/deposit/thb/verify |
Cookie + Staff | Verify/reject THB slip |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/escrow/init |
Cookie + Organizer | Initialize on-chain escrow PDA + vault ATA (single TX) |
| POST | /api/escrow/mark-checked-in |
Cookie + Organizer | Mark attendee checked-in on-chain |
| POST | /api/escrow/refund |
No | Build refund TX for attendee's wallet to sign |
| POST | /api/escrow/claim-forfeited |
Cookie + Organizer | Claim forfeited deposit (no-show) |
| POST | /api/escrow/deactivate-event |
Cookie + Organizer | Build deactivate escrow TX |
| POST | /api/escrow/close-event |
Cookie + Organizer | Build close escrow TX (reclaim rent) |
| POST | /api/escrow/close-deposit |
No | Close individual deposit PDA (rent reclaim) |
| POST | /api/escrow/backfill-wallets |
Cookie + Organizer | Backfill wallet addresses from KV to on-chain |
| GET | /api/escrow/events/{event_id} |
Cookie + Organizer | Get on-chain escrow event data |
| POST | /api/escrow/sync |
Cookie + Organizer | Sync on-chain escrow events to KV cache |
| POST | /api/escrow/onchain-webhook |
No | Webhook for on-chain escrow events |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/refund/queue |
Cookie + Staff | List pending refunds |
| POST | /api/refund/mark/{id} |
Cookie + Staff | Mark refund as completed |
| POST | /api/refund/batch-thb |
Cookie + Staff | Batch THB refund for event cancellation |
| GET | /api/escrow/refund-queue |
Cookie + Staff | USDC refund queue (cancellation workflow) |
| GET | /api/escrow/cancel-status |
Cookie + Staff | Event cancellation status overview |
| Path | Page | Auth |
|---|---|---|
/ |
Landing — marketing page, upcoming events, My Registrations (auth-aware nav) | Public |
/login |
Login — Google OAuth sign-in (staff/organizer entry point) | Public |
/e/{slug} |
Public Event — event details, countdown, registration with Google Sign-In | Public |
/deposit/{attendee_id} |
Deposit — wallet adapter + QR for USDC/THB deposit | Public |
/ticket/{attendee_id} |
Ticket — QR code slip with check-in status | Public |
/claim/{token} |
Claim — quiz + NFT badge + refund | Token-gated |
/staff |
Scanner — camera QR + manual lookup + walk-in registration | Staff |
/admin |
Dashboard — attendee list, stats, escrow, cancellation, walk-in export/sync | Staff |
/admin/events |
Events — create, edit, manage events | SuperAdmin |
/adventure |
Rust Adventures — educational game | Public |
worker/src/ — Cloudflare Worker
handlers/ — API endpoints (auth, check-in, QR, attendee, events, quiz, claim, adventure, health)
deposit/ — Deposit/refund handlers (split by payment track)
usdc.rs — USDC deposit flow: status, initiate, TX callback, confirm, webhook
thb.rs — THB slip upload/verify, refund queue, batch refund
escrow.rs — On-chain escrow: init, mark-checked-in, close, claim-forfeited, cancel
ext.rs — Shared utilities (EventIdQuery, resolve_event_with_access, resolve_kv)
adventure.rs — Adventure business logic (save progress, check completion)
auth.rs — Google OAuth + JWT + role resolution (super_admin/organizer/staff)
error.rs — Typed AppError → Axum IntoResponse integration
event_store.rs — KV event registry CRUD, seed, migration, hard_delete_event
audit_store.rs — Append-only audit trail (per-event + global, 27 action types)
quiz.rs — Quiz business logic (scoring, KV interaction)
sheets/ — Google Sheets API
mod.rs — Access token, column mapping, attendee/staff queries, KV cache
write.rs — Sheet mutations: check-in, claim, QR URLs, row append
solana.rs — Helius cNFT minting (mintCompressedNft RPC, MintRequest struct)
solana_escrow/ — Solana escrow TX builders
mod.rs — Types, constants, EscrowError
crypto.rs — SHA-256, base58, PDA/ATA derivation (WASM SubtleCrypto + native)
wire.rs — Blockhash cache, tx serialization, message account ordering
tx_builders.rs — 9 build_* functions using shared EscrowCtx + finalize_tx
crypto.rs — SubtleCrypto bridge (RSA-SHA256, HMAC-SHA256)
http.rs — HTTP client wrapping worker::Fetch
middleware.rs — Security headers, auth guard, correlation IDs
state.rs — AppState from Env bindings
domain/src/ — Shared (compiles x86_64 + wasm32)
config/ — AppConfig (grouped: OAuth, Sheets, Solana, Nft, Server, EventDefaults)
models/ — Attendee, Claims, EventConfig, AdventureConfig, AppError, API response types
qr/ — QR URL generation + base64 image
frontend-leptos/src/
pages/ — Landing, Login, Scanner, Admin, Claim, Quiz Editor, Adventure
pages/adventure/ — Game engine, level definitions, types
api.rs — API client types and fetch wrappers
components.rs — Shared components + role helpers
utils.rs — Helpers (timestamps, badges, participation)
js/ — Camera + QR detection module
The escrow system uses PDAs (Program Derived Addresses) to hold attendee USDC deposits on-chain. The escrow program is deployed on devnet at C6HDeZES9aPpNwe3UvS9ecmfcRhH1XeJb8PGJmLG3z3T.
Escrow Flow (5 steps, all validated on devnet):
1. create_event → Organizer signs → EventEscrow PDA + Vault ATA initialized (single TX)
2. deposit → Attendee signs → USDC → vault (Solana Pay)
3. mark_checked_in → Organizer signs → Attendee checked-in on-chain
4. refund → Attendee signs → USDC → attendee (after event ends)
5. claim_forfeited → Organizer signs → Forfeited deposits → organizer (after refund deadline)
PDA Seeds:
EventEscrow:["escrow", organizer_pubkey, event_id_u64_le]AttendeeDeposit:["deposit", event_escrow_pubkey, attendee_pubkey]Vault ATA: Associated Token Account for (EventEscrow, USDC mint)
Important constraints:
- Refund requires
clock > event_end(event must have ended) — no check-in required - Deposits are rejected after the event has ended (
event_end > nowcheck) mark_checked_inrejects afterevent_end(SEC-011: prevents post-event attendance manipulation)claim_forfeitedrequiresclock > refund_deadline(post-deadline only)- All SPL token transfers use
transfer_checked()with 6-decimal USDC (Token-2022 compatible)
Security note: SEC-001 (check-in gate rug pull) has been fixed — refunds no longer require
checked_in == true. Attendees can refund afterevent_endregardless of check-in status. Seedocs/security_audit.mdfor full audit.
Constants:
| Constant | Devnet | Mainnet |
|---|---|---|
| Program ID | C6HDeZES9aPpNwe3UvS9ecmfcRhH1XeJb8PGJmLG3z3T |
TBD |
| USDC Mint | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1m |
Transaction building: All TX builders are in worker/src/solana_escrow/tx_builders.rs. They share an EscrowCtx that resolves program IDs + derives PDAs once, and a finalize_tx() helper that handles message building → blockhash → serialization → base64. This eliminates ~480 lines of duplicated boilerplate across the 9 builder functions.
| Layer | Mechanism | TTL | Benefit |
|---|---|---|---|
| Google access token | KV cache | 3500s | Eliminates RSA-JWT signing per request (~100ms saved) |
| Attendee data | KV cache | 30s | Eliminates Google Sheets full-scan per request (~200-800ms saved) |
| Solana blockhash | KV cache | 30s | Eliminates RPC call per TX build (~50-200ms saved) |
| TX builder context | EscrowCtx |
Per-request | Resolves 7 program IDs + 3 PDAs once instead of per-instruction (~3-5ms saved) |
| Cache invalidation | On write-through | Immediate | Mutations (check-in, claim) invalidate cache instantly |
# All unit tests
cargo test
# Individual crates
cargo test -p event-checkin-domain # Shared types, QR logic
cargo test -p event-checkin-worker # Crypto, auth, sheets, events
# Full 10-step E2E test (requires running worker + worker/.dev.vars with HELIUS_API_KEY)
./scripts/e2e/test_full_e2e.sh
# Devnet API test suite (7 tests, no browser needed)
./scripts/e2e/test_devnet.sh
# Mint-only test (single cNFT mint on devnet)
./scripts/e2e/test_devnet.sh --mint-only
# Worker WASM build check
cargo check -p event-checkin-worker --target wasm32-unknown-unknown
# Clippy
cargo clippy --all-targets# Full 5-step escrow E2E on Solana devnet (requires USDC-funded attendee wallet)
ATTENDEE_WALLET=~/.config/solana/id.json bash scripts/e2e/test_escrow_devnet.shSee scripts/e2e/test_escrow_devnet.sh for the complete test flow. All 24 tests validated on devnet.
- Camera QR Scanner — BarcodeDetector (Chrome) + jsQR fallback (Firefox/Safari)
- Staff check-in logging — Records which staff member checked in each attendee (column J)
- Sheet-based staff list — Staff emails loaded from "staff" sheet tab + env var (unioned)
- Event format model — In-Person / Online / Hybrid with participation-type badges
- Admin stats — Checked-in count, In-Person vs Online breakdown
- Force QR regenerate — Admin can regenerate codes per attendee
- CSP compliant — Zero
eval()calls, nounsafe-evaldirective - Edge deployment — Cloudflare Workers with SubtleCrypto for JWT signing
- Multi-event support — KV-based event registry with per-event config, staff, quiz
- Per-event access control — 4-tier role system: super_admin → organizer → staff → attendee
- Google Sign-In for attendees — Dual-purpose OAuth for staff and attendees; email locked to Google account
- Self-registration — Attendees register via public event page (
/e/{slug}) with Google identity - My Registrations — Signed-in attendees see their events + status on landing page with auth-aware nav
- Quiz-gated claim — Attendees complete quiz before claiming NFT badge
- Landing page — Auth-aware nav bar, upcoming events, interactive swimlane, waitlist, FAQ, social proof
- Rust Adventures — Educational tile-based game teaching Solana/Rust concepts
- Security hardened — Cookie Secure flag, secret redaction in Debug, attendee-validated adventure saves
- Automated E2E tests — 10-step full E2E suite + 7-test devnet suite
- PDA escrow deposits — USDC deposits held in on-chain PDAs, refundable after event
- Solana Pay integration — Deposit via QR code scan or wallet adapter (Phantom, Backpack, Solflare)
- Dual-track deposits — USDC (on-chain escrow) or THB (PromptPay QR + slip verification)
- Single-TX escrow init — Admin creates vault ATA + event escrow in one transaction via wallet signing
- On-chain check-in — Staff marks attendees checked in on-chain via wallet-signed TX (escrow refund gate)
- Wallet adapter interop — Shared JS module for wallet detection, connection, TX signing across scanner + admin
- Wallet error recovery — Structured error classification with user-friendly guidance (wrong network, insufficient funds, user rejected, program error)
- Escrow lifecycle management — Full deactivate → close flow in admin UI, rent reclamation
- Event cancellation workflow — THB batch refund + USDC refund queue + cancel status (organizer-initiated)
- Walk-in attendee management — On-the-spot registration, CSV export, Google Sheet sync with idempotency
- Force delete for devnet cleanup — SuperAdmin can hard-delete events with
?force=true - Slug auto-deduplication — Recurring events get auto-incremented suffix on name collision
- Audit trail — Append-only event log tracking all state-changing operations with actor attribution
- Dev-mode payment gating — Solana wallet options hidden in production, shown only when
dev_mode: true - Attendee flow persistence — localStorage resume for partial registrations, auto-redirect to deposit/ticket page
| Area | Status | Notes |
|---|---|---|
| Auth | ✅ Secure | JWT HMAC-SHA256, constant-time comparison, 24h expiry |
| Cookie | ✅ Secure | HttpOnly; Secure; SameSite=Lax; Path=/api |
| Admin routes | ✅ Secure | require_auth middleware, staff email verification |
| Claim gates | ✅ Secure | Sequential check-in → quiz → adventure → mint, no bypass |
| Solana RPC | ✅ Secure | Hardcoded method, serde serialization, null-safe deserialization, no user-controlled params |
| Secrets | ✅ Secure | All via env.secret(), redacted from Debug output |
| Escrow (on-chain) | ✅ Secure | Immutable params after creation, checked arithmetic, canonical PDA derivation, transfer_checked() (SEC-009 fixed), event_end guard (SEC-011 fixed) |
| Escrow (business logic) | ✅ Secure | SEC-001/002/003/004 all fixed — refunds don't require check-in, fields locked after escrow init, $1K deposit cap, archive guards escrow |
| Escrow (Token-2022) | ✅ Secure | SEC-009 fixed — all transfers use transfer_checked() with 6-decimal USDC |
| Double-claim | KV dedup lock recommended before high-traffic events | |
| Audit logging | 🟡 Basic | Append-only audit trail per event (CRUD + escrow + check-in + deposits). Global audit for deletions. Missing: on-chain CPI event indexing, UI viewer |
| JWT revocation | KV blacklist recommended for compromised tokens | |
| Dev mode | DEV_MODE=1 bypasses JWT verification — only for .dev.vars, never production |
See docs/security_audit.md for the full escrow security audit (11 findings, 8 fixed, Safe Solana Builder cross-reference). See .handovers/025_security_audit_e2e_nft_config.md for the earlier auth/RPC audit.
| Role | Can Do |
|---|---|
super_admin |
Create/edit/delete events, manage all events, full dashboard |
organizer |
Edit assigned event config, manage quiz, view dashboard |
staff |
Check in attendees, view attendee list for assigned event |
attendee |
Register for events, view own registrations, deposit, claim NFT |
| (unauthenticated) | View landing page, public event pages, play adventure, take quiz |
✅ 10 phases complete — from check-in to escrow to attendee identity. Everything runs on Solana devnet with real wallets.
| Core Flow | Status | Details |
|---|---|---|
| QR check-in | ✅ | Camera scan + manual lookup, staff logging |
| cNFT badges | ✅ | Compressed NFTs via Helius, $0.001 mint |
| Quiz gating | ✅ | Per-event quiz before NFT claim |
| Adventure gating | ✅ | Rust-themed educational game (10 levels) |
| Multi-event | ✅ | KV registry, 4-tier roles (super_admin/organizer/staff/attendee) |
| USDC escrow | ✅ | PDA-based deposits, refund, claim forfeited |
| Dual-track payments | ✅ | USDC (on-chain) + PromptPay THB (fiat QR) |
| Wallet adapter | ✅ | Phantom, Solflare, Backpack, Coinbase |
| Attendee identity | ✅ | Google Sign-In for registration, email locked to JWT |
| Self-registration | ✅ | Public event page /e/{slug} with countdown + deposit CTA |
| My Registrations | ✅ | Landing page auth-aware nav, event status tracking |
| Walk-in management | ✅ | On-the-spot registration, CSV export, Sheet sync |
| Event cancellation | ✅ | THB batch refund, USDC refund queue, status tracking |
| Wallet error recovery | ✅ | Structured error classification + user-friendly guidance |
| Security audit | ✅ | 11 findings, 8 fixed, SEC-001–011 addressed |
| E2E tests | ✅ | 68 tests (39 worker + 29 on-chain), devnet validated |
| Feature | BeThere | Luma | Eventbrite | POAP | Kickback* |
|---|---|---|---|---|---|
| On-chain deposits | ✅ USDC escrow | ❌ | ❌ | ❌ | ✅ ETH (defunct) |
| Attendance NFTs | ✅ cNFT | ❌ | ❌ | ✅ (Ethereum) | ❌ |
| Deposit refund | ✅ Auto | ❌ | Manual | ❌ | ✅ Payout pool |
| No-show penalty | ✅ Forfeit to org | ❌ | ❌ | ❌ | ✅ Pool split |
| Quiz/Adventure gating | ✅ Built-in | ❌ | ❌ | ❌ | ❌ |
| Cost per NFT | $0.001 | N/A | N/A | ~$0.50 | N/A |
| Stablecoin deposits | ✅ USDC | ❌ | ❌ | ❌ | ❌ (volatile ETH) |
| Open source | ✅ | ❌ | ❌ | ❌ | ✅ (archived) |
*Kickback (2016–2022) — Ethereum event deposit platform, shut down due to gas costs and team burnout. BeThere addresses every structural weakness. See docs/competitive_analysis_kickback.md for full analysis.
| Phase | Feature | Status |
|---|---|---|
| 1–6 | Check-in → NFT → Quiz → Multi-event → Adventure → Security | ✅ Done |
| 7 | NFT config + production deployment | 🟡 Devnet working |
| 8–9 | USDC escrow + security hardening | ✅ Done (devnet deployed) |
| 10 | Mainnet deployment | 📋 Next (~1.5 SOL cost) |
| 11 | Platform fees (1-2% on forfeited deposits) | 📋 Planned |
| 12 | Multi-organizer SaaS | 📋 Planned |
See DISCUSSION.md for the full architecture direction and decisions.