You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.ts — ReferralCode (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.
prisma/schema.prisma — ReferralCode, 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.
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
Abuse signals use data the platform already holds (on-chain funding graph, Session IP/UA, referral graph) — no new tracking or third-party fingerprinting.
abuseReasons are admin-only; the user sees a generic "under review" / "not eligible" status, never another user's data or the detection logic in detail.
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
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 realTransactioninside 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.ts—ReferralCode(8-char unambiguous alphabet),ReferralConversionlifecyclePENDING → ACTIVATED(verified against a confirmed depositTransactionin the deposit DB transaction)→ REWARDED(by the payout job).checkAndActivateOnDepositnever throws. Reward amounts are fixed config.src/jobs/referralPayout.ts—payoutActivatedConversions; idempotent, retriable; failed payout staysACTIVATEDwithpayoutError.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.prisma—ReferralCode,ReferralConversion(statusPENDING|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
ReferralRewardSchedule: reward =f(referredDepositAmount)×g(referrerLifetimeActivations), both piecewise/stepped and fully documented, with a hardmaxRewardPerConversionand a rollingmaxRewardPerReferrerPerMonthcap.ReferralConversionat 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:MIN_HOLD_WINDOW(the deposit existed only to trigger the reward).Session.ipAddress/userAgentcluster across referrer and many referred users (data already captured onSession).Each signal is individually weighted and configurable; the module is unit-tested against fixture clusters.
3. Payout gating
ReferralConversiongainsabuseRiskScore,abuseReasons Json,payoutHold(NONE|AUTO_REVIEW|DENIED).ACTIVATED+payoutHold = NONEconversions.AUTO_REVIEWwaits for an admin decision;DENIEDis terminal (with a reason, appealable via support).MIN_HOLD_WINDOWdelay 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).docs/REFERRAL_PROGRAM.mdrewritten for the tiered schedule + abuse policy;ASSUMPTIONS.mdrecords the schedule constants and hold window.Edge Cases & Failure Modes
AUTO_REVIEW, notDENIED; admin can approve; the appeal path is documented.cappedreason surfaced to the referrer.AUTO_REVIEW, never auto-pay on an unscored conversion.Security & Privacy Considerations
SessionIP/UA, referral graph) — no new tracking or third-party fingerprinting.abuseReasonsare admin-only; the user sees a generic "under review" / "not eligible" status, never another user's data or the detection logic in detail.REFERRAL_REWARDtransactions — auditable and tax-identifiable (Tax Reporting & Cost-Basis Lot Tracking #284).Out of Scope
modelVersionseam is included).Suggested Implementation Plan
ReferralRewardScheduleconfig +scheduleVersion,abuseRiskScore,abuseReasons,payoutHold, hold-window fields onReferralConversion+ migration/rollback.src/referral/abuseSignals.ts— the six signals, pure/bounded, fixture-tested against sybil clusters.payoutHold = NONEfilter;AUTO_REVIEW/DENIEDhandling.GET /api/v1/referrals/me; audit logging; Tamper-Evident Hash-Chained Audit Ledger #315 feed.docs/REFERRAL_PROGRAM.md+ASSUMPTIONS.md.Acceptance Criteria
abuseRiskScore+ reasons at activation; the module is fixture-tested against sybil clustersACTIVATED+payoutHold = NONE;AUTO_REVIEWwaits for an audited admin decision;DENIEDis terminal with a reason; unscored conversions never auto-payGET /api/v1/referrals/meshows tier/thresholds/pending/paid/hold-status (generic); admin review endpoints are audit-logged and feed Tamper-Evident Hash-Chained Audit Ledger #315docs/REFERRAL_PROGRAM.md+ASSUMPTIONS.md+docs/openapi.yamlupdated; unit + integration tests green