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 @@ Your board · PolHunter - +
@@ -47,6 +47,6 @@
- + diff --git a/public/app.js b/public/app.js index 57e1827..c37c6a6 100644 --- a/public/app.js +++ b/public/app.js @@ -60,6 +60,7 @@ + '

Send your link. When someone new opens PolHunter and finds their first code, you get ' + rf.bountyPol + ' POL. ' + 'After that you earn ' + rf.matchPct + '% on top of everything they find for their first ' + rf.matchDays + ' days. ' + 'It comes out of our pool, never theirs.

' + + (rf.firstBonusPol ? '

\u{1F947} ' + rf.firstBonusPol + ' POL bonus, still unclaimed. It goes to the first hunter who brings someone all the way through. Nobody has yet.

' : '') + '
' + '

' + today + ' brought today ' + '· ' + total + ' all time · ' + rf.earnedPol + ' POL earned

' diff --git a/public/style.css b/public/style.css index 9a81725..a573440 100644 --- a/public/style.css +++ b/public/style.css @@ -136,3 +136,4 @@ textarea{width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(-- } .ref-watch{margin-top:14px} .ref-video{width:100%;max-width:560px;border-radius:14px;display:block;background:#000} +.ref-bonus{margin-top:10px;padding:9px 12px;border:1px solid rgba(243,190,67,.38);border-radius:12px;background:rgba(243,190,67,.07);font-size:13.5px;color:var(--ink)} diff --git a/server.js b/server.js index c30c8e6..6614b91 100644 --- a/server.js +++ b/server.js @@ -74,6 +74,8 @@ async function notifyOther(kind, p) { if (kind === 'failed') return telegram('❌ PolHunter · drip to #' + p.memberId + ' failed: ' + p.error); // a bounty is the one payout that advertises the mission itself, so it goes to the room if (kind === 'referral') { const d = p.drip; return telegram('\u{1F91D} PolHunter · ' + (d.username ? '@' + d.username : '#' + d.memberId) + ' brought ' + d.ref.forName + ' in and they are hunting · ' + fmt(d.pol) + ' POL bounty on the way\nBring yours'); } + // Marty's launch bonus, paid once to whoever gets a referral all the way through first + if (kind === 'firstbonus') { const d = p.drip; return telegram('\u{1F947} First referral home! ' + (d.username ? '@' + d.username : '#' + d.memberId) + ' is the first to bring a hunter all the way through, so Marty’s ' + fmt(d.pol) + ' POL bonus is theirs on top of the bounty.\nThat one is claimed. The mission is not — bring yours.'); } if (kind === 'prize') { const medal = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; return telegram('\u{1F3C6} PolHunter weekly prizes for the week of ' + p.week + '\n' + p.winners.map(w => (medal[w.rank - 1] || '#' + w.rank) + ' ' + w.who + ' \u00b7 ' + w.finds + ' finds \u00b7 ' + fmt(w.prizePol) + ' POL').join('\n') + '\nLeaderboard'); } return false; } @@ -253,7 +255,7 @@ const server = http.createServer(async (req, res) => { // whoever brought this hunter gets paid for it: the bounty on their first find, a match while they are new let refDrips = []; try { refDrips = referrals.onFind(me, g.rec); } catch (e) { console.error('referral', e.message); } - for (const d of refDrips) if (d.ref.kind === 'bounty') notify('referral', { drip: d }).catch(() => {}); + for (const d of refDrips) if (d.ref.kind === 'bounty' || d.ref.kind === 'first-bonus') notify(d.ref.kind === 'bounty' ? 'referral' : 'firstbonus', { drip: d }).catch(() => {}); // achievements unlocked by this find: told to the board, posted to Telegram (gated) const unlocked = social.badgesFor(me.memberId).filter(b => !before.has(b.id)); for (const b of unlocked) notify('badge', { me, id: b.id }).catch(() => {});