Launch bonus: pay it automatically to the first referral home
Marty offered 5 POL to the first hunter who gets a referral all the way through. It now pays itself: the bonus is written alongside the very first bounty anyone earns, from the same faucet, and can never be written twice. It sits outside the daily referral ceiling on purpose, because it is one payment rather than a rate, and it announces itself to Telegram so the room knows the race is over. While it is unclaimed the board card says so, and the moment it is won the card stops advertising it. Set refFirstBonusPol to 0 to retire it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+17
-2
@@ -27,6 +27,9 @@ const DEFAULTS = {
|
|||||||
// happens to start hunting years after their sponsor signed them up
|
// happens to start hunting years after their sponsor signed them up
|
||||||
refCapPol: 20, // referral pool per Central day, separate from the find pool
|
refCapPol: 20, // referral pool per Central day, separate from the find pool
|
||||||
refMatchMinPol: 0.005,// do not write a transaction for dust
|
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', {})); }
|
function cfg() { return Object.assign({}, DEFAULTS, store.read('settings', {})); }
|
||||||
const round4 = n => Math.round(Number(n) * 10000) / 10000;
|
const round4 = n => Math.round(Number(n) * 10000) / 10000;
|
||||||
@@ -89,7 +92,8 @@ function write(referrer, pol, ref) {
|
|||||||
const rec = {
|
const rec = {
|
||||||
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
|
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,
|
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(),
|
pol: round4(pol), day: ctDay(), at: Date.now(),
|
||||||
status: 'due', tx: null, paidAt: null, error: null, ref,
|
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');
|
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)) {
|
if (finds.length <= 1 && ageDays <= Number(c.refNewDays) && !bountyPaidFor(id)) {
|
||||||
const pol = Number(c.refBountyPol);
|
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
|
// the match, on every find inside the window, paid on top and never deducted from the newcomer
|
||||||
if (ageDays <= Number(c.refMatchDays)) {
|
if (ageDays <= Number(c.refMatchDays)) {
|
||||||
@@ -150,6 +163,7 @@ function state(member) {
|
|||||||
return {
|
return {
|
||||||
on: String(c.refEnabled) === '1',
|
on: String(c.refEnabled) === '1',
|
||||||
bountyPol: Number(c.refBountyPol),
|
bountyPol: Number(c.refBountyPol),
|
||||||
|
firstBonusPol: refRows().some(p => p.ref.kind === 'first-bonus') ? 0 : Number(c.refFirstBonusPol) || 0,
|
||||||
matchPct: Number(c.refMatchPct),
|
matchPct: Number(c.refMatchPct),
|
||||||
matchDays: Number(c.refMatchDays),
|
matchDays: Number(c.refMatchDays),
|
||||||
brought: mine.length,
|
brought: mine.length,
|
||||||
@@ -167,6 +181,7 @@ function totals() {
|
|||||||
return {
|
return {
|
||||||
bounties: rows.filter(p => p.ref.kind === 'bounty').length,
|
bounties: rows.filter(p => p.ref.kind === 'bounty').length,
|
||||||
matches: rows.filter(p => p.ref.kind === 'match').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)),
|
pol: round4(rows.reduce((n, p) => n + p.pol, 0)),
|
||||||
paidPol: round4(paid.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,
|
referrers: new Set(rows.map(p => p.memberId)).size,
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Your board · PolHunter</title>
|
<title>Your board · PolHunter</title>
|
||||||
<meta name="robots" content="noindex">
|
<meta name="robots" content="noindex">
|
||||||
<link rel="stylesheet" href="/style.css?v=15">
|
<link rel="stylesheet" href="/style.css?v=16">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
@@ -47,6 +47,6 @@
|
|||||||
<footer class="foot">Rewards are for completed missions, not income. The daily pool is limited; when it is spent, claims close until midnight Central. Cryptocurrency involves risk of loss.</footer>
|
<footer class="foot">Rewards are for completed missions, not income. The daily pool is limited; when it is spent, claims close until midnight Central. Cryptocurrency involves risk of loss.</footer>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal" id="shareModal" hidden><div class="modal-card"><button class="modal-x" id="shareClose" type="button" aria-label="Close">×</button><div id="shareBody"></div></div></div>
|
<div class="modal" id="shareModal" hidden><div class="modal-card"><button class="modal-x" id="shareClose" type="button" aria-label="Close">×</button><div id="shareBody"></div></div></div>
|
||||||
<script src="/app.js?v=15"></script>
|
<script src="/app.js?v=16"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
+ '<p>Send your link. When someone new opens PolHunter and finds their first code, you get <b>' + rf.bountyPol + ' POL</b>. '
|
+ '<p>Send your link. When someone new opens PolHunter and finds their first code, you get <b>' + rf.bountyPol + ' POL</b>. '
|
||||||
+ 'After that you earn <b>' + rf.matchPct + '%</b> on top of everything they find for their first ' + rf.matchDays + ' days. '
|
+ 'After that you earn <b>' + rf.matchPct + '%</b> on top of everything they find for their first ' + rf.matchDays + ' days. '
|
||||||
+ 'It comes out of our pool, never theirs.</p>'
|
+ 'It comes out of our pool, never theirs.</p>'
|
||||||
|
+ (rf.firstBonusPol ? '<p class="ref-bonus">\u{1F947} <b>' + rf.firstBonusPol + ' POL bonus, still unclaimed.</b> It goes to the first hunter who brings someone all the way through. Nobody has yet.</p>' : '')
|
||||||
+ '<div class="codebox"><input id="refLink" readonly value="' + esc(link) + '"><button class="btn pol" id="refCopy">Copy</button></div>'
|
+ '<div class="codebox"><input id="refLink" readonly value="' + esc(link) + '"><button class="btn pol" id="refCopy">Copy</button></div>'
|
||||||
+ '<p style="margin-top:10px"><span class="range">' + today + ' brought today</span> '
|
+ '<p style="margin-top:10px"><span class="range">' + today + ' brought today</span> '
|
||||||
+ '<span style="color:var(--dim);font-size:12px">· ' + total + ' all time · ' + rf.earnedPol + ' POL earned</span></p>'
|
+ '<span style="color:var(--dim);font-size:12px">· ' + total + ' all time · ' + rf.earnedPol + ' POL earned</span></p>'
|
||||||
|
|||||||
@@ -136,3 +136,4 @@ textarea{width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--
|
|||||||
}
|
}
|
||||||
.ref-watch{margin-top:14px}
|
.ref-watch{margin-top:14px}
|
||||||
.ref-video{width:100%;max-width:560px;border-radius:14px;display:block;background:#000}
|
.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)}
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ async function notifyOther(kind, p) {
|
|||||||
if (kind === 'failed') return telegram('❌ <b>PolHunter</b> · drip to #' + p.memberId + ' failed: ' + p.error);
|
if (kind === 'failed') return telegram('❌ <b>PolHunter</b> · drip to #' + p.memberId + ' failed: ' + p.error);
|
||||||
// a bounty is the one payout that advertises the mission itself, so it goes to the room
|
// 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} <b>PolHunter</b> · ' + (d.username ? '@' + d.username : '#' + d.memberId) + ' brought ' + d.ref.forName + ' in and they are hunting · <b>' + fmt(d.pol) + ' POL</b> bounty on the way\n<a href="' + SITE + '">Bring yours</a>'); }
|
if (kind === 'referral') { const d = p.drip; return telegram('\u{1F91D} <b>PolHunter</b> · ' + (d.username ? '@' + d.username : '#' + d.memberId) + ' brought ' + d.ref.forName + ' in and they are hunting · <b>' + fmt(d.pol) + ' POL</b> bounty on the way\n<a href="' + SITE + '">Bring yours</a>'); }
|
||||||
|
// 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} <b>First referral home!</b> ' + (d.username ? '@' + d.username : '#' + d.memberId) + ' is the first to bring a hunter all the way through, so Marty’s <b>' + fmt(d.pol) + ' POL</b> bonus is theirs on top of the bounty.\nThat one is claimed. The mission is not — <a href="' + SITE + '">bring yours</a>.'); }
|
||||||
if (kind === 'prize') { const medal = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; return telegram('\u{1F3C6} <b>PolHunter weekly prizes</b> for the week of ' + p.week + '\n' + p.winners.map(w => (medal[w.rank - 1] || '#' + w.rank) + ' ' + w.who + ' \u00b7 ' + w.finds + ' finds \u00b7 <b>' + fmt(w.prizePol) + ' POL</b>').join('\n') + '\n<a href="' + SITE + '/leaders">Leaderboard</a>'); }
|
if (kind === 'prize') { const medal = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; return telegram('\u{1F3C6} <b>PolHunter weekly prizes</b> for the week of ' + p.week + '\n' + p.winners.map(w => (medal[w.rank - 1] || '#' + w.rank) + ' ' + w.who + ' \u00b7 ' + w.finds + ' finds \u00b7 <b>' + fmt(w.prizePol) + ' POL</b>').join('\n') + '\n<a href="' + SITE + '/leaders">Leaderboard</a>'); }
|
||||||
return false;
|
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
|
// whoever brought this hunter gets paid for it: the bounty on their first find, a match while they are new
|
||||||
let refDrips = [];
|
let refDrips = [];
|
||||||
try { refDrips = referrals.onFind(me, g.rec); } catch (e) { console.error('referral', e.message); }
|
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)
|
// 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));
|
const unlocked = social.badgesFor(me.memberId).filter(b => !before.has(b.id));
|
||||||
for (const b of unlocked) notify('badge', { me, id: b.id }).catch(() => {});
|
for (const b of unlocked) notify('badge', { me, id: b.id }).catch(() => {});
|
||||||
|
|||||||
Reference in New Issue
Block a user