A simple Yes/No prediction market that demonstrates Somnia Reactivity for both on-chain automation and off-chain real-time monitoring.
Live Demo: https://prediction-market-reactivity.vercel.app/
User calls resolve()
│
▼
PredictionMarket emits MarketResolved
│
▼ (Somnia Reactivity — within ~2 seconds)
Validator invokes PayoutHandler._onEvent()
│
▼
PayoutHandler calls market.distribute()
│
▼
Winners receive payouts automatically
Two contracts:
| Contract | Role |
|---|---|
PredictionMarket.sol |
Market logic — create, bet, resolve, distribute |
PayoutHandler.sol |
Reactivity handler — auto-triggers payouts on resolve |
Two Reactivity modes demonstrated:
- On-chain: Subscription triggers
PayoutHandlerwhenMarketResolvedis emitted → automatic payout in ~2 seconds - Off-chain: WebSocket subscription streams live events to the frontend and CLI
prediction-market/
├── contracts/
│ ├── PredictionMarket.sol # Market logic
│ └── PayoutHandler.sol # Reactivity event handler (with try/catch safety)
├── scripts/
│ ├── config.ts # Shared config (chain, SDK, ABIs)
│ ├── 1-deploy.ts # Deploy both contracts
│ ├── 2-setup-subscription.ts # Create on-chain Reactivity subscription
│ ├── 3-interact.ts # CLI demo (create → bet → resolve)
│ └── 4-watch.ts # Live CLI event watcher
├── frontend/ # React + Vite UI
│ ├── src/
│ │ ├── App.tsx # Wallet connection + layout
│ │ ├── config.ts # Chain & contract config
│ │ └── components/
│ │ ├── MarketPanel.tsx # Create, bet, resolve, distribute UI
│ │ └── EventFeed.tsx # Live WebSocket event stream
│ └── ...
├── .env.example # Environment template
├── package.json # Backend scripts
└── hardhat.config.ts # Solidity compiler (0.8.30)
- Node.js 20+
- MetaMask (for the frontend)
- Somnia Testnet wallet with 32+ STT
npm install
npm run compile
cd frontend && npm install && cd ..cp .env.example .envEdit .env with your private key.
npm run deployCopy the printed addresses into:
.env→MARKET_CONTRACTandHANDLER_CONTRACTfrontend/src/config.ts→MARKET_ADDRESSandHANDLER_ADDRESS
npm run setupThis creates an on-chain subscription: when MarketResolved is emitted, validators automatically invoke PayoutHandler which calls distribute().
cd frontend
npm run devOpen the URL shown, connect MetaMask (Somnia Testnet), and interact with the market.
# Terminal 1 — live event watcher
npm run watch
# Terminal 2 — create market, bet, resolve
npm run interactawait sdk.createSoliditySubscription({
handlerContractAddress: handlerAddr,
emitter: marketAddr,
eventTopics: [keccak256(toHex('MarketResolved(uint256,bool)'))],
gasLimit: 2_000_000n, // Must cover handler + distribute() gas
isGuaranteed: true,
isCoalesced: false,
...gasParams,
})contract PayoutHandler is SomniaEventHandler {
function _onEvent(address, bytes32[] calldata topics, bytes calldata)
internal override
{
uint256 marketId = uint256(topics[1]);
// try/catch prevents revert → no infinite retry loop
try market.distribute(marketId) {
emit AutoPayout(marketId, true);
} catch {
emit AutoPayout(marketId, false);
}
}
}wsClient.watchContractEvent({
address: MARKET_ADDRESS,
abi: MarketABI,
onLogs: (logs) => {
// Real-time updates: MarketCreated, BetPlaced, MarketResolved, PayoutSent
},
})| Issue | Cause | Fix |
|---|---|---|
| Handler reverts → infinite retry | isGuaranteed: true retries on revert |
Wrap external calls in try/catch |
distribute() fails from handler |
500K gas wasn't enough for storage + ETH transfers | Increased to 2M based on estimateContractGas |
| Contract | Address |
|---|---|
| PredictionMarket | 0xe7a5bb2078ccb8ae7ed60f9dd027b11c92d65665 |
| PayoutHandler | 0x409116f1489f1e0f649a4efffad78bf619f7bc14 |
@somnia-chain/reactivity— TypeScript SDK (scripts)@somnia-chain/reactivity-contracts— Solidity base contractsviem— Ethereum client library- React + Vite — Frontend