Crypto Casino Free Bet System Development
A free bet is a free wager that the casino provides to the player. Unlike a deposit bonus, a free bet does not require a deposit. The user gets the opportunity to play for free, but when winning, receives only net winnings, not the full stake + profit.
Free Bet Mechanics
Standard mechanics:
- Player receives a free bet of $10
- Bets the free bet on an event with odds of 2.0
- On winning, receives $10 (profit), not $20 (stake return + profit)
- On losing — loses nothing (the bet was free)
Free Spin (slots) — free bet equivalent for slot machines. A certain number of spins without using real balance.
Data Model
class FreeBet(BaseModel):
id: str
user_id: str
type: str # 'SPORTS_BET', 'CASINO_BET', 'FREE_SPIN'
status: str # 'AVAILABLE', 'USED', 'EXPIRED', 'WON', 'LOST'
amount: Decimal # free bet amount
currency: str
# For free spins
spin_count: int = 0
spins_used: int = 0
eligible_games: list[str] = [] # game IDs
# For sports bets
min_odds: Optional[Decimal] # minimum odds
eligible_markets: list[str] = [] # market types
# Results
bet_id: Optional[str] # ID of used bet
winnings: Decimal = Decimal(0) # only net profit
# Wagering on winnings
wagering_required: bool = True
wagering_multiplier: int = 1 # 1x for sports, up to 30x for casino
expires_at: datetime
issued_at: datetime
source: str # 'WELCOME', 'PROMO', 'REWARD', 'REFERRAL'
Applying Free Bet to a Bet
class FreeBetService:
async def apply_free_bet(
self,
user_id: str,
free_bet_id: str,
bet_params: BetRequest,
) -> BetResult:
free_bet = await self.freebet_repo.get(free_bet_id)
# Validation
if free_bet.user_id != user_id:
raise AuthorizationError()
if free_bet.status != "AVAILABLE":
raise InvalidStatusError(f"Free bet status: {free_bet.status}")
if datetime.utcnow() > free_bet.expires_at:
raise ExpiredError()
# Check eligibility
if free_bet.min_odds and bet_params.odds < free_bet.min_odds:
raise ValidationError(f"Minimum odds: {free_bet.min_odds}")
# Mark as used (atomically)
async with self.db.transaction():
updated = await self.freebet_repo.claim(free_bet_id, bet_params.bet_id)
if not updated:
raise ConflictError("Free bet already used")
# Create bet without deducting from real balance
bet = await self.bet_service.place_bet(
user_id=user_id,
amount=free_bet.amount,
is_free_bet=True,
free_bet_id=free_bet_id,
**bet_params.dict(),
)
return bet
async def settle_free_bet(self, bet: Bet, outcome: str):
"""Calculate free bet winnings"""
free_bet = await self.freebet_repo.get(bet.free_bet_id)
if outcome == "WIN":
# Winnings = profit (without stake return)
winnings = bet.potential_profit # (bet.amount * odds) - bet.amount
# Apply wagering if needed
if free_bet.wagering_required:
# Credit as locked funds with wagering requirement
await self.bonus_service.create_winnings_bonus(
user_id=free_bet.user_id,
amount=winnings,
wagering_multiplier=free_bet.wagering_multiplier,
reference=f"FREEBET_WIN:{free_bet.id}",
)
else:
# Direct credit
await self.balance_service.credit(
user_id=free_bet.user_id,
amount=winnings,
reference=f"FREEBET_WIN:{free_bet.id}",
)
await self.freebet_repo.update_status(free_bet.id, "WON", winnings=winnings)
else:
await self.freebet_repo.update_status(free_bet.id, "LOST")
Free Spins Mechanics
class FreeSpinService:
async def use_free_spin(self, user_id: str, free_bet_id: str, game_id: str) -> SpinResult:
free_bet = await self.freebet_repo.get(free_bet_id)
if game_id not in free_bet.eligible_games:
raise ValidationError("Game not eligible for this free spin")
if free_bet.spins_used >= free_bet.spin_count:
raise ValidationError("All spins already used")
# Atomically increment used spins counter
remaining = await self.freebet_repo.consume_spin(free_bet_id)
if remaining < 0:
raise ConflictError("Race condition: spin already consumed")
# Run game engine without real money
result = await self.game_engine.spin(
game_id=game_id,
bet_amount=free_bet.amount / free_bet.spin_count,
is_free=True,
)
if result.winnings > 0:
await self.credit_free_spin_winnings(user_id, free_bet, result.winnings)
# If all spins are used
if remaining == 0:
await self.freebet_repo.update_status(free_bet_id, "USED")
return result
Marketing Analytics
Free bets are a marketing tool, so effectiveness analytics are needed:
- Conversion rate: % of users who received a free bet and made a real deposit
- LTV uplift: comparison of LTV for users with free bet vs without
- Cost per acquisition: cost of acquisition through free bet vs other channels
- Abuse rate: % of users abusing free bets without intent to deposit







