Zum Inhalt

Bonus-System

Übersicht

Die BO-Engine verwaltet drei Bonus-Programme:

Programm Trigger Belohnung Schedule
Affiliate-Bonus Referral-Aktivierung 5 Tipps 2x täglich
Leaderboard-Bonus Monatliches Ranking 5-15 Tipps 1. des Monats
Re-Engagement Inaktive Spieler Konfigurierbar Täglich 10:00 UTC

Alle Boni werden als extra_bonus_vouchers in der Datenbank gespeichert.


Affiliate-Bonus

Datei: workers/awardAffiliateBonus.ts (128 Zeilen) Schedule: 00:30 + 12:00 UTC

Bedingungen

Ein Affiliate erhält 5 Bonus-Tipps wenn der geworbene Spieler:

  1. Profil vollständig: first_name, last_name, email, date_of_birth, address_country
  2. Mind. 5 Tipps gekauft: SUM(tip_count) >= 5
  3. Bonus noch nicht vergeben: bonus_awarded = false

Query

SELECT ar.id, ar.affiliate, ar.referred_player
FROM affiliate_rates ar
JOIN users u ON u.wallet_address = ar.referred_player
JOIN LATERAL (
  SELECT COALESCE(SUM(tip_count), 0) AS total_tips
  FROM tickets WHERE player = ar.referred_player
) t ON true
WHERE ar.active = true
  AND ar.bonus_awarded = false
  AND u.first_name IS NOT NULL
  AND u.last_name IS NOT NULL
  AND u.email IS NOT NULL
  AND u.date_of_birth IS NOT NULL
  AND u.address_country IS NOT NULL
  AND t.total_tips >= 5

Aktion

// awardAffiliateBonus.ts:80-102
// In einer Transaktion:
await pg.query("BEGIN");

// 1. Bonus-Voucher erstellen
await pg.query(
  `INSERT INTO extra_bonus_vouchers (buyer, amount, reason, created_by)
   VALUES ($1, 5, $2, 'affiliate_bonus')`,
  [affiliate, `Referral ${referredPlayer} activated`]
);

// 2. Flag setzen
await pg.query(
  `UPDATE affiliate_rates SET bonus_awarded = true WHERE id = $1`,
  [rateId]
);

await pg.query("COMMIT");

Idempotenz

bonus_awarded = false-Bedingung in der WHERE-Klausel verhindert Doppelvergabe.


Leaderboard-Bonus

Datei: workers/awardLeaderboardBonus.ts (125 Zeilen) Schedule: 1. des Monats um 01:00 UTC

Belohnungen

Platz Bonus-Tipps
1. 15 Tipps
2. 10 Tipps
3. 5 Tipps

Bedingung

  • Mindestens 3 Logins im Vormonat
  • Ranking nach monthly_logins (absteigend)

Query

SELECT wallet_address, display_name, monthly_logins
FROM users
WHERE monthly_logins_month = $1  -- Vormonat (z.B. '2026-01')
  AND monthly_logins >= 3
ORDER BY monthly_logins DESC
LIMIT 3

Idempotenz

// awardLeaderboardBonus.ts:46-55
const existing = await pg.query(
  `SELECT COUNT(*) FROM extra_bonus_vouchers
   WHERE created_by = 'leaderboard_bonus'
     AND reason LIKE $1`,
  [`%${targetMonth}%`]
);

if (existing.rows[0].count > 0) {
  console.log("Leaderboard bonus already awarded for", targetMonth);
  return;
}

Re-Engagement-System

Datei: workers/reEngagementEmail.ts (206 Zeilen) Schedule: Täglich um 10:00 UTC

Konfiguration (bo_config)

Key Beschreibung Default
reengagement_enabled On/Off-Schalter -
reengagement_inactive_days Tage seit letztem Login 7
reengagement_bonus_amount Bonus-Tipps pro E-Mail 5
reengagement_max_active_bonuses Max ungenutzte Marketing-Boni 3
reengagement_cooldown_days Min. Tage zwischen E-Mails 14

Zielgruppen-Query

SELECT u.*,
  (SELECT COUNT(*) FROM extra_bonus_vouchers
   WHERE buyer = u.wallet_address
     AND created_by = 'marketing_bonus'
     AND used = false) AS active_marketing_bonuses
FROM users u
WHERE u.email IS NOT NULL
  AND u.last_login_at < $1        -- Inaktiv seit X Tagen
  AND u.self_exclusion_until IS NULL  -- Kein Selbstausschluss
  AND NOT EXISTS (
    SELECT 1 FROM re_engagement_emails
    WHERE wallet_address = u.wallet_address
      AND sent_at > $2             -- Cooldown respektieren
  )
LIMIT 100

Ablauf pro Spieler

// reEngagementEmail.ts:118-194
for (const player of inactivePlayers) {
  // 1. Bonus-Limit prüfen
  if (player.active_marketing_bonuses >= config.max_active_bonuses) continue;

  // 2. Platform-Stats laden (7-Tage + Allzeit)
  const stats = await getRecentStats(pg);

  // 3. Bonus-Voucher erstellen
  await pg.query(
    `INSERT INTO extra_bonus_vouchers (buyer, amount, reason, created_by)
     VALUES ($1, $2, 'Re-engagement bonus', 'marketing_bonus')`,
    [player.wallet_address, config.bonus_amount]
  );

  // 4. E-Mail senden
  const html = buildReEngagementEmail({
    playerName: player.first_name,
    bonusAmount: config.bonus_amount,
    stats,
    locale: player.preferred_language
  });

  await sendEmail(player.email, subject, html);

  // 5. E-Mail loggen (mit View-Token)
  const viewToken = crypto.randomUUID();
  await pg.query(
    `INSERT INTO re_engagement_emails
     (wallet_address, sent_at, bonus_amount, view_token)
     VALUES ($1, NOW(), $2, $3)`,
    [player.wallet_address, config.bonus_amount, viewToken]
  );

  // 6. Rate-Limiting (Resend Free Tier: 2 req/s)
  await sleep(600);
}

Schutzmaßnahmen

Maßnahme Beschreibung
Cooldown Min. 14 Tage zwischen E-Mails
Max-Boni-Limit Max. 3 ungenutzte Marketing-Boni
Selbstausschluss self_exclusion_until wird respektiert
Batch-Limit Max. 100 E-Mails pro Run
Rate-Limiting 600ms Pause zwischen E-Mails

extra_bonus_vouchers-Tabelle

extra_bonus_vouchers (
  id         SERIAL PRIMARY KEY,
  buyer      VARCHAR NOT NULL,     -- Wallet-Adresse
  amount     INT NOT NULL,          -- Anzahl Bonus-Tipps
  reason     TEXT,                   -- Beschreibung
  created_by VARCHAR NOT NULL,      -- 'affiliate_bonus' / 'leaderboard_bonus' / 'marketing_bonus'
  used       BOOLEAN DEFAULT false, -- Bereits eingelöst?
  expires_at TIMESTAMP,             -- Ablaufdatum (optional)
  created_at TIMESTAMP DEFAULT NOW()
)

created_by-Werte

Wert Quelle
affiliate_bonus Affiliate-Bonus Worker
leaderboard_bonus Leaderboard-Bonus Worker
marketing_bonus Re-Engagement Worker