Mini-games are weekly bonus challenges that run within a parent challenge. They add an extra competitive layer by pairing participants, creating predator-prey dynamics, or challenging personal records.
Participants are paired based on leaderboard rank at game start. Each player earns a percentage of their partner's points during the game week.
Pairing Algorithm:
- Rank 1 pairs with Rank N (last place)
- Rank 2 pairs with Rank N-1
- Rank 3 pairs with Rank N-2
- ...and so on
- If odd number of participants, the middle-ranked player pairs with themselves
Config:
| Field | Default | Description |
|---|---|---|
bonusPercentage |
10 |
Percentage of partner's earned points awarded as bonus |
Bonus Calculation:
bonusPoints = round(partnerPointsDuringGame * bonusPercentage / 100)
Only "real" activities count — source: "mini_game" bonus activities are excluded from the partner's point total.
Example: Partner earns 200 points during the week with 10% config → you receive 20 bonus points.
A predator-prey chain based on the leaderboard at game start. Each participant "hunts" the person directly above them and is "hunted" by the person directly below.
Assignment:
- Each player's prey is the person one rank above them (lower rank number = higher on leaderboard)
- Each player's hunter is the person one rank below them
- Rank 1 (first place) has no prey — they earn the catch bonus by holding #1 (not getting caught)
- Last place has no hunter chasing them
Config:
| Field | Default | Description |
|---|---|---|
catchBonus |
75 |
Points awarded for surpassing your prey on the leaderboard |
caughtPenalty |
25 |
Points deducted if your hunter surpasses you |
Outcome Determination:
- Caught Prey: Your current rank is now lower (better) than your prey's current rank
- Was Caught: Your hunter's current rank is now lower (better) than yours
Bonus Calculation:
bonusPoints = (caughtPrey ? catchBonus : 0) - (wasCaught ? caughtPenalty : 0)
Examples:
- Catch prey only:
+75points - Caught by hunter only:
-25points - Catch prey AND caught by hunter:
+75 - 25 = +50points - Rank 1 holds position (not caught):
+75points - Neither:
0points
Challenge participants to beat their personal record for highest single-day points total. The PR is calculated from all days before the game starts.
Initial State Capture:
- At game start, the system calculates each participant's maximum daily points across all challenge days prior to the game
- Activities with
source: "mini_game"are excluded from PR calculation - If the participant has no prior activities, their PR is
0
Config:
| Field | Default | Description |
|---|---|---|
prBonus |
100 |
Points awarded for beating your personal daily record |
Outcome Determination:
- The system finds the maximum single-day points total during the game period
- Multiple activities on the same day are summed together
- The PR must be strictly exceeded (equal does not count)
Bonus Calculation:
bonusPoints = weekMaxPoints > initialPr ? prBonus : 0
Example: PR before game is 50 points/day. During game week, you log 30 + 25 = 55 points on one day → PR beaten → +100 bonus.
draft → active → calculating → completed
- Draft: Game is created with type, dates, and config. Can be edited or deleted.
- Active: Game has been started. Participant records are created with initial state snapshots. No further edits allowed.
- Calculating: Transitional state while outcomes are being computed and bonuses awarded.
- Completed: All bonuses have been awarded. Final state and outcomes are stored on each participant record.
| From | To | Trigger |
|---|---|---|
draft |
active |
start mutation |
active |
calculating |
end mutation (first step) |
calculating |
completed |
end mutation (second step, automatic) |
- Only
draftgames can be edited or deleted - Only
draftgames can be started - Only
activegames can be ended - Starting requires at least 1 participant in the challenge
When a game ends and bonuses are calculated:
- A
Mini-Game Bonusactivity type is created for the challenge (if one doesn't already exist). This type hascontributesToStreak: false. - For each participant with a non-zero bonus, an activity is inserted with:
source: "mini_game"externalId: "mini_game_{gameId}_{userId}"pointsEarned: the calculated bonus (can be negative for hunt week penalties)notes: human-readable description of the bonus
- The participant's
userChallenges.totalPointsis updated atomically.
All endpoints require API key authentication (Authorization: Bearer mf_...).
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/api/v1/challenges/:id/mini-games |
List all mini-games | Challenge admin |
POST |
/api/v1/challenges/:id/mini-games |
Create a new mini-game | Challenge admin |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/api/v1/mini-games/:id |
Get mini-game with participants | Challenge admin |
PATCH |
/api/v1/mini-games/:id |
Update a draft mini-game | Challenge admin |
DELETE |
/api/v1/mini-games/:id |
Delete a draft mini-game | Challenge admin |
GET |
/api/v1/mini-games/:id/preview-start |
Dry run: Preview assignments before starting | Challenge admin |
GET |
/api/v1/mini-games/:id/preview-end |
Dry run: Preview scores before ending | Challenge admin |
POST |
/api/v1/mini-games/:id/start |
Real run: Start a draft mini-game (irreversible) | Challenge admin |
POST |
/api/v1/mini-games/:id/end |
Real run: End an active mini-game (irreversible) | Challenge admin |
{
"type": "partner_week",
"name": "Partner Week #1",
"startsAt": 1704844800000,
"endsAt": 1705449600000,
"config": {
"bonusPercentage": 15
}
}{
"name": "Updated Name",
"startsAt": 1704844800000,
"endsAt": 1705449600000,
"config": {
"bonusPercentage": 20
}
}All fields are optional. Only provided fields are updated.
Both start and end are irreversible operations. Since participants may be in different time zones, admins should always preview the result before committing.
Shows what assignments would be created if the game were started right now. The game must be in draft status. Returns:
- Partner Week: Each participant with their proposed partner, rank, and points
- Hunt Week: Each participant with their proposed prey and hunter, rank, and points
- PR Week: Each participant with their current daily PR snapshot
The response uses the exact same calculation logic as the real start mutation — the only difference is no records are created.
Example response:
{
"gameId": "...",
"type": "partner_week",
"status": "draft",
"config": { "bonusPercentage": 10 },
"assignments": [
{ "userId": "...", "name": "Alice", "rank": 1, "points": 500, "partner": { "userId": "...", "name": "Bob" } },
{ "userId": "...", "name": "Bob", "rank": 2, "points": 300, "partner": { "userId": "...", "name": "Alice" } }
]
}Shows what outcomes and bonus points would be awarded if the game were ended right now. The game must be in active status. Returns:
- Partner Week: Each participant with their partner's earned points and the resulting bonus
- Hunt Week: Each participant with rank changes, catch/caught status, and net bonus
- PR Week: Each participant with initial PR, best day during game, hit/miss status, and bonus
The response uses the exact same calculation logic as the real end mutation — the only difference is no points are awarded and no bonus activities are created.
Example response:
{
"gameId": "...",
"type": "hunt_week",
"status": "active",
"config": { "catchBonus": 75, "caughtPenalty": 25 },
"outcomes": [
{ "userId": "...", "name": "Alice", "initialRank": 2, "currentRank": 1, "caughtPrey": true, "wasCaught": false, "bonusPoints": 75 }
],
"totalBonusPoints": 75
}- Create a mini-game in
draftstatus - Preview start → review proposed assignments with stakeholders
- Start the game (locks in assignments, irreversible)
- Wait for the game period to complete
- Preview end → review proposed scores and bonuses
- End the game (awards points, irreversible)
| Query | Description |
|---|---|
queries.miniGames.list |
All mini-games for a challenge with participant counts |
queries.miniGames.getById |
Single game with all participants and user data |
queries.miniGames.getActive |
Active games only |
queries.miniGames.getUserStatus |
Current user's live status in active games |
queries.miniGames.getUserHistory |
User's completed games with outcomes |
queries.miniGames.previewStart |
Dry run: preview assignments for a draft game |
queries.miniGames.previewEnd |
Dry run: preview outcomes for an active game |
| Field | Type | Description |
|---|---|---|
challengeId |
Id<"challenges"> |
Parent challenge |
type |
"partner_week" | "hunt_week" | "pr_week" |
Game type |
name |
string |
Display name |
startsAt |
number |
Game start timestamp (UTC ms) |
endsAt |
number |
Game end timestamp (UTC ms) |
status |
"draft" | "active" | "calculating" | "completed" |
Current state |
config |
any |
Game-type-specific configuration |
createdAt |
number |
Creation timestamp |
updatedAt |
number |
Last update timestamp |
| Field | Type | Description |
|---|---|---|
miniGameId |
Id<"miniGames"> |
Parent game |
userId |
Id<"users"> |
Participant |
initialState |
any |
Snapshot at game start ({ rank, points, dailyPr? }) |
partnerUserId |
Id<"users">? |
Partner (partner_week only) |
preyUserId |
Id<"users">? |
Hunt target (hunt_week only) |
hunterUserId |
Id<"users">? |
Who's hunting them (hunt_week only) |
finalState |
any? |
Snapshot at game end |
bonusPoints |
number? |
Calculated bonus |
outcome |
any? |
Game-specific results |
bonusActivityId |
Id<"activities">? |
Reference to the awarded bonus activity |
createdAt |
number |
Creation timestamp |
| File | Description |
|---|---|
mutations/miniGames.ts |
Game creation, lifecycle, and outcome calculation |
queries/miniGames.ts |
Real-time queries for game state and user status |
mutations/apiMutations.ts |
Internal mutations for HTTP API (create/update/delete) |
lib/miniGameCalculations.ts |
Shared read-only calculation logic for previews |
httpApi.ts |
HTTP API route handlers |
schema.ts |
Table definitions and indexes |