Rewards & contributor identity
The Rewards system transforms the Mushi SDK from a passive collection tool into an engagement loop. Users earn points for activity, climb tiers, and unlock perks — while you collect richer, higher-quality reports from users who are motivated to stay engaged.
Design principles
-
Identity is optional but cumulative. An anonymous user who later calls
identify()does not lose credit for their prior activity. The server back-patches theend_user_idon pre-existing reports and activity events using the stablereporter_token_hashas the bridge. -
Awards are server-authoritative. Points are never computed in the SDK. The SDK sends raw events; a PostgreSQL trigger (
private.apply_activity_points) applies the rules atomically. This means your cap and multiplier logic is tamper-proof even if the SDK is patched by the user. -
Anti-gaming composability. Rewards reuse the existing anonymous reputation scorer (see Anti-gaming & reputation). A reporter whose token-hash reputation drops below −100 has their activity events accepted but scored at 0 until their reputation recovers. They cannot farm points by spamming bad reports.
-
Privacy first. No PII is required. If
identify()is called withoutemailornametraits,end_users.emailanddisplay_nameremain null. The Contributors leaderboard shows an anonymised handle in that case (User-a4f3from the last 4 chars ofexternal_user_id).
Data model
end_users
id uuid PK
organization_id uuid FK → organizations
external_user_id text — the ID from your auth provider
display_name text?
email text?
provider text? — 'google' | 'apple' | 'supabase' | …
opted_in boolean
jwt_verified_at timestamptz? — set when P2 host JWT is verified
reporter_token_hash text? — bridges anonymous activity
last_seen_at timestamptz
created_at timestamptz
updated_at timestamptz
end_user_activity
id uuid PK
end_user_id uuid FK → end_users
organization_id uuid FK → organizations
action text — matches a reward_rules.action
points_awarded int — 0 if capped or anti-gamed
metadata jsonb
occurred_at timestamptz
end_user_points
end_user_id uuid PK FK → end_users (1:1)
organization_id uuid
total_points int — all-time, always ≥ 0
points_30d int — rolling 30-day window
points_lifetime int — synonym for total (never reset)
updated_at timestamptz
reward_rules
id uuid PK
organization_id uuid FK → organizations
action text — e.g. 'page_view', 'report_submit'
base_points int — negative = deduction
max_per_day int?
max_per_user_lifetime int?
multiplier_eligible boolean
requires_jwt_verification boolean
enabled boolean
reward_tiers
id uuid PK
organization_id uuid FK → organizations
slug text — 'free' | 'explorer' | 'contributor' | 'champion'
display_name text
points_threshold int
perks jsonb
multiplier numeric — point multiplier for eligible rules
sort_order intServer-side award flow
Key guarantee: each end_user_activity insert triggers
private.apply_activity_points in the same transaction, so end_user_points
is always consistent — there is no separate aggregation job to fall behind.
SDK-side flow
trackActivity: true in MushiRewardsConfig auto-captures:
session_start— once per session (new tab / app foreground after 30 min idle)page_view— every pathname change in the web SDK, every screen change viasetScreen()in React Nativenavigate— explicit<a>clicks androuter.push()callsbutton_press— clicks on elements withdata-mushi-trackor[data-testid]attributes
For custom actions, call mushi.submitActivity() directly.
Identity verification (P2 — monetary awards)
When monetary payouts are enabled, a rule marked requires_jwt_verification: true will not award points unless a host-app JWT is present and valid.
The JWKS endpoint for your auth provider is registered once in the admin
console under Rewards → Settings → Identity Providers. Supported providers:
Supabase Auth, Apple Sign In, Google Sign In, and any provider that serves a
standard /.well-known/jwks.json.
Tier multipliers
When a user reaches a tier with multiplier > 1, all subsequent
multiplier_eligible rule awards are scaled:
points_awarded = floor(base_points × tier.multiplier)Example: an Explorer (multiplier 1.0) earns 2 pts for a page_view. A
Champion (multiplier 2.0) earns 4 pts for the same action.
Non-eligible rules (e.g. anti-gaming deductions) are always applied at face value — multipliers cannot amplify a penalty.
Anti-gaming integration
The rewards system inherits all guarantees from the anti-gaming pipeline:
- The
reporter_token_hashreputation gate blocks known bad actors from accruing points even before theirend_usersrecord is created. - Activity events from tokens flagged
spamorrate_limitedare stored withpoints_awarded = 0so the audit trail is complete without the fraud being rewarded. - The
anomaly-detection-cron(P3) runs nightly to identify users whoseend_user_activitypattern deviates statistically from the cohort median. Flagged records land inreward_disputesfor manual review. - Monthly org caps in
project_settingsput a hard ceiling on total points issued in a calendar month — protecting you from an unexpected viral spike.
Webhooks reference
Register webhook endpoints in the admin console under Rewards → Settings →
Webhooks. All payloads are POST, Content-Type: application/json, signed
with X-Mushi-Signature (HMAC-SHA256).
points_awarded
{
"event": "points_awarded",
"userId": "usr_abc123",
"organizationId": "org_xyz",
"action": "report_submit",
"pointsAwarded": 50,
"totalPoints": 220,
"tier": { "slug": "explorer", "displayName": "Explorer" },
"occurredAt": "2026-05-17T12:34:56Z"
}tier_up
{
"event": "tier_up",
"userId": "usr_abc123",
"organizationId": "org_xyz",
"previousTier": { "slug": "free", "displayName": "Free" },
"newTier": { "slug": "explorer", "displayName": "Explorer" },
"totalPoints": 103,
"occurredAt": "2026-05-17T12:34:56Z"
}quest_complete
{
"event": "quest_complete",
"userId": "usr_abc123",
"organizationId": "org_xyz",
"questId": "quest_001",
"questName": "First Feedback",
"bonusPoints": 100,
"totalPoints": 320,
"occurredAt": "2026-05-17T12:34:56Z"
}payout_created (P2 — monetary)
{
"event": "payout_created",
"userId": "usr_abc123",
"organizationId": "org_xyz",
"amount": 25.00,
"currency": "usd",
"stripeTransferId": "tr_xxx",
"occurredAt": "2026-05-17T12:34:56Z"
}