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:
- Profil vollständig: first_name, last_name, email, date_of_birth, address_country
- Mind. 5 Tipps gekauft:
SUM(tip_count) >= 5 - 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 |