This section covers the full reward model: rule evaluation with priority resolution (ALWAYS/FALLBACK), currency management, transaction lifecycle, and balance tracking. It is relevant for anyone configuring reward rules or integrating the virtual economy via API. For a high-level overview, see the Gamification Fundamentals. For shared patterns (JsonLogic expressions, entity matching), see Cross-Cutting Patterns.
RewardRule (when and how much to award)
│
└──▶ VirtualTransaction (a ledger entry: credit or debit)
│
└──▶ VirtualBalance (the user's current balance per currency)VirtualCurrency (defines a currency: XP, credits, tokens, etc.)| Field | Type | Description |
|---|---|---|
rewardRuleId | string | Unique identifier |
name | string | Human-readable name |
ruleType | INSTANCE | ENTITY | TAG | How to match events |
matchEntity | enum | Which entity type triggers this rule (see below) |
matchEntityId | string? | Specific entity or tag ID (required for INSTANCE and TAG) |
matchCondition | JsonLogic | Additional event filtering |
applicationMode | ALWAYS | FALLBACK | DISABLED | When this rule fires |
rewards | Reward[] | 1–10 reward payouts |
origin | CATALOG | CUSTOM | Creation source |
defaultLang | lang | Default language code |
langs | lang[] | Supported languages (1–10) |
| Match Entity | Triggers On |
|---|---|
Mission | Mission completion |
Activity | Activity completion |
Quiz | Quiz completion |
Tag | Any entity with a matching tag |
LearningPath | Learning path progress/completion |
LearningGroup | Learning group progress/completion |
Slide | Slide completion |
INSTANCE: matches a specific entity by ID.ENTITY: matches any entity of the given type.TAG: matches any entity with the given tag.ALWAYS — Primary rules. These are evaluated first and fire whenever their conditions are met.FALLBACK — Backup rules. These fire only if no ALWAYS rules matched for the same event. This prevents reward stacking while ensuring a baseline reward always exists.DISABLED — Inactive rules. Not evaluated.ALWAYS rules that match the event's entity type and ID (across INSTANCE, ENTITY, and TAG rule types).matchCondition is evaluated against the event data.FALLBACK rules, evaluate conditions, use those.matchCondition receives { event, previousEvent } as context, where previousEvent is the entity's state before the current update. This enables conditions based on state transitions:// Only reward when progress changes to COMPLETE
{
"and": [
{ "===": [{ "var": "event.progress" }, "COMPLETE"] },
{ "!==": [{ "var": "previousEvent.progress" }, "COMPLETE"] }
]
}// Only reward successful quizzes
{ "===": [{ "var": "event.outcome" }, "SUCCESS"] }| Field | Type | Description |
|---|---|---|
rewardType | VIRTUAL_CURRENCY | Identifies this as a currency reward |
virtualCurrencyId | nanoid | Which currency to award |
redemptionMode | AUTO | MANUAL | How the transaction is finalized |
expression | JsonLogic | Evaluates to the amount to award |
AUTO: The transaction is immediately completed — the user's balance is credited instantly. Typical for XP and automatic rewards.MANUAL: The transaction is created as PENDING and requires explicit action to redeem. Useful for prize redemption flows where the user must claim the reward.// Static: always award 100 points
100
// Dynamic: award based on quiz difficulty
{
"if": [
{ "===": [{ "var": "event.difficulty" }, "HARD"] }, 20,
{ "===": [{ "var": "event.difficulty" }, "MEDIUM"] }, 10,
5
]
}{ event } as context. If it evaluates to 0 or a non-numeric value, the transaction is silently skipped.| Field | Type | Description |
|---|---|---|
rewardType | BADGE | Identifies this as a badge reward |
badgeConfigurationId | nanoid | Which badge to award |
PUBLISHED state to be awarded.{
"rewards": [
{
"rewardType": "VIRTUAL_CURRENCY",
"virtualCurrencyId": "vc-xp",
"redemptionMode": "AUTO",
"expression": 100
},
{
"rewardType": "BADGE",
"badgeConfigurationId": "bc-mission-master"
}
]
}
### Event Remapping
Source events are remapped to their parent entity types before rule matching:
| Source Event | Maps To |
|-------------|---------|
| `ActivityLog` | `Activity` |
| `LearningPathLog` | `LearningPath` |
| `LearningGroupLog` | `LearningGroup` |
| `SlideLog` | `Slide` |
This means rules are defined against the parent entity (e.g., `matchEntity: "Activity"`), not against the log entity.
## Virtual Currency
A **Virtual Currency** defines a type of point, credit, or token within a workspace. Each workspace can have multiple currencies serving different purposes.
### Fields
| Field | Type | Description |
|-------|------|-------------|
| `virtualCurrencyId` | nanoid | Unique identifier |
| `name` | string | Display name |
| `icon` | URL? | Optional icon for UI |
| `minAllowedBalance` | number? | Optional floor for user balances |
| `maxAllowedBalance` | number? | Optional ceiling for user balances |
| `origin` | `CATALOG` \| `CUSTOM` | Creation source |
| `defaultLang` | lang | Default language code |
| `langs` | lang[] | Supported languages (1–10) |
### Typical Configurations
| Currency | Purpose | Balance Constraints |
|----------|---------|-------------------|
| Experience Points (XP) | Drives leaderboard rankings and levels | No cap (min: 0) |
| Credits | Earned and spent on rewards | Min: 0, optional max |
| Streak Tokens | Spent to freeze streaks | Min: 0 |
A common setup uses two currencies: **XP** for progression (cannot be spent) and **credits** for rewards (earned and redeemable). Clients can define any number of currencies.
## Virtual Transaction
A **Virtual Transaction** is a ledger entry recording a credit or debit to a user's virtual currency balance.
### Fields
| Field | Type | Description |
|-------|------|-------------|
| `virtualTransactionId` | nanoid | Unique identifier |
| `virtualTransactionGroupId` | nanoid | Groups related transactions together |
| `redemptionGroupId` | nanoid? | Groups redemptions |
| `userId` | nanoid | The user whose balance is affected |
| `virtualCurrencyId` | nanoid | Which currency |
| `direction` | `CREDIT` \| `DEBIT` | Adding or removing |
| `amount` | number | The amount (must not be zero) |
| `state` | `PENDING` \| `COMPLETED` \| `EXPIRED` \| `REJECTED` | Lifecycle state |
| `redemptionMode` | `AUTO` \| `MANUAL` | How finalization works |
| `initiatorType` | enum | What caused this transaction (see below) |
| `initiator` | string | Identifier of the initiator (e.g., `"rewardRuleId#rr-123"`) |
| `counterpartType` | `USER` \| `SYSTEM` | The other party |
| `counterpart` | string | Identifier of the counterpart |
| `expiresAt` | ISO datetime? | Optional expiration date |
| `redeemedAt` | string? | When the transaction was redeemed |
| `additionalData` | record? | Custom metadata |
### Initiator Types
| Initiator Type | Description |
|---------------|-------------|
| `USER` | Direct user action (e.g., spending credits) |
| `REWARD_RULE` | Automatic payout from a reward rule evaluation |
| `STREAK_RULE` | Deduction for streak freeze |
| `SYSTEM` | Automated system process |
| `ADMIN` | Administrator manual action |
### Transaction Lifecycle
- **PENDING**: The transaction exists but has not been applied to the balance yet.
- **COMPLETED**: The transaction is finalized and reflected in the balance.
- **EXPIRED**: The transaction was not redeemed before `expiresAt`. Terminal state.
- **REJECTED**: The transaction was denied (e.g., insufficient balance for a debit). Terminal state.
For `AUTO` redemption mode, the transaction transitions directly to COMPLETED upon creation. For `MANUAL`, it stays PENDING until the user or admin explicitly redeems it.
## Virtual Balance
A **Virtual Balance** tracks a user's current balance for a specific currency.
### Fields
| Field | Type | Description |
|-------|------|-------------|
| `userId` | nanoid | The user |
| `virtualCurrencyId` | nanoid | The currency |
| `amount` | number | Total balance (including pending transactions) |
| `availableAmount` | number | Balance available for spending (completed transactions only) |
### Amount vs. Available Amount
- **`amount`**: The total, including PENDING transactions. Represents what the user _will_ have if all pending transactions complete.
- **`availableAmount`**: Only COMPLETED transactions. Represents what the user _can spend right now_.
For `AUTO` redemption mode, `amount` and `availableAmount` are always equal (since transactions complete immediately). The difference only matters for `MANUAL` mode, where a reward might be pending user redemption.
## How Rewards Are Processed
When a user performs an action that might trigger rewards:
1. **Event arrives**: The source system (Activity, Quiz, LearningPath, etc.) publishes an event.
2. **Entity remapping**: `ActivityLog` → `Activity`, `LearningPathLog` → `LearningPath`, etc.
3. **Rule lookup**: The system queries for matching rules across all three rule types (INSTANCE, ENTITY, TAG) in parallel.
4. **ALWAYS/FALLBACK resolution**: If any ALWAYS rules match, use those. Otherwise, use FALLBACK rules.
5. **Condition evaluation**: Each rule's `matchCondition` is evaluated against `{ event, previousEvent }`.
6. **Transaction calculation**: For each matching rule, for each reward in the rule:
- The reward `expression` is evaluated against `{ event }`.
- If the result is a non-zero number, a transaction input is prepared.
- If the result is zero or non-numeric, the reward is skipped.
7. **Transaction creation**: Virtual transactions are created in the database.
8. **Balance update**: For AUTO transactions, the user's balance is updated immediately.
### Example: Multi-Currency Reward Rule
A single rule that awards both XP and credits for completing a learning path:
```json
{
"rewardRuleId": "rr-lp-complete",
"ruleType": "ENTITY",
"matchEntity": "LearningPath",
"matchCondition": { "===": [{ "var": "event.progress" }, "COMPLETE"] },
"applicationMode": "ALWAYS",
"rewards": [
{
"virtualCurrencyId": "vc-xp",
"redemptionMode": "AUTO",
"expression": 50
},
{
"virtualCurrencyId": "vc-credits",
"redemptionMode": "AUTO",
"expression": 100
}
]
}{
"rewardRuleId": "rr-quiz-difficulty",
"ruleType": "ENTITY",
"matchEntity": "Quiz",
"matchCondition": { "===": [{ "var": "event.outcome" }, "SUCCESS"] },
"applicationMode": "ALWAYS",
"rewards": [
{
"virtualCurrencyId": "vc-xp",
"redemptionMode": "AUTO",
"expression": {
"if": [
{ "===": [{ "var": "event.difficulty" }, "HARD"] }, 20,
{ "===": [{ "var": "event.difficulty" }, "MEDIUM"] }, 10,
5
]
}
}
]
}// ALWAYS rule: 20 XP for any activity tagged "premium"
{
"ruleType": "TAG",
"matchEntity": "Tag",
"matchEntityId": "premium",
"matchCondition": { "===": [{ "var": "event.progress" }, "COMPLETE"] },
"applicationMode": "ALWAYS",
"rewards": [{ "virtualCurrencyId": "vc-xp", "redemptionMode": "AUTO", "expression": 20 }]
}
// FALLBACK rule: 5 XP for any activity (baseline)
{
"ruleType": "ENTITY",
"matchEntity": "Activity",
"matchCondition": { "===": [{ "var": "event.progress" }, "COMPLETE"] },
"applicationMode": "FALLBACK",
"rewards": [{ "virtualCurrencyId": "vc-xp", "redemptionMode": "AUTO", "expression": 5 }]
}| Concept | Purpose |
|---|---|
| RewardRule | Defines when and how much currency to award (7 match entities, ALWAYS/FALLBACK resolution) |
| VirtualCurrency | Defines a currency type with optional balance constraints |
| VirtualTransaction | Ledger entry: CREDIT or DEBIT, with lifecycle (PENDING→COMPLETED/EXPIRED/REJECTED) |
| VirtualBalance | User's current balance per currency (total vs. available) |
| applicationMode | ALWAYS (primary) vs FALLBACK (backup if no ALWAYS matched) vs DISABLED |
| rewards array | 1–10 payouts per rule — VIRTUAL_CURRENCY (currency + mode + expression) or BADGE (badge ID only) |
| redemptionMode | AUTO (instant) vs MANUAL (requires explicit redemption) |
| initiatorType | What caused the transaction: USER, REWARD_RULE, STREAK_RULE, SYSTEM, ADMIN |
| Event remapping | Log entities (ActivityLog, etc.) are mapped to parent entities for rule matching |
| previousEvent | Enables transition-based conditions ("only reward when progress changes to COMPLETE") |
rewardType: BADGE to assign badges upon mission or learning path completion.