Perk Effects System
The Perk Effects System allows any perk to carry configurable effects that activate when claimed. Effects can multiply earnings, protect streaks, grant raffle entries, award badges, and assign Discord roles.
Key principle: Effects are brand-deployable. Any brand can create perks with effects through the Partner Portal — no developer assistance needed.
How It Works
1. Brand creates a perk in the Partner Portal PerkWizard
2. Brand enables one or more effects (checkboxes) and configures values
3. Perk is published to the marketplace with effect metadata
4. User claims the perk (burns RSNC, receives NFT)
5. Post-claim hook creates activation records in the database
6. Bot checks activation records during reward calculation
7. Effects are applied: multiplied earnings, protected streaks, etc.
Effect Types
| Effect | What It Does | Duration | Stacking |
|---|---|---|---|
| Earning Multiplier | Multiplies RSNC earned per reward event | 7 days (configurable) | Replace (latest wins) |
| Event Cap Boost | Increases daily event claim limits for count-based events | 7 days (configurable) | Replace (latest wins) |
| Streak Shield | Prevents streak reset when a day is missed | 7-day shelf life | Accumulate (up to 5 charges) |
| Raffle Entry | Grants entries into a weekly raffle drawing | Instant | Accumulate (up to 15/week) |
| Profile Badge | Permanent cosmetic badge on user profile | Permanent | Collect (own multiple) |
| Discord Role | Grants a Discord role when claimed | Permanent or duration | Collect |
Creating a Perk with Effects
In the Partner Portal
- Open the PerkWizard (Create Perk)
- Fill in basic info (name, description, price, supply)
- Scroll to the Perk Effects section
- Check the effects you want this perk to have
- Configure the values for each enabled effect
- Set the Effect Scope (where effects apply)
- Review and publish
Effect Scope
Effects can apply beyond the creating brand's servers:
| Scope | Behavior |
|---|---|
| Own Brand (default) | Effects only apply in the creating brand's servers |
| Selected Partners | Effects apply in the creating brand's servers + named partner brands |
| Network | Effects apply across all Resonance servers (admin-only) |
Cross-brand consent required: For "Selected Partners" scope, each partner brand must explicitly accept external effects via the Brand Consent settings in their Partner Portal.
Effect Metadata
Effects are stored in the perk's metadata as a JSON array:
{
"effects": [
{
"type": "earning_multiplier",
"value": { "multiplier": 2.75 },
"duration_days": 7,
"stacking": "replace",
"scope": { "type": "own_brand" },
"max_active_per_user": 1
},
{
"type": "streak_shield",
"value": { "charges": 3 },
"duration_days": 7,
"stacking": "accumulate",
"scope": { "type": "own_brand" },
"max_active_per_user": 5
}
]
}
Effect Resolution
When the bot calculates a reward, the PerkEffectEngine resolves active effects through a 7-step pipeline:
- Cache check — KV cache lookup (5-min TTL)
- Fetch activations — Query
perk_effect_activationsfor user - Scope filter — Does this effect apply in the current brand context?
- Consent filter — Does the current brand accept effects from the source brand?
- Group by type — Organize effects by effect_type
- Apply stacking — Replace (latest wins), accumulate (sum with cap), highest, or collect
- Cache result — Store resolved set in KV
Reward Calculation Chain
final_reward = base_reward
x role_multiplier (from Discord role config)
x perk_earning_multiplier (from active Amplifier effect)
+ flat_bonus (from role config)
x streak_multiplier (from streak days)
The combined role_multiplier x perk_earning_multiplier is capped at 10x (security limit).
API Reference
All endpoints require internal authentication (CF-Worker header or X-Internal-Secret).
Resolve Effects
GET /perk-effects/:brandId/:userIdentifier
Returns the resolved effect set for a user in a brand context.
Response:
{
"earning_multiplier": { "multiplier": 2.75 },
"event_cap_multiplier": null,
"streak_shield": { "charges": 3 },
"badges": [{ "badge_id": "signal_carrier", "badge_tier": "standard" }],
"role_grants": []
}
Activate Effects
POST /perk-effects/activate
Called after a perk claim to create activation records.
Body:
{
"user_identifier": "discord:123456789",
"source_brand_id": "0xBrandAddress",
"collection_id": 42,
"effects": [
{
"type": "earning_multiplier",
"value": { "multiplier": 1.5 },
"duration_days": 7,
"scope": { "type": "own_brand" }
}
]
}
Consume Shield Charge
POST /perk-effects/consume-shield
Atomically consumes one streak shield charge.
Body:
{
"user_identifier": "discord:123456789",
"context_brand_id": "0xBrandAddress"
}
Response:
{
"consumed": true,
"chargesRemaining": 2
}
Cleanup Expired Effects
POST /perk-effects/cleanup
Deactivates expired effects. Called by scheduled CRON.
Brand Consent API
Get Consent Configuration
GET /brand-consent/:brandId
Grant Consent
POST /brand-consent/:brandId
Body:
{
"source_brand_id": "0xStartaleAddress",
"allowed_effect_types": ["earning_multiplier", "badge"],
"max_multiplier": 3.0
}
Revoke Consent
DELETE /brand-consent/:brandId/:sourceBrandId
Raffle API
Add Entries
POST /raffle/enter
Body:
{
"brand_id": "0xBrandAddress",
"user_identifier": "discord:123456789",
"collection_id": 42,
"entry_count": 5,
"rsnc_spent": 400
}
Get Draw Pool
GET /raffle/pool/:brandId/:drawWeek
Execute Draw
POST /raffle/draw
Body:
{
"brand_id": "0xBrandAddress",
"draw_week": "2026-W14",
"prize_config": {
"type": "rsnc_pot",
"winner_share": 0.7,
"burn_share": 0.3
}
}
Security
Validation Limits
| Effect | Max Value | Enforced At |
|---|---|---|
| Earning Multiplier | 5.0x per perk | PerkWizard + post-claim hook |
| Event Cap Multiplier | 3.0x per perk | PerkWizard + post-claim hook |
| Streak Shield | 5 charges max | PerkWizard + activation logic |
| Raffle Entries | 15 per purchase, 15 per week | PerkWizard + entry logic |
| Combined Multiplier | 10x (role + perk) | Bot reward calculation |
Brand Isolation
- Effects are scoped by
source_brand_idandeffect_scope - Cross-brand effects require explicit consent via
brand_effect_consent - Consent can cap multiplier values independently
- The
brandIdused for resolution comes from trusted server config, not user input
Deduplication
A unique index prevents duplicate active effects from the same collection:
UNIQUE(user_identifier, collection_id, effect_type) WHERE is_active = true
Re-claiming the same perk replaces (UPSERT) rather than stacking.
Database Tables
perk_effect_activations
Core table tracking active effects per user.
| Column | Type | Description |
|---|---|---|
source_brand_id | TEXT | Brand that created the perk |
collection_id | BIGINT | Perk collection ID |
user_identifier | TEXT | discord:{userId} |
effect_type | TEXT | Registry key |
effect_value | JSONB | Type-specific payload |
effect_scope | JSONB | Where the effect applies |
expires_at | TIMESTAMPTZ | NULL for permanent effects |
charges_remaining | INT | For consumable effects |
is_active | BOOLEAN | Deactivated on expiry/consumption |
brand_effect_consent
Cross-brand effect acceptance.
| Column | Type | Description |
|---|---|---|
brand_id | TEXT | Brand accepting effects |
source_brand_id | TEXT | Brand whose effects are accepted |
allowed_effect_types | TEXT[] | NULL = all types |
max_multiplier | NUMERIC | Safety cap on multiplier values |
raffle_entries / raffle_draws
Weekly raffle system tables. See Raffle API for details.