// Five Dollar Friday (Marty, 2026-09-20). Every Friday, Central time, any ad package of $5 or more // earns +20% bonus credits (purchased-grade, so they run login ads too), credited the moment the // purchase indexes and labelled in the credit log. The day's wave is shown live (landing strip, // Overview card, /api/friday), posted to Telegram at every ten packs and wrapped up at midnight, // and five Fridays in a row earn the Five Fridays badge. Thursday 6 PM CT: an on-site notice and // an email to active members (drip unsubscribes honoured). Friday 9 AM CT: the Telegram kickoff. // // Settings (siteConfig): fridayPromo '1'|'0', fridayBonusPct (20), fridayStart 'YYYY-MM-DD' (first // Friday, 2026-09-25). Everything reads the chain index, never a marketing counter. 'use strict'; const fs = require('fs'); const path = require('path'); let X = {}, FILE = null, S = null; const TZ = 'America/Chicago'; const dayOf = ts => new Date(ts).toLocaleDateString('en-CA', { timeZone: TZ }); const weekdayOf = ts => new Date(ts).toLocaleDateString('en-US', { timeZone: TZ, weekday: 'short' }); const hourOf = ts => Number(new Date(ts).toLocaleString('en-US', { timeZone: TZ, hour: 'numeric', hour12: false })); const isFriday = ts => weekdayOf(ts) === 'Fri'; function init(opts) { X = opts; FILE = path.join(opts.dataDir, 'friday.json'); S = load(); } function load() { try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (e) { return { granted: {}, posts: {}, badges: {}, sent: {} }; } } function save() { try { fs.writeFileSync(FILE, JSON.stringify(S)); } catch (e) {} } function cfg() { const c = (X.siteConfig && X.siteConfig()) || {}; return { on: String(c.fridayPromo == null ? '1' : c.fridayPromo) === '1', pct: Math.max(0, Number(c.fridayBonusPct) || 20), start: String(c.fridayStart || '2026-09-25'), }; } // the next Friday on or after today (Central), as a day key and a label function nextFriday(now) { const t0 = now || Date.now(); for (let d = 0; d < 8; d++) { const ts = t0 + d * 86400000; if (isFriday(ts) && dayOf(ts) >= cfg().start) return dayOf(ts); } return null; } function labelOf(dayKey) { const [y, m, d] = dayKey.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d, 12)).toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' }); } // is the promo live right now (a Friday on or after the start, switched on) function liveNow(now) { const c = cfg(); const t = now || Date.now(); return c.on && isFriday(t) && dayOf(t) >= c.start; } function counts(dayKey) { return purchasesOn(dayKey).length; } // every $5+ purchase on a given Central day, from the chain index function purchasesOn(dayKey) { const out = []; try { for (const ev of X.chain.recentEvents(1e9)) if (ev.type === 'Purchase' && ev.priceCents >= 500 && dayOf(ev.ts) === dayKey) out.push(ev); } catch (e) {} return out; } function paidOn(dayKey) { let wei = 0n, n = 0; try { for (const ev of X.chain.recentEvents(1e9)) if ((ev.type === 'TierPaid' || ev.type === 'AwardPaid') && dayOf(ev.ts) === dayKey) { wei += BigInt(ev.amountWei); n++; } } catch (e) {} return { pol: Number(wei / 10n ** 14n) / 10000, n }; } function stats(dayKey) { const day = dayKey || dayOf(Date.now()); const p = purchasesOn(day); const paid = paidOn(day); const usd = p.reduce((s, e) => s + e.priceCents, 0) / 100; const buyers = new Set(p.map(e => e.buyerId)).size; const bonus = Object.values(S.granted || {}).filter(g => g.day === day).reduce((s, g) => s + g.bonus, 0); return { day, packs: p.length, usd, buyers, polPaid: paid.pol, payouts: paid.n, bonusCredits: bonus }; } // public view function view() { const c = cfg(); const now = Date.now(); const live = liveNow(now); const next = nextFriday(now); return { on: c.on, pct: c.pct, live, today: live ? stats(dayOf(now)) : null, nextFriday: next, nextLabel: next ? labelOf(next) : null, start: c.start }; } // the bonus: once per purchase tx, on a Friday on or after the start async function onEvent(ev) { if (!ev || ev.type !== 'Purchase' || !liveNow(ev.ts) || ev.priceCents < 500) return null; if (S.granted[ev.tx]) return null; const c = cfg(); const bonus = Math.round((Number(ev.creditAmount) || 0) * c.pct / 100); if (bonus <= 0) return null; const a = await X.accounts.byMemberId(ev.buyerId); if (!a || !a.email) return null; const usd = ('$' + (ev.priceCents / 100).toFixed(2)).replace(/\.00$/, ''); await X.ads.addEarned(a.email, bonus, { purchased: true, log: { kind: 'bonus', note: 'Five Dollar Friday: +' + c.pct + '% on your ' + usd + ' package', ref: 'fdf:' + String(ev.tx).slice(0, 30) } }); S.granted[ev.tx] = { day: dayOf(ev.ts), memberId: ev.buyerId, bonus, at: Date.now() }; save(); try { await X.notify(ev.buyerId, 'Five Dollar Friday: +' + bonus + ' bonus credits', 'Your ' + usd + ' package landed on a Five Dollar Friday, so ' + bonus + ' bonus credits (+' + c.pct + '%) were added to your balance. Every pack bought on a Friday gets this. See your balance: https://instantadpay.com/my#campaigns', 'notice'); } catch (e) {} // the wave, every ten packs const st = stats(dayOf(ev.ts)); if (st.packs > 0 && st.packs % 10 === 0 && !S.posts[st.day + ':' + st.packs]) { S.posts[st.day + ':' + st.packs] = Date.now(); save(); X.telegram('\u{1F4B5} Five Dollar Friday · ' + st.packs + ' packs so far today from ' + st.buyers + ' buyers, ' + st.polPaid.toFixed(2) + ' POL paid to members in the same transactions.\nEvery $5+ pack today earns +' + c.pct + '% credits: https://instantadpay.com/my#buy').catch(() => {}); } return bonus; } // five Fridays in a row (the most recent five promo Fridays that have happened), any $5+ pack each function fridayStreak(memberId) { const c = cfg(); const now = Date.now(); const days = []; for (let d = 0; d < 7 * 8 && days.length < 5; d++) { const ts = now - d * 86400000; const k = dayOf(ts); if (isFriday(ts) && k >= c.start && (k < dayOf(now) || liveNow(now))) days.push(k); } let streak = 0; for (const k of days) { if (purchasesOn(k).some(e => e.buyerId === Number(memberId))) streak++; else break; } return streak; } function earnedBadge(memberId) { if (S.badges[memberId]) return true; if (fridayStreak(memberId) >= 5) { S.badges[memberId] = Date.now(); save(); return true; } return false; } // scheduled moments, checked every few minutes: Thursday 18:00 CT notice + email, Friday 09:00 CT kickoff, // Saturday 00:xx CT wrap-up of the Friday that just ended async function tick() { const c = cfg(); if (!c.on) return; const now = Date.now(); const day = dayOf(now); const wd = weekdayOf(now); const h = hourOf(now); const next = nextFriday(now); if (wd === 'Thu' && h >= 18 && next && !S.sent['thu:' + next]) { S.sent['thu:' + next] = now; save(); const subject = 'Tomorrow is Five Dollar Friday'; const text = 'Tomorrow is Five Dollar Friday on InstantAdPay.\n\nAny ad package of $5 or more bought on Friday (Central time) earns +' + c.pct + '% bonus credits, added the moment the purchase settles. Your $5 pays your sponsor line the second it clears, and your line’s $5s pay you. Friday is when we all push at once.\n\nLine up your pack: https://instantadpay.com/my#buy\nWatch the wave on the live ledger: https://instantadpay.com/ledger'; await broadcast(subject, text); } if (wd === 'Fri' && h >= 9 && day >= c.start && !S.sent['kick:' + day]) { S.sent['kick:' + day] = now; save(); X.telegram('\u{1F4B5} It’s Five Dollar Friday. Any ad package from $5 up earns +' + c.pct + '% credits today, and every pack pays its sponsor line in the same transaction. Buy yours, then watch the ledger fill up: https://instantadpay.com/my#buy').catch(() => {}); } if (wd === 'Sat' && h < 3) { const fri = dayOf(now - 86400000); if (isFriday(now - 86400000) && fri >= c.start && !S.sent['wrap:' + fri]) { S.sent['wrap:' + fri] = now; save(); const st = stats(fri); const top = await topLines(fri); X.telegram('\u{1F3C1} Five Dollar Friday wrap-up (' + labelOf(fri) + ')\n' + st.packs + ' packs from ' + st.buyers + ' buyers · $' + st.usd.toFixed(0) + ' in ad packages · ' + st.polPaid.toFixed(2) + ' POL paid to members · ' + st.bonusCredits.toLocaleString() + ' bonus credits handed out' + (top.length ? '\nBiggest lines today: ' + top.map(t => t.who + ' (' + t.packs + ')').join(', ') : '') + '\nNext one is a week away. Line it up: https://instantadpay.com/my#buy').catch(() => {}); } } } // most packs bought by a sponsor's directs on a day (the line that pushed hardest) async function topLines(dayKey) { const by = new Map(); for (const ev of purchasesOn(dayKey)) { let sp = 0; try { const m = await X.chain.member(ev.buyerId); sp = m && m.sponsorId ? Number(m.sponsorId) : 0; } catch (e) {} if (!sp) continue; by.set(sp, (by.get(sp) || 0) + 1); } const rows = [...by.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3); const out = []; for (const [id, packs] of rows) { let who = '#' + id; try { const a = await X.accounts.byMemberId(id); if (a && a.username) who = '@' + a.username; } catch (e) {} out.push({ memberId: id, who, packs }); } return out; } // on-site notice + email to every active member (quiet 45+ days skipped, drip unsubscribes honoured) async function broadcast(subject, text) { let all = []; try { all = await X.accounts.listAll(5000); } catch (e) { return; } const now = Date.now(); let n = 0; for (const a of all) { if (!a.email) continue; if (now - (a.lastSeen || a.created || 0) > 45 * 86400000) continue; try { await X.notify(a.memberId, subject, text, 'notice'); } catch (e) {} try { if (X.mailer && X.mailer.hasKey() && !(X.drip && await X.drip.isUnsubscribed(a.email))) { await X.mailer.send(a.email, subject, text + '\n\nInstantAdPay'); n++; } } catch (e) {} } return n; } module.exports = { init, cfg, view, stats, onEvent, tick, fridayStreak, earnedBadge, liveNow, nextFriday, isFriday, labelOf, topLines };