Skip to main content
Volmarket is four components: an on-chain program, an off-chain keeper, a fallback validator, and a web app.

Components

The on-chain core. It holds escrow vault PDAs, Market and Position accounts, a permissionless resolve_market that CPIs the validator, a pro-rata claim minus fee_bps, and a full group layer (Group, GroupMember, GroupPool, GroupPosition).Non-custodial throughout — no operator ever holds user funds.Deployed on devnet at 86hERt8cdRZUBpc1Ng8coX2jwLmWGUcyc9JNfspw39yr.
A TypeScript service that watches the TxLINE odds stream, matches updates to open markets, reads the settling outcome’s probability, fetches the Merkle proof, and submits resolve_market.It also seeds the opposing pool of new two-sided markets so payouts are real, and serves the frontend’s live feed over HTTP — /signal, /fixtures, /receipt, plus /health for host probes.Because it watches a stream and polls the chain, it must run as a long-lived process. See Deployment.Resilience. A rate-limit response from the devnet RPC surfaces from inside the web3.js jayson client callback with no application frames on the stack, so an ordinary try/catch around an interval tick cannot see it — it escapes as an uncaught exception. Process-level guards (src/crashGuard.ts) swallow transient RPC and network faults and let the keeper’s own retry timers pick the work back up. Anything unrecognized still exits, so real bugs stay loud, and a sustained flood (40+ per minute) exits deliberately for a clean restart.
A small native program that approves any proof.The real validate_odds CPI is verified on devnet, but proofs publish on a 5-minute epoch that short prediction windows outrun, so the demo runs on the mock. See epoch timing.Deployed on devnet at FPnwSSp2DXcNvJnxXWc2JXvU4MLNfrWDT6wBcU5Eptse.
React + Vite + Privy. It renders the match board and the per-odd signal terminal, plus the deposit/claim, combo-slip, and group flows.It talks only to public RPC and read-only chain state, and holds no secrets. Country flags render from bundled SVGs rather than regional-indicator emoji, which do not render on Windows.

Groups

A group is a named roster with its own fee that pools predictions into a shared market position.
  • create_group(group_id, name, fee_bps, visibility, roster) — identity is (owner, group_id), so one owner can run several groups. The owner is the implicit first member: member_count starts at 1 and no GroupMember is minted for them. update_group lets the owner edit name, fee, visibility, and roster later.
  • request_join mints a pending GroupMember. approve_member (owner only) flips it to approved and bumps member_count. leave_group closes a member’s GroupMember; the owner cannot leave.
  • group_deposit(side, amount) stakes into a market as part of the group. Funds enter the same market vault and count toward market.total_*, so group money competes in the real pool, but per-member accounting lives in a shared GroupPool (group + market aggregate) and a GroupPosition (per member, per side). The owner deposits without a GroupMember; other members must be approved first.
  • claim_group() pays a member their pro-rata payout. The winnings math is identical to claim, but it is keyed off the GroupPosition, and the group’s fee_bps routes to the group owner as the group’s house.
Because group stake flows into the market’s own vault and totals, group and individual predictions settle against the same market outcome. Each side is accounted exactly once — either a Position or a GroupPosition — so the vault conserves.

Group stats

The Predictions / PnL / Win rate figures on a group card are derived from on-chain data, not stored on-chain and not stubbed. fetchGroupActivity reads every GroupPosition account, joins each to its market, and filters to the canonical USDC mint. groupStats then computes the three figures, mirroring the claim_group payout math — a settled winner nets its pro-rata share of the losing pool minus the group fee, a settled loser is −stake. preds counts all of a group’s calls. pnl and wr count only settled ones, so a group whose calls are all still open reads 0 for both. That is an accurate reading of no settled activity, not a placeholder.

Deployed addresses