diff --git a/lib/referrals.js b/lib/referrals.js index c10d755..12a3fdb 100644 --- a/lib/referrals.js +++ b/lib/referrals.js @@ -29,6 +29,8 @@ const DEFAULTS = { // existed before. That's why we're doing it."). Anyone whose account predates this moment was // already a member, by definition, and can never earn their sponsor a bounty or a match. refNewAfter: '2026-09-23T10:00:00Z', + refRequireOrigin: 1, // only pay for members PolHunter actually sent (joinedRef) + refNewDays: 30, // secondary: a newcomer only earns their sponsor a bounty in their first // month, so a dormant signup cannot trigger one a year later refCapPol: 20, // referral pool per Central day, separate from the find pool @@ -54,6 +56,7 @@ function seen(claims) { if (claims.wallet) cur.wallet = norm(claims.wallet); if (claims.sponsorRef) cur.sponsorRef = norm(claims.sponsorRef); if (claims.joinedAt) cur.joinedAt = Number(claims.joinedAt); + if (claims.joinedRef) cur.joinedRef = String(claims.joinedRef).toLowerCase(); cur.lastSeen = Date.now(); h[id] = cur; return h; @@ -130,6 +133,16 @@ function onFind(member, find) { const floor = Date.parse(String(c.refNewAfter || '')); if (!Number.isFinite(floor) || floor <= 0) return []; if (!me.joinedAt || Number(me.joinedAt) < floor) return []; + // AND PolHunter has to have actually sent them. The account being new is not enough: somebody who + // joined InstantAdPay off an email, a banner or another site and only later wandered over here was + // never a PolHunter acquisition, and paying a bounty for them defeats the point (Marty, + // 2026-09-24). The proof is InstantAdPay's first-touch source, handed over signed in the sign-in + // payload; a share link stamps it polhunter.com from the URL itself, so a stripped referrer does + // not lose a genuine referral. Fails closed: no origin on file pays nobody. + // only an explicit '0' turns this off: a typo or a stray value must leave the gate standing, + // the same way the floor does + if (String(c.refRequireOrigin) !== '0' + && !/polhunter/i.test(String(me.joinedRef || ''))) return []; const who = { forMemberId: id, forName: me.username || ('#' + id) }; // the bounty, once, on their first find, and only while they are genuinely a new member @@ -165,10 +178,13 @@ function state(member) { // the same floor onFind pays by, so the card can never promise an earning it will not make const floor = Date.parse(String(c.refNewAfter || '')); const gateOk = Number.isFinite(floor) && floor > 0; + const needOrigin = String(c.refRequireOrigin) !== '0'; const mine = broughtBy(id).map(x => { const theirFinds = store.read('payouts', []).filter(p => Number(p.memberId) === Number(x.memberId) && !p.ref && !p.prize && p.status !== 'failed'); const paid = bountyPaidFor(x.memberId); - const isNew = !!(gateOk && x.joinedAt && Number(x.joinedAt) >= floor); + const viaHunt = /polhunter/i.test(String(x.joinedRef || '')); + // qualifying means BOTH: new to InstantAdPay, and sent here by PolHunter + const isNew = !!(gateOk && x.joinedAt && Number(x.joinedAt) >= floor && (!needOrigin || viaHunt)); return { name: x.username || ('#' + x.memberId), // when they actually joined InstantAdPay, not when this directory first noticed them: @@ -177,8 +193,12 @@ function state(member) { finds: theirFinds.length, // an account that predates the mission was already a member: say so plainly rather than // show them as 'hunting' next to a bounty that will never arrive - isNew, - state: !isNew ? 'already a member before the mission, earns nothing' + isNew, viaHunt, + // say exactly which condition they miss, so nobody waits on a bounty that will not come + state: !isNew + ? (gateOk && x.joinedAt && Number(x.joinedAt) < floor + ? 'already a member before the mission, earns nothing' + : 'joined InstantAdPay some other way, not through your link') : paid ? 'earning' : theirFinds.length ? 'hunting' : 'signed up, no find yet', stillNew: isNew && (Date.now() - Number(x.joinedAt)) / 86400000 <= Number(c.refMatchDays), };