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¶
- 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.
- 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. - Award winners. After challenge end, ops calls
POST /admin/challenge/:id/award. Backend selects top N qualifying bets, writes Transactions to winners, marksChallenge.awardedAt. - Rollback after dispute. A winner is later flagged for multi-accounting. Ops calls
POST /admin/challenge/:id/rollback. Backend reverses Transactions, setsChallenge.rolledBack=true. Re-awards (after manual exclusion list update) via a newawardcall — atomicity is per-call. - Clone a successful challenge. Currently no clone button. Operator copies fields manually into a new challenge.
Edge cases / gotchas¶
awardis one-shot. Calling twice on the same challenge errors (ApiCode.CHALLENGE_ALREADY_AWARDED). To re-award: rollback, then award again.rollbackreverses 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 inGET /admin/challenge/:id/betsif 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
rtservice; ifrtis down, players don't see the toast but balance is still credited.
Cross-links¶
- Per-challenge qualifying bets: bets-history.md (same
GameBetsHistorycomponent 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→ searchChallenge - 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