From 9825e7a4e046c23a74e86f02a762172655fce98e Mon Sep 17 00:00:00 2001 From: martbost Date: Thu, 24 Sep 2026 13:11:27 -0500 Subject: [PATCH] Referrals pay only for people PolHunter actually sent The floor already blocked anyone who existed in InstantAdPay before the mission. It did NOT ask who brought them. A member who joined off an email, a banner or another site and only later wandered over to PolHunter still earned their sponsor a bounty, which is the case Marty called out: "not just members that come on board IAP and work their way over to POL Hunter." The proof is InstantAdPay's own first-touch source, handed across signed in the sign-in payload. A PolHunter share link stamps it polhunter.com from the URL itself, so a stripped referrer (in-app browser, a link pasted into chat) does not lose a genuine referral. Fails closed, and only an explicit '0' can disable it: no origin on file pays nobody, and a typo in the setting leaves the gate standing. The board says the same thing the payout code does. Somebody who joined another way now reads "joined InstantAdPay some other way, not through your link" instead of sitting there looking like a bounty that never arrives. Of the one hunter currently past the floor, linkmohd arrived direct, so under the live rules they would have paid opgnetwerk a bounty PolHunter never earned. That stops now. Co-Authored-By: Claude Opus 5 --- lib/referrals.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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), };