Five Dollar Friday bridge page, with the member's link on it

Marty's ask: use the Friday promo as a tool where the sponsor gets the
credit, and as a bridge-page entry point. The sponsor part already worked
(any public page + ?ref sets the sponsor cookie, 2026-09-21); what was
missing was a page that is ABOUT the offer, so a click on a "20% bonus
credits today" image did not land on the general pitch and leak.

/friday: the banner as the hero, the live state from /api/friday (live now
with today's pack count and bonus credits issued, or the next date with a
countdown to midnight Central found by scanning minutes so DST cannot put it
an hour out), three steps, the email-first join with a new `friday` angle so
it shows as its own row in Admin > Traffic, and the credit definition. No
attribution logic added: last touch, walk-up, as everywhere.

Promo tools: a {{FRIDAY}} token that turns the member's invite link into
/friday?ref=<username>; posts for X, Facebook and Telegram and a text that
carry it; the three new banners (loft 1200x630, dark 1200x630, square
1080x1080) as a group in the kit with a copy-my-Friday-link button on it.
Chatbot answer points at the page and drops the stale "first one is
September 25" line. /friday in the traffic list, the sitemap and the QA
public walk. Cache tags bumped on my.js, promo.js and site.css.

QA member walk: 0 bugs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-25 06:26:11 -05:00
parent 1b72a9a18c
commit 5b694c8ab4
11 changed files with 1391 additions and 1253 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ 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.' },
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 page instantadpay.com/friday shows whether it is live right now and the next date; add ?ref=yourusername to that address and anyone who joins from it is your referral. 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
// signed out, and the honest answer reads far better than letting it look like a fault.
{ re: /(logged out|log(ged)? me out|signed (me |us )?out|keep.{0,20}(logging|signing) in|session (expired|timed? out)|why.{0,25}(log ?in|sign ?in) again|have to log ?in again|stay logged in)/i,
+17
View File
@@ -2088,6 +2088,10 @@
bwrap.dataset.filled = '1';
// one collapsed accordion per size / use, so the kit stays scannable as it grows
const GROUPS = [
{ title: 'Five Dollar Friday · share with your /friday link', friday: true, items: [
{ file: 'iap-five-dollar-friday-v2-1200x630.jpg', size: '1200×630 · Five Dollar Friday · loft, Central-time window' },
{ file: 'iap-five-dollar-friday-dark-1200x630.jpg', size: '1200×630 · Five Dollar Friday · dark studio' },
{ file: 'iap-five-dollar-friday-v2-1080x1080.jpg', size: '1080×1080 · Five Dollar Friday · square for Instagram and feeds' } ] },
{ title: '1200×630 · social posts, link previews, Daily News covers', items: [
{ file: 'iap-hero-1200x630.png', size: '1200×630 · hero · advertise and earn' },
{ file: 'iap-advertise-earn-1200x630.jpg', size: '1200×630 · advertise and earn instantly, locked in code' },
@@ -2135,6 +2139,19 @@
det.className = 'pb-acc';
det.innerHTML = '<summary><span>' + esc(g.title) + '</span><span class="pb-count">' + g.items.length + (g.items.length === 1 ? ' banner' : ' banners') + '</span></summary><div class="pb-grid"></div>';
const grid = det.querySelector('.pb-grid');
if (g.friday) {
// the bridge page with this member's referral: any public page + ?ref sets the sponsor cookie
const inv = (document.getElementById('inviteLine') || {}).textContent || '';
const fl = inv.replace(/\/join\/([^/?#]+).*$/, '/friday?ref=$1');
const note = document.createElement('div');
note.className = 'pb-note';
note.innerHTML = '<span>Post any of these with your Five Dollar Friday link: <b class="mono"></b></span>';
note.querySelector('b').textContent = fl;
const cp = document.createElement('button'); cp.className = 'btn small'; cp.textContent = 'Copy my Friday link';
cp.addEventListener('click', async () => { try { await navigator.clipboard.writeText(fl); IAP.status('Friday link copied.', 'ok'); } catch (e) { IAP.status('Copy failed.', 'bad'); } });
note.appendChild(cp);
grid.before(note);
}
for (const b of g.items) {
const url = location.origin + '/banners/' + b.file;
const d = document.createElement('div');
+8 -1
View File
@@ -8,9 +8,15 @@ window.IAPPromo = (function () {
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
const status = (m, c) => (window.IAP && IAP.status) ? IAP.status(m, c) : console.log(m);
const angleLink = (link, angle) => angle ? link + '?v=' + angle : link;
// The Five Dollar Friday bridge page with the member's referral: /friday?ref=<username>. Any
// public page plus ?ref sets the same sponsor cookie as the invite link (viral links, 2026-09-21).
const fridayLink = link => link.replace(/\/join\/([^/?#]+).*$/, '/friday?ref=$1');
// ── social posts ──
const POSTS = [
{ net: 'X', label: 'X · Five Dollar Friday', text: 'Five Dollar Friday on InstantAdPay: any ad package from $5 up earns 20% bonus credits, every Friday, Central time. The pack pays the sponsor line in the same transaction, on a public ledger. Free to join: {{FRIDAY}}' },
{ net: 'Facebook', label: 'Facebook · Five Dollar Friday', text: 'Every Friday I point people at this. Any ad package from $5 up gets 20% extra credits on InstantAdPay, added the second the purchase settles, no code, nothing to claim. And every pack pays the sponsor line in the same blockchain transaction, so there is no pending anything. It runs midnight to midnight Central. Free to join by email: {{FRIDAY}}' },
{ net: 'Telegram', label: 'Telegram or WhatsApp group · Five Dollar Friday', text: 'Quick one for the group: Five Dollar Friday on InstantAdPay. Any ad package from $5 up earns 20% bonus credits today (Central time, until midnight). The page shows whether it is live right now: {{FRIDAY}}' },
{ net: 'X', label: 'X · Five Dollar Friday', text: 'Five Dollar Friday on InstantAdPay: every Friday, any ad package from $5 earns +20% bonus credits, and every pack pays its sponsor line in the same on-chain transaction. The whole network buys on the same day. Join free and line yours up: {{LINK}}' },
{ net: 'Telegram', label: 'Telegram or WhatsApp · Five Dollar Friday', text: 'Reminder for the group: tomorrow is Five Dollar Friday on InstantAdPay. Any pack from $5 up earns +20% credits, and your pack pays your sponsor line the second it settles. Everyone pushes on the same day, and every line gets paid at once. Free to join: {{LINK}}' },
{ net: 'X', label: 'X · instant payout', text: 'No withdraw button. A smart contract on Polygon splits every ad package the second it sells: 50% to the sponsor, 20% to level 2, 10% to level 3. Lands in your own wallet. Watch the ledger move: {{LINK:instant}}' },
@@ -25,6 +31,7 @@ window.IAPPromo = (function () {
// ── text a friend (SMS-sized) ──
const TEXTS = [
{ angle: 'friday', title: '"Five Dollar Friday"', text: 'Heads up: Fridays on InstantAdPay any ad package from $5 up gets 20% bonus credits, Central time. Page tells you if it is live right now: {{FRIDAY}}' },
{ angle: '', title: '"Thought of you"', text: 'Hey, found an ad platform where the commission lands in your wallet the second someone buys. No pending, no withdraw button. Free to look: {{LINK}}' },
{ angle: 'instant', title: '"Before the page reloads"', text: 'Random thought. What if your commission showed up before the thank-you page finished loading? That is literally how this works: {{LINK:instant}}' },
{ angle: 'adspend', title: '"You buy ads anyway"', text: 'You already buy traffic. This one pays your line every time someone in it buys ads, same transaction, on Polygon. Two-minute look: {{LINK:adspend}}' },
@@ -55,7 +62,7 @@ window.IAPPromo = (function () {
];
function fillLink(text, link) {
return text.replace(/\{\{LINK(?::([a-z]+))?\}\}/g, (m, a) => angleLink(link, a));
return text.replace(/\{\{FRIDAY\}\}/g, fridayLink(link)).replace(/\{\{LINK(?::([a-z]+))?\}\}/g, (m, a) => angleLink(link, a));
}
function copyBtn(text, label) {
const b = document.createElement('button');
+2
View File
@@ -317,6 +317,8 @@ textarea{resize:vertical;font:inherit}
}
/* ── promo banners ── */
.promo-banners{display:flex;flex-direction:column;gap:10px;margin-top:12px}
.pb-note{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 14px;padding:10px 16px 12px;font-size:13.5px;color:var(--muted);border-bottom:1px solid var(--line)}
.pb-note b.mono{color:var(--ink);font-family:var(--mono);font-size:12.5px;word-break:break-all}
.pb-acc{margin:0}
.pb-acc>summary{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:13px 16px;font-size:14.5px}
.pb-acc>summary::before{content:none}
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+110
View File
@@ -0,0 +1,110 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Five Dollar Friday: 20% bonus credits on every $5+ ad package | InstantAdPay</title>
<meta name="description" content="Every Friday (US Central time), any InstantAdPay ad package from $5 up earns 20% bonus credits the moment it settles. Every pack pays its sponsor line in the same transaction. Join free by email.">
<meta name="theme-color" content="#043b2f">
<meta property="og:type" content="website"><meta property="og:site_name" content="InstantAdPay">
<meta property="og:title" content="Five Dollar Friday: 20% bonus credits on every $5+ ad package">
<meta property="og:description" content="Every Friday, US Central time, midnight to midnight. Any ad package from $5 up earns 20% bonus credits, added the moment the purchase settles. Join free by email.">
<meta property="og:image" content="https://instantadpay.com/banners/iap-five-dollar-friday-v2-1200x630.jpg">
<meta property="og:url" content="https://instantadpay.com/friday">
<meta name="twitter:card" content="summary_large_image">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="icon" type="image/png" href="/logo-icon.png">
<link rel="stylesheet" href="/assets/site.css?v=20260912b">
<style>
.fr{max-width:860px}
.fr .hero{padding:44px 0 10px}
.fr-banner{display:block;width:100%;max-width:860px;border-radius:16px;border:1px solid var(--line);margin:18px 0 14px}
.fr-state{display:flex;flex-wrap:wrap;align-items:center;gap:10px 18px;padding:14px 18px;border-radius:14px;border:1px solid var(--line);background:rgba(67,232,195,.06);margin:0 0 26px;font-size:15px}
.fr-state.live{border-color:rgba(255,178,56,.55);background:rgba(255,178,56,.09)}
.fr-tag{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;padding:4px 9px;border-radius:999px;background:var(--mint);color:#04120e}
.fr-state.live .fr-tag{background:#ffb238}
.fr-state b{font-family:var(--disp)}
.fr-num{font-family:var(--mono);font-variant-numeric:tabular-nums}
.fr-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin:8px 0 26px}
.fr-step{border:1px solid var(--line);border-radius:14px;padding:16px 18px;background:rgba(255,255,255,.02)}
.fr-step .n{font-family:var(--mono);font-size:11px;letter-spacing:.12em;color:var(--mint);text-transform:uppercase}
.fr-step h3{font-family:var(--disp);font-size:17px;margin:6px 0 6px}
.fr-step p{margin:0;font-size:14.5px;color:var(--muted)}
.fr-cta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:6px 0 30px}
.fr-cta .btn.big{font-size:17px;padding:14px 22px}
.fr-rule{border-left:4px solid var(--mint);background:rgba(67,232,195,.07);padding:14px 18px;border-radius:0 12px 12px 0;margin:0 0 22px;font-size:14.5px}
.fr-share{border:1px dashed var(--line);border-radius:14px;padding:16px 18px;margin:0 0 30px;font-size:14.5px;color:var(--muted)}
.fr-share b{color:var(--ink)}
</style>
</head>
<body>
<div class="wrap fr">
<section class="hero">
<p class="eyebrow">Every Friday · US Central time · midnight to midnight</p>
<h1>Five Dollar Friday: <em>20% bonus credits</em> on every $5+ ad package.</h1>
<p class="lead">Buy any InstantAdPay ad package from $5 up on a Friday and 20% extra credits land the moment the purchase settles. No code, nothing to claim. And like every pack, it pays the sponsor line in the same transaction.</p>
</section>
<img class="fr-banner" src="/banners/iap-five-dollar-friday-v2-1200x630.jpg" alt="Five Dollar Friday: 20% bonus credits on every $5+ ad package. Fridays 12:00 AM to 11:59 PM US Central time." width="1200" height="630">
<div class="fr-state" id="frState"><span class="fr-tag">Checking</span><span>Loading today’s status…</span></div>
<div class="fr-cta">
<a class="btn big" id="frJoin" href="/join?v=friday">Join free by email</a>
<a class="btn sec" href="/my#buy">Already a member? Get your pack</a>
</div>
<div class="fr-steps">
<div class="fr-step"><div class="n">Step 1</div><h3>Join free, by email.</h3><p>Email first, wallet later, inside your account. Nothing to install to look around.</p></div>
<div class="fr-step"><div class="n">Step 2</div><h3>Buy any pack from $5 on a Friday.</h3><p>Central time, midnight to midnight. The 20% bonus is added to your credits the moment the purchase settles, and labelled in your credit log.</p></div>
<div class="fr-step"><div class="n">Step 3</div><h3>Your pack pays the line above you. Theirs pays you.</h3><p>Every package is one Polygon transaction that splits to the sponsor line instantly. Public ledger, nothing held, nothing pending.</p></div>
</div>
<div class="fr-rule"><b>What a credit is.</b> One credit is one cent of ad delivery across the network: banners, text ads, video, verified visits, solo ads and more. Credits are not money and cannot be withdrawn. They buy advertising for your own offers. The POL the contract pays to wallets is separate and comes only from packages bought in your line.</div>
<div class="fr-share" id="frShare"><b>Members: this page is your Friday promo tool.</b> Add <span class="fr-num">?ref=yourusername</span> to this address and anyone who joins from it is your referral, the same as your invite link. Banners, ready-made posts and your exact link are in <a href="/my#promo">Promo tools</a>.</div>
<p class="muted small" style="margin-top:10px">No income is guaranteed. Results depend on your effort. Cryptocurrency involves risk of loss. InstantAdPay sells advertising; it is not an investment product.</p>
<footer>
<div>InstantAdPay · <a href="/">Home</a> · <a href="/ledger">Live ledger</a> · <a href="/earning">How earning works</a></div>
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees.</div>
</footer>
</div>
<script>
(function () {
var el = document.getElementById('frState');
function n(x) { return Number(x || 0).toLocaleString('en-US'); }
// the minute (Central) when the next Friday starts, found by scanning rather than assumed,
// so daylight-saving changes cannot put the countdown an hour out
function startsAt(dayKey) {
var now = Date.now();
for (var m = 0; m < 8 * 1440; m++) {
var t = now + m * 60000;
var d = new Date(t).toLocaleDateString('en-CA', { timeZone: 'America/Chicago' });
if (d === dayKey) return t;
}
return null;
}
function render(f) {
if (!f || !f.on) { el.innerHTML = '<span class="fr-tag">Paused</span><span>Five Dollar Friday is not running right now. Join free and you will hear when it is back.</span>'; return; }
if (f.live) {
var t = f.today || {};
el.className = 'fr-state live';
el.innerHTML = '<span class="fr-tag">Live now</span><span><b>It is Five Dollar Friday.</b> Bonus runs until midnight Central.</span>'
+ '<span><span class="fr-num">' + n(t.packs) + '</span> pack' + (t.packs === 1 ? '' : 's') + ' so far today</span>'
+ '<span><span class="fr-num">' + n(t.bonusCredits) + '</span> bonus credits issued</span>';
return;
}
var at = f.nextFriday ? startsAt(f.nextFriday) : null;
var left = at ? Math.max(0, at - Date.now()) : 0;
var hrs = Math.floor(left / 3600000), mins = Math.floor((left % 3600000) / 60000), days = Math.floor(hrs / 24);
var when = at ? (days >= 1 ? days + ' day' + (days === 1 ? '' : 's') + ' ' + (hrs % 24) + 'h' : hrs + 'h ' + mins + 'm') : '';
el.className = 'fr-state';
el.innerHTML = '<span class="fr-tag">Next</span><span><b>Next Five Dollar Friday: ' + (f.nextLabel || f.nextFriday || 'soon') + '.</b> Starts at midnight Central' + (when ? ', in ' + when : '') + '.</span>'
+ '<span>Join now so your account is ready before it starts.</span>';
}
fetch('/api/friday').then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }).then(render);
})();
</script>
<script src="/assets/common.js?v=20260923b"></script>
</body>
</html>
+1079 -1079
View File
File diff suppressed because it is too large Load Diff
+170 -170
View File
@@ -1,170 +1,170 @@
// InstantAdPay QA harness: site walk.
// node qa/walk.mjs public -> live public pages (no sign-in): errors, failed requests, broken images, dead links, mobile
// node qa/walk.mjs member -> local copy: sign in, every member pane + sub-tab, every admin pane, forms
// node qa/walk.mjs all -> both
// Env: LIVE (default https://instantadpay.com), LOCAL (default http://127.0.0.1:8796), OUT (report dir),
// PW (playwright package dir; default D:/Projects/MarketingAgent/qa-tester/node_modules/playwright)
// Exit code 1 when any [bug] finding remains after noise filtering.
import { pathToFileURL } from 'node:url';
import fs from 'node:fs';
const PW = process.env.PW || 'D:/Projects/MarketingAgent/qa-tester/node_modules/playwright';
const { chromium } = (await import(pathToFileURL(PW + '/index.js').href)).default;
const MODE = process.argv[2] || 'all';
const LIVE = process.env.LIVE || 'https://instantadpay.com';
const LOCAL = process.env.LOCAL || 'http://127.0.0.1:8796';
const OUT = process.env.OUT || 'qa/out';
fs.mkdirSync(OUT, { recursive: true });
const findings = [];
const note = (sev, where, what) => findings.push({ sev, where, what });
const NOISE = /walletconnect|reown|web3modal|coingecko|fonts\.|\/api\/feed\/live|\/api\/auth\/logout/;
function watch(page, base) {
const bag = { console: [], failed: [], status: [] };
page.on('pageerror', e => bag.console.push('pageerror: ' + e.message));
page.on('console', m => {
if (m.type() !== 'error') return;
const loc = (m.location() && m.location().url) || '';
if (loc && !loc.startsWith(base)) return; // third-party or framed page, not ours
if (/status of (400|401|404)/.test(m.text())) return; // expected API answers surface as console noise
bag.console.push(m.text());
});
page.on('requestfailed', r => { const u = r.url(); if (u.startsWith(base) && !NOISE.test(u)) bag.failed.push(u + ' ' + (r.failure() && r.failure().errorText)); });
page.on('response', r => { const st = r.status(); const u = r.url(); if (st >= 500 && u.startsWith(base)) bag.status.push(st + ' ' + u); });
return bag;
}
function flush(bag, label) {
for (const c of bag.console) note('bug', label, 'console: ' + c.slice(0, 200));
for (const f of bag.failed) note('bug', label, 'request failed: ' + f.slice(0, 200));
for (const s of bag.status) note('bug', label, 'HTTP ' + s.slice(0, 200));
bag.console.length = bag.failed.length = bag.status.length = 0;
}
async function domChecks(page, label) {
const r = await page.evaluate(() => {
const vis = el => el.offsetParent !== null;
const brokenImgs = [...document.images].filter(i => i.complete && i.naturalWidth === 0 && i.src && vis(i)).map(i => i.src);
const unfilled = [...document.querySelectorAll('body *')].filter(el => el.children.length === 0 && (el.textContent || '').trim() === '…' && vis(el)).length;
const overflow = document.documentElement.scrollWidth > document.documentElement.clientWidth + 2;
return { brokenImgs, unfilled, overflow, title: document.title };
});
if (r.brokenImgs.length) note('bug', label, 'broken images: ' + r.brokenImgs.slice(0, 3).join(', '));
if (r.unfilled) note('warn', label, r.unfilled + ' element(s) still showing the loading ellipsis');
if (r.overflow) note('warn', label, 'page scrolls horizontally');
return r;
}
const hide = page => page.evaluate(() => { document.querySelectorAll('.modal-back,.lgate,.iap-welcome').forEach(m => m.hidden = true); });
const browser = await chromium.launch();
if (MODE === 'public' || MODE === 'all') {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
const page = await ctx.newPage(); const bag = watch(page, LIVE);
const PUBLIC = ['/', '/ledger', '/contract', '/terms', '/privacy', '/disclaimer', '/wall/martbost', '/join/martbost',
'/join/martbost?v=instant', '/join/martbost?v=adspend', '/join/martbost?v=free', '/join/martbost?v=ledger', '/join/martbost?v=two',
'/admin', '/my', '/shorts', '/nope-404'];
for (const p of PUBLIC) {
const label = 'LIVE ' + p;
try {
const resp = await page.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(2500);
const st = resp ? resp.status() : 0;
if (p === '/nope-404') { if (st !== 404) note('warn', label, 'expected 404, got ' + st); }
else if (st >= 400) note('bug', label, 'page HTTP ' + st);
const d = await domChecks(page, label);
const hrefs = await page.evaluate(() => [...new Set([...document.querySelectorAll('a[href]')].map(a => a.href).filter(h => h.startsWith(location.origin) && !h.includes('#') && !h.includes('/api/')))]);
for (const h of hrefs.slice(0, 40)) {
try { const r = await page.request.head(h, { timeout: 15000 }); if (r.status() >= 400) note('bug', label, 'dead link ' + h + ' -> ' + r.status()); }
catch (e) { note('warn', label, 'link check failed ' + h); }
}
flush(bag, label); console.log('ok', label, '|', d.title);
} catch (e) { note('bug', label, 'navigation failed: ' + e.message.slice(0, 160)); flush(bag, label); }
}
const m = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true });
await m.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
const mp = await m.newPage(); const mbag = watch(mp, LIVE);
for (const p of ['/', '/join/martbost?v=instant', '/wall/martbost', '/my']) {
await mp.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(e => note('bug', 'LIVE mobile ' + p, e.message));
await mp.waitForTimeout(2000); await domChecks(mp, 'LIVE mobile ' + p); flush(mbag, 'LIVE mobile ' + p);
await mp.screenshot({ path: OUT + '/mobile' + p.replace(/[^a-z0-9]+/gi, '-') + '.png' }).catch(() => {});
}
await ctx.close(); await m.close();
}
if (MODE === 'member' || MODE === 'all') {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
const page = await ctx.newPage(); const bag = watch(page, LOCAL);
page.on('dialog', d => d.accept());
const L = 'LOCAL ';
await page.goto(LOCAL + '/my', { waitUntil: 'networkidle' });
await page.fill('#mcEmail', 'qa-walk@example.com'); await page.click('#mcSendBtn');
await page.waitForSelector('#mcVerifyBtn:not([hidden])'); await page.click('#mcVerifyBtn'); await page.waitForTimeout(2000);
if (await page.$('#onboardModal:not([hidden])')) { await page.fill('#obUsername', 'qawalker'); await page.click('#obSave'); await page.waitForTimeout(1200); }
await hide(page); flush(bag, L + 'sign-in');
const PANES = ['overview', 'line', 'pipeline', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile'];
for (const pn of PANES) {
const label = L + 'my#' + pn;
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200); await hide(page);
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
if (!vis) note('bug', label, 'pane did not render');
await domChecks(page, label);
const subs = await page.$$('#pane-' + pn + ' .subtabs [data-earn], #pane-' + pn + ' .promo-pills [data-promo]');
for (const s of subs) { try { await s.click(); await page.waitForTimeout(500); } catch (e) {} }
if (subs.length) await domChecks(page, label + ' (sub-tabs)');
flush(bag, label);
await page.screenshot({ path: OUT + '/my-' + pn + '.png' }).catch(() => {});
console.log('ok', label, 'subtabs:', subs.length);
}
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=<member> 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;
// served in place (no redirect: Facebook drops the name otherwise), cookie on the response, og:url carries the ref
const rr = await page.request.get(LOCAL + '/?ref=' + tok + '&x=1', { maxRedirects: 0 });
const sc = (rr.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
const rb = await rr.text();
if (rr.status() !== 200 || !/<title>/.test(rb)) note('bug', L + 'viral/inplace', 'expected the page at the decorated address, got ' + rr.status());
if (!new RegExp('iap\\.sponsor=' + tok + ';').test(sc) || !/iap\.angle=page;/.test(sc)) note('bug', L + 'viral/cookie', 'sponsor/angle cookie not set: ' + sc);
if (!new RegExp('property="og:url" content="[^"]*[?&]ref=' + tok + '"').test(rb)) note('bug', L + 'viral/og', 'og:url does not carry the ref');
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('; ');
const ub = await ru.text();
if (ru.status() !== 200 || /iap\.sponsor=/.test(su) || /ref=nobody_zz9/.test(ub)) note('bug', L + 'viral/unknown', 'unknown ref must serve the plain page with no cookie and a clean og:url: ' + 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');
flush(bag, L + 'logout');
// admin
await page.goto(LOCAL + '/admin', { waitUntil: 'networkidle' });
await page.fill('#adEmail', process.env.ADMIN_EMAIL || 'martybostick@gmail.com'); await page.click('#adSend'); await page.waitForSelector('#adVerify:not([hidden])'); await page.click('#adVerify'); await page.waitForTimeout(1200);
for (const pn of ['overview', 'house', 'campaigns', 'members', 'reports', 'traffic', 'blog', 'releases', 'pnl', 'settings']) {
const label = L + 'admin#' + pn;
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200);
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
if (!vis) note('bug', label, 'pane did not render');
await domChecks(page, label); flush(bag, label);
await page.screenshot({ path: OUT + '/admin-' + pn + '.png' }).catch(() => {});
console.log('ok', label);
}
await page.click('.bo-menu [data-pane="house"]'); await page.waitForTimeout(500);
for (const t of ['banner', 'text', 'login', 'solo', 'video', 'featured', 'visits']) { await page.selectOption('#hType', t); await page.waitForTimeout(120); }
await page.selectOption('#hType', 'text'); await page.click('#hCreate'); await page.waitForTimeout(800);
if (!(await page.$('#hErr:not([hidden])'))) note('warn', L + 'admin house form', 'empty submit showed no validation message');
flush(bag, L + 'admin house form');
await ctx.close();
}
await browser.close();
const bugs = findings.filter(f => f.sev === 'bug'), warns = findings.filter(f => f.sev === 'warn');
const lines = ['===== QA WALK (' + MODE + ') ' + new Date().toISOString() + ' =====', 'bugs: ' + bugs.length + ' | warnings: ' + warns.length,
...findings.map(f => '[' + f.sev + '] ' + f.where + ' :: ' + f.what)];
console.log('\n' + lines.join('\n'));
fs.writeFileSync(OUT + '/walk-report.txt', lines.join('\n') + '\n');
process.exit(bugs.length ? 1 : 0);
// InstantAdPay QA harness: site walk.
// node qa/walk.mjs public -> live public pages (no sign-in): errors, failed requests, broken images, dead links, mobile
// node qa/walk.mjs member -> local copy: sign in, every member pane + sub-tab, every admin pane, forms
// node qa/walk.mjs all -> both
// Env: LIVE (default https://instantadpay.com), LOCAL (default http://127.0.0.1:8796), OUT (report dir),
// PW (playwright package dir; default D:/Projects/MarketingAgent/qa-tester/node_modules/playwright)
// Exit code 1 when any [bug] finding remains after noise filtering.
import { pathToFileURL } from 'node:url';
import fs from 'node:fs';
const PW = process.env.PW || 'D:/Projects/MarketingAgent/qa-tester/node_modules/playwright';
const { chromium } = (await import(pathToFileURL(PW + '/index.js').href)).default;
const MODE = process.argv[2] || 'all';
const LIVE = process.env.LIVE || 'https://instantadpay.com';
const LOCAL = process.env.LOCAL || 'http://127.0.0.1:8796';
const OUT = process.env.OUT || 'qa/out';
fs.mkdirSync(OUT, { recursive: true });
const findings = [];
const note = (sev, where, what) => findings.push({ sev, where, what });
const NOISE = /walletconnect|reown|web3modal|coingecko|fonts\.|\/api\/feed\/live|\/api\/auth\/logout/;
function watch(page, base) {
const bag = { console: [], failed: [], status: [] };
page.on('pageerror', e => bag.console.push('pageerror: ' + e.message));
page.on('console', m => {
if (m.type() !== 'error') return;
const loc = (m.location() && m.location().url) || '';
if (loc && !loc.startsWith(base)) return; // third-party or framed page, not ours
if (/status of (400|401|404)/.test(m.text())) return; // expected API answers surface as console noise
bag.console.push(m.text());
});
page.on('requestfailed', r => { const u = r.url(); if (u.startsWith(base) && !NOISE.test(u)) bag.failed.push(u + ' ' + (r.failure() && r.failure().errorText)); });
page.on('response', r => { const st = r.status(); const u = r.url(); if (st >= 500 && u.startsWith(base)) bag.status.push(st + ' ' + u); });
return bag;
}
function flush(bag, label) {
for (const c of bag.console) note('bug', label, 'console: ' + c.slice(0, 200));
for (const f of bag.failed) note('bug', label, 'request failed: ' + f.slice(0, 200));
for (const s of bag.status) note('bug', label, 'HTTP ' + s.slice(0, 200));
bag.console.length = bag.failed.length = bag.status.length = 0;
}
async function domChecks(page, label) {
const r = await page.evaluate(() => {
const vis = el => el.offsetParent !== null;
const brokenImgs = [...document.images].filter(i => i.complete && i.naturalWidth === 0 && i.src && vis(i)).map(i => i.src);
const unfilled = [...document.querySelectorAll('body *')].filter(el => el.children.length === 0 && (el.textContent || '').trim() === '…' && vis(el)).length;
const overflow = document.documentElement.scrollWidth > document.documentElement.clientWidth + 2;
return { brokenImgs, unfilled, overflow, title: document.title };
});
if (r.brokenImgs.length) note('bug', label, 'broken images: ' + r.brokenImgs.slice(0, 3).join(', '));
if (r.unfilled) note('warn', label, r.unfilled + ' element(s) still showing the loading ellipsis');
if (r.overflow) note('warn', label, 'page scrolls horizontally');
return r;
}
const hide = page => page.evaluate(() => { document.querySelectorAll('.modal-back,.lgate,.iap-welcome').forEach(m => m.hidden = true); });
const browser = await chromium.launch();
if (MODE === 'public' || MODE === 'all') {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
const page = await ctx.newPage(); const bag = watch(page, LIVE);
const PUBLIC = ['/', '/ledger', '/contract', '/terms', '/privacy', '/disclaimer', '/wall/martbost', '/join/martbost',
'/join/martbost?v=instant', '/join/martbost?v=adspend', '/join/martbost?v=free', '/join/martbost?v=ledger', '/join/martbost?v=two',
'/admin', '/my', '/shorts', '/nope-404'];
for (const p of PUBLIC) {
const label = 'LIVE ' + p;
try {
const resp = await page.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(2500);
const st = resp ? resp.status() : 0;
if (p === '/nope-404') { if (st !== 404) note('warn', label, 'expected 404, got ' + st); }
else if (st >= 400) note('bug', label, 'page HTTP ' + st);
const d = await domChecks(page, label);
const hrefs = await page.evaluate(() => [...new Set([...document.querySelectorAll('a[href]')].map(a => a.href).filter(h => h.startsWith(location.origin) && !h.includes('#') && !h.includes('/api/')))]);
for (const h of hrefs.slice(0, 40)) {
try { const r = await page.request.head(h, { timeout: 15000 }); if (r.status() >= 400) note('bug', label, 'dead link ' + h + ' -> ' + r.status()); }
catch (e) { note('warn', label, 'link check failed ' + h); }
}
flush(bag, label); console.log('ok', label, '|', d.title);
} catch (e) { note('bug', label, 'navigation failed: ' + e.message.slice(0, 160)); flush(bag, label); }
}
const m = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true });
await m.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
const mp = await m.newPage(); const mbag = watch(mp, LIVE);
for (const p of ['/', '/join/martbost?v=instant', '/wall/martbost', '/my']) {
await mp.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(e => note('bug', 'LIVE mobile ' + p, e.message));
await mp.waitForTimeout(2000); await domChecks(mp, 'LIVE mobile ' + p); flush(mbag, 'LIVE mobile ' + p);
await mp.screenshot({ path: OUT + '/mobile' + p.replace(/[^a-z0-9]+/gi, '-') + '.png' }).catch(() => {});
}
await ctx.close(); await m.close();
}
if (MODE === 'member' || MODE === 'all') {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
const page = await ctx.newPage(); const bag = watch(page, LOCAL);
page.on('dialog', d => d.accept());
const L = 'LOCAL ';
await page.goto(LOCAL + '/my', { waitUntil: 'networkidle' });
await page.fill('#mcEmail', 'qa-walk@example.com'); await page.click('#mcSendBtn');
await page.waitForSelector('#mcVerifyBtn:not([hidden])'); await page.click('#mcVerifyBtn'); await page.waitForTimeout(2000);
if (await page.$('#onboardModal:not([hidden])')) { await page.fill('#obUsername', 'qawalker'); await page.click('#obSave'); await page.waitForTimeout(1200); }
await hide(page); flush(bag, L + 'sign-in');
const PANES = ['overview', 'line', 'pipeline', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile'];
for (const pn of PANES) {
const label = L + 'my#' + pn;
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200); await hide(page);
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
if (!vis) note('bug', label, 'pane did not render');
await domChecks(page, label);
const subs = await page.$$('#pane-' + pn + ' .subtabs [data-earn], #pane-' + pn + ' .promo-pills [data-promo]');
for (const s of subs) { try { await s.click(); await page.waitForTimeout(500); } catch (e) {} }
if (subs.length) await domChecks(page, label + ' (sub-tabs)');
flush(bag, label);
await page.screenshot({ path: OUT + '/my-' + pn + '.png' }).catch(() => {});
console.log('ok', label, 'subtabs:', subs.length);
}
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=<member> 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;
// served in place (no redirect: Facebook drops the name otherwise), cookie on the response, og:url carries the ref
const rr = await page.request.get(LOCAL + '/?ref=' + tok + '&x=1', { maxRedirects: 0 });
const sc = (rr.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
const rb = await rr.text();
if (rr.status() !== 200 || !/<title>/.test(rb)) note('bug', L + 'viral/inplace', 'expected the page at the decorated address, got ' + rr.status());
if (!new RegExp('iap\\.sponsor=' + tok + ';').test(sc) || !/iap\.angle=page;/.test(sc)) note('bug', L + 'viral/cookie', 'sponsor/angle cookie not set: ' + sc);
if (!new RegExp('property="og:url" content="[^"]*[?&]ref=' + tok + '"').test(rb)) note('bug', L + 'viral/og', 'og:url does not carry the ref');
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('; ');
const ub = await ru.text();
if (ru.status() !== 200 || /iap\.sponsor=/.test(su) || /ref=nobody_zz9/.test(ub)) note('bug', L + 'viral/unknown', 'unknown ref must serve the plain page with no cookie and a clean og:url: ' + 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');
flush(bag, L + 'logout');
// admin
await page.goto(LOCAL + '/admin', { waitUntil: 'networkidle' });
await page.fill('#adEmail', process.env.ADMIN_EMAIL || 'martybostick@gmail.com'); await page.click('#adSend'); await page.waitForSelector('#adVerify:not([hidden])'); await page.click('#adVerify'); await page.waitForTimeout(1200);
for (const pn of ['overview', 'house', 'campaigns', 'members', 'reports', 'traffic', 'blog', 'releases', 'pnl', 'settings']) {
const label = L + 'admin#' + pn;
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200);
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
if (!vis) note('bug', label, 'pane did not render');
await domChecks(page, label); flush(bag, label);
await page.screenshot({ path: OUT + '/admin-' + pn + '.png' }).catch(() => {});
console.log('ok', label);
}
await page.click('.bo-menu [data-pane="house"]'); await page.waitForTimeout(500);
for (const t of ['banner', 'text', 'login', 'solo', 'video', 'featured', 'visits']) { await page.selectOption('#hType', t); await page.waitForTimeout(120); }
await page.selectOption('#hType', 'text'); await page.click('#hCreate'); await page.waitForTimeout(800);
if (!(await page.$('#hErr:not([hidden])'))) note('warn', L + 'admin house form', 'empty submit showed no validation message');
flush(bag, L + 'admin house form');
await ctx.close();
}
await browser.close();
const bugs = findings.filter(f => f.sev === 'bug'), warns = findings.filter(f => f.sev === 'warn');
const lines = ['===== QA WALK (' + MODE + ') ' + new Date().toISOString() + ' =====', 'bugs: ' + bugs.length + ' | warnings: ' + warns.length,
...findings.map(f => '[' + f.sev + '] ' + f.where + ' :: ' + f.what)];
console.log('\n' + lines.join('\n'));
fs.writeFileSync(OUT + '/walk-report.txt', lines.join('\n') + '\n');
process.exit(bugs.length ? 1 : 0);
+4 -2
View File
@@ -47,7 +47,7 @@ const refill = require('./refill'); // campaign running-low / ran-out notices,
const snapshot = require('./snapshot'); // daily growth snapshot -> Telegram payments feed (Marty, 2026-09-15)
const pipeline = require('./pipeline'); // sponsor follow-up board (coming soon until site setting pipelineMode = on) (Marty, 2026-09-15)
const videomaker = require('./videomaker'); // Circuit tool: promo videos with the member's own end card (ffmpeg in the image) // badge-gated promo toolkit + AI Copy Engine (Surge and up) (Marty, 2026-09-14) // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard']);
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard', '/friday']);
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
@@ -331,6 +331,7 @@ const JOIN_ANGLES = {
instant: { t: 'Paid before the page reloads.', d: 'A smart contract on Polygon splits every ad package the moment it sells. Same transaction, real wallets, public ledger. Join free by email.' },
adspend: { t: 'You were buying ads anyway.', d: 'Here the ad spend in your line pays you, in the same transaction, on a public ledger. Seven formats, packages from $5. Join free.' },
free: { t: 'Watch first. Spend never.', d: 'Join free, view a few ads, earn credits, run your first campaign for zero dollars. Every payout public on Polygon.' },
friday: { t: 'Five Dollar Friday: 20% bonus credits.', d: 'Every Friday, US Central time, any ad package from $5 up earns 20% bonus credits the moment it settles, and every pack pays its sponsor line in the same transaction. Join free by email.' },
ledger: { t: 'No back office. No payday.', d: 'Every payout is a public transaction on Polygon you can read yourself. Nothing is ever held. Join free by email.' },
two: { t: 'Two buyers open level two.', d: 'Every direct buyer pays you 50 percent from their first package. Two qualifying buyers open level two, five open level three. Written in a verified contract.' }
};
@@ -1267,7 +1268,7 @@ const server = http.createServer(async (req, res) => {
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' }];
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: '/friday', title: 'Five Dollar Friday' }, { 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 });
}
@@ -3390,6 +3391,7 @@ const server = http.createServer(async (req, res) => {
return res.end(blog.renderPost(post, related));
}
if (p === '/wallets') return sendFile(res, path.join(PUBLIC_DIR, 'wallets.html'));
if (p === '/friday') return sendFile(res, path.join(PUBLIC_DIR, 'friday.html')); // Five Dollar Friday bridge page: ?ref= works like any page (Marty, 2026-09-25)
if (p === '/launch') return sendFile(res, path.join(PUBLIC_DIR, 'launch.html'));
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);