Components
signal_markets — the Anchor program
signal_markets — the Anchor program
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.keeper — the off-chain service
keeper — the off-chain service
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.mock_validator — the fallback
mock_validator — the fallback
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.frontend-react — the app
frontend-react — the app
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_countstarts at 1 and noGroupMemberis minted for them.update_grouplets the owner edit name, fee, visibility, and roster later.request_joinmints a pendingGroupMember.approve_member(owner only) flips it to approved and bumpsmember_count.leave_groupcloses a member’sGroupMember; 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 towardmarket.total_*, so group money competes in the real pool, but per-member accounting lives in a sharedGroupPool(group + market aggregate) and aGroupPosition(per member, per side). The owner deposits without aGroupMember; other members must be approved first.claim_group()pays a member their pro-rata payout. The winnings math is identical toclaim, but it is keyed off theGroupPosition, and the group’sfee_bpsroutes to the group owner as the group’s house.
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.
