Skip to content

Challenges

Purpose

The Challenges screen manages time-bounded mini-tournaments (e.g., "Reach 10× on Plinko in 24h"). Marketing creates challenges; risk picks winners or rolls back disputed wins; players see them on the player site as opt-in goals.

Audience

Marketing (configuration), risk (winner selection / rollback), customer support (dispute investigation).

Path in admin-fe

Screen URL Page
Challenges list /challenges ebit-admin-fe/src/app/(dashboard)/challenges/page.tsx
Per-challenge bets (audit) /challenges/bets/[id] ebit-admin-fe/src/app/(dashboard)/challenges/bets/[id]/page.tsx

Backing API endpoints

Endpoint Source
GET /admin/challenge (list) apps/api/src/challenge/controller/admin-challenge.controller.ts:136
GET /admin/challenge/:id (one) apps/api/src/challenge/controller/admin-challenge.controller.ts:144
GET /admin/challenge/:id/bets (qualifying bets) apps/api/src/challenge/controller/admin-challenge.controller.ts:155
PUT /admin/challenge (create) apps/api/src/challenge/controller/admin-challenge.controller.ts:103
PATCH /admin/challenge/:id (update) apps/api/src/challenge/controller/admin-challenge.controller.ts:116
POST /admin/challenge/:id/image (upload art) apps/api/src/challenge/controller/admin-challenge.controller.ts:51
POST /admin/challenge/:id/award (pick winners) apps/api/src/challenge/controller/admin-challenge.controller.ts:63
POST /admin/challenge/:id/rollback (revert award) apps/api/src/challenge/controller/admin-challenge.controller.ts:83
DELETE /admin/challenge/:id apps/api/src/challenge/controller/admin-challenge.controller.ts:128

Frontend wiring: ebit-admin-fe/src/queries/challenges/.

Key actions

Action Required permission API call DB tables touched Audit-logged?
List challenges challenge.view GET /admin/challenge Challenge yes
View challenge challenge.view GET /admin/challenge/:id Challenge, ChallengeWinner yes
List qualifying bets challenge.view GET /admin/challenge/:id/bets Bet filtered by challenge criteria yes
Create challenge challenge.edit PUT /admin/challenge Challenge yes
Update challenge challenge.edit PATCH /admin/challenge/:id Challenge yes
Upload challenge art challenge.edit POST /admin/challenge/:id/image S3, Challenge.imageUrl yes
Pick winners (award) challenge.action POST /admin/challenge/:id/award ChallengeWinner, Transaction, Account yes
Rollback award challenge.action POST /admin/challenge/:id/rollback reverse Transaction yes
Delete challenge challenge.edit DELETE /admin/challenge/:id Challenge (soft) yes

Filters and views

  • Status — upcoming / live / ended / awarded / rolled-back.
  • Game — challenges scoped to one game (slug).
  • Type — multiplier / streak / total-wager / first-to-X.
  • Date range.

Common workflows

  1. Create a Plinko 100× multiplier challenge. Marketing fills challenge: title, criteria (Plinko bet with multiplier ≥ 100×), prize pool (1000 USDT split among winners), window (24h). Uploads banner art. Saves.
  2. Live monitor. During challenge, ops opens /challenges/bets/[id] to see qualifying bets. Spots fraud signals (same wallet across users) — flags via user-profile.md Notes.
  3. Award winners. After challenge end, ops calls POST /admin/challenge/:id/award. Backend selects top N qualifying bets, writes Transactions to winners, marks Challenge.awardedAt.
  4. Rollback after dispute. A winner is later flagged for multi-accounting. Ops calls POST /admin/challenge/:id/rollback. Backend reverses Transactions, sets Challenge.rolledBack=true. Re-awards (after manual exclusion list update) via a new award call — atomicity is per-call.
  5. Clone a successful challenge. Currently no clone button. Operator copies fields manually into a new challenge.

Edge cases / gotchas

  • award is one-shot. Calling twice on the same challenge errors (ApiCode.CHALLENGE_ALREADY_AWARDED). To re-award: rollback, then award again.
  • rollback reverses Transactions but not bets. Bets that qualified still happened; only the prize is undone.
  • Bot bets are excluded at award time (filtered on User.isBot=true). Bot bets DO show in GET /admin/challenge/:id/bets if not filtered.
  • Image upload uses multipart/form-data. Backend stores in S3; URL pattern is Challenge.imageUrl.
  • Award splits prize pool by formula in Challenge.payoutMethod — top-N flat / weighted / progressive. Not all challenges use the same.
  • Per-game scope — challenges filter qualifying bets by Challenge.gameSlug. Cross-game challenges are not modeled.
  • Award notification sends a websocket event to winners via the rt service; if rt is down, players don't see the toast but balance is still credited.
  • Per-challenge qualifying bets: bets-history.md (same GameBetsHistory component is reused at /challenges/bets/[id])
  • Bot exclusion: bots.md
  • Permission keys: libs/auth/src/permissions/const.ts:216-227
  • Challenge schema: libs/_prisma/src/schema/api.prisma → search Challenge
  • Audit: admin-logs.md
  • Customer comms (announce challenge): handover/customer-comms/

Sequence — awarding challenge winners

sequenceDiagram
    actor risk
    participant admin-fe
    participant api
    participant pg as Postgres
    Note over pg: challenge ended — qualifying bets logged in Challenge.qualifyingBets
    risk->>admin-fe: open /challenges, click "Award winners" on ended challenge
    admin-fe->>api: POST /admin/challenge/:id/award
    api->>api: PermissionGuard('challenge.action')
    api->>pg: SELECT qualifying bets (filter bot=false), pick top N by Challenge.payoutMethod
    api->>pg: BEGIN
    loop per winner
        api->>pg: INSERT ChallengeWinner, INSERT Transaction (PRIZE), UPDATE Account
    end
    api->>pg: UPDATE Challenge SET awardedAt=now()
    api->>pg: INSERT AdminActionLog
    api->>pg: COMMIT
    api-->>admin-fe: ChallengeWinner[]
    Note over admin-fe: rt service later notifies winners