---
title: "Credit Rewards"
description: "Activity credit-balance awards from credit.rewards — global enabled kill switch, idempotent triggers, and the profile quest widget"
locale: "en"
---
# Credit Rewards

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. Activity rewards are **on** when `credit.rewards.enabled` is omitted or `true`. Set `false` to stop every credit-balance-add award and hide the profile quest widget. Credit purchase, admin/MCP mint, and referral ShareEarn stay independent.

Ring can award **credit-balance** units when a member completes a real contribution (profile fields, KYC, comments, reviews, requests, approved news). The unit label is `credit.creditBalanceUnitLabel` (often `points`). Amounts, role multipliers, and UTC daily caps live in `ring-config.json` → `credit.rewards`. This is **not** on-chain token minting and **not** the referral rail.

| Surface | When `enabled` is omitted / `true` | When `enabled` is `false` |
|---|---|---|
| `enqueueRewardCreditAddEvent` | Evaluates per-trigger rules, caps, idempotency | Skips immediately (`reason: rewards_disabled`) |
| Profile quest widget | Renders on `/profile` (`UserProgressWidget`) | Does not render |
| Onboarding KYC hints | `/login/onboarding` lists earn amounts | Hints empty; lead copy omits earn language |
| Reward toast / celebration | `CreditRewardReceivedListener` mounted | Listener not mounted |
| Catalog / quest board | Enabled event rules | Empty catalog; no summary query |
| Card credit top-up | Unchanged | Unchanged |
| `POST /api/mcp/v1/credit/add` | Unchanged (explicit `confirm: true`) | Unchanged |
| Referral ShareEarn / refcodes | Unchanged | Unchanged |
| `/admin/rewards` | Charts historical `credit_add_events` | Same desk with a disabled banner — no new activity awards |

### For founders

## Why this matters for your clone

Contribution rewards are optional clone policy. Some rings want a published quest board; others (for example a store-first overlay) want members to **buy** credits and never earn them from comments.

  
- **[Wallet & credits](/docs/features/wallet.md)** — Credit ledger, Token Desk, and card top-up — independent of activity rewards.

  
- **[Profile widgets](/docs/features/profile-account.md)** — `/profile` hosts the quest board when rewards are enabled.

  
- **[Admin console](/docs/features/admin.md)** — `/admin/rewards` charts mint history for platform admins.

  
- **[Refcodes](/docs/features/refcodes.md)** — Wallet-linked share links mint a different rail — not `credit.rewards`.

### Typical scenarios

1. **Community ring** — leave `enabled` omitted or `true`. Members see quest progress on `/profile` and earn within daily caps.
2. **Commerce ring** — set `"enabled": false` on the clone overlay `web/ring-config.json`. The quest widget disappears; store and top-up still work.
3. **Overlay owns `credit.rewards`** — compose copies that file as a whole. You must set `enabled` on the overlay; Layer1’s `"enabled": true` does not leak through.

### For developers

## Implementation

Kill switch lives in `lib/wallet/credit-rewards-gate.ts` (`resolveCreditRewardsEnabled`). `isCreditRewardsEnabled()` in `lib/ring-config-core.ts` (React `cache()`, client-safe) reads `credit.rewards.enabled`, then legacy `credits.rewards.enabled`. Omitted = `true`. Client UI calls `isClientCreditRewardsEnabled()` from `lib/ring-config-client.ts`. Server re-export: `@/lib/ring-oracle`.

| Module | Path | Job |
|---|---|---|
| Gate | `lib/wallet/credit-rewards-gate.ts` | Pure boolean resolver |
| Accessor | `lib/ring-config-core.ts` → `isCreditRewardsEnabled` | Snapshot read |
| Client | `lib/ring-config-client.ts` → `isClientCreditRewardsEnabled` | Widget / listener |
| Schema | `lib/zod/credit-reward-schemas.ts` → `CreditRewardsConfigSchema.enabled` | Optional, default `true` |
| Write path | `lib/wallet/reward-credit-service.ts` | First-line skip `rewards_disabled` |
| Catalog | `lib/ring-config-chain.ts` → `getPublicRewardCatalog` | `[]` when disabled |
| Quest board | `app/_actions/wallet.ts` → `getRewardQuestBoard` | Empty payload when disabled |
| Widget | `features/auth/components/user-progress-widget.tsx` | `return null` when disabled |
| Profile mount | `features/auth/components/profile-content.tsx` | Conditional render |
| Listener | `features/wallet/components/credit-reward-received-listener.tsx` | Gated in `locale-app-chrome.tsx` |
| Profile hooks | `lib/wallet/profile-reward-hooks.ts` | Still calls enqueue; skip is inside the engine |

Read fallback for **event rules** (not the kill switch): `credit.rewards.events` → `credit.creditAddEvents` → `credits.rewards.events`.

### Config shape

{`{
  "credit": {
    "creditBalanceUnitLabel": "points",
    "creditBalanceUnitToMainCurrency": 0.1,
    "rewards": {
      "enabled": true,
      "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" }
      }
    }
  }
}`}

Per-trigger `events.*.enabled` still applies when the global flag is on. Global `false` wins without evaluating triggers.

### Engine

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

1. Global gate → `skipped` / `rewards_disabled`
2. Load rule → per-event `enabled`, `minRole`, username, verified
3. Idempotency key → existing completed → `existing`
4. `floor(base × role multiplier)` (minimum 1 if base ≥ 1) + UTC daily cap
5. `creditBalanceService.addCredits(..., 'reward_credit_add', getMainCurrencyCreditAccountingRate())`
6. Audit row + `appendEvent('reward_credit_add')` + `notifyRewardCreditReceived`

Daily cap skip returns `reason: 'daily_cap'` and **does not** write an `idempotency_key` row (would permanently block a later award). Ledger failures → status `failed`.

| 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, requests |

Unique index on `credit_add_events.idempotency_key` prevents races.

### Wired callers (all go through enqueue)

| Trigger | Caller |
|---|---|
| `ringUsername`, `addedBio`, `addedLocation`, `addedWebsite`, `addedTelegram`, `addedWhatsapp`, `profileCompleted` | `maybeAwardProfileRewards` after `updateProfile` / vitals onboarding |
| `addedTelegram` (OAuth / Mini App link) | `features/auth/services/user-resolve.ts` |
| `adminVerify` | `features/verification/services/admin-manual-user-verification.ts` |
| `newsStoryApproved` | `features/news/services/news-promotion-workflow.ts` (user-authored main-page approval) |
| `commentCreated` | `features/comments/services/comment-service.ts` |
| `reviewCreated` | `app/api/store/products/[id]/reviews/route.ts` |
| `requestCreated` | `features/opportunities/services/create-opportunity.ts` |

Visitors earn nothing when `minRole` is `subscriber` (default).

### Admin monitoring

| Surface | Path |
|---|---|
| Console | `/admin/rewards` (`AdminRewardsDashboard`, `minRole: admin`) |
| Stats | `GET /api/admin/rewards/stats?range=7d\|28d\|90d` |
| Events | `GET /api/admin/rewards/events` |
| Per-user | `GET /api/admin/rewards/users/[id]` |

Disabling awards does **not** hide this desk. Historical rows remain.

Layer2 packs (`ring-presets/`) do not copy reward chrome. Clone TS overlays do not copy `UserProgressWidget`; compose delivers Layer1. Only overlay `ring-config.json` files that already contain a `credit.rewards` object need the `enabled` key.

## Recommended path

Use **clone-customization-with-implementer** (L3 overlay only). Do not copy `ring/web` onto the clone.

> Set `credit.rewards.enabled` to `false` on this clone’s overlay `web/ring-config.json` at the top of the existing `credit.rewards` object. Do not fork `reward-credit-service.ts` or `UserProgressWidget`. Confirm card top-up, `POST /api/mcp/v1/credit/add`, and referral ShareEarn stay enabled. Pause for approval before compose or deploy.

Inputs: clone slug, overlay `web/ring-config.json` already has `credit.rewards`, operator wants activity awards off.

## Manual path

**Confirm the overlay owns `credit.rewards`**

If the clone’s `web/ring-config.json` already has a `credit.rewards` block, add `"enabled": false` there. Compose replaces the whole file — Layer1 `"enabled": true` will not apply.

**If the clone inherits Layer1 config**

Add `"enabled": false` on Layer1 only when **every** compose of that image should stop awards. Prefer overlay for a single white-label.

**Do not disable adjacent rails**

Leave `CreditAddFsModal`, Token Desk, MCP credit add, and refcodes alone. Those paths do not call `enqueueRewardCreditAddEvent`.

## Frequently asked questions

### Impact

#### Will members still see a quest board on `/profile`?

No. `UserProgressWidget` returns null and `profile-content.tsx` does not mount it when `isClientCreditRewardsEnabled()` is false. Profile hero credit **balances** still show.

#### Do comments and KYC still write `credit_add_events`?

No new awards. Callers still invoke enqueue; the engine returns `{ status: 'skipped', reason: 'rewards_disabled' }` before DB writes.

### Migration

#### What if my overlay omits `enabled`?

Awards stay **on** (backward compatible). Layer1 documents `"enabled": true` explicitly.

#### Do Layer2 packs need an upgrade?

No. Presets have no `credit.rewards` copies and no quest widget.

### Ops

#### Does `/admin/rewards` go away?

No. Admins can still chart history. New contribution awards stop.

#### Is there an env var for this?

No. SSOT is `ring-config.json` → `credit.rewards.enabled`. `env.local.template` has no `REWARDS_ENABLED` flag.

## Related documentation

  
- [features/wallet](/docs/features/wallet.md) — Depends-on: credit ledger, Token Desk, and card top-up stay independent of this kill switch.

  
- [features/ring-oracle](/docs/features/ring-oracle.md) — Depends-on: awards stamp `getMainCurrencyCreditAccountingRate` and display `getCreditUnitLabel`.

  
- [features/profile-account](/docs/features/profile-account.md) — Same-workflow: `/profile` mounts UserProgressWidget only when rewards are enabled.

  
- [features/admin](/docs/features/admin.md) — Next-step: `/admin/rewards` charts `credit_add_events` for platform admins.

  
- [features/refcodes](/docs/features/refcodes.md) — See-also: referral token minting is a separate rail from credit.rewards.

  
- [api/wallet](/docs/api/wallet.md) — Deep-dive: `getRewardQuestBoard` and credit Server Actions in `app/_actions/wallet.ts`.

  
- [customization/ringization-playbook](/docs/customization/ringization-playbook.md) — Same-workflow: clone overlay ring-config is whole-file compose — set enabled on the overlay you own.
