diff --git a/lib/referrals.js b/lib/referrals.js index 692121f..0693187 100644 --- a/lib/referrals.js +++ b/lib/referrals.js @@ -27,6 +27,9 @@ const DEFAULTS = { // happens to start hunting years after their sponsor signed them up refCapPol: 20, // referral pool per Central day, separate from the find pool refMatchMinPol: 0.005,// do not write a transaction for dust + // Marty's launch bonus (2026-09-23): an extra drip to the FIRST hunter whose referral gets all + // the way through. Once ever, never again. Set to 0 to retire it. + refFirstBonusPol: 5, }; function cfg() { return Object.assign({}, DEFAULTS, store.read('settings', {})); } const round4 = n => Math.round(Number(n) * 10000) / 10000; @@ -89,7 +92,8 @@ function write(referrer, pol, ref) { const rec = { id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8), memberId: Number(referrer.memberId), email: referrer.email || null, wallet: referrer.wallet, username: referrer.username || null, - missionId: 'ref:' + ref.kind, site: ref.kind === 'bounty' ? 'Referral bounty' : 'Referral match', + missionId: 'ref:' + ref.kind, + site: ref.kind === 'bounty' ? 'Referral bounty' : ref.kind === 'first-bonus' ? 'First referral bonus' : 'Referral match', pol: round4(pol), day: ctDay(), at: Date.now(), status: 'due', tx: null, paidAt: null, error: null, ref, }; @@ -117,7 +121,16 @@ function onFind(member, find) { const finds = store.read('payouts', []).filter(p => Number(p.memberId) === id && !p.ref && !p.prize && p.status !== 'failed'); if (finds.length <= 1 && ageDays <= Number(c.refNewDays) && !bountyPaidFor(id)) { const pol = Number(c.refBountyPol); - if (pol <= room) { out.push(write(referrer, pol, Object.assign({ kind: 'bounty' }, who))); room -= pol; } + if (pol <= room) { + const firstEver = !refRows().some(p => p.ref.kind === 'bounty'); + out.push(write(referrer, pol, Object.assign({ kind: 'bounty' }, who))); room -= pol; + // the launch bonus rides along with the very first bounty anyone earns, once ever. It is + // deliberately outside the daily referral ceiling: it is a single payment, not a rate. + const bonus = Number(c.refFirstBonusPol) || 0; + if (firstEver && bonus > 0 && !refRows().some(p => p.ref.kind === 'first-bonus')) { + out.push(write(referrer, bonus, Object.assign({ kind: 'first-bonus' }, who))); + } + } } // the match, on every find inside the window, paid on top and never deducted from the newcomer if (ageDays <= Number(c.refMatchDays)) { @@ -150,6 +163,7 @@ function state(member) { return { on: String(c.refEnabled) === '1', bountyPol: Number(c.refBountyPol), + firstBonusPol: refRows().some(p => p.ref.kind === 'first-bonus') ? 0 : Number(c.refFirstBonusPol) || 0, matchPct: Number(c.refMatchPct), matchDays: Number(c.refMatchDays), brought: mine.length, @@ -167,6 +181,7 @@ function totals() { return { bounties: rows.filter(p => p.ref.kind === 'bounty').length, matches: rows.filter(p => p.ref.kind === 'match').length, + firstBonus: (() => { const b = rows.find(p => p.ref.kind === 'first-bonus'); return b ? { who: b.username || ('#' + b.memberId), pol: b.pol, at: b.at, status: b.status } : null; })(), pol: round4(rows.reduce((n, p) => n + p.pol, 0)), paidPol: round4(paid.reduce((n, p) => n + p.pol, 0)), referrers: new Set(rows.map(p => p.memberId)).size, diff --git a/public/app.html b/public/app.html index a817f82..bc2bbfa 100644 --- a/public/app.html +++ b/public/app.html @@ -5,7 +5,7 @@