Add a new house game¶
Goal: ship a new in-house casino game with provably-fair RNG, REST endpoint, and frontend route (analogous to dice / plinko / mines / limbo). Audience: customer engineering team adding a custom game (e.g. "wheel of fortune", "hi-lo", "crash"). Time: 2–4 days backend + 2–4 days frontend. Friction: low — the abstraction is well-formed.
What you'll change¶
| Layer | Path | Action |
|---|---|---|
| Game slug enum | libs/games/src/house-games.config.ts:23 |
Add an entry to enum HouseGameSlug. |
| Game config | libs/games/src/house-games.config.ts (struct around line 100+) |
Add the config record (rtp, min/max bet, payout schedule). |
| RNG helper | libs/games/src/rng/rng.games.ts |
Add a getRandom<Game> static for the new game's RNG. |
| Backend module | apps/api/src/casino/house/<game>/ |
New folder. Copy from dice/. |
| Backend wiring | apps/api/src/casino/casino.module.ts (or relevant casino module) |
Add <Game>Module to imports. |
| Frontend route | ebit-fe/src/app/[locale]/games/originals/<game>/ |
New folder; new page.tsx. |
| Translations | ebit-fe/messages/{en,de}.json |
Add UI copy. |
| Tests | apps/api/src/casino/house/<game>/__tests__/ |
Vitest specs for service + RNG bound. |
Canonical example¶
apps/api/src/casino/house/dice/ is the cleanest reference; it has the smallest surface area:
dice/
├── dice.module.ts # imports BetService, ProvablyFairService, etc.
├── dice.controller.ts # POST /casino/games/house/dice/bet · GET /casino/games/house/dice/config
├── dice.service.ts # @PlaceBetLock + RNG + payout calc + DB write
├── dice.const.ts
├── dto/
│ └── dice.dto.ts # request/response DTOs with class-validator
└── utils/
├── dice.mapper.ts # serialize bet payload for storage
└── dice.utils.ts # calculateDiceMultiplier
Read dice.controller.ts and dice.service.ts first — together ~150 lines, they show every pattern you need.
Steps¶
1. Add the game slug¶
Edit libs/games/src/house-games.config.ts:23:
export enum HouseGameSlug {
PLINKO = 'plinko',
MINES = 'mines',
LIMBO = 'limbo',
ROULETTE = 'roulette',
DICE = 'dice',
KENO = 'keno',
// NEW:
HI_LO = 'hi-lo',
}
The slug is also used as the URL segment in both backend (/casino/games/house/<slug>) and frontend (/games/originals/<slug>).
2. Add the game config¶
Same file, around line 100+ (look for the array literal of HouseGameConfig):
{
slug: HouseGameSlug.HI_LO,
originalSlug: HouseGameSlug.HI_LO,
rtp: 0.99, // return-to-player ratio
minBet: { /* per-currency min */ },
maxBet: { /* per-currency max */ },
settings: {
[HouseGameSlug.HI_LO]: {
// your game-specific config
},
},
}
The rtp and min/max bet values feed BetAmountValidationService which the controller uses to reject out-of-range bets.
3. Implement the RNG¶
Edit libs/games/src/rng/rng.games.ts and add a static method analogous to getRandomDice:
static getRandomHiLo(args: { serverSeed: string; clientSeed: string; nonce: number }): number {
// Use the existing HMAC-SHA256 helper; produce a uniformly distributed
// value in [0, 1) and map it to the game's outcome space.
…
}
The RNG must be deterministic and reproducible from (serverSeed, clientSeed, nonce) — that's the provably-fair contract. See docs/api-reference/api.md under "Fairness API" for the verification endpoints.
4. Scaffold the backend module¶
cd ebit-api
cp -r apps/api/src/casino/house/dice apps/api/src/casino/house/hi-lo
# rename: hi-lo.module.ts, hi-lo.controller.ts, hi-lo.service.ts, etc.
Adapt hi-lo.controller.ts (pattern from dice.controller.ts:24-44):
@ApiTags('Casino API')
@SkipThrottle({ profile: true, externalApi: true })
@ApiBearerAuth()
@Controller('casino/games/house/hi-lo')
export class HiLoController {
constructor(
private readonly hiLoService: HiLoService,
private houseGamesService: HouseGamesService,
) {}
@Post('bet')
@UseGuards(JwtGuard, UserHttpThrottlerGuard)
@Throttle({ bets: { limit: 25, ttl: 5000 } })
@UsePipes(new ValidationPipe({ transform: true }))
async bet(@Body() dto: HiLoBetRequestDto, @Request() request: RequestExt) {
return this.hiLoService.play(dto, request.user);
}
@Get('config')
getConfig(): HiLoConfigResponseDto { /* … */ }
}
The throttle limit 25 bets / 5 s matches the other house games.
5. Implement the service¶
Pattern from dice.service.ts:25-80:
@Injectable()
export class HiLoService {
constructor(
private readonly provablyFairService: ProvablyFairService,
private readonly betService: BetService,
private readonly houseGamesService: HouseGamesService,
private readonly betAmountValidationService: BetAmountValidationService,
) {}
@PlaceBetLock({ userId: (_: never, user: UserDto) => user.id })
async play(args: HiLoBetRequestDto, user: UserDto): Promise<HiLoBetResponseDto> {
this.validateBet(args);
const game = this.houseGamesService.getGame(HouseGameSlug.HI_LO);
return await PrismaTransactional.execute(async () => {
const seed = await this.provablyFairService.popUserSeed(user.id);
const random = RngGames.getRandomHiLo({ ... });
const outcome = this.calculateOutcome(args, random);
// settle the bet via betService.createAndSettleBet(...)
});
}
}
Key invariants:
- @PlaceBetLock prevents concurrent bets for the same user (per-user mutex).
- PrismaTransactional.execute wraps the seed-pop + settle into one DB transaction.
- provablyFairService.popUserSeed consumes the next nonce; the seed is verifiable post-hoc.
6. Wire the module¶
Add HiLoModule to whichever casino module aggregates house games (search for import.*DiceModule to find the parent — typically apps/api/src/casino/house/).
7. Frontend route¶
Create ebit-fe/src/app/[locale]/games/originals/hi-lo/page.tsx:
// ebit-fe/src/app/[locale]/games/originals/hi-lo/page.tsx
import { HiLoBoard } from '@/features/games/hi-lo';
export default function HiLoPage() { return <HiLoBoard />; }
Build the HiLoBoard component under ebit-fe/src/features/games/hi-lo/ — pattern follows the existing originals/{dice,limbo,mines,plinko,...} pages. See ebit-fe/src/app/[locale]/games/originals/dice/page.tsx for the canonical layout.
The dynamic route [slug]/page.tsx already exists for catalog browsing; adding a new game does not require touching it.
8. Translations¶
Add UI copy to ebit-fe/messages/en.json and ebit-fe/messages/de.json under a new hi-lo namespace. See add-locale.md for the namespace conventions.
9. BullMQ async settlement (if applicable) [optional]¶
Most house games settle synchronously inside BetService.createAndSettleBet. If your game needs deferred settlement (e.g. a reveal animation across multiple ticks), follow add-bullmq-queue.md and emit a job from the play() method. Note: the BullMQ bet-settled consumer currently starts an orphan trace because traceparent is not stored in the job payload — see docs/audits/perf-trace-coverage-audit.md. Plan for this when debugging cross-job traces.
10. Observability¶
@PlaceBetLock and PrismaTransactional are auto-instrumented. The bet-place HTTP entry-span is captured. However:
HiLoService.playitself has no manual span by default — same blind spot asDiceService.playdocumented indocs/audits/perf-trace-coverage-audit.md. To add one, seeadd-otel-span.md.- The bet appears in Grafana's
ebit-perf-testdashboard automatically once it's emittingcalls_totalspans.
11. Tests¶
Required specs:
1. RNG bound — for N samples from getRandomHiLo, every value lies in the expected range.
2. Payout calculation — table-driven specs of (input, random) → expected payout.
3. Service end-to-end — uses the test database; bets settle correctly, balance updates, Bet row created.
4. API surface — Vitest e2e against the controller.
12. Update the API surface SOT¶
Append a docs/api/changelog.md entry: POST /casino/games/house/hi-lo/bet and GET /casino/games/house/hi-lo/config under "New endpoints", tag Casino API.
Verification¶
- API smoke:
Expect a
curl -X POST http://localhost:4000/casino/games/house/hi-lo/bet \ -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \ -d '{"amount":"1.0","currency":"DBC", /* game-specific args */}'200 OKwith the outcome + payout.403means the throttle / bet-lock rejected (try again later). - Database:
select id, payload, total_amount from bets order by id desc limit 1;shows the new bet withpayload->>'gameSlug' = 'hi-lo'. - Provably fair:
GET /fairness/verify(perdocs/api-reference/api.md"Fairness API") returns a verification record for the new bet. - Frontend: navigate to
http://localhost:3000/games/originals/hi-lo; play a round. - Grafana: open
ebit-perf-test; the newCasino APIroute shows traffic in the per-route panels.
Cross-links¶
integration-cookbook.md— index.docs/api-reference/api.md→ tagsCasino APIandFairness API.docs/data-model/—Bet,Payment,ProvablyFairSeedmodels.- Existing house games:
apps/api/src/casino/house/{dice,plinko,mines,limbo,roulette,keno,monkey-run,double-loyalty,blackjack}/. add-rest-endpoint.md,add-otel-span.md,add-bullmq-queue.md,add-prisma-model.md.