Referrals System
1. Summary
Goal: Механизм привлечения новых пользователей через систему реферальных ссылок с двусторонними наградами и пассивным доходом для реферера.
User Value: Возможность получить бонус за приглашение друзей и пассивный доход (10%) от заработка приглашённых пользователей до конца текущего сезона. Путь: Поделился ссылкой → Друг зарегистрировался → Оба получили бонус → Реферер получает % от заработка друга (в течение сезона).
2. Business Logic
Reward Types
- Referrer Rewards
- Referred Rewards
Кто получает: Пользователь, который пригласил
Одноразовая награда: +100 XP (при активации реферала)
Пассивный доход: 10% от каждого заработка Scrap приглашённого друга (до конца сезона)
Пассивный доход работает только в том сезоне, когда был создан реферал. После смены сезона реферер перестаёт получать % от заработка приглашённого.
Дополнительно: +1 к счётчику friendsInvited, прогресс квестов SOCIAL
Кто получает: Приглашённый пользователь
Одноразовая награда: +500 Scrap (при активации)
Цель: Стартовый капитал для новичка
Invite Flow
Алгоритм приглашения (реализован в ReferralCodeService + InviteSessionService + ActivationRewardService).
Подробнее о сессиях см. Activation.
1. Генерация ссылки
- Пользователь получает уникальный реферальный код (авто-генерация или кастомный)
- Share URL:
https://start.goloot.online/ref_CODE(красивый превью в соцсетях) - Redirect-service перенаправляет на:
https://t.me/bot/app?startapp=ref_CODE
2. Переход по ссылке
- Клик по ссылке → создаётся
InviteSession(state: PENDING) - Записывается
ReferralClickAnalytics(для аналитики конверсий) - Session живёт 72 часа, затем EXPIRED
3. Регистрация и активация
- При регистрации проверяется наличие PENDING сессии по
telegramId - Создаётся запись
Referral(связь referrer → referred) - Session переходит в state: ACTIVATED
- Выдаются награды обеим сторонам
4. Passive Income (Accumulate + Claim)
Пассивный доход НЕ зачисляется напрямую на баланс. Он накапливается в буфере passiveIncomeBalance и пользователь забирает его вручную через кнопку в ReferralModal.
Накопление (автоматически):
- При каждом заработке Scrap приглашённым вызывается
PassiveIncomeService.processPassiveIncome() - Проверяется:
referral.seasonId === currentSeason.id - Если сезоны совпадают:
passiveAmount = floor(scrapAmount × 10%) - Минимум 1 Scrap если исходный заработок > 0
- Если сезоны НЕ совпадают:
passiveAmount = 0(пассивный доход не начисляется) - Результат →
user.passiveIncomeBalance += passiveAmount(буфер)
Claim (по кнопке):
- Эндпоинт:
POST /api/referrals/claim-passive-income - Optimistic locking:
updateMany WHERE passiveIncomeBalance = exactValue— защита от double-click - При успехе:
scrap += claimAmount,scrapTotalEarned += claimAmount,scrapFromReferrals += claimAmount,passiveIncomeBalance = 0 - Обновляет сезонную статистику через
updateSeasonScrap()
Season Reset Auto-Claim:
- При смене сезона невостребованный
passiveIncomeBalanceавтоматически зачисляется вscrapTotalEarned(lifetime stat) через raw SQL - Пользователь не теряет накопленный доход
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| Create code | general (100/min) | Telegram | CreateReferralCodeSchema |
| Get code info | general (100/min) | None | ReferralCodeParamsSchema |
| Get analytics | general (100/min) | Telegram | ReferralAnalyticsQuerySchema |
| Get my codes | general (100/min) | Telegram | - |
| Get my stats | general (100/min) | Telegram | GetMyReferralStatsSchema |
| Claim passive income | achievementClaim (15/min) | Telegram + Active Season | - |
См. Security Matrix для полного обзора защит.
Edge Cases
Что видит пользователь (UI):
| Ситуация | UI поведение |
|---|---|
| Самореферал | Блокируется, ошибка "Cannot create self-referral" |
| Повторная регистрация | Реферал уже существует, возвращается существующий |
| Истекшая сессия | 72ч прошло — пользователь регистрируется без реферера |
| Нет кодов | При запросе /my автоматически создаётся код |
| Passive income = 0 | Карточка claim не показывается, вместо неё — зелёная статистика (если был доход ранее) |
| Passive income > 0 | Золотая карточка с суммой и кнопкой "Забрать" |
| Double-click claim | Optimistic locking → ошибка CONCURRENT_CLAIM, UI показывает 0 |
| Нет активного сезона | Claim заблокирован middleware requireActiveSeason |
Backend Error Codes (для API/тестов)
| Код | HTTP | Сообщение |
|---|---|---|
| Self-referral | 400 | "Cannot create self-referral" |
| Referral exists | 200 | Возвращает существующий referral |
| Code not found | 404 | "Referral code not found" |
| Access denied | 403 | "You can only view analytics for your own referral codes" |
3. ADR (Architectural Decisions)
Почему InviteSession, а не прямая привязка?
Проблема: Пользователь кликает по реферальной ссылке ДО регистрации в боте. Нужно сохранить информацию о реферере до момента создания аккаунта.
Решение: Промежуточная сущность InviteSession с lifecycle:
- PENDING → ACTIVATED (при регистрации)
- PENDING → EXPIRED (через 72ч)
Альтернативы (отклонены):
- Cookie/localStorage — не работает в Telegram Mini App
- Передача кода в start_param без сессии — теряется аналитика кликов
Последствия: Полная аналитика конверсий (клик → регистрация), TTL защищает от бесконечных PENDING сессий.
Почему Passive Income в % а не фиксированный бонус?
Проблема: Как мотивировать рефереров после одноразового бонуса?
Решение: 10% от заработка реферала. Это создаёт:
- Мотивацию приглашать активных друзей
- Интерес к успеху приглашённых (помогать им)
Альтернативы (отклонены):
- Фиксированный бонус за каждое действие реферала — сложно балансировать
Последствия: Простая понятная механика.
Почему Passive Income Accumulate + Claim, а не Auto-Credit?
Проблема: При автоматическом зачислении пассивного дохода пользователь не видит, сколько именно приносят рефералы. Это снижает ощущение ценности реферальной программы.
Решение: Доход копится в буфере passiveIncomeBalance. Пользователь видит сумму и забирает вручную:
- Золотая карточка в ReferralModal показывает накопленную сумму
- Кнопка "Забрать" с haptic feedback и TopFloatingReward анимацией
- Optimistic locking защищает от race conditions
Альтернативы (отклонены):
- Auto-credit при каждом заработке реферала — пользователь не замечает начислений
- Ежедневная автовыплата — сложнее, не даёт контроль пользователю
Последствия:
- Пользователь видит конкретную пользу от рефералов
- Геймификация: приятный момент "забрать" награду
- При смене сезона — auto-claim в
scrapTotalEarned(ничего не теряется)
Почему Passive Income ограничен сезоном?
Бесконечный пассивный доход создаёт неконтролируемую инфляцию и снижает ценность активной игры.
Проблема: Пользователь, пригласивший 100 друзей год назад, получает огромный пассивный доход, не прилагая усилий. Это создаёт:
- Перекос в экономике (ранние игроки слишком богаты)
- Снижение мотивации к активной игре
- Неконтролируемую инфляцию
Решение: Пассивный доход работает только в сезоне создания реферала.
- При создании реферала сохраняется
seasonId PassiveIncomeServiceпроверяет:referral.seasonId === currentSeason.id- Связь реферер → приглашённый остаётся навсегда (для статистики)
Referral.passiveScrapEarnedхранит lifetime total (не сбрасывается)User.scrapFromReferralsсбрасывается каждый сезон (сезонная статистика)
Альтернативы (отклонены):
- Passive income навсегда — создаёт инфляцию и перекос
- Ограничение по времени (30 дней) — не привязано к игровым циклам
Последствия:
- Чёткая привязка к игровым сезонам
- Мотивация приглашать друзей каждый сезон
- Контролируемая экономика
- UI показывает
isPassiveIncomeActiveфлаг для каждого реферала
4. Architecture
Services Overview
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| ReferralService | backend/src/domains/referrals/services/referral.service.ts | CRUD для Referral связей |
| ReferralCodeService | backend/src/domains/referrals/services/referral-code.service.ts | Управление кодами, генерация URL |
| ReferralRewardService | backend/src/domains/referrals/services/referral-reward.service.ts | Начисление наград при активации |
| PassiveIncomeService | backend/src/domains/referrals/services/passive-income.service.ts | Расчёт и начисление пассивного дохода |
| ReferralCodesController | backend/src/domains/referrals/controllers/referral-codes.controller.ts | User API для кодов |
| MyReferrals Routes | backend/src/domains/referrals/routes/my-referrals.routes.ts | User API для статистики |
| Admin Routes | backend/src/domains/referrals/routes/admin-referrals.routes.ts | Admin API |
Bootstrap Integration
Referral data is preloaded via /api/bootstrap endpoint during splash screen to eliminate loading states in UI.
Included in Bootstrap:
- Referral Config — static configuration (bonusXp, bonusScrapReferred, passiveIncomePercent)
- User Stats — totalReferrals, activeReferrals, totalPassiveScrapEarned
- My Code — user's referral code and URL (null if not created yet)
- My Referrer — information about who invited the user (null if no referrer)
- User Profile —
passiveIncomeBalance(для отображения кнопки claim в ReferralModal)
Cache Strategy:
staleTime: 5-10 minutes(data rarely changes)- React Query cache keys:
['referralStats'],['referralConfig'],['myReferralCode'],['myReferrer'] - Hydrated via
hydrateReferralData()inbootstrapHydration.ts
Backend Optimization:
getUserReferralStats()uses Prisma aggregation instead offindMany()- Performance: 50KB → 12 bytes, 20ms → 3ms
- Method:
Promise.all([count(), count(), aggregate()])
User Experience:
- Before: ReferralModal showed loading skeleton (200-400ms)
- After: Instant modal opening with 0ms delay (native app effect)
Implementation:
// Backend: BootstrapService.getReferralBootstrapData()
const [stats, userCodes, referralRelation] = await Promise.all([
this.referralService.getUserReferralStats(userId),
this.referralCodeService.getUserReferralCodes(userId),
this.referralService.getReferralByReferredId(userId),
]);
// Frontend: bootstrapHydration.ts
queryClient.setQueryData(['referralStats'], data.stats);
queryClient.setQueryData(['referralConfig'], data.config);
queryClient.setQueryData(['myReferralCode'], data.myCode);
queryClient.setQueryData(['myReferrer'], data.myReferrer);
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| ReferralCode | Уникальный код пользователя для приглашений | userId (unique), code (unique), clicksCount, isActive |
| InviteSession | Сессия от клика до регистрации | telegramId, state (PENDING/ACTIVATED/EXPIRED), metadata (JSON с referralCode), expiresAt |
| ReferralClickAnalytics | Детальная аналитика кликов | referralCodeId, telegramHash, inviteSessionId, clickedAt |
| Referral | Связь реферер → приглашённый | referrerId, referredId (unique), seasonId, bonusXpClaimed, bonusScrapClaimed, passiveScrapEarned |
| User (поля) | Буфер пассивного дохода | passiveIncomeBalance (Int, default 0) — накапливается автоматически, обнуляется при claim |
Relationships
Key Constraints
Referral.referredId— UNIQUE (один пользователь = один реферер навсегда)ReferralCode.userId— UNIQUE (один пользователь = один код)InviteSession— UNIQUE по[telegramId, type](один PENDING на type)
6. API Endpoints
- User: Codes
- User: Stats
- Admin
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| POST | /api/referral-codes/create | Создать реферальный код | → |
| GET | /api/referral-codes/:code | Получить информацию о коде | → |
| GET | /api/referral-codes/my | Мои реферальные коды | → |
| GET | /api/referral-codes/:code/analytics | Аналитика кода (владелец) | → |
7. Configuration
Конфигурация из backend/src/domains/referrals/config.ts:
| Параметр | Дефолт | ENV Variable | Описание |
|---|---|---|---|
BONUS_XP | 100 | REFERRAL_BONUS_XP | XP награда рефереру |
BONUS_SCRAP_REFERRED | 500 | REFERRAL_BONUS_SCRAP_REFERRED | Scrap награда приглашённому |
PASSIVE_INCOME_PERCENT | 10 | REFERRAL_PASSIVE_INCOME_PERCENT | Процент пассивного дохода |
BONUS_SCRAP_UTM | 200 | UTM_BONUS_SCRAP | Бонус Scrap для UTM кампаний |
Изменения применяются при рестарте backend без перекомпиляции.
8. Related
- Activation — домен InviteSession (создание и активация сессий)
- Quests — SOCIAL квесты на приглашение друзей
- Achievements — SOCIAL достижения за рефералов
- Live Feed — события REFERRAL_BONUS в ленте
- Users — поле
friendsInvitedв профиле - UTM Tracking — альтернативная система отслеживания (маркетинг)