Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

118 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spark-Predict-Viber

Audience-side live prediction market for the Viber Night Grand Prix — 14 teams, one tap to bet, on Solana.

Forked from MathisSpark/1vs1ThenEveryoneJoin (the 1v1→audience pm-AMM base) and rebuilt over 5 days to run a single multi-outcome event at Network School: 100+ phones in the room, 14 teams on stage, live odds projected on the big screen, the house keeping the book tight via an off-chain arb daemon.

Live on devnet License: MIT

🌐 Live demohttps://spark-predict-viber.vercel.app 🔗 Anchor program (unchanged)EwanBorgPad/pmAMM at EvWE8LGzzyZRDASKLnLBy9qZRuL8iaJYiPf2mRZh75yV


What we built

A live-event prediction market for one specific shape: a single multi-outcome bet — "Which team wins the Grand Prix?" — across 14 teams, trading in parallel with hundreds of audience phones.

Three jobs the system has to do, all under pressure during a 60-minute event:

  1. Onboard a non-crypto audience in <5s — single QR on the screen, phone scans, gets a pre-funded burner wallet, picks a pseudo + country flag, ready to trade.
  2. Keep a 14-leg multi-outcome book coherent while audience flow hits one leg at a time. Σ p_i must stay ≈ 1 or the house bleeds free arb.
  3. Be 100% reset-to-zero between dress rehearsals — KV wipe + on-chain wallet rebalance + market rebuild, single button.

Everything sits on the existing pm-AMM Anchor program (binary YES/NO). The multi-outcome behavior is achieved off-chain by composition, not by extending the on-chain math — see below.


Multi-outcome: N parallel binaries + off-chain coherent dispatch

This is the core technical bet of the build, and the part worth reading even if you skip the rest.

The problem

The on-chain pmAMM is binary: one pool = one YES vs NO market. The Viber event needs a 14-outcome market: "Team 1 wins" vs "Team 2 wins" vs … vs "Team 14 wins", mutually exclusive, Σ p_i = 1.

Paradigm hasn't published a multi-outcome pm-AMM variant (the original 2024 paper is binary-only). Extending the Gaussian invariant to N-dim is open research. We had 5 days.

The solution (Option B in the PRD)

Run 14 binary YES/NO pmAMM pools in parallel, one per team. Each leg's YES token represents "Team i wins", each NO represents "Team i doesn't win".

Seed each leg at initialYesProbability = 1/N = 1/14 ≈ 7.14% so that at open Σ p_i = 1. Without this initial split, every leg seeded 50/50 would mean Σ = N/2 = 7.0 — a massive free arb against the house.

See app/src/lib/event-markets-config.ts for the preset, and SEED_TEAM_LEG_TOTAL_USD = $250.

Coherent dispatch (audience side)

When an audience phone clicks "Bet $20 on Team 7":

  • The tx hits only Team 7's pool (buy_outcome_tokens + swap NO→YES).
  • Team 7's YES price moves up; teams 1–6 and 8–14 don't move.
  • After the tx, Σ p_i > 1 by the size of the move on leg 7.

That's the off-chain dispatcher's job done: route a single user bet to the single right pool, in one atomic tx pair. No need for the user to think about the other 13 legs.

Coherent rebalance (house side)

Σ p_i drift is corrected by scripts/arb-house.mjs — the house arb daemon running on the operator's laptop during the event. Each tick (default 4s):

  1. Read YES price of every leg (14 RPC reads, parallelized).
  2. Compute Σ p_i. If |Σ − 1| ≤ THRESHOLD (default 0.02), sleep.
  3. Otherwise: proportional-scaling batch. Multiplier = 1/Σ. For every leg, target = current × multiplier. If target < current (leg over-priced post-flow), buy NO; if target > current, buy YES. Trades sized via a linear approx of the pmAMM sensitivity (PRICE_TO_USD_FACTOR = 600, calibrated to the $250-seed pool curvature).
  4. Fire all trades in parallel the same tick (Promise.all over executeArbTrade) — same way Wintermute / JaneStreet desks keep multi-outcome books synced after a flow shock.

The daemon signs from the faucet wallet (= platform treasury, holds the USDSpark mint authority, already SOL-funded). PnL on its book = the spread it captures over the event; settled when markets resolve.

A localhost webhook (POST /wake-up on :7331) lets the admin UI fire the daemon instantly when Mathis opens markets — no waiting for the next 4s tick.

Why off-chain dispatch and not on-chain

We considered (and rejected for the event):

  • NegRisk-style conditional tokens — too complex to ship in 5 days. (Memory note: also decided post-event that combinatorial markets aren't the right fit for the Spark Predict thesis — too LP-heavy for thin markets.)
  • Single on-chain dispatcher program — one tx that routes through N pools atomically. The right long-term move. See "Where to go next" below.

The off-chain daemon is a deliberate hackathon trade-off: more moving parts operationally (a laptop must stay online), but zero on-chain changes and a proven pattern from TradFi market-making.


How an audience member experiences it

                 ┌───────────────────┐
                 │  Big screen + QR  │   (single shared QR for all 100+ phones)
                 └─────────┬─────────┘
                           │ phone scans
                           ▼
        ┌──────────────────────────────────────┐
        │  POST /api/event/claim-next-wallet   │  ── atomic pop from
        │  → returns burner secret in URL      │     pre-funded pool
        └──────────────────┬───────────────────┘     (150 wallets,
                           │                          $1000 + 0.25 SOL each)
                           ▼
        ┌──────────────────────────────────────┐
        │  POST /api/pseudo                    │  ── pseudo + country flag
        │  → "alice 🇫🇷" locked to this pubkey  │     namespaced by event
        └──────────────────┬───────────────────┘
                           │
                           ▼
        ┌──────────────────────────────────────┐
        │  Grid view: 7 × 2 = 14 team tiles    │
        │  each tile shows live YES %          │
        └──────────────────┬───────────────────┘
                           │ tap Team 7
                           ▼
        ┌──────────────────────────────────────┐
        │  Inline panel below the grid:        │
        │  • "Bet on Team 7 — 7.4%"            │
        │  • Amount: $20 (chips $5/20/50/100)  │
        │  • "If Team 7 wins ≈ $X" (live)      │
        │  • [Bet] → 1-click buy_outcome+swap  │
        └──────────────────────────────────────┘

The bet preview is simulation-accurate: it runs simulateBuyOutcome — the same Gaussian-invariant math the on-chain program runs, ported to float64 and cross-validated against 82 reference vectors. What you see before clicking is what you redeem after resolution, within rounding.


Admin orchestration (the live-event console)

Everything Mathis touches during the event lives at /admin:

  • Setup — one-shot setup-event-markets.mjs script creates all 14 vaults, commits seed YES + seed NO at 1/14 ratio, waits the 60s commit window, launches each market on-chain. ~3 minutes end-to-end.

  • Lifecycle controls — per-market open / freeze / resolve buttons. Freeze gates the audience UI mid-event (e.g. just before a team is eliminated). Resolve cascades the YES/NO winner for that leg.

  • Reset (danger zone) — type-RESET confirmation cascades three operations:

    1. POST /api/event/reset-audience-state — wipes pseudos, user records, wallet-claimed flags via SCAN over viber:event-*:* keys; bumps the reset-cursor to kick every connected phone out.
    2. POST 127.0.0.1:7331/rebalance-wallets — daemon refunds every burner to exactly $1000 USDSpark from the faucet.
    3. POST 127.0.0.1:7331/reset-markets — daemon spawns setup-event-markets.mjs as a child process. Wipes the existing 14-team line-up from KV + recreates fresh ones on-chain at 1/14.

    Real-time progress badge during the ~3-min cascade; auto-clears the instant the operator takes any non-pending action (= leaves the "fresh baseline" state).

  • Sim observatory (/admin/sim) — live chart of every daemon tick: Σ p_i drift, per-leg price + arb counter-trades, cumulative house book.

  • Leaderboard — pseudo-aggregated PnL, snapshot-frozen at resolution time via SETNX so it doesn't shift during redemptions.

  • Print QRs + Room QR — operator-side helpers to print individual wallet QRs (for stage hand-outs) and the single shared audience QR (for the big screen).


Architecture

app/
├── src/
│   ├── app/
│   │   ├── page.tsx                     — audience home: 7×2 team grid + inline bet panel
│   │   ├── screen/page.tsx              — projector view: same grid + leaderboard
│   │   ├── import/page.tsx              — roster import (team labels, country flags)
│   │   ├── admin/
│   │   │   ├── page.tsx                 — market lifecycle controls + Reset
│   │   │   ├── sim/page.tsx             — live daemon observatory
│   │   │   ├── leaderboard/page.tsx     — pseudo leaderboard with snapshot freeze
│   │   │   ├── print-qrs/page.tsx       — printable wallet sheet
│   │   │   ├── room-qr/page.tsx         — fullscreen shared audience QR
│   │   │   └── layout.tsx               — sticky nav across /admin/*
│   │   ├── bet/[id]/page.tsx            — legacy 1v1 fallback (deep-link only)
│   │   └── api/event/
│   │       ├── markets/                 — KV-backed market registry
│   │       ├── markets-with-prices/     — aggregated KV + on-chain reads, edge-cached 3s
│   │       ├── claim-next-wallet/       — atomic pop from pre-funded pool
│   │       ├── claim-wallet/            — manual claim by ID (fallback)
│   │       ├── wallets/                 — wallet manifest (admin)
│   │       ├── seed-available-pool/     — repopulates the pool after reset
│   │       ├── reset-audience-state/    — SCAN-based pseudo wipe + cursor bump
│   │       ├── reset-cursor/            — per-event reset epoch for client invalidation
│   │       └── leaderboard/             — frozen leaderboard at resolution
│   ├── lib/
│   │   ├── pm-math.ts                   — pm-AMM math (Gaussian, swap solver, payout)
│   │   ├── event-markets-config.ts      — PRESET_MARKETS, 14-team config
│   │   ├── event-keys.ts                — KV key shape for viber:event-2026-05-16:*
│   │   ├── admin-actions.ts             — open/freeze/resolve wrappers
│   │   ├── country-flags.ts             — pseudo country picker
│   │   ├── pseudo-client.ts             — POST /api/pseudo wrapper
│   │   ├── burner-wallet.ts             — keypair gen + localStorage persistence
│   │   ├── active-wallet.ts             — Phantom (admin) ↔ burner (audience) switcher
│   │   ├── pda.ts                       — Anchor PDA derivation
│   │   ├── read-only-program.ts         — IDL wrapper with no signer (RPC reads only)
│   │   ├── kv.ts                        — Upstash client w/ graceful fallback
│   │   └── constants.ts                 — TEAM_COUNT, EVENT_ID, fees, seed sizes
│   └── components/providers.tsx         — wallet adapter + Anchor provider
├── scripts/
│   ├── setup-event-markets.mjs          — one-shot creates+commits+launches 14 vaults
│   ├── generate-event-wallets.mjs       — pre-funds 150 burners + persists manifest
│   ├── arb-house.mjs                    — live daemon: proportional-scaling batch arb
│   ├── sim-event.mjs                    — replay/synthetic flow harness (no real audience)
│   ├── verify-end-to-end.mjs            — predicted vs realized payout cross-check
│   ├── launchd/                         — macOS plist for arb daemon (auto-start)
│   └── test-pm-math.mjs                 — 82-vector math cross-validation
└── package.json

docs/
└── PAYOUT_MATH.md                       — derivation of the pro-rata redemption clamp

Stack

Layer Tech
On-chain Solana + Anchor 0.32 (pm-AMM, unchanged), Token-2022 collateral, classic SPL outcome mints
Frontend Next.js 16 (App Router) + React 19 + TypeScript
AMM Paradigm pm-AMM (y−x)·Φ((y−x)/L) + L·ϕ((y−x)/L) − y = 0, fee 100 bps
Off-chain math pm-math.ts (float64) cross-validated against on-chain math via 82 vectors
State Upstash Redis, namespaced by EVENT_ID = viber:event-2026-05-16
Hosting Vercel (auto-deploy on main), Helius devnet RPC
Daemon host Operator laptop (Node 22, optional launchd plist)
Brand Viber DA — pure black + purple/orange + Inter

Setup

Prerequisites

  • Node 22+ (--experimental-strip-types for the scripts)
  • pnpm
  • A Solana wallet (Phantom on devnet for admin)
  • A funded faucet keypair (devnet SOL + USDSpark, holds USDSpark mint authority)
  • Upstash Redis credentials
  • Helius devnet API key (separate from mainnet — see DevNet caveats)

Local dev

git clone https://github.com/MathisSpark/Spark-Predict-Viber.git
cd Spark-Predict-Viber/app
pnpm install
cp .env.example .env.local
# fill in:
#   NEXT_PUBLIC_RPC_URL=https://devnet.helius-rpc.com/?api-key=…
#   FAUCET_SECRET_KEY=<base58 keypair>
#   KV_REST_API_URL=… / KV_REST_API_TOKEN=…
pnpm dev

Pre-event setup (run once)

cd app
# 1. Mint + fund 150 audience burners. Persists manifest to KV.
node --experimental-strip-types scripts/generate-event-wallets.mjs

# 2. Create + commit + launch 14 team markets on-chain.
node --experimental-strip-types scripts/setup-event-markets.mjs

# 3. Run the house arb daemon (keep running for the whole event).
node --experimental-strip-types scripts/arb-house.mjs \
  --threshold 0.02 --tick-ms 4000 --max-size 200

Math sanity check

pnpm test:math
# 82/82 passed within tolerance.

Run after any change to pm-math.tsoracle/test_vectors.json is the on-chain reference.

Replay / dry-run

node --experimental-strip-types scripts/sim-event.mjs

Synthetic audience flow that hits the same API surface as real phones — useful for tuning PRICE_TO_USD_FACTOR and watching the daemon converge without booking a stage.


Known limits (devnet hackathon, not mainnet)

  • No oracle, no dispute window — admin marks the winning team. Buddy-vs- buddy + visible stage = fine. Adversarial = not fine.
  • Burner secrets in URL + localStorage — devnet-only by design.
  • Platform fee enforced off-chain — anyone bypassing the UI skips it. The on-chain LP fee is enforced by the pool.
  • No rate-limit on /api/event/claim-next-wallet — pool drainable by anyone POSTing repeatedly. Devnet, doesn't matter.
  • Helius devnet API key hardcoded in scripts — fine for the demo (devnet, zero financial exposure); rotate before any real deployment.
  • Daemon = single point of failure during the event — if the laptop dies, Σ drifts until restart. Two operators / dual-host failover would be the obvious next step.
  • No Σ-coherence guarantee inside a single slot — audience tx and daemon counter-trades land in different blocks. Theoretical 2-tick window where a fast actor could front-run the daemon. See "Jito bundles" below.

Where to go next

If we were keeping this alive past the event:

🚀 Jito bundles for Σ-coherent execution (the big one)

Today the daemon corrects Σ ~4s after an audience trade lands. A fast bot watching the mempool could:

  1. Spot Team 7's YES being bought (Σ now > 1).
  2. Predict the daemon's next counter-trade (sell YES on the over-priced legs, buy YES on the under-priced ones).
  3. Front-run the daemon's correction.

Fix: route both the audience trade and the daemon's counter-trades through a single Jito bundle, guaranteeing all of them land in the same slot or none of them land. Σ stays ≈ 1 at every block height an outside observer can read.

Wiring sketch — audience flow becomes:

  • Phone signs buy_outcome_tokens + swap (Team N pool only).
  • Tx is sent to the daemon's bundler endpoint, not the public RPC.
  • Daemon adds the proportional-scaling counter-trades for the other 13 legs.
  • Bundle ships to Jito block engine with a tip.
  • Either the whole 14-tx bundle lands in slot S, or nothing does.

Cost: ~1k lamports tip per bundle on devnet, way below the LP fee revenue.

🔁 On-chain coherent dispatcher (kill the daemon entirely)

The natural endgame. New Anchor program:

  • dispatch_buy(user, team_idx, amount) — routes user's collateral through pmAMM pool for team team_idx, and atomically issues offsetting trades on the other 13 pools to enforce Σ = 1 post-trade.
  • Liquidity ledger lives on-chain, no off-chain bot, no laptop dependency.

This is the right long-term shape (and what the next Spark Predict iteration will likely converge to — see Predict tech actuelle — pm-AMM multi-outcome (coherent dispatch)). The Viber event was the off-chain prototype that proved the dispatch model works in production under flow.

🧮 Native multi-outcome pm-AMM

Replace the "N parallel binaries" hack with a true multi-outcome pmAMM. The Paradigm 2024 paper gives the binary invariant; extending to N-dim is open research. If it works:

  • No need to enforce Σ = 1 — it's the invariant.
  • Capital efficiency is dramatically better (one shared L pool vs N siloed $250 pools).
  • No arb leak windows by construction.

🛰️ Helius webhook subscribe

Replace the 1.5s polling on /page.tsx with subscribed pool-reserve events.

  • Cuts RPC load ~30×.
  • Sub-second UI refresh after every audience trade.
  • Daemon ticks become event-driven rather than wall-clock.

🛡️ Optimistic oracle for resolution

UMA-style. Admin proposes the winning team; 24h challenge window; if unchallenged, resolution finalizes. Devnet = same UX (admin still proposes); mainnet = adversarial-safe by construction.

🪪 Custodial burner signer

For mainnet: route burner signing through a custodial KMS (Squads, Fordefi, or in-house) instead of leaving secrets in localStorage. Same audience UX (scan → bet), real money OK.

🧪 More replay tooling

sim-event.mjs is one synthetic. Run a Monte Carlo across 100 audience profiles (degen / passive / coordinated whale) and measure daemon convergence

  • realized PnL distribution. Tunes PRICE_TO_USD_FACTOR and THRESHOLD without needing a real stage.

Origin

Spark-Predict-Viber is a fork of MathisSpark/1vs1ThenEveryoneJoin (itself a rewrite of as1fansar1/NSHackathon). Built for the Viber Night Live event at Network School, Singapore, May 2026.

Inherited unchanged from the 1v1 base: the on-chain pm_amm Anchor program, the pm-math.ts math library, the pmAMM swap mechanics, the pro-rata redemption clamp (docs/PAYOUT_MATH.md).

Net-new for this repo:

  • 14-team multi-outcome composition + off-chain coherent dispatch
  • House arb daemon (scripts/arb-house.mjs)
  • One-shot event setup (scripts/setup-event-markets.mjs)
  • Pre-funded burner pool + atomic claim flow
  • Single-page audience UX (grid + inline bet panel)
  • Admin orchestration (/admin/* with type-RESET cascade)
  • Big-screen display (/screen)
  • Sim observatory (/admin/sim)
  • Pseudo + country-flag registry, namespaced per event

The on-chain program is unchanged. Everything novel here lives client-side or in the daemon — that was the bet, and it shipped on time.


License

MIT

About

Spark Predict — Viber event edition. Forked from Viber-Prediction-Market (1v1 base). Audience-side live betting with admin-creates-markets flow + multi-outcome team picker + arb bot.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages