---
title: "Credit Rewards"
description: "Config-driven credit-balance rewards for subscriber+ contribution — idempotent, role-multiplied, UTC daily-capped"
locale: "en"
---
# Credit Rewards

Ring awards **credit balance** units (`credit.creditUnitLabel`, e.g. `points` on ring-platform.org) when users complete real contribution. Credits are platform utility (spendable for store, membership, generative media) — not speculative crypto. Earn rules are deterministic, capped, and auditable.

## Config SSOT

Write path: `ring-config.json` → `credit.rewards`

```json
{
  "credit": {
    "creditUnitLabel": "points",
    "creditBalanceUnitToMainCurrency": 1,
    "rewards": {
      "minRole": "subscriber",
      "multipliers": { "subscriber": 1, "member": 1.5, "confidential": 2 },
      "dailyEarnCap": { "subscriber": 50, "member": 100, "confidential": 150 },
      "events": {
        "adminVerify": { "amount": "50", "enabled": true, "idempotencyMode": "once_per_user" },
        "commentCreated": { "amount": "2", "idempotencyMode": "once_per_object" }
      }
    }
  }
}
```

Read fallback order (migration): `credit.rewards.events` → `credit.creditAddEvents` → `credits.rewards.events`.

## Idempotency

| Mode | Key | Use |
|------|-----|-----|
| `once_per_user` | `credit_add_event:{trigger}:{userId}` | Profile fields, KYC, completion bonus |
| `once_per_object` | `…:{objectType}:{objectId}` | Comments, reviews, approved news |

Unique index on `credit_add_events.idempotency_key` prevents races.

## Role multipliers & daily caps

Final award = `floor(base × multiplier)` (minimum 1 if base ≥ 1). Daily cap is **UTC calendar day**; if the award would exceed remaining budget, the entire award is **skipped** (`skip_reason: daily_cap`) — never partial-minted.

Visitors earn nothing (`minRole: subscriber`).

## Engine

`enqueueRewardCreditAddEvent` in `lib/wallet/reward-credit-service.ts`:

1. Load rule → enabled / minRole / username / verified gates  
2. Build idempotency key → existing completed → `existing`  
3. Multiplier + UTC daily cap  
4. `creditBalanceService.addCredits(..., 'reward_credit_add', getFiatCreditAccountingRate())`  
5. Audit row + `appendEvent('reward_credit_add')`

Ledger failures → status `failed` (not `skipped`).

## Wired actions (Phase 1)

| Trigger | When |
|---------|------|
| Profile fields / `profileCompleted` | After `updateProfile` empty→filled / completeness |
| `adminVerify` | Admin KYC verification |
| `newsStoryApproved` | Main-page approval for **user-authored** articles |
| `commentCreated` / `reviewCreated` / `requestCreated` | After create (once per object id) |

`profileCompleted` is a **completion bonus** in addition to field awards.

## Admin monitoring

- `/admin/rewards` — charts (daily mint, by trigger, by role, top earners)
- Users activity tab **Rewards**
- APIs under `/api/admin/rewards/*`

## Ethics

No chance mechanics, no client-trusted amounts, published rules, affiliate token rail stays separate. See LegioX lens `ua_non_speculative_reputation_wealth_designer`.
