Live App: https://oracle-racer.vercel.app
Oracle Racer is a decentralized, event-driven racing game built on Base Sepolia, where players compete using real crypto price movements.
Each player selects a cryptocurrency price feed (ETH, BTC, SOL, etc.), and the winner is determined by comparing price snapshots at race start and race finish.
The project is designed specifically to demonstrate RedStone Pull Oracles in a realistic production-grade architecture, combining:
- on-chain game logic
- on-demand oracle reads
- automated relayer execution
- an integrated USDC betting pool
🏆 Built for RedStone Buildathon – Showcasing Pull oracles with a real, interactive, event-based dApp.
Oracle Racer is a multiplayer on-chain game where:
- Players race crypto assets, not avatars
- Each race lasts a fixed duration (30–300 seconds)
- Prices are fetched only twice using RedStone Pull oracles
- Performance + volatility determine the winner
- Players can bet USDC on the outcome
- Winners are paid automatically
- Create & join races
- Unique price feed per player
- Automatic race start after countdown
- Deterministic on-chain scoring
- Global leaderboard & player stats
- RedStone Pull Model
- No on-chain price storage
- Fresh prices at exact execution time
- Only 2 oracle reads per race
- USDC betting pool
- Equal odds system
- 5% house edge
- Automatic settlement
- No claim transaction
Oracle Racer uses RedStone Pull Oracles, not Push.
| Pull Model (Used) | Push Model (Not Used) |
|---|---|
| On-demand data | Periodic updates |
| No storage | On-chain storage |
| Event-based | Continuous |
| Cheaper | More expensive |
| Perfect for games | Overkill here |
Prices are read directly from transaction calldata using:
getOracleNumericValueFromTxMsg(feedId)This ensures:
- always fresh prices
- no stale data
- minimal gas usage
- clean and auditable logic
This architecture explains how a user experiences the system.
User
│
│ Wallet transactions (create/join/bet)
▼
Frontend (Next.js + Wagmi)
│
│ Reads game state via RPC
│ Calls backend for automation
▼
Backend Relayer (API Routes)
│
│ Fetches RedStone data
│ Sends automated TXs
▼
Base Sepolia Blockchain
│
├─ OracleRacer.sol
└─ BettingPool.sol
- User creates or joins a race
- Race fills → countdown starts
- User may place USDC bets
- Race starts automatically
- Race finishes automatically
- Winner, payouts, leaderboard updated
This architecture explains how the system actually works internally.
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER (Wallet) │
│ - createRace() │
│ - joinRace() │
│ - placeBet() │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js + Wagmi + Viem) │
│ - Displays races, timers, odds, leaderboard │
│ - Calls backend for automation │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ BACKEND RELAYER (Next.js API Routes) │
│ - Detects startable / finishable races │
│ - Fetches RedStone signed price packages │
│ - Appends oracle payload to calldata │
│ - Sends TXs paying gas │
│ - Locking mechanism (no double execution) │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ REDSTONE GATEWAY │
│ - Signed price data │
│ - 3+ independent signers │
│ - Timestamp validation │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ BASE SEPOLIA BLOCKCHAIN │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ OracleRacer.sol │ │ BettingPool.sol │ │
│ │ - Game logic │ │ - USDC bets │ │
│ │ - Oracle Pull reads │────▶│ - Auto settlement │ │
│ │ - Scoring & winner │ │ - Instant payouts │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
│ USDC Token (ERC20) │
└─────────────────────────────────────────────────────────────────────────────┘
0x26252A77d82EBD4E8ba70880d5E8a94E6E9E6B4d
Purpose: race lifecycle, player registration, RedStone Pull reads (start & finish snapshots), scoring, leaderboard, and optional betting settlement callback.
-
createRace(uint256 duration, uint256 maxPlayers, bytes32 feedId) → (uint256 raceId)
Creates a race and auto-joins the creator as the first player. -
joinRace(uint256 raceId, bytes32 feedId)
Joins an existing race with a unique feed.
When the race becomes full, it starts the countdown window.
-
startRace(uint256 raceId)
Starts the race after the countdown and pulls start prices for each feed using RedStone payload in calldata. -
finishRace(uint256 raceId)(nonReentrant)
Finishes the race after the duration, pulls end prices, computes scores, selects the winner, awards points, and notifies BettingPool.
✅ These are the two oracle transactions using Pull Model:
TX #1:startRace()reads start prices
TX #2:finishRace()reads end prices + settles result
-
getRace(uint256 raceId) → (...)
Returns the full race state (timings, status, winner, etc.). -
canStart(uint256 raceId) → (bool)
Returns true if countdown ended and the race can start. -
getCountdownRemaining(uint256 raceId) → (uint256)
Seconds remaining before the race can start (0 if startable). -
getRacers(uint256 raceId) → (Racer[] memory)
Returns all racers (players + feedIds + scores). -
getAvailableFeeds(uint256 raceId) → (bytes32[] memory)
Returns which feeds are still free in that race. -
getSupportedFeeds() → (bytes32[] memory)
Returns the global supported feed list (ETH, BTC, SOL, ...). -
getStats(address player) → (PlayerStats memory)
Returns player stats (races, wins, points). -
getTopPlayers(uint256 count) → (address[] memory players, uint256[] memory points)
Returns the current leaderboard top N. -
canJoin(uint256 raceId) → (bool)
True if the race is still open for joining. -
needsOneMorePlayer(uint256 raceId) → (bool)
True if race is waiting and missing exactly one player. -
canFinish(uint256 raceId) → (bool)
True if the race duration ended and can be finished. -
getTimeRemaining(uint256 raceId) → (uint256)
Remaining seconds while race is active (0 otherwise).
-
addFeed(bytes32 feedId)
Adds a supported feed (owner only). -
setBettingPool(address bettingPool)
Sets the BettingPool contract address (owner only).
-
_startRace(uint256 raceId)
Sets Active status, times, reads start prices from oracle calldata. -
_notifyBettingPool(uint256 raceId, bytes32 winningFeed)
CallsBettingPool.settleRace(...)in a try/catch (non-blocking). -
_join(uint256 raceId, address player, bytes32 feedId)
Adds a player to the race and marks feed as taken. -
_awardPoints(uint256 raceId)
Sorts players by final score and assigns ranking points. -
_updateLeaderboard(address player)
Inserts / reorders player in leaderboard based on points.
0x60B383B3C107E0BAdf2CFA872021d4dD123bf947
Purpose: accept USDC bets during Waiting, compute equal odds, reserve bankroll for payouts, and automatically settle winners after the race finishes.
-
placeBet(uint256 raceId, bytes32 feedId, uint256 amount) → (uint256 betId)(nonReentrant)
Places a USDC bet on a feed while race is Waiting.
Transfers USDC in, records odds, reserves pending payouts, and tracks bet. -
claimWinnings(uint256 betId)(nonReentrant)
Allows manual claim if not auto-paid (fallback UX).
In your current flow, winners are typically auto-paid during settlement.
settleRace(uint256 raceId, bytes32 winningFeed)(nonReentrant)
Called automatically byOracleRacerafterfinishRace().
Marks race settled, iterates over bets, auto-pays winners, processes losers.
✅ Authorization: only
OracleRaceror owner can callsettleRace().
-
depositBankroll(uint256 amount)
Owner deposits USDC liquidity to cover payouts. -
withdrawBankroll(uint256 amount)
Owner withdraws available bankroll (cannot withdraw reserved pending payouts).
-
getOddsForRace(uint256 raceId) → (uint256 odds)
Returns equal odds for that race:
odds = maxPlayers * (100 - HOUSE_EDGE_PERCENT)(min 1.0x) -
getRaceBets(uint256 raceId) → (uint256[] memory)
Returns all bet IDs for a race. -
getUserBets(address user) → (uint256[] memory)
Returns all bet IDs for a user. -
getBet(uint256 betId) → (bettor, raceId, feedId, amount, odds, claimed, won)
Returns full bet details. -
calculatePayout(uint256 amount, uint256 odds) → (uint256)
Helper function to compute payout. -
getAvailableBankroll() → (uint256)
Returns bankroll not reserved for pending payouts.
- Equal odds for all feeds simplifies UX and avoids “oracle manipulation of odds”.
- Odds depend on race size:
maxPlayers × 95
(5% house edge embedded directly in the multiplier) - This creates a clean, predictable betting model for a hackathon demo.
-
IOracleRacer
A minimal read-only interface used by BettingPool to fetch race state & racer list without importing OracleRacer implementation. -
IBettingPool
A minimal interface used by OracleRacer to notify BettingPool of the winning feed without tight coupling.
Oracle Pull – Start Price Snapshot
Triggered after countdown.
What happens:
- Race becomes Active
- startTime and endTime set
- Oracle prices fetched from calldata
uint256 price = getOracleNumericValueFromTxMsg(feedId);Purpose:
- Snapshot initial prices
- No storage, no push
- First oracle read
Oracle Pull – End Price Snapshot & Settlement
Triggered after race duration.
What happens:
- Oracle prices fetched again
- Scores computed on-chain
- Winner selected
- BettingPool settled automatically
uint256 endPrice = getOracleNumericValueFromTxMsg(feedId);Purpose:
- Snapshot final prices
- Determine outcome
- Second and final oracle read
- Makes the game economically meaningful
- Creates real incentives
- Showcases composability
- Demonstrates cross-contract settlement
- Equal odds for all feeds
- 5% house edge
- Bets only during countdown
- Automatic payouts
- No claim transaction
- List all races
- Join / create race
- See available feeds
- Countdown / timer
- Live race state
- Final results
- Select feed
- Place USDC bet
- See odds & limits
- Global ranking
- Player stats
- Wins & points
- RedStone signature verification
- Timestamp validation
- Reentrancy protection
- Relayer locking
- Try/catch on betting settlement
- No blocking external calls
- Event-driven oracle usage
- Minimal oracle reads
- No on-chain price storage
- Real-world UX
- Clear separation of concerns
- Production-ready architecture
Oracle Racer demonstrates how RedStone Pull Oracles can be used to build:
- Interactive games
- Time-based competitions
- Deterministic on-chain outcomes
- Gas-efficient oracle consumption




