Skip to content

Tiered Referral Rewards with Sybil & Abuse Detection #363

Description

@devsimze

Problem Statement

The referral program (src/referral/service.ts) is single-level and flat: one code per user, a fixed reward to each side when a referred user's on-chain-confirmed deposit crosses the activation threshold. It has good integrity (attribution at source, activation verified against a real Transaction inside the deposit DB transaction, payout split into a separate irreversible sweep) but no economics and no abuse defense. A flat bounty per signup is exactly what a sybil farmer optimizes: many throwaway wallets, each depositing the minimum, self-referred through one hub account. This issue adds tiered rewards (reward scales with referred deposit size and with the referrer's cumulative successful referrals) and a sybil / abuse detection layer that holds or denies payouts on suspicious clusters — without weakening the existing activation integrity.

Current State

  • src/referral/service.tsReferralCode (8-char unambiguous alphabet), ReferralConversion lifecycle PENDING → ACTIVATED (verified against a confirmed deposit Transaction in the deposit DB transaction) → REWARDED (by the payout job). checkAndActivateOnDeposit never throws. Reward amounts are fixed config.
  • src/jobs/referralPayout.tspayoutActivatedConversions; idempotent, retriable; failed payout stays ACTIVATED with payoutError.
  • src/compliance/scoring.ts + AML pipeline (AML Transaction Monitoring & Sanctions Screening Pipeline #321) — transaction screening; can freeze users.
  • src/audit/chain.ts (Tamper-Evident Hash-Chained Audit Ledger #315) — hash-chained audit ledger.
  • prisma/schema.prismaReferralCode, ReferralConversion (status PENDING|ACTIVATED|REWARDED|EXPIRED), User.referralCode, User.referralConversion (a user has one conversion — they were referred once), TransactionType.REFERRAL_REWARD.
  • docs/REFERRAL_PROGRAM.md.

Proposed Solution

1. Tiered reward schedule

  • Config-driven ReferralRewardSchedule: reward = f(referredDepositAmount) × g(referrerLifetimeActivations), both piecewise/stepped and fully documented, with a hard maxRewardPerConversion and a rolling maxRewardPerReferrerPerMonth cap.
  • Both legs (referrer + referred) scale; the referred-user bonus can be a deposit-matched percentage up to a cap.
  • The schedule version is stamped onto each ReferralConversion at activation so a later schedule change never retroactively re-prices a pending payout.

2. Sybil / abuse signals (src/referral/abuseSignals.ts, new — mostly pure)

Computed at activation time (before the payout job is eligible to pay), producing a riskScore + reasons:

  • Self-funding chain: referred wallet funded (on-chain) primarily from the referrer's wallet or a wallet in the referrer's cluster.
  • Deposit-then-withdraw: referred user withdraws ≥ X% of the activating deposit within MIN_HOLD_WINDOW (the deposit existed only to trigger the reward).
  • Burst: > N activations for one referrer within a short window.
  • Shared fingerprint: same Session.ipAddress / userAgent cluster across referrer and many referred users (data already captured on Session).
  • Circular: A refers B refers C … refers A, or a dense referral subgraph.
  • Fresh-wallet cohort: referred wallets all created on-chain within minutes of signup with no prior history.

Each signal is individually weighted and configurable; the module is unit-tested against fixture clusters.

3. Payout gating

  • ReferralConversion gains abuseRiskScore, abuseReasons Json, payoutHold (NONE | AUTO_REVIEW | DENIED).
  • Payout job pays only ACTIVATED + payoutHold = NONE conversions. AUTO_REVIEW waits for an admin decision; DENIED is terminal (with a reason, appealable via support).
  • A MIN_HOLD_WINDOW delay between activation and payout eligibility (config, e.g. 72h) so deposit-then-withdraw is caught before money goes out — this is the single highest-leverage change and is on by default.

4. Admin + API + docs

  • GET /api/v1/admin/referrals/review — held conversions with signals; POST .../:id/decision (approve / deny + note), audit-logged.
  • GET /api/v1/referrals/me — the caller's tier, lifetime activations, next-tier threshold, pending vs. paid rewards, and any hold status (generic reason only).
  • Metrics: activations, hold rate by reason, denied $ vs. paid $, payout latency.
  • docs/REFERRAL_PROGRAM.md rewritten for the tiered schedule + abuse policy; ASSUMPTIONS.md records the schedule constants and hold window.

Edge Cases & Failure Modes

  • Legit referral that looks like abuse (a user genuinely funds a friend's first deposit): lands in AUTO_REVIEW, not DENIED; admin can approve; the appeal path is documented.
  • Referred user withdraws for a real reason within the hold window: reward denied (policy), clearly communicated at referral time ("your friend must keep their deposit N days"). Not clawed back later — the hold window is the mechanism.
  • Schedule change mid-flight: pending conversions keep their stamped schedule version; only new activations use the new schedule.
  • Monthly cap reached: further activations still record and activate, but reward is capped/zeroed with capped reason surfaced to the referrer.
  • Abuse scoring fails (DB error): fail to AUTO_REVIEW, never auto-pay on an unscored conversion.
  • Cluster detection cost: graph/cluster queries are bounded (depth/'fanout limits); run in the payout sweep, not the deposit path.
  • Idempotency: gating fields are set once at activation; re-runs of the sweep never re-pay or double-deny.

Security & Privacy Considerations

Out of Scope

  • Multi-level / MLM referral trees (stays single-level).
  • Real-time (pre-activation) blocking of signups — detection gates payout, not account creation.
  • Reward payouts in anything other than the existing asset/rails.
  • ML-based fraud models (v1 signals are explicit and weighted; a modelVersion seam is included).

Suggested Implementation Plan

  1. ReferralRewardSchedule config + scheduleVersion, abuseRiskScore, abuseReasons, payoutHold, hold-window fields on ReferralConversion + migration/rollback.
  2. Tiered reward computation stamped at activation; caps.
  3. src/referral/abuseSignals.ts — the six signals, pure/bounded, fixture-tested against sybil clusters.
  4. Payout job: hold-window eligibility + payoutHold = NONE filter; AUTO_REVIEW/DENIED handling.
  5. Admin review endpoints + GET /api/v1/referrals/me; audit logging; Tamper-Evident Hash-Chained Audit Ledger #315 feed.
  6. Metrics + docs/REFERRAL_PROGRAM.md + ASSUMPTIONS.md.

Acceptance Criteria

  • Reward scales with referred deposit size and referrer lifetime activations per a documented, versioned schedule, with per-conversion and per-referrer-per-month caps
  • The schedule version is stamped at activation; schedule changes never retroactively re-price pending payouts
  • Six weighted, individually configurable sybil signals compute an abuseRiskScore + reasons at activation; the module is fixture-tested against sybil clusters
  • A default hold window between activation and payout eligibility catches deposit-then-withdraw before money leaves
  • Payout job pays only ACTIVATED + payoutHold = NONE; AUTO_REVIEW waits for an audited admin decision; DENIED is terminal with a reason; unscored conversions never auto-pay
  • GET /api/v1/referrals/me shows tier/thresholds/pending/paid/hold-status (generic); admin review endpoints are audit-logged and feed Tamper-Evident Hash-Chained Audit Ledger #315
  • docs/REFERRAL_PROGRAM.md + ASSUMPTIONS.md + docs/openapi.yaml updated; unit + integration tests green

Metadata

Metadata

Assignees

Labels

Stellar WaveIssues in the Stellar wave program

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions