diff --git a/chatbot.js b/chatbot.js
index a35aa3d..7af2bb0 100644
--- a/chatbot.js
+++ b/chatbot.js
@@ -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,
diff --git a/public/assets/my.js b/public/assets/my.js
index 1543bb3..333f919 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -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 = '' + esc(g.title) + '' + g.items.length + (g.items.length === 1 ? ' banner' : ' banners') + '
';
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 = 'Post any of these with your Five Dollar Friday link: ';
+ 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');
diff --git a/public/assets/promo.js b/public/assets/promo.js
index 51899b5..fc6157e 100644
--- a/public/assets/promo.js
+++ b/public/assets/promo.js
@@ -8,9 +8,15 @@ window.IAPPromo = (function () {
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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=. 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');
diff --git a/public/assets/site.css b/public/assets/site.css
index 5b538ca..4369e20 100644
--- a/public/assets/site.css
+++ b/public/assets/site.css
@@ -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}
diff --git a/public/banners/iap-five-dollar-friday-dark-1200x630.jpg b/public/banners/iap-five-dollar-friday-dark-1200x630.jpg
new file mode 100644
index 0000000..0cddaa9
Binary files /dev/null and b/public/banners/iap-five-dollar-friday-dark-1200x630.jpg differ
diff --git a/public/banners/iap-five-dollar-friday-v2-1080x1080.jpg b/public/banners/iap-five-dollar-friday-v2-1080x1080.jpg
new file mode 100644
index 0000000..e6c57fd
Binary files /dev/null and b/public/banners/iap-five-dollar-friday-v2-1080x1080.jpg differ
diff --git a/public/banners/iap-five-dollar-friday-v2-1200x630.jpg b/public/banners/iap-five-dollar-friday-v2-1200x630.jpg
new file mode 100644
index 0000000..7f76b00
Binary files /dev/null and b/public/banners/iap-five-dollar-friday-v2-1200x630.jpg differ
diff --git a/public/friday.html b/public/friday.html
new file mode 100644
index 0000000..708a0b5
--- /dev/null
+++ b/public/friday.html
@@ -0,0 +1,110 @@
+
+
+
+
+Five Dollar Friday: 20% bonus credits on every $5+ ad package | InstantAdPay
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Every Friday · US Central time · midnight to midnight
+
Five Dollar Friday: 20% bonus credits on every $5+ ad package.
+
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.
Email first, wallet later, inside your account. Nothing to install to look around.
+
Step 2
Buy any pack from $5 on a Friday.
Central time, midnight to midnight. The 20% bonus is added to your credits the moment the purchase settles, and labelled in your credit log.
+
Step 3
Your pack pays the line above you. Theirs pays you.
Every package is one Polygon transaction that splits to the sponsor line instantly. Public ledger, nothing held, nothing pending.
+
+
+
What a credit is. 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.
+
+
Members: this page is your Friday promo tool. Add ?ref=yourusername 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 Promo tools.
+
+
No income is guaranteed. Results depend on your effort. Cryptocurrency involves risk of loss. InstantAdPay sells advertising; it is not an investment product.
- InstantAdPay
- Login ad sponsor — click "Open Ad" to begin.
- …
-
-
-
-
-
Today's login sponsor
-
-
-
The ad opens in a new tab. The timer counts down right here —
- when it hits zero, your dashboard button appears above.
-
Why you are seeing this again:
- to keep resources optimised we do not keep anyone signed in for longer than
- 4 hours. After that your session ends and you re-authenticate here.
- It is not an error, and nothing in your account is affected.
-
-
-
-
-
-
-
- InstantAdPay
- Welcome tour: meet the three levels your growth pays.
- …
-
-
-
-
-
-
-
-
-
-
-
-
-
Welcome to InstantAdPay
-
Pick your username to continue
-
This is how your team sees you, and it becomes your invite link and your public page.
- It is permanent once saved, so pick the one you want to be known by. We suggested one from your email; keep it or change it.
-
-
Optional — a short bio for your public page (you can skip this).
name bought a $20+ package: counts as a qualifying buyer for their sponsor (on level 1, that is you) · name2 their own qualifying buyers · name joined, no $20+ buy yet · + open spot still open
Codes from partner sites add free ad credits to your account. One use per code.
-
-
-
-
-
-
-
-
-
-
Your sponsor has your back
-
Questions getting started? Message your sponsor directly.
- If they are online you chat live; if not, it lands in their inbox and they are notified.
-
-
-
-
Your invite link, live from day one
-
Share it anywhere, starting now. Everyone who joins through it becomes part of
- your line, and you earn 50 percent of every ad package they ever buy, plus levels 2 and 3 as you
- qualify. Just switch on payouts before your people start buying so every payment locks to you.
-
…
-
-
-
-
-
-
-
-
Your links: what is working
-
Views, joins and qualifying buyers for each hook. Add ?v=instant, ?v=adspend, ?v=free, ?v=ledger or ?v=two to the end of your link to pick the hook the page opens with.
-
-
Where your visitors came from (referring site of the first visit; "direct" is a typed or pasted link, a text message, or an email)
-
-
-
-
Your line
-
Everyone who joined through your link, newest first. When they buy their
- first package of $20 or more, they count toward your qualification.
-
Nobody yet. Your link is ready above; share it and this list starts filling.
-
-
-
Holding tank
members who arrived with no sponsor
-
People who joined without a sponsor wait here. Adopt one and you become their sponsor: a chat opens, they get an email with your message, and their first purchase binds to you on the contract. First come. At most 2 open adoptions at a time, and an adoption goes back to the tank after 7 days if they have not linked a wallet or bought.
-
-
-
Loading…
-
-
-
Coach your directs
-
Reach your free referrals. If there is no message from you to a free referral for 10 days, you get a warning; at 14 days they move to the holding tank so someone else can help them. A chat, a nudge, or tapping Contacted them resets the clock. Nothing on-chain ever moves.
-
Where each person you referred sits on the ladder, who has gone quiet, and what to say next. Nudge opens a chat with the message already written; edit it if you like, then send. Quiet members also get one automatic reminder email per step from InstantAdPay, so your nudge is the personal one.
-
-
Loading…
-
-
-
People you have talked to
-
Your own follow-up list. Add anyone you mentioned InstantAdPay to, give them a status and a follow-up day, and the next conversation stays in front of you. Only you can see this.
-
-
Nobody on the list yet.
-
-
-
-
Your downline, three levels deep
-
Everyone below you across levels 1–3. The POL figure is what each person has paid you so far, straight from the ledger. You see email addresses for your
- direct referrals only; deeper levels show username and ID. Message your direct referrals right here.
-
Loading…
-
-
-
-
Message your team
-
Send one broadcast a day to your downline. It lands in their on-site inbox
- and their email. Reaches the people who joined under you — they can't opt out of their sponsor,
- so make it count.
-
-
Start from a template:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Messages from your upline
-
-
-
-
-
-
-
-
Pipeline: coming soon
-
One board for everyone you are working with. Every prospect and every direct sits in a column by what they have actually done: talking, joined, wallet linked, payouts on, bought, building. The site moves the cards when the event happens on the ledger. You add the part it cannot know: a note, a follow-up date, and the message for that exact stage, sent through the chat you already have.
Cards with a follow-up date of today or earlier. Cards flagged stalled have been quiet for three days or more.
-
Nothing due. Set a follow-up date on any card and it shows up here.
-
-
-
Your board
-
Every column is a stage the site can prove. Cards move on their own; open one to add a note, a follow-up date or a tag.
-
-
-
-
-
-
-
-
-
Message for this stage
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Buy an ad package
-
Priced in dollars, settled from your wallet at the live rate. Credits mint
- to this account instantly, and every payout in your sponsor line lands the moment you confirm.
-
- Using Trust Wallet? It blocks purchases that spend most of the POL in the wallet (a "drain your wallet" warning). Start with a smaller package, keep a little extra POL, or connect MetaMask, Phantom or SafePal instead. Extra POL always stays yours.
-
- Buy from
-
- Your wallet app must be on this account when you confirm.
-
-
Loading live prices…
-
-
-
Qualified Start: your own positions
-
Qualification is earned by buyers you refer, and it is never for sale. But nothing stops you from being your own first buyers, openly. Link a second wallet of your own as a position under your account. When it buys a $20 or larger package, the contract counts it as a qualifying buyer, half the purchase comes straight back to your main wallet, and the credits it mints pool with yours. Two positions open level 2 the same day. Five open level 3.
-
-
Link your main wallet and switch on payouts (Wallet tab) so positions can register under you.
-
In your wallet app, create or switch to a different account (MetaMask: account menu, Add account. Trust or SafePal: switch wallet). Put enough POL in it for a $20 package plus gas.
-
Click Add a position, pick that account in the picker, and sign once.
-
Choose it under Buy from above and buy a $20 or larger package.
-
-
-
-
-
Your own money, your own wallets, a faster start. It is not an income promise: qualification only pays on future purchases in your line, and every purchase here is a real ad package you can spend.
-
Positions are for qualifying. Up to five per account, each registered directly under your main member. Once you are qualified, buy from your main wallet: that keeps your own sponsor paid in full, the same way your people's purchases pay you.
-
-
-
Qualified Start calculator
-
What it costs to open each level with your own positions, what comes straight back to your main wallet, and the ad credits you end up holding.
-
-
-
-
-
-
-
-
-
-
-
-
Campaigns unlock with your first package
-
Campaigns spend the on-chain credits a package mints. Grab any package on the
- home page and this tab turns into your campaign manager.
-
-
-
Available to spend
-
0credits
-
-
-
-
-
-
Where your ads run
-
Every format has its own surfaces inside InstantAdPay, and banner and text ads are also pushed into Network Ad Space, the Crypto Team Build Network's ad rotation that runs across the network's other member sites and tools. Impressions delivered out there are reconciled into your campaign stats here, so one budget works the whole network.
-
-
Format
Shows up on
-
Banner
The ad viewer members open to earn (10-second dwell plus a human check), the home page, the live ledger, every member's Overview, the member-area sidebar tile, and the Network Ad Space rotation across the wider network.
-
Text ad
The ad viewer, the live ledger's text slot, and the Network Ad Space rotation.
-
Login ad
Full screen for every member who signs in, ten seconds, once a day per member.
-
Solo ad
Member inboxes under Earn credits, each read timed and rewarded, so the message actually gets opened.
-
Video ad
Watch videos and the Shorts feed under Earn credits, credited only on a completed watch.
-
Featured link
The Featured strip at the top of every member's Overview, for one, two or seven days.
-
Verified visits
A distinct member opens your site in a new tab, stays eight seconds and passes a check. Nobody counts twice.
-
-
-
-
Your ad campaigns
-
…
-
-
New campaign
-
-
-
-
-
-
- Network Ad Space can deliver a lot of impressions fast. A daily cap spreads a banner or text budget over days instead of hours.
-
- Show to:
-
-
-
- All three ticked = everyone. Untick tiers to narrow it. Geo applies to delivery on this site; a narrowed banner or text ad is kept off the worldwide partner network. IP geolocation by DB-IP.
-
-
-
-
-
-
- Leave both empty to start now and run until the budget is spent. Times are your local time. Anything left when a campaign ends goes back to Available.
-
-
-
-
Banner and text targets are shown full screen in the
- ad viewer, so those URLs must allow framing — we check them the moment you submit. Login and
- solo links open in a fresh tab, so any working page is fine.
-
-
A link to a web page will not work here. Get one from Promo tools > Banners > Copy image URL, or upload your own.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Pick a start day (occupancy shown per day):
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Earn credits by viewing ads
-
You will never be shown your own ads here. Everything you view, watch, visit or read on this tab comes from other members. Your own campaigns are seen by them, and by the wider network.
-
Each ad opens full screen in its own tab, pointed at the advertiser's
- real site. A countdown runs while you watch; when it hits zero, pass a quick check and the
- view counts. Finish today's set, claim your credits, and spend them on banner and text
- campaigns just like purchased ones.
- Press the button to open the first ad in a new tab.
-
-
-
-
-
-
-
-
-
-
-
-
Watch videos, earn credits
-
Watch a member's video for its required time and earn credits. The player
- can't be skipped — the timer runs on our server. You never see your own videos.
- Prefer a feed? Open Shorts for a full-screen, swipe-to-next experience.
Visit a member's site in a new tab, stay the required few seconds,
- pass a quick check, and earn. Each site counts once for you — real, unique human visits.
Full-message ads from members land here — each one delivered to you at
- most once, never your own. Read one and click through to the advertiser to claim ….
Wondering why someone did or did not get paid on a purchase? Enter a member number or username (yourself, anyone up to 3 levels below you, or your own uplines) and every purchase they were part of is laid out tier by tier: who was paid, who was skipped and why.
-
-
-
-
-
-
-
-
-
-
-
Earnings
-
Every payout the contract has sent to your wallet. Each line links to the transaction.
-
Nothing yet.
-
-
-
Your referrals
-
Activations and qualifying buys in your line.
-
Nothing yet.
-
-
-
-
Your purchases
-
Nothing yet.
-
-
-
-
Your on-chain history starts with payouts
-
Once payouts are switched on, every payment, referral event, and purchase in
- your line shows here with a verify link to the blockchain. Nothing to claim, nothing to request:
- the money is already in your wallet when the line appears.
-
-
-
-
-
-
-
Your links
one invite link, six front doors
-
-
Your invite link
…
-
-
-
Every link below is your invite link with an angle code on the end. Same account, same credit, different first page. Pick the one that matches what you said to the person, and Link stats on the Coaching pane shows which angles bring joins and buyers.
free to play, pays in POL, every sign-up lands on your line
-
PolHunter is the easy front door. A person visits one of our sites, finds their code, taps it, and a drip of POL lands in their wallet. To play they need a free InstantAdPay account with a wallet switched on, so everyone you send in is set up and on your line before they ever see an ad package.
two forty-second vertical spots, captions built in, no voice-over
-
- Where each one works best
TikTok: the animated one first. Upload the file itself, never a link. The hook is built into the first two seconds. Paste the caption, keep three to five hashtags, and put your PolHunter link in your bio; TikTok does not make caption links clickable. Answer comments with "link in bio".
Instagram Reels and Stories: either spot; animated tends to travel further on Reels. The link goes in your bio, or post the spot to your Story with the link sticker, which is the one place on Instagram the link is tappable.
YouTube Shorts: the photoreal one. Title it with PolHunter, put the link in the description and pin it as the first comment.
Facebook feed, Reels and groups: the photoreal one for an older crowd. Feed and group posts can carry the link right in the caption. Facebook keeps your name on the link when you use the share buttons here.
X: either spot, uploaded natively, with the caption and the link in the post.
Telegram and WhatsApp: send the video file with your link and one line under it, or use the share buttons to send the link with the caption.
Two rules: post one spot, then the other a few days later, never both in one post. And never promise amounts; the drips are small and random, and the caption already says free to play.
Pick a page. The link you get carries your name on the end. Whoever opens it is tagged to you for 30 days, exactly like your invite link, so a blog article you share can bring you a member.
-
-
Your viral link
…
-
-
Views and joins from these links show under Link stats as the "any page" hook. Last touch still wins: the most recent link a person opened is the one that gets the credit.
-
-
-
Your toolkit
-
Every badge unlocks more. Free tools are yours from day one; the AI Copy Engine opens at Surge, your first qualifying buyer.
-
-
-
AI Copy Engine
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Spark: campaign templates and handout
-
-
-
One tap builds a campaign aimed at your invite link, funded from the credits you already hold. Edit or pause it any time under Campaigns.
Every promo video, re-rendered with your own end card: your name, your link and your QR on the last four seconds. Made once, hosted for you, ready to download or post.
-
-
Which angle is pulling for you
-
-
-
-
-
Nexus: Leader Ops
-
-
-
-
-
Team credit grant
-
Move ad credits from your earned pool to anyone in your three levels. They get a message from you with it.
-
-
Your partner code and kit
-
-
Co-branded join page: your bio (Profile) shows under your name on your invite page, so a visitor sees who is inviting them.
-
-
-
-
-
Social posts, personalized with your link
-
Copy and paste, or post straight from here. Every post already carries your invite link,
- so the credit is always yours. Angle links keep working when the matching hook pages ship.
-
-
- Want these in your own voice?
- Branded Voice writes posts, emails and stories the way you actually talk.
- mybrandedvoice.com
-
-
-
-
Text a friend
-
Short, low-pressure messages sized for a text. On your phone, Text it opens your
- messaging app with the message ready, WhatsApp pre-fills it there, Telegram shares your link
- with a lead-in, and Copy works for Messenger, Instagram DMs, anywhere.
-
-
-
-
Email swipes
-
Short, standard and long versions plus a follow-up. Subject line included. Edit freely, keep the honesty lines.
-
-
-
-
Banners and graphics
-
Branded InstantAdPay banners in every standard ad size, plus square, story and Telegram
- group sizes. Open a size to see its banners. Pair one with your invite link. Download, or copy the image URL for ad networks.
-
-
-
-
Your banner wall
-
A public page showing your line banner and your upline ladder, with your
- join link front and center. Every visit puts eyes on your whole line — send traffic here and
- the wall does the pitching.
The walls every builder hits, with answers that hold up. Open one: you get
- the truth, so you never say something a prospect can poke a hole in, and a
- ready-to-send reply with your link. Golden rule: lead with a real example, never a promise.
-
-
-
-
Hook videos
-
Short videos, one per angle, each with a matched invite link that lands on a
- page continuing the same hook. Post the video with the matched link, or download it and use it anywhere.
- Every caption below already carries your link.
-
-
-
-
-
-
-
-
Your profile
-
Your username is how other members see you: on the live ledger, in line
- rosters, and on your personal invite link. Pick something you would put on a business card.
-
-
-
-
-
-
Avatar & bio
-
Your face and your story, shown on your public profile page
- (set a username to get your link).
-
-
-
-
-
-
Social links (https URLs — shown on your public page):
-
-
-
-
-
-
-
-
-
-
-
Intro video on your public page (optional): a YouTube, Vimeo or direct .mp4 link. It plays right under your bio, before the line and the join button.
Set a destination link (and a banner image for your wall), and every new
- member joining your line meets it on their welcome tour: position 1 for your directs, position 2
- for their people, position 3 below that — the same three levels the contract pays you on.
-
-
-
-
-
-
-
-
-
-
-
Your wall: positions 2 and 3
-
Your public wall shows three ads. Position 1 is your line banner above. Positions 2 and 3
- start out showing your upline (or InstantAdPay). Two qualifying buyers make position 2 yours,
- five make position 3 yours: your own links, your own offers, nobody else's ad on your page.
- An unlocked slot you leave empty keeps showing your upline until you fill it.
-
-
-
-
Position 2
-
-
-
-
-
-
-
-
-
-
Position 3
-
-
-
-
-
-
-
-
-
-
-
-
-
Sponsor chat
-
Your line can message you one-to-one for help, and you can message anyone
- below you. When you are online you chat live; when you are away, messages wait in your inbox and
- we email you. Turn this off and you stop taking live chats — people can still leave you a note,
- you just reply on your own time.
-
-
-
-
Account details
-
…
-
-
-
-
-
-
-
Training center
-
Everything to help you get started and grow: walkthrough videos and materials.
- New lessons land here as they are added.
-
-
-
-
-
-
-
-
-
Get test POL
-
This is the Polygon Amoy testnet, so purchases use valueless test POL, not real money.
- Grab free test POL for your connected wallet from the Amoy faucet, then buy a package and watch the payouts
- move on the live ledger exactly like mainnet will.
-
-
-
-
-
Wallet connection
-
Everything here is POL on the Polygon network. Packages are paid in POL and every payout lands as POL in this wallet. No other coin or chain is used, so keep the wallet on Polygon and keep a little POL for gas.
-
Connected the wrong wallet, or stuck on a transaction prompt? Disconnect to clear the session, then reconnect or pick a different wallet.
-
-
-
-
-
Your positions
-
Extra wallets you own, registered under your main member. Their credits pool with yours and each one that buys $20 or more counts toward your qualification.
1 credit = 1 cent of ad delivery across the network. Purchased credits are recorded on-chain; earned credits (viewing, promo codes, welcome bonus) live on your account. Only your campaigns can spend them.
-
-
-
Link your wallet
-
Your earnings pay straight to your own wallet, so we need to know which one is yours.
- One free signature links it. It cannot move funds or approve anything. Buying any package links it automatically too.
-
-
-
-
Switch on payouts, free
-
One free transaction registers your linked wallet on-chain so commissions can reach it,
- and it unlocks the full flow. Buying any package does this automatically.
+ InstantAdPay
+ Login ad sponsor — click "Open Ad" to begin.
+ …
+
+
+
+
+
Today's login sponsor
+
+
+
The ad opens in a new tab. The timer counts down right here —
+ when it hits zero, your dashboard button appears above.
+
Why you are seeing this again:
+ to keep resources optimised we do not keep anyone signed in for longer than
+ 4 hours. After that your session ends and you re-authenticate here.
+ It is not an error, and nothing in your account is affected.
+
+
+
+
+
+
+
+ InstantAdPay
+ Welcome tour: meet the three levels your growth pays.
+ …
+
+
+
+
+
+
+
+
+
+
+
+
+
Welcome to InstantAdPay
+
Pick your username to continue
+
This is how your team sees you, and it becomes your invite link and your public page.
+ It is permanent once saved, so pick the one you want to be known by. We suggested one from your email; keep it or change it.
+
+
Optional — a short bio for your public page (you can skip this).
name bought a $20+ package: counts as a qualifying buyer for their sponsor (on level 1, that is you) · name2 their own qualifying buyers · name joined, no $20+ buy yet · + open spot still open
Codes from partner sites add free ad credits to your account. One use per code.
+
+
+
+
+
+
+
+
+
+
Your sponsor has your back
+
Questions getting started? Message your sponsor directly.
+ If they are online you chat live; if not, it lands in their inbox and they are notified.
+
+
+
+
Your invite link, live from day one
+
Share it anywhere, starting now. Everyone who joins through it becomes part of
+ your line, and you earn 50 percent of every ad package they ever buy, plus levels 2 and 3 as you
+ qualify. Just switch on payouts before your people start buying so every payment locks to you.
+
…
+
+
+
+
+
+
+
+
Your links: what is working
+
Views, joins and qualifying buyers for each hook. Add ?v=instant, ?v=adspend, ?v=free, ?v=ledger or ?v=two to the end of your link to pick the hook the page opens with.
+
+
Where your visitors came from (referring site of the first visit; "direct" is a typed or pasted link, a text message, or an email)
+
+
+
+
Your line
+
Everyone who joined through your link, newest first. When they buy their
+ first package of $20 or more, they count toward your qualification.
+
Nobody yet. Your link is ready above; share it and this list starts filling.
+
+
+
Holding tank
members who arrived with no sponsor
+
People who joined without a sponsor wait here. Adopt one and you become their sponsor: a chat opens, they get an email with your message, and their first purchase binds to you on the contract. First come. At most 2 open adoptions at a time, and an adoption goes back to the tank after 7 days if they have not linked a wallet or bought.
+
+
+
Loading…
+
+
+
Coach your directs
+
Reach your free referrals. If there is no message from you to a free referral for 10 days, you get a warning; at 14 days they move to the holding tank so someone else can help them. A chat, a nudge, or tapping Contacted them resets the clock. Nothing on-chain ever moves.
+
Where each person you referred sits on the ladder, who has gone quiet, and what to say next. Nudge opens a chat with the message already written; edit it if you like, then send. Quiet members also get one automatic reminder email per step from InstantAdPay, so your nudge is the personal one.
+
+
Loading…
+
+
+
People you have talked to
+
Your own follow-up list. Add anyone you mentioned InstantAdPay to, give them a status and a follow-up day, and the next conversation stays in front of you. Only you can see this.
+
+
Nobody on the list yet.
+
+
+
+
Your downline, three levels deep
+
Everyone below you across levels 1–3. The POL figure is what each person has paid you so far, straight from the ledger. You see email addresses for your
+ direct referrals only; deeper levels show username and ID. Message your direct referrals right here.
+
Loading…
+
+
+
+
Message your team
+
Send one broadcast a day to your downline. It lands in their on-site inbox
+ and their email. Reaches the people who joined under you — they can't opt out of their sponsor,
+ so make it count.
+
+
Start from a template:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Messages from your upline
+
+
+
+
+
+
+
+
Pipeline: coming soon
+
One board for everyone you are working with. Every prospect and every direct sits in a column by what they have actually done: talking, joined, wallet linked, payouts on, bought, building. The site moves the cards when the event happens on the ledger. You add the part it cannot know: a note, a follow-up date, and the message for that exact stage, sent through the chat you already have.
Cards with a follow-up date of today or earlier. Cards flagged stalled have been quiet for three days or more.
+
Nothing due. Set a follow-up date on any card and it shows up here.
+
+
+
Your board
+
Every column is a stage the site can prove. Cards move on their own; open one to add a note, a follow-up date or a tag.
+
+
+
+
+
+
+
+
+
Message for this stage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Buy an ad package
+
Priced in dollars, settled from your wallet at the live rate. Credits mint
+ to this account instantly, and every payout in your sponsor line lands the moment you confirm.
+
+ Using Trust Wallet? It blocks purchases that spend most of the POL in the wallet (a "drain your wallet" warning). Start with a smaller package, keep a little extra POL, or connect MetaMask, Phantom or SafePal instead. Extra POL always stays yours.
+
+ Buy from
+
+ Your wallet app must be on this account when you confirm.
+
+
Loading live prices…
+
+
+
Qualified Start: your own positions
+
Qualification is earned by buyers you refer, and it is never for sale. But nothing stops you from being your own first buyers, openly. Link a second wallet of your own as a position under your account. When it buys a $20 or larger package, the contract counts it as a qualifying buyer, half the purchase comes straight back to your main wallet, and the credits it mints pool with yours. Two positions open level 2 the same day. Five open level 3.
+
+
Link your main wallet and switch on payouts (Wallet tab) so positions can register under you.
+
In your wallet app, create or switch to a different account (MetaMask: account menu, Add account. Trust or SafePal: switch wallet). Put enough POL in it for a $20 package plus gas.
+
Click Add a position, pick that account in the picker, and sign once.
+
Choose it under Buy from above and buy a $20 or larger package.
+
+
+
+
+
Your own money, your own wallets, a faster start. It is not an income promise: qualification only pays on future purchases in your line, and every purchase here is a real ad package you can spend.
+
Positions are for qualifying. Up to five per account, each registered directly under your main member. Once you are qualified, buy from your main wallet: that keeps your own sponsor paid in full, the same way your people's purchases pay you.
+
+
+
Qualified Start calculator
+
What it costs to open each level with your own positions, what comes straight back to your main wallet, and the ad credits you end up holding.
+
+
+
+
+
+
+
+
+
+
+
+
Campaigns unlock with your first package
+
Campaigns spend the on-chain credits a package mints. Grab any package on the
+ home page and this tab turns into your campaign manager.
+
+
+
Available to spend
+
0credits
+
+
+
+
+
+
Where your ads run
+
Every format has its own surfaces inside InstantAdPay, and banner and text ads are also pushed into Network Ad Space, the Crypto Team Build Network's ad rotation that runs across the network's other member sites and tools. Impressions delivered out there are reconciled into your campaign stats here, so one budget works the whole network.
+
+
Format
Shows up on
+
Banner
The ad viewer members open to earn (10-second dwell plus a human check), the home page, the live ledger, every member's Overview, the member-area sidebar tile, and the Network Ad Space rotation across the wider network.
+
Text ad
The ad viewer, the live ledger's text slot, and the Network Ad Space rotation.
+
Login ad
Full screen for every member who signs in, ten seconds, once a day per member.
+
Solo ad
Member inboxes under Earn credits, each read timed and rewarded, so the message actually gets opened.
+
Video ad
Watch videos and the Shorts feed under Earn credits, credited only on a completed watch.
+
Featured link
The Featured strip at the top of every member's Overview, for one, two or seven days.
+
Verified visits
A distinct member opens your site in a new tab, stays eight seconds and passes a check. Nobody counts twice.
+
+
+
+
Your ad campaigns
+
…
+
+
New campaign
+
+
+
+
+
+
+ Network Ad Space can deliver a lot of impressions fast. A daily cap spreads a banner or text budget over days instead of hours.
+
+ Show to:
+
+
+
+ All three ticked = everyone. Untick tiers to narrow it. Geo applies to delivery on this site; a narrowed banner or text ad is kept off the worldwide partner network. IP geolocation by DB-IP.
+
+
+
+
+
+
+ Leave both empty to start now and run until the budget is spent. Times are your local time. Anything left when a campaign ends goes back to Available.
+
+
+
+
Banner and text targets are shown full screen in the
+ ad viewer, so those URLs must allow framing — we check them the moment you submit. Login and
+ solo links open in a fresh tab, so any working page is fine.
+
+
A link to a web page will not work here. Get one from Promo tools > Banners > Copy image URL, or upload your own.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pick a start day (occupancy shown per day):
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Earn credits by viewing ads
+
You will never be shown your own ads here. Everything you view, watch, visit or read on this tab comes from other members. Your own campaigns are seen by them, and by the wider network.
+
Each ad opens full screen in its own tab, pointed at the advertiser's
+ real site. A countdown runs while you watch; when it hits zero, pass a quick check and the
+ view counts. Finish today's set, claim your credits, and spend them on banner and text
+ campaigns just like purchased ones.
+ Press the button to open the first ad in a new tab.
+
+
+
+
+
+
+
+
+
+
+
+
Watch videos, earn credits
+
Watch a member's video for its required time and earn credits. The player
+ can't be skipped — the timer runs on our server. You never see your own videos.
+ Prefer a feed? Open Shorts for a full-screen, swipe-to-next experience.
Visit a member's site in a new tab, stay the required few seconds,
+ pass a quick check, and earn. Each site counts once for you — real, unique human visits.
Full-message ads from members land here — each one delivered to you at
+ most once, never your own. Read one and click through to the advertiser to claim ….
Wondering why someone did or did not get paid on a purchase? Enter a member number or username (yourself, anyone up to 3 levels below you, or your own uplines) and every purchase they were part of is laid out tier by tier: who was paid, who was skipped and why.
+
+
+
+
+
+
+
+
+
+
+
Earnings
+
Every payout the contract has sent to your wallet. Each line links to the transaction.
+
Nothing yet.
+
+
+
Your referrals
+
Activations and qualifying buys in your line.
+
Nothing yet.
+
+
+
+
Your purchases
+
Nothing yet.
+
+
+
+
Your on-chain history starts with payouts
+
Once payouts are switched on, every payment, referral event, and purchase in
+ your line shows here with a verify link to the blockchain. Nothing to claim, nothing to request:
+ the money is already in your wallet when the line appears.
+
+
+
+
+
+
+
Your links
one invite link, six front doors
+
+
Your invite link
…
+
+
+
Every link below is your invite link with an angle code on the end. Same account, same credit, different first page. Pick the one that matches what you said to the person, and Link stats on the Coaching pane shows which angles bring joins and buyers.
free to play, pays in POL, every sign-up lands on your line
+
PolHunter is the easy front door. A person visits one of our sites, finds their code, taps it, and a drip of POL lands in their wallet. To play they need a free InstantAdPay account with a wallet switched on, so everyone you send in is set up and on your line before they ever see an ad package.
two forty-second vertical spots, captions built in, no voice-over
+
+ Where each one works best
TikTok: the animated one first. Upload the file itself, never a link. The hook is built into the first two seconds. Paste the caption, keep three to five hashtags, and put your PolHunter link in your bio; TikTok does not make caption links clickable. Answer comments with "link in bio".
Instagram Reels and Stories: either spot; animated tends to travel further on Reels. The link goes in your bio, or post the spot to your Story with the link sticker, which is the one place on Instagram the link is tappable.
YouTube Shorts: the photoreal one. Title it with PolHunter, put the link in the description and pin it as the first comment.
Facebook feed, Reels and groups: the photoreal one for an older crowd. Feed and group posts can carry the link right in the caption. Facebook keeps your name on the link when you use the share buttons here.
X: either spot, uploaded natively, with the caption and the link in the post.
Telegram and WhatsApp: send the video file with your link and one line under it, or use the share buttons to send the link with the caption.
Two rules: post one spot, then the other a few days later, never both in one post. And never promise amounts; the drips are small and random, and the caption already says free to play.
Pick a page. The link you get carries your name on the end. Whoever opens it is tagged to you for 30 days, exactly like your invite link, so a blog article you share can bring you a member.
+
+
Your viral link
…
+
+
Views and joins from these links show under Link stats as the "any page" hook. Last touch still wins: the most recent link a person opened is the one that gets the credit.
+
+
+
Your toolkit
+
Every badge unlocks more. Free tools are yours from day one; the AI Copy Engine opens at Surge, your first qualifying buyer.
+
+
+
AI Copy Engine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Spark: campaign templates and handout
+
+
+
One tap builds a campaign aimed at your invite link, funded from the credits you already hold. Edit or pause it any time under Campaigns.
Every promo video, re-rendered with your own end card: your name, your link and your QR on the last four seconds. Made once, hosted for you, ready to download or post.
+
+
Which angle is pulling for you
+
+
+
+
+
Nexus: Leader Ops
+
+
+
+
+
Team credit grant
+
Move ad credits from your earned pool to anyone in your three levels. They get a message from you with it.
+
+
Your partner code and kit
+
+
Co-branded join page: your bio (Profile) shows under your name on your invite page, so a visitor sees who is inviting them.
+
+
+
+
+
Social posts, personalized with your link
+
Copy and paste, or post straight from here. Every post already carries your invite link,
+ so the credit is always yours. Angle links keep working when the matching hook pages ship.
+
+
+ Want these in your own voice?
+ Branded Voice writes posts, emails and stories the way you actually talk.
+ mybrandedvoice.com
+
+
+
+
Text a friend
+
Short, low-pressure messages sized for a text. On your phone, Text it opens your
+ messaging app with the message ready, WhatsApp pre-fills it there, Telegram shares your link
+ with a lead-in, and Copy works for Messenger, Instagram DMs, anywhere.
+
+
+
+
Email swipes
+
Short, standard and long versions plus a follow-up. Subject line included. Edit freely, keep the honesty lines.
+
+
+
+
Banners and graphics
+
Branded InstantAdPay banners in every standard ad size, plus square, story and Telegram
+ group sizes. Open a size to see its banners. Pair one with your invite link. Download, or copy the image URL for ad networks.
+
+
+
+
Your banner wall
+
A public page showing your line banner and your upline ladder, with your
+ join link front and center. Every visit puts eyes on your whole line — send traffic here and
+ the wall does the pitching.
The walls every builder hits, with answers that hold up. Open one: you get
+ the truth, so you never say something a prospect can poke a hole in, and a
+ ready-to-send reply with your link. Golden rule: lead with a real example, never a promise.
+
+
+
+
Hook videos
+
Short videos, one per angle, each with a matched invite link that lands on a
+ page continuing the same hook. Post the video with the matched link, or download it and use it anywhere.
+ Every caption below already carries your link.
+
+
+
+
+
+
+
+
Your profile
+
Your username is how other members see you: on the live ledger, in line
+ rosters, and on your personal invite link. Pick something you would put on a business card.
+
+
+
+
+
+
Avatar & bio
+
Your face and your story, shown on your public profile page
+ (set a username to get your link).
+
+
+
+
+
+
Social links (https URLs — shown on your public page):
+
+
+
+
+
+
+
+
+
+
+
Intro video on your public page (optional): a YouTube, Vimeo or direct .mp4 link. It plays right under your bio, before the line and the join button.
Set a destination link (and a banner image for your wall), and every new
+ member joining your line meets it on their welcome tour: position 1 for your directs, position 2
+ for their people, position 3 below that — the same three levels the contract pays you on.
+
+
+
+
+
+
+
+
+
+
+
Your wall: positions 2 and 3
+
Your public wall shows three ads. Position 1 is your line banner above. Positions 2 and 3
+ start out showing your upline (or InstantAdPay). Two qualifying buyers make position 2 yours,
+ five make position 3 yours: your own links, your own offers, nobody else's ad on your page.
+ An unlocked slot you leave empty keeps showing your upline until you fill it.
+
+
+
+
Position 2
+
+
+
+
+
+
+
+
+
+
Position 3
+
+
+
+
+
+
+
+
+
+
+
+
+
Sponsor chat
+
Your line can message you one-to-one for help, and you can message anyone
+ below you. When you are online you chat live; when you are away, messages wait in your inbox and
+ we email you. Turn this off and you stop taking live chats — people can still leave you a note,
+ you just reply on your own time.
+
+
+
+
Account details
+
…
+
+
+
+
+
+
+
Training center
+
Everything to help you get started and grow: walkthrough videos and materials.
+ New lessons land here as they are added.
+
+
+
+
+
+
+
+
+
Get test POL
+
This is the Polygon Amoy testnet, so purchases use valueless test POL, not real money.
+ Grab free test POL for your connected wallet from the Amoy faucet, then buy a package and watch the payouts
+ move on the live ledger exactly like mainnet will.
+
+
+
+
+
Wallet connection
+
Everything here is POL on the Polygon network. Packages are paid in POL and every payout lands as POL in this wallet. No other coin or chain is used, so keep the wallet on Polygon and keep a little POL for gas.
+
Connected the wrong wallet, or stuck on a transaction prompt? Disconnect to clear the session, then reconnect or pick a different wallet.
+
+
+
+
+
Your positions
+
Extra wallets you own, registered under your main member. Their credits pool with yours and each one that buys $20 or more counts toward your qualification.
1 credit = 1 cent of ad delivery across the network. Purchased credits are recorded on-chain; earned credits (viewing, promo codes, welcome bonus) live on your account. Only your campaigns can spend them.
+
+
+
Link your wallet
+
Your earnings pay straight to your own wallet, so we need to know which one is yours.
+ One free signature links it. It cannot move funds or approve anything. Buying any package links it automatically too.
+
+
+
+
Switch on payouts, free
+
One free transaction registers your linked wallet on-chain so commissions can reach it,
+ and it unlocks the full flow. Buying any package does this automatically.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Messages
+
+
+
+
Loading…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qa/walk.mjs b/qa/walk.mjs
index 5996f90..c774393 100644
--- a/qa/walk.mjs
+++ b/qa/walk.mjs
@@ -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= 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 || !//.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= 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 || !//.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);
diff --git a/server.js b/server.js
index 05ae802..566192f 100644
--- a/server.js
+++ b/server.js
@@ -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);