Sponsor attribution: last touch (30-day cookie), locked at account creation; 'Joining under' line at the code step; chatbot answer
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,8 @@ const CANNED = [
|
|||||||
a: 'Your wall (instantadpay.com/wall/yourname) shows three ads. Position 1 is always your line banner. Positions 2 and 3 show your upline (or InstantAdPay) until you earn them: 2 qualifying buyers ($20 or more) make position 2 yours, 5 make position 3 yours, so a fully qualified member owns the whole page with their own links and offers. Set them in Profile > Your wall; an unlocked slot you leave empty keeps showing your upline until you fill it.' },
|
a: 'Your wall (instantadpay.com/wall/yourname) shows three ads. Position 1 is always your line banner. Positions 2 and 3 show your upline (or InstantAdPay) until you earn them: 2 qualifying buyers ($20 or more) make position 2 yours, 5 make position 3 yours, so a fully qualified member owns the whole page with their own links and offers. Set them in Profile > Your wall; an unlocked slot you leave empty keeps showing your upline until you fill it.' },
|
||||||
{ re: /(solo ad|inbox ad|inbox)/i,
|
{ re: /(solo ad|inbox ad|inbox)/i,
|
||||||
a: 'Solo ads are full-message ads delivered straight into member inboxes on-site. Compose one under Campaigns (pick "Solo ad"): subject line, a rich-text message with a real editor (bold, headings, lists, links), an attached image or video if you want one, and a call-to-action button with your own label. You pay 5 credits per guaranteed delivery, 10 deliveries minimum. On the reading side, your Inbox section collects solos from other members — give one a real read (10 seconds on the open message) and claim 2 credits, up to 5 rewarded reads a day. You never receive your own solo.' },
|
a: 'Solo ads are full-message ads delivered straight into member inboxes on-site. Compose one under Campaigns (pick "Solo ad"): subject line, a rich-text message with a real editor (bold, headings, lists, links), an attached image or video if you want one, and a call-to-action button with your own label. You pay 5 credits per guaranteed delivery, 10 deliveries minimum. On the reading side, your Inbox section collects solos from other members — give one a real read (10 seconds on the open message) and claim 2 credits, up to 5 rewarded reads a day. You never receive your own solo.' },
|
||||||
|
{ re: /(wrong sponsor|who is my sponsor|which sponsor|sponsor (cookie|link|credit)|clicked (someone|another|two).*link|last (link|click)|first (link|click|touch)|change (my )?sponsor)/i,
|
||||||
|
a: 'The invite link you opened most recently sets your sponsor, and the join page shows it: "Personal invitation from @name". It locks the moment you create your account, so opening someone else\'s link afterwards changes nothing, and your first purchase binds you to that same sponsor on the contract. If you have not joined yet and the page shows the wrong name, open the right person\'s link and it updates. If you already joined under the wrong sponsor by mistake, message support before your first purchase and the admin can move you.' },
|
||||||
{ re: /(chat|message (my )?(sponsor|upline|team|downline)|talk to (my )?sponsor|contact (my )?sponsor|ask (my )?sponsor|reach (my )?sponsor)/i,
|
{ re: /(chat|message (my )?(sponsor|upline|team|downline)|talk to (my )?sponsor|contact (my )?sponsor|ask (my )?sponsor|reach (my )?sponsor)/i,
|
||||||
a: 'Yes, Members has a two-way Sponsor Chat. You can message your direct sponsor for help (from Overview or the chat button), and sponsors can message anyone in their line. If they are online you chat live; if not, your message waits in their on-site inbox and they get an email. Sponsors can set themselves unavailable in Profile (you can still leave a note, they just reply later) and mute a member if needed. It is separate from the once-a-day team broadcast.' },
|
a: 'Yes, Members has a two-way Sponsor Chat. You can message your direct sponsor for help (from Overview or the chat button), and sponsors can message anyone in their line. If they are online you chat live; if not, your message waits in their on-site inbox and they get an email. Sponsors can set themselves unavailable in Profile (you can still leave a note, they just reply later) and mute a member if needed. It is separate from the once-a-day team broadcast.' },
|
||||||
{ re: /((view|watch|see).{0,12}ads?|earn.{0,12}credits?|daily (set|ads|views))/i,
|
{ re: /((view|watch|see).{0,12}ads?|earn.{0,12}credits?|daily (set|ads|views))/i,
|
||||||
@@ -63,6 +65,7 @@ function systemPrompt() {
|
|||||||
|
|
||||||
FACTS:
|
FACTS:
|
||||||
- Free to join with email only (6-digit code sign-in, no passwords). Wallet appears only at purchase or payout activation. Every new member gets a small welcome batch of ad credits — unlocked by the WELCOME TOUR on first sign-in: they visit their upline's line-banner sites (up to 3, 10 seconds each — the same 3 levels the contract pays), then claim the credits. Members with no upline banners get the credits instantly.
|
- Free to join with email only (6-digit code sign-in, no passwords). Wallet appears only at purchase or payout activation. Every new member gets a small welcome batch of ad credits — unlocked by the WELCOME TOUR on first sign-in: they visit their upline's line-banner sites (up to 3, 10 seconds each — the same 3 levels the contract pays), then claim the credits. Members with no upline banners get the credits instantly.
|
||||||
|
- SPONSOR ATTRIBUTION: last touch. The invite link opened most recently sets the sponsor (30-day cookie), shown on the join page and again at the code step ("Joining under @name"). It locks when the account is created; the contract binds the buyer to that sponsor at their first purchase. Wrong sponsor before joining: open the right link. Already joined wrong: admin can move the account before the first purchase.
|
||||||
- YOUR LINKS (top of Members > Promo tools): the plain invite link plus five angle links (?v=instant paid-in-seconds, ?v=adspend you-buy-ads-anyway, ?v=free costs-nothing-to-try, ?v=ledger no-back-office, ?v=two two-buyers-open-level-two), each with a who-it-is-for note, Copy and Share. Same credit either way; only the first page differs. Link stats (Coaching pane) shows views/joins/buyers per angle.
|
- YOUR LINKS (top of Members > Promo tools): the plain invite link plus five angle links (?v=instant paid-in-seconds, ?v=adspend you-buy-ads-anyway, ?v=free costs-nothing-to-try, ?v=ledger no-back-office, ?v=two two-buyers-open-level-two), each with a who-it-is-for note, Copy and Share. Same credit either way; only the first page differs. Link stats (Coaching pane) shows views/joins/buyers per angle.
|
||||||
- PROMO TOOLS (Members > Promo tools, pill menu): Social posts for X/Facebook/LinkedIn/Telegram-WhatsApp with post/share buttons; Text a friend (5 SMS-sized messages with Text it / WhatsApp / Telegram / Copy); Email swipes (short, standard, long, follow-up); Banners in every standard ad size plus 1080x1080, 1080x1920 and 1280x720 (download or copy URL); the member's Banner wall link; an Objection handling bank (truth + ready-to-send reply); a Videos tab (hook videos in production). Every piece carries the member's invite link; angle links add ?v=instant|adspend|free|ledger. Members who want copy in their own voice can use mybrandedvoice.com.
|
- PROMO TOOLS (Members > Promo tools, pill menu): Social posts for X/Facebook/LinkedIn/Telegram-WhatsApp with post/share buttons; Text a friend (5 SMS-sized messages with Text it / WhatsApp / Telegram / Copy); Email swipes (short, standard, long, follow-up); Banners in every standard ad size plus 1080x1080, 1080x1920 and 1280x720 (download or copy URL); the member's Banner wall link; an Objection handling bank (truth + ready-to-send reply); a Videos tab (hook videos in production). Every piece carries the member's invite link; angle links add ?v=instant|adspend|free|ledger. Members who want copy in their own voice can use mybrandedvoice.com.
|
||||||
- INVITE PAGES: a member's link instantadpay.com/join/<username> opens a lead-capture page (email first, wallet later); add ?v=instant|adspend|free|ledger|two for an angle-matched headline. New free members get a short getting-started email series over the first week (unsubscribe link in every email; the admin edits the sequence in /admin > Settings).
|
- INVITE PAGES: a member's link instantadpay.com/join/<username> opens a lead-capture page (email first, wallet later); add ?v=instant|adspend|free|ledger|two for an angle-matched headline. New free members get a short getting-started email series over the first week (unsubscribe link in every email; the admin edits the sequence in /admin > Settings).
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
const r = await api('/api/auth/email/start', { email: $('jnEmail').value });
|
const r = await api('/api/auth/email/start', { email: $('jnEmail').value });
|
||||||
$('jnCodeRow').hidden = false; $('jnVerify').hidden = false; $('jnSend').hidden = true; $('jnResend').hidden = false;
|
$('jnCodeRow').hidden = false; $('jnVerify').hidden = false; $('jnSend').hidden = true; $('jnResend').hidden = false;
|
||||||
if (r.devCode) $('jnCode').value = r.devCode;
|
if (r.devCode) $('jnCode').value = r.devCode;
|
||||||
|
// the last thing they see before the account is created: who they are joining under
|
||||||
|
if (sponsorName) { $('jnUnder').textContent = 'Joining under ' + sponsorName + '. Not who invited you? Open their invite link first, then come back for the code.'; $('jnUnder').hidden = false; }
|
||||||
IAP.status(r.sent ? 'Code sent. Check your inbox (and spam, the first time).' : 'Dev mode: code filled in.', 'ok');
|
IAP.status(r.sent ? 'Code sent. Check your inbox (and spam, the first time).' : 'Dev mode: code filled in.', 'ok');
|
||||||
$('jnCode').focus();
|
$('jnCode').focus();
|
||||||
});
|
});
|
||||||
@@ -45,9 +47,11 @@
|
|||||||
$('jnEmail').addEventListener('keydown', e => { if (e.key === 'Enter') ($('jnVerify').hidden ? $('jnSend') : $('jnVerify')).click(); });
|
$('jnEmail').addEventListener('keydown', e => { if (e.key === 'Enter') ($('jnVerify').hidden ? $('jnSend') : $('jnVerify')).click(); });
|
||||||
$('jnCode').addEventListener('keydown', e => { if (e.key === 'Enter') $('jnVerify').click(); });
|
$('jnCode').addEventListener('keydown', e => { if (e.key === 'Enter') $('jnVerify').click(); });
|
||||||
|
|
||||||
// sponsor line
|
// sponsor line (the link opened most recently sets the sponsor; it locks at account creation)
|
||||||
|
let sponsorName = '';
|
||||||
fetch('/api/sponsor').then(r => r.json()).then(sp => {
|
fetch('/api/sponsor').then(r => r.json()).then(sp => {
|
||||||
if (sp && sp.invited && sp.name) {
|
if (sp && sp.invited && sp.name) {
|
||||||
|
sponsorName = sp.name;
|
||||||
$('jnSponName').textContent = sp.name;
|
$('jnSponName').textContent = sp.name;
|
||||||
if (sp.avatarUrl) { $('jnSponImg').src = sp.avatarUrl; $('jnSponImg').hidden = false; }
|
if (sp.avatarUrl) { $('jnSponImg').src = sp.avatarUrl; $('jnSponImg').hidden = false; }
|
||||||
$('jnSpon').hidden = false;
|
$('jnSpon').hidden = false;
|
||||||
|
|||||||
+2
-1
@@ -88,6 +88,7 @@
|
|||||||
<p class="muted small" style="margin:0 0 12px">Type your email and we send a 6-digit code. No password, no wallet needed today.</p>
|
<p class="muted small" style="margin:0 0 12px">Type your email and we send a 6-digit code. No password, no wallet needed today.</p>
|
||||||
<p><input id="jnEmail" type="email" placeholder="Your email" autocomplete="email"></p>
|
<p><input id="jnEmail" type="email" placeholder="Your email" autocomplete="email"></p>
|
||||||
<p id="jnCodeRow" hidden><input id="jnCode" inputmode="numeric" placeholder="6-digit code from your inbox"></p>
|
<p id="jnCodeRow" hidden><input id="jnCode" inputmode="numeric" placeholder="6-digit code from your inbox"></p>
|
||||||
|
<p id="jnUnder" class="small muted" hidden style="margin:-4px 0 12px"></p>
|
||||||
<label class="small muted" style="display:flex;gap:8px;align-items:flex-start;margin:0 0 12px;cursor:pointer">
|
<label class="small muted" style="display:flex;gap:8px;align-items:flex-start;margin:0 0 12px;cursor:pointer">
|
||||||
<input type="checkbox" id="jnNews" checked style="margin-top:3px;width:auto">
|
<input type="checkbox" id="jnNews" checked style="margin-top:3px;width:auto">
|
||||||
<span>Send me the getting-started emails (a few short ones over the first week). Unsubscribe any time.</span>
|
<span>Send me the getting-started emails (a few short ones over the first week). Unsubscribe any time.</span>
|
||||||
@@ -128,6 +129,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260910c"></script>
|
<script src="/assets/common.js?v=20260910c"></script>
|
||||||
<script src="/assets/join.js?v=20260910a"></script>
|
<script src="/assets/join.js?v=20260910b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -489,9 +489,12 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const u = new URL(req.url, 'http://x');
|
const u = new URL(req.url, 'http://x');
|
||||||
const p = u.pathname;
|
const p = u.pathname;
|
||||||
|
|
||||||
// -- join links: /join/<memberId or share code> — first-touch cookie.
|
// -- join links: /join/<memberId or share code> — LAST-touch cookie (Marty,
|
||||||
// Codes resolve LATE (at buy time) to whatever chain id the referrer
|
// 2026-09-10): the link a visitor opened most recently is the sponsor shown
|
||||||
// has by then, so free members refer from day one.
|
// and used, and it locks the moment the account is created (accounts.ensure
|
||||||
|
// never changes an existing account's sponsor; the contract binds the buyer at
|
||||||
|
// their first purchase). Codes resolve LATE (at buy time) to whatever chain id
|
||||||
|
// the referrer has by then, so free members refer from day one.
|
||||||
let m = /^\/join\/([A-Za-z0-9_]{1,20})$/.exec(p);
|
let m = /^\/join\/([A-Za-z0-9_]{1,20})$/.exec(p);
|
||||||
if (m && (req.method === 'GET' || req.method === 'HEAD')) {
|
if (m && (req.method === 'GET' || req.method === 'HEAD')) {
|
||||||
// lead-capture page: email first, wallet later. ?v=<angle> picks the hook
|
// lead-capture page: email first, wallet later. ?v=<angle> picks the hook
|
||||||
@@ -501,9 +504,9 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const angle = String(u.searchParams.get('v') || '').toLowerCase();
|
const angle = String(u.searchParams.get('v') || '').toLowerCase();
|
||||||
const ang = JOIN_ANGLES[angle] || null;
|
const ang = JOIN_ANGLES[angle] || null;
|
||||||
if (req.method === 'GET') coach.recordView(tok, ang ? angle : '', req.headers.referer); // link stats per angle + source
|
if (req.method === 'GET') coach.recordView(tok, ang ? angle : '', req.headers.referer); // link stats per angle + source
|
||||||
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
|
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; // 30 days: whoever brings them back gets the credit
|
||||||
const set = [];
|
const set = [];
|
||||||
if (!cookies['iap.sponsor']) set.push('iap.sponsor=' + tok + cookieTail);
|
set.push('iap.sponsor=' + tok + cookieTail); // last touch wins
|
||||||
if (ang) set.push('iap.angle=' + angle + cookieTail);
|
if (ang) set.push('iap.angle=' + angle + cookieTail);
|
||||||
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
|
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
|
||||||
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
|
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
|
||||||
@@ -628,7 +631,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
// out only at purchase / payout-activation time and gets linked then)
|
// out only at purchase / payout-activation time and gets linked then)
|
||||||
if (p === '/api/signup' && req.method === 'POST') {
|
if (p === '/api/signup' && req.method === 'POST') {
|
||||||
const b = await readBody(req);
|
const b = await readBody(req);
|
||||||
const ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution
|
const ref = parseCookies(req)['iap.sponsor'] || ''; // last-touch attribution, locked at account creation
|
||||||
const r = await accounts.signup(b.email, b.password, ref);
|
const r = await accounts.signup(b.email, b.password, ref);
|
||||||
if (r.error) return json(res, 400, r);
|
if (r.error) return json(res, 400, r);
|
||||||
// sponsor is notified once the new member picks a username (onboarding),
|
// sponsor is notified once the new member picks a username (onboarding),
|
||||||
|
|||||||
Reference in New Issue
Block a user