Skip to content

Bet2Gether

Built Oct–Nov 2025Testnet

Chainlink Automation-Driven Round Resolution

SBSiegfried Bozza
Bet2Gether landing page with the Start Betting button
Bet2Gether create-game form: market pair, target price, side, duration and bet amount
Bet2Gether: placing a bet on LINK/USD above 13.25 USD, with the MetaMask request on Sepolia
Bet2Gether: claiming rewards on a won game, with the MetaMask request
Bet2Gether games table with round statuses and a reward-NFT minted notice
Tenderly Web3 Action logs for a Bet2Gether reward-NFT mint

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 performData is 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

Solidity prediction pools with Chainlink Price Feeds, Automation, and VRF; rounds go from Active to Resolved on an Automation upkeep, on Ethereum Sepolia (Next.js + TypeScript + wagmi + Tenderly). Has a known payout bug, listed under Known limitations.
  • Next.js
  • Type­Script
  • Solidity
  • Foundry
  • Chainlink
  • Open­Zeppelin
  • viem
  • wagmi
  • Rainbow­Kit
  • Alchemy
  • Tenderly
  • React
  • Tan­Stack Query
  • Tan­Stack 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: claimReward reentrancy 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). checkUpkeep itself has no test; the tests call performUpkeep directly.

Transferable patterns: Oracle-driven settlement, hybrid on/off-chain automation, event-driven UI architecture.

Known limitations

  • Known bug, payout: claimReward divides 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 performUpkeep resolves due rounds in one batch, one revert blocks every due round.
  • Unbounded loops: checkUpkeep scans every round ever created and claimReward loops 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() when performUpkeep runs and ignores updatedAt. performUpkeep is permissionless, so anyone can resolve a round after its deadline and the settlement price depends on when that call lands, not on the price at round.end; a stale answer would also be accepted. The fix is to walk back with getRoundData to 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 RoundResolved event 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: NotStarted and Ended exist 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.
Next projectForgeAtomic ERC-1155 Crafting