From e2f88dad42325b987c8783ef78cc071d1671bb78 Mon Sep 17 00:00:00 2001 From: martbost Date: Mon, 21 Sep 2026 05:54:55 -0500 Subject: [PATCH] Viral links: any public page + ?ref= sets the same last-touch sponsor cookie as /join, counts under a 'page' hook in link stats, then redirects to the clean URL; Promo tools > Viral links builder (pages list + paste any address, copy + share); blog posts show a signed-in member's own link; chatbot answer; QA walk proves redirect + cookie Co-Authored-By: Claude Fable 5.1 --- chatbot.js | 2 ++ coach.js | 2 +- public/assets/blog-page.js | 17 +++++++++++++++++ public/assets/my.js | 4 ++-- public/assets/promo.js | 33 +++++++++++++++++++++++++++++++++ public/my.html | 14 +++++++++++++- qa/walk.mjs | 16 ++++++++++++++++ server.js | 30 +++++++++++++++++++++++++++++- 8 files changed, 113 insertions(+), 5 deletions(-) diff --git a/chatbot.js b/chatbot.js index f4404c8..a35aa3d 100644 --- a/chatbot.js +++ b/chatbot.js @@ -22,6 +22,8 @@ function enabled() { return !!key(); } // ---- canned answers: instant, free, always in voice ---- const KNOWLEDGE_DATE = '2026-09-18'; // bump whenever the canned answers / prompt are brought up to date const CANNED = [ + { re: /(viral link|any page.{0,30}(link|referr)|blog.{0,40}(referral|my link|credit)|(referral|invite|my) link.{0,30}(blog|article|any page)|\?ref=|ref=)/i, + a: 'Any page on instantadpay.com becomes your referral link when you add ?ref= and your username to the end, for example instantadpay.com/blog/what-is-a-feeder-program?ref=yourname. Whoever opens it is tagged to you for 30 days, exactly like your invite link, so if they join from any page on the site the credit is yours. Promo tools > Viral links builds these links for you, and every blog article shows your link when you are signed in.' }, { re: /(five dollar friday|5 dollar friday|\$5 friday|friday (bonus|promo|deal|special)|what happens on friday)/i, a: 'Five Dollar Friday: every Friday (Central time), any ad package from $5 up earns +20% bonus credits, added the moment the purchase settles and labelled in your credit log. The first one is Friday, September 25. Every pack pays its sponsor line in the same on-chain transaction, so when the whole network buys on the same day every line gets paid at once. Five Fridays in a row earn the Five Fridays badge. Buy under My account > Buy packages.' }, // 2026-09-18: sessions were shortened to 4h on purpose. Members WILL ask why they keep being diff --git a/coach.js b/coach.js index 9395dab..7f8b094 100644 --- a/coach.js +++ b/coach.js @@ -189,7 +189,7 @@ async function linkStats(email) { const now = Date.now(); const views = await impl().views(tokens, 0); const joined = await accounts.listByReferrer(tokens); - const ANG = ['', 'instant', 'adspend', 'free', 'ledger', 'two']; + const ANG = ['', 'instant', 'adspend', 'free', 'ledger', 'two', 'page']; // page = a viral link (any page + ?ref=) const rows = {}; for (const k of ANG) rows[k] = { angle: k || 'plain', views30: 0, views: 0, joins: 0, buyers: 0 }; const src = {}; diff --git a/public/assets/blog-page.js b/public/assets/blog-page.js index 711fc16..7620de6 100644 --- a/public/assets/blog-page.js +++ b/public/assets/blog-page.js @@ -1,2 +1,19 @@ // public blog pages: the shared nav (with wallet status) and footer, nothing else (function () { try { IAP.renderNav(location.pathname.indexOf('/leaderboard') === 0 ? 'leaderboard' : 'blog'); } catch (e) {} })(); +// Viral links (2026-09-21): a signed-in member sees this article's address with their own ?ref= on the +// end, and the share buttons carry it too. Whoever opens it is tagged to them like an invite link. +(async function () { + try { + const share = document.querySelector('p.share'); if (!share) return; + const me = await (await fetch('/api/me')).json(); + const tok = me && me.signedIn && (me.username || me.refCode); if (!tok) return; + const link = 'https://instantadpay.com' + location.pathname + '?ref=' + encodeURIComponent(tok); + share.querySelectorAll('a').forEach(a => { a.href = a.href.replace(encodeURIComponent('https://instantadpay.com' + location.pathname), encodeURIComponent(link)); }); + const bar = document.createElement('div'); + bar.id = 'viralBar'; + bar.style.cssText = 'margin-top:18px;padding:12px 14px;border:1px solid var(--line);border-radius:12px;background:var(--card, rgba(255,255,255,.03));display:flex;gap:10px;align-items:center;flex-wrap:wrap'; + bar.innerHTML = '
Your link for this article
' + link.replace(/[&<>]/g, '') + '
Share this and anyone who joins from it is yours, same as your invite link.
'; + bar.querySelector('button').onclick = async () => { try { await navigator.clipboard.writeText(link); IAP.status('Link copied.', 'ok'); } catch (e) { IAP.status('Copy failed.', 'bad'); } }; + share.parentNode.insertBefore(bar, share); + } catch (e) {} +})(); diff --git a/public/assets/my.js b/public/assets/my.js index 50a9c11..eaa5b6b 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -1523,7 +1523,7 @@ const rows = (r.angles || []).filter(a => a.views || a.joins || a.buyers); if (!rows.length) { t.innerHTML = 'No views yet. Share your link and the numbers start here.'; return; } t.innerHTML = 'HookViews (30d)Views (all)JoinedQualifying buyers' - + rows.map(a => '' + esc(a.angle === 'plain' ? 'plain link' : '?v=' + a.angle) + '' + a.views30 + '' + a.views + '' + a.joins + '' + a.buyers + '').join(''); + + rows.map(a => '' + esc(a.angle === 'plain' ? 'plain link' : a.angle === 'page' ? 'any page (viral link)' : '?v=' + a.angle) + '' + a.views30 + '' + a.views + '' + a.joins + '' + a.buyers + '').join(''); const st = $('linkSrcTable'); if (st) { const src = (r.sources || []).filter(x => x.views || x.joins); @@ -1887,7 +1887,7 @@ if ($('tkGo')) { $('tkGo').addEventListener('click', tkGenerate); $('tkAgain').addEventListener('click', tkGenerate); $('tkCopy').addEventListener('click', async () => { try { await navigator.clipboard.writeText($('tkText').value); IAP.status('Copied.', 'ok'); } catch (e) { $('tkText').select(); } }); $('tkSendBc').addEventListener('click', tkSendBroadcast); } // promo tools: pill menu switches between posts / swipes / banners / wall / videos function setPromoSub(name) { - const ids = ['toolkit', 'posts', 'text', 'swipe', 'banners', 'wall', 'objections', 'videos']; + const ids = ['toolkit', 'posts', 'text', 'swipe', 'banners', 'wall', 'objections', 'videos', 'viral']; if (name === 'toolkit') loadToolkit(); if (!ids.includes(name)) name = 'posts'; ids.forEach(id => { const el = $('promo-' + id); if (el) el.hidden = id !== name; }); diff --git a/public/assets/promo.js b/public/assets/promo.js index e9663f7..f02744a 100644 --- a/public/assets/promo.js +++ b/public/assets/promo.js @@ -198,6 +198,39 @@ window.IAPPromo = (function () { vw.appendChild(card); } } + // viral link builder: any page on the site + ?ref= (same cookie as the invite link) + const vp = $('viralPage'); + if (vp && token && vp.dataset.filled !== token) { + vp.dataset.filled = token; + const origin = 'https://instantadpay.com', enc = encodeURIComponent; + const build = raw => { + let s = String(raw || '').trim(); if (!s) return ''; + if (!/^https?:\/\//i.test(s)) s = origin + (s.charAt(0) === '/' ? '' : '/') + s; + let x; try { x = new URL(s); } catch (e) { return ''; } + if (!/(^|\.)instantadpay\.com$/i.test(x.hostname) || /^\/(join|from|api|admin)(\/|$)/.test(x.pathname)) return ''; + x.searchParams.delete('ref'); x.searchParams.set('ref', token); + return origin + x.pathname + x.search; + }; + const render = () => { + const typed = $('viralUrl').value.trim(); + const l = build(typed || vp.value); + $('viralLink').textContent = l || (typed ? 'That is not a page on instantadpay.com.' : '…'); + const sh = $('viralShare'); sh.innerHTML = ''; + if (!l) return; + const title = (!typed && vp.selectedOptions[0]) ? vp.selectedOptions[0].textContent.replace(/^Article: /, '') : 'InstantAdPay'; + [linkBtn('https://x.com/intent/tweet?url=' + enc(l) + '&text=' + enc(title), 'X'), + linkBtn('https://www.facebook.com/sharer/sharer.php?u=' + enc(l), 'Facebook'), + linkBtn('https://t.me/share/url?url=' + enc(l) + '&text=' + enc(title), 'Telegram'), + linkBtn('https://wa.me/?text=' + enc(title + ' ' + l), 'WhatsApp'), + linkBtn('https://www.linkedin.com/sharing/share-offsite/?url=' + enc(l), 'LinkedIn'), + linkBtn('mailto:?subject=' + enc(title) + '&body=' + enc(l), 'Email')].forEach(b => sh.appendChild(b)); + if (navigator.share) { const b = document.createElement('button'); b.className = 'btn small sec'; b.type = 'button'; b.textContent = 'Share'; b.onclick = async () => { try { await navigator.share({ title, url: l }); } catch (e) {} }; sh.appendChild(b); } + }; + fetch('/api/pages').then(r => r.json()).then(r => { vp.innerHTML = (r.pages || []).map(pg => '').join(''); render(); }).catch(() => { vp.innerHTML = ''; render(); }); + vp.onchange = () => { $('viralUrl').value = ''; render(); }; + $('viralUrl').oninput = render; + $('viralCopy').onclick = async () => { const l = $('viralLink').textContent; if (!/^https:/.test(l)) return; try { await navigator.clipboard.writeText(l); status('Link copied.', 'ok'); } catch (e) { status('Copy failed.', 'bad'); } }; + } // objections const ob = $('promoObjections'); if (ob && ob.dataset.filled !== link) { diff --git a/public/my.html b/public/my.html index eddb53c..8ac0393 100644 --- a/public/my.html +++ b/public/my.html @@ -744,6 +744,18 @@ + + + - + diff --git a/qa/walk.mjs b/qa/walk.mjs index 76f750d..4199324 100644 --- a/qa/walk.mjs +++ b/qa/walk.mjs @@ -114,6 +114,22 @@ if (MODE === 'member' || MODE === 'all') { } await page.click('.bo-menu [data-pane="promo"]'); await page.waitForTimeout(800); if (!(await page.$$('#promoPosts .promo-block')).length) note('bug', L + 'promo', 'no post cards rendered'); + // viral links: the builder renders a ?ref= link; any page + ?ref= redirects clean and sets the sponsor cookie; an unknown ref sets nothing + await page.click('.promo-pills [data-promo="viral"]'); await page.waitForTimeout(1200); + const vl = await page.$eval('#viralLink', e => e.textContent).catch(() => ''); + if (!/^https:\/\/instantadpay\.com\/.*[?&]ref=[a-z0-9_]+$/i.test(vl)) note('bug', L + 'promo/viral', 'builder link not rendered: ' + vl); + const me = await page.evaluate(() => fetch('/api/me').then(r => r.json())); + const tok = me.username || me.refCode; + const rr = await page.request.get(LOCAL + '/blog?ref=' + tok + '&x=1', { maxRedirects: 0 }); + const sc = (rr.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; '); + if (rr.status() !== 302 || rr.headers()['location'] !== '/blog?x=1') note('bug', L + 'viral/redirect', 'expected 302 to /blog?x=1, got ' + rr.status() + ' ' + rr.headers()['location']); + if (!new RegExp('iap\\.sponsor=' + tok + ';').test(sc) || !/iap\.angle=page;/.test(sc)) note('bug', L + 'viral/cookie', 'sponsor/angle cookie not set: ' + sc); + const ru = await page.request.get(LOCAL + '/?ref=nobody_zz9', { maxRedirects: 0 }); + const su = (ru.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; '); + if (ru.status() !== 302 || /iap\.sponsor=/.test(su)) note('bug', L + 'viral/unknown', 'unknown ref must redirect without a sponsor cookie: ' + ru.status() + ' ' + su); + const rj = await page.request.get(LOCAL + '/join/' + tok + '?ref=' + tok, { maxRedirects: 0 }); + if (rj.status() !== 200) note('bug', L + 'viral/join', '/join keeps its own ?ref handling, got ' + rj.status()); + console.log('ok viral links: builder + redirect + cookie'); const chat = await page.$('#chatMenuBtn'); if (chat) { await chat.click(); await page.waitForTimeout(800); await domChecks(page, L + 'messages'); flush(bag, L + 'messages'); } const lo = await page.$('#logoutLink'); if (lo) { await lo.click(); await page.waitForTimeout(1000); } if (!(await page.$('#authArea:not([hidden])'))) note('bug', L + 'logout', 'auth card not shown after log out'); diff --git a/server.js b/server.js index 28c80a2..cdfe59a 100644 --- a/server.js +++ b/server.js @@ -1076,6 +1076,28 @@ const server = http.createServer(async (req, res) => { // -- traffic log: public page views by referring domain (admin > Traffic) if (req.method === 'GET' && (TRAFFIC_PAGES.has(p) || /^\/(join|from|wall|blog)\/[^/]+$/.test(p))) traffic.hit(p.startsWith('/blog/') ? '/blog/*' : p, req.headers.referer, req.headers['user-agent']); + // -- viral links (Marty, 2026-09-21): ANY public page + ?ref= is that member's + // referral link. Same 30-day last-touch sponsor cookie as /join, the view counts under the + // "page" hook in link stats, then a redirect to the clean URL so canonical links stay clean + // for search engines. /join, /from and the APIs keep their own meaning of ?ref. + if ((req.method === 'GET' || req.method === 'HEAD') && u.searchParams.has('ref') && !p.startsWith('/api/') && !p.startsWith('/admin') && !/^\/(join|from)\//.test(p) && !/\.[a-z0-9]{2,5}$/i.test(p)) { + const raw = String(u.searchParams.get('ref') || '').trim().toLowerCase().slice(0, 40); + const tok = JOIN_ALIASES[raw] || raw; + u.searchParams.delete('ref'); + const clean = p + (u.searchParams.toString() ? '?' + u.searchParams.toString() : ''); + let known = false; + if (/^[a-z0-9_]{1,20}$/.test(tok)) { try { known = !!((await accounts.byCode(tok)) || (await accounts.byUsername(tok)) || (/^\d+$/.test(tok) && (await accounts.byMemberId(Number(tok))))); } catch (e) {} } + const set = []; + if (known) { + const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; + set.push('iap.sponsor=' + tok + cookieTail); // last touch wins, exactly like /join + set.push('iap.angle=page' + cookieTail); // the join shows under the "any page" hook + if (!parseCookies(req)['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source + if (req.method === 'GET') coach.recordView(tok, 'page', req.headers.referer); + } + res.writeHead(302, baseHeaders(Object.assign({ Location: clean, 'Cache-Control': 'no-store' }, set.length ? { 'Set-Cookie': set } : {}))); + return res.end(); + } // -- join links: /join/ — LAST-touch cookie (Marty, // 2026-09-10): the link a visitor opened most recently is the sponsor shown // and used, and it locks the moment the account is created (accounts.ensure @@ -1129,6 +1151,12 @@ const server = http.createServer(async (req, res) => { // -- public API if (p === '/api/friday' && req.method === 'GET') return json(res, 200, friday.view(), { 'Cache-Control': 'public, max-age=60' }); + // -- the pages a member can turn into a viral link (promo tools > Viral links) + if (p === '/api/pages' && req.method === 'GET') { + const pages = [{ path: '/', title: 'Home page' }, { path: '/blog', title: 'Blog' }, { path: '/earning', title: 'How earning works' }, { path: '/ledger', title: 'Live ledger' }, { path: '/leaderboard', title: 'Leaderboard' }, { path: '/contract', title: 'The contract' }, { path: '/plays', title: 'Team-building plays' }, { path: '/wallets', title: 'Wallet guide' }, { path: '/whats-new', title: "What's new" }, { path: '/shorts', title: 'Shorts' }]; + try { for (const b of (await blog.listPublished()).slice(0, 40)) pages.push({ path: '/blog/' + b.slug, title: 'Article: ' + b.title, cover: b.cover || null }); } catch (e) {} + return json(res, 200, { pages }); + } if (p === '/api/config' && req.method === 'GET') { const c = chain.getConfig(); // public copy of the site settings: never anything that looks like a credential @@ -3144,7 +3172,7 @@ const server = http.createServer(async (req, res) => { + '' + '
' + esc(label) + ' badge for @' + esc(un) + '

@' + esc(un) + ' unlocked ' + label + '

' + esc(sub.charAt(0).toUpperCase() + sub.slice(1)) + '. On InstantAdPay every ad package that sells pays the sponsor in the same transaction, straight to their wallet, on-chain.

' + 'Join @' + esc(un) + '\u2019s line free

Advertising with a performance referral program. Not an investment; no income is guaranteed.

' - + ''; + + ''; res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=300' })); return res.end(html); }