Bet2Gether
Chainlink Automation-Driven Round Resolution






Autonomous oracle settlement
Chainlink Automation calls performUpkeep and the pool reads the price feed; a Tenderly action then requests the VRF-picked reward mint.
Overview
Prediction markets only stay interesting when settlement is credibly neutral. Operator-settled designs ask you to trust whoever calls resolve and picks the price. Permissionless on-chain designs still need something to trigger resolution at the deadline: if only winners call it, timing becomes gameable; if a single bot runs it, trust shifts to that operator. Random rewards driven by block metadata or admin-picked tokenIds are easy to bias or predict.
Once a round opens, resolution is an on-chain transition tied to oracle inputs. PredictionPool reads Chainlink Price Feeds when the round resolves; Chainlink Automation calls performUpkeep after the deadline (the call is also open to anyone, which leaves the timing gap described under Known limitations); Chainlink VRF v2.5 picks the ERC-1155 reward tokenId; the mint itself is requested by a Tenderly Web3 Action that signs with my own key, when the round creator wins.
Technical Architecture
Bet2Gether is a small prediction-market case study on Ethereum Sepolia. Rounds resolve from an allow-listed Chainlink Price Feed when the pool's resolve step runs after the deadline; the settlement price is whatever the feed returns at resolution time, not an ad hoc admin choice.
PredictionPool implements AutomationCompatibleInterface: checkUpkeep flags due rounds and performUpkeep resolves them. PredictionPoolToken mints randomized ERC-1155 collectibles via VRFConsumerBaseV2Plus; a Tenderly Web3 Action listens for PredictionPool_RoundResolved and, when creatorIsWinner is true, calls mint with MINTER_ROLE protection.
The Next.js and TypeScript client uses wagmi, viem, Alchemy WebSockets, and RainbowKit for wallet UX and event-driven UI.
Key features
- Prediction games with oracle pricing: Players bet on crypto price movements; settlement reads Chainlink Price Feeds.
- CEI Pattern & Reentrancy Guard: In claimReward, internal state (e.g. claimed) is updated before the external ETH transfer (Checks-Effects-Interactions). nonReentrant protects claimReward.
- Automation-Driven Resolution: PredictionPool implements AutomationCompatibleInterface: Chainlink Automation calls performUpkeep for due rounds. It is permissionless but defensive: each round ID in
performDatais re-checked, so a bogus call cannot resolve a round early or twice. - VRF-Picked NFT Rewards: When a round creator wins, a Tenderly action requests a mint and Chainlink VRF v2.5 picks the ERC-1155 tokenId. The consumer stores each request and rejects unknown or already-fulfilled ones.
- Time-Weighted Payouts: getBetWeight weights each bet by
(round.end - bet.time) / (round.end - round.start), computed in 1e18 fixed point, so an earlier bet weighs more than a late one. (See the known payout bug under limitations.) - Real-Time Updates: Toasts and table refreshes are driven by contract events (
PredictionPool_NewBet,PredictionPool_NewRoundStatus,PredictionPoolToken_Mint) over Alchemy WebSockets and wagmi listeners. - Consistent Price Decimals: Feeds with up to 18 decimals are normalized to 1e18 and answers at or below zero are rejected; the frontend sends the target through parseEther, so both sides compare on the same scale.
- Smart Contract Testing: 65 Foundry tests, including a time-weight edge case: a last-second bet in a 365-day round.
Technologies Used
- Next.js
- TypeScript
- Solidity
- Foundry
- Chainlink
- OpenZeppelin
- viem
- wagmi
- RainbowKit
- Alchemy
- Tenderly
- React
- TanStack Query
- TanStack Table
- React Hook Form
- Zod
- shadcn/ui
- Tailwind CSS
- Solidity + Foundry: Core game logic, payout calculations and VRF integration.
- Next.js + TypeScript + wagmi: Frontend framework with wallet integration and event-driven data flow.
- Chainlink Services: Price Feeds for the settlement price, Automation to trigger resolution, and VRF for NFT randomness.
- Alchemy WebSockets: Live updates for new games, bets, resolutions and NFT rewards.
- OpenZeppelin: Ownable, AccessControl, ReentrancyGuard, and ERC1155:
claimRewardreentrancy guard, MINTER_ROLE-gated mint, and the PredictionPoolToken ledger.
Challenges and Learnings
Wiring three Chainlink surfaces (Price Feeds, Automation, VRF) into one round lifecycle, and keeping the permissionless upkeep safe against bogus calls. Real-time UI needed selective refetching from event listeners instead of polling.
Outcome
Bet2Gether is deployed on Ethereum Sepolia with verified PredictionPool and PredictionPoolToken on Etherscan. It is a learning exercise in oracle-driven resolution, autonomous upkeep, verifiable randomness for collectibles, and a small event-driven Next.js and TypeScript client, not a production prediction market.
Key Metrics
- Sepolia addresses: PredictionPool · PredictionPoolToken
- Chainlink: Price Feeds (settlement), Automation (performUpkeep), VRF v2.5 (ERC-1155 reward randomness)
- Settlement: the Chainlink Price Feed's latest answer, read when the round is resolved (see the oracle limitation below)
- Real-time UI: Alchemy WebSockets + wagmi listeners
- Tests: 65 Foundry tests. Coverage report from 23 Nov 2025 (
be/lcov.info): PredictionPool 88.9% lines / 77.8% branches; PredictionPoolToken 85.7% lines / 25% branches. The payout path with players on both sides is not tested (see the known bug).checkUpkeepitself has no test; the tests callperformUpkeepdirectly.
Transferable patterns: Oracle-driven settlement, hybrid on/off-chain automation, event-driven UI architecture.
Known limitations
- Known bug, payout:
claimRewarddivides each winner's weight by the total weight of both sides, so winners together receive only part of the pool and the rest stays locked in the contract; if nobody bets on the winning side, the whole pool is locked. No refund or sweep exists. The fix is to divide by the winning side's weight only and add a refund path for one-sided rounds. - Owner can freeze resolution: the owner can disable a price feed; rounds on that feed then revert when resolved, and because
performUpkeepresolves due rounds in one batch, one revert blocks every due round. - Unbounded loops:
checkUpkeepscans every round ever created andclaimRewardloops over every player, so gas grows with use until Automation's check limit or a block limit is hit. - Oracle timing and staleness: PredictionPool reads
latestRoundData()whenperformUpkeepruns and ignoresupdatedAt.performUpkeepis permissionless, so anyone can resolve a round after its deadline and the settlement price depends on when that call lands, not on the price atround.end; a stale answer would also be accepted. The fix is to walk back withgetRoundDatato the last answer at or before the deadline and to bound staleness against the feed heartbeat. - Testnet only: Deployed on Ethereum Sepolia for experimentation, not mainnet money at risk.
- Tenderly bridge: The reward mint depends on an off-chain Web3 Action signing with my own key, which holds MINTER_ROLE and also owns PredictionPool. It handles only the first
RoundResolvedevent per transaction and logs failures without retrying, so a winning creator can miss an NFT. - Rounding dust: the remainder of the weight math stays in the contract.
- Unused states:
NotStartedandEndedexist in the round enum but are never kept: a round goes from Active to Resolved.
Roadmap
- L2 convergence: Deploy to Arbitrum, Optimism, or similar to compare settlement cost and UX vs Sepolia.
- Fix the payout: Divide by the winning side's weight and refund one-sided rounds.
- On-chain reward path: Replace Tenderly-triggered minting with a call from PredictionPool, so no off-chain key is needed.
- Bounded loops: Track due rounds and claim per player instead of scanning every round and player.