diff --git a/ads.js b/ads.js index 224f4a0..15a656b 100644 Binary files a/ads.js and b/ads.js differ diff --git a/chatbot.js b/chatbot.js index 8ae9d1f..029f959 100644 --- a/chatbot.js +++ b/chatbot.js @@ -28,7 +28,7 @@ const CANNED = [ { re: /(how (much|do i) earn|commission|percent|split)/i, a: 'Every package splits the same way: 50 percent to the direct sponsor, 20 percent to level 2, 10 percent to level 3, 20 percent to the platform. Those numbers are constants in the contract and cannot be changed. Play with scenarios on the home page calculator. No income is promised; nobody earns unless real ad buying happens.' }, { re: /(balance (went|going|keeps going) down|credits? (disappear|missing|gone|dropping|went down|ticking)|why (is|did) my (balance|credits)|in campaigns|set aside|reserved credits|available to spend)/i, - a: 'One rule: your balance is what is NOT committed to a live campaign. When you start a campaign its whole budget is set aside right then (earned credits first, then purchased), so your Available number drops once and then sits still while the ad runs. The budget spends down inside that campaign in Members > Campaigns, where you can see delivered views, network views and what is left. The dashboard shows Purchased available, Earned available and In live campaigns as three separate numbers; a paused campaign keeps its unspent budget set aside so it can resume. Nothing is lost when a balance reads low while a campaign is live. Purchased credits live on the blockchain; if we ever refund or credit purchased money it shows under Purchased as "credited to you" and spends like a purchase, login ads included. Earned credits fund every format except login ads.' }, + a: 'One rule: your balance is what is NOT committed to a live campaign. When you start a campaign its whole budget is set aside right then (earned credits first, then purchased), so your Available number drops once and then sits still while the ad runs. The budget spends down inside that campaign in Members > Campaigns, where you can see delivered views, network views and what is left. The dashboard shows Purchased available, Earned available and In live campaigns as three separate numbers; a paused campaign keeps its unspent budget set aside so it can resume. Nothing is lost when a balance reads low while a campaign is live. Purchased credits live on the blockchain; if we ever refund or credit purchased money it shows under Purchased as "credited to you" and spends like a purchase, login ads included. Earned credits fund every format except login ads. Two formats are flat buys paid in full when booked: featured links (sold by the day) and verified-visit packs. Adding credits to a featured link does nothing, so its row offers Extend run instead, which books more days at the daily rate; a visit pack offers Buy more visits.' }, { re: /(qualif|unlock level|level 2|level 3|pass.?up)/i, a: 'Two different things are earned here. CREDITS: every member, free ones included, earns ad credits by viewing ads, videos, visits and the daily sign-in; credits buy your own ads and are never withdrawn. POL: real crypto paid to your wallet when your referrals buy packages, once you are activated: the $20 starter package plus payouts switched on from your wallet. Level 1 pays 50 percent of the packages your direct referrals buy, from the day you are activated. Bring 2 buyers of $20 or more and level 2 unlocks; 5 unlock level 3. When a level is not qualified, its share climbs the sponsor line, checking up to 25 positions, and pays the first qualified person. Qualification never expires and cannot be bought. Qualified Start: you may link extra wallets of your own as positions under your account (Buy packages > Qualified Start); each one that buys a $20+ package counts as a qualifying buyer, its credits pool with yours, and 50% of its purchase comes back to your main wallet. Your own money, your own wallets, a faster start, never an income promise.' }, { re: /(free member|without (a )?wallet|do i (have to|need to) buy|earn (in )?credits|credits (vs|versus|or) pol|earn pol|get paid in)/i, @@ -91,7 +91,7 @@ FACTS: - PIPELINE (coming soon, built 2026-09-15; opens when the site setting pipelineMode is on): a follow-up board on the dashboard, tab 'Pipeline' between My line and Buy packages. Columns: Talking to, Joined, Wallet linked, Payouts on, Bought, Building, Later. Prospects (from the My line prospect list) and directs are placed automatically from what they have actually done (coaching rung, on-chain buys); nobody drags cards. The sponsor adds a note, a follow-up date and a tag (hot, later, no response, not interested); a 'Follow up today' strip lists due cards; stalled cards (quiet 3+ days) are flagged; each card carries the message for its exact stage with 'Open chat with this message' (member chat) or copy. Until it opens, the tab shows a coming-soon card with the roadmap date. - SPONSOR NUDGE ON A HELD BUY (2026-09-15): when a referral's purchase is held because the sponsor has not switched on payouts, the sponsor is emailed and gets an on-site message the same minute (once an hour at most) telling them who is waiting and to link a wallet / switch on payouts on the Wallet tab; nothing is charged to the buyer, who can simply try again once the sponsor is ready. - DRILL DOWN FROM THE OVERVIEW (2026-09-15): every name chip in 'Your line at a glance' on the Overview is clickable; it jumps to My line with that person's Activity panel already open and the row highlighted. -- VISIT PACKS ARE PAID UP FRONT (2026-09-15, Michael's question): a verified-visits campaign charges the whole pack when it is created (3 credits per visit, so 1,000 visits = 3,000 credits) and then delivers over time as members complete verified visits (each member can do at most 20 a day, so packs fill gradually: tens per day at today's size). The Campaigns row shows 'paid up front' and 'N of M visits delivered'; the credits are not being consumed again, they were reserved once. Same for featured-day bookings. +- VISIT PACKS ARE PAID UP FRONT (2026-09-15, Michael's question): a verified-visits campaign charges the whole pack when it is created (3 credits per visit, so 1,000 visits = 3,000 credits) and then delivers over time as members complete verified visits (each member can do at most 20 a day, so packs fill gradually: tens per day at today's size). The Campaigns row shows 'paid up front' and 'N of M visits delivered'; the credits are not being consumed again, they were reserved once. Same for featured-day bookings: a featured link is a flat buy (featuredPerDay credits per day, whole run charged when booked), its row shows 'paid up front', the day count and 'runs through ', and it ends on its last day ('run ended'). FEATURED CANNOT TAKE ADDED CREDITS (2026-09-16, Hugh and Michael both tried): the row offers 'Extend run' which books more days after the current run (or from today if it ended) and charges them right away; there is no 'Buy more views' on a featured row any more. Visit packs offer 'Buy more visits' (charged now, adds to the pack). Credits added to a campaign are set aside immediately (Available drops at once) and whatever is unspent returns to Available when the campaign ends. - PAYMENT + MISSED-PAYMENT NOTICES (2026-09-15): every payout that lands (who bought, level, share in POL and dollars, transaction link) and every missed payout is delivered BOTH by email and as an on-site inbox message from the company account, shown in the login pop-up and the Messages card, so it is waiting when the member signs in. MISSED-PAYOUT detail: when a level-2 or level-3 share passes a member by because that level is not open on their account, the member gets an email the same minute: who bought, the POL and dollar amount they missed, how many qualifying buyers they have versus the 2 or 5 needed, and the two ways to close the gap (bring buyers, or Qualified Start). Qualified members whose wallet rejected a transfer get a different email telling them to link a regular wallet. - LAUNCH WEEK SWIPES (2026-09-15): the founding-week checklist page (/launch) ends with four promoter emails members send to their OWN lists, one a day toward Monday's opening, with the member's invite link and the FOUNDER code (500 credits for anyone who joins before Mon 2026-09-21 9 AM Central) filled in, each with a Copy button. These are swipes for members, not emails the site sends. - LOGIN ADS CHARGE ONLY ON DAYS SHOWN (2026-09-15): the login ad daily fee (100 credits) is charged only for days the ad was actually shown at least once; with many login ads sharing the sign-ins, an ad that was not picked that day pays nothing. Featured links now count views (one per viewer per hour) as well as clicks. diff --git a/public/assets/my.js b/public/assets/my.js index 3732ff0..43650d8 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -819,19 +819,43 @@ + '' + (['banner', 'text'].includes(c.type) ? (c.impsNas || 0).toLocaleString() : 'n/a') + '' + '' + c.clicks + (r.clickSources && r.clickSources[c.id] ? '
' + Object.entries(r.clickSources[c.id]).sort((a, b) => b[1] - a[1]).map(([k, v]) => '' + esc(k) + ' ' + v + '').join('') + (c.impsNas ? ' · network: see Network views' : '') + '
' : '') + '' + (c.type === 'visits' ? 'paid up front' + c.imps.toLocaleString() + ' of ' + Number(c.body || 0).toLocaleString() + ' visits' + : c.type === 'featured' ? 'paid up front' + Number(c.body || 0) + ' day' + (Number(c.body || 0) === 1 ? '' : 's') + '' : '' + c.spent + '' + c.budget + '') - + '' + (c.status === 'out' ? (c.type === 'visits' ? 'pack delivered' : 'budget spent') : c.status === 'done' ? 'ended' : c.scheduled ? 'scheduled' : c.status) + '' + + '' + (c.status === 'out' ? (c.type === 'visits' ? 'pack delivered' : 'budget spent') : c.status === 'done' ? '' + (c.type === 'featured' ? 'run ended' : 'ended') + '' : c.scheduled ? 'scheduled' : c.status) + '' + '' + (c.status === 'active' ? '' : c.status === 'paused' ? '' : '') - + ' ' + + (c.type === 'featured' ? ' ' + : c.type === 'visits' ? ' ' + : ' ') + '').join('') + ''; el.appendChild(tbl); + const note = document.createElement('p'); note.className = 'muted small'; + note.textContent = 'Credits you put into a campaign are set aside the moment you start it, so your Available number drops right away and the campaign spends from its own budget. Featured links and verified-visit packs are paid in full up front; a featured run is sold by the day, so use Extend run to keep it going. Whatever a campaign has not spent comes back to Available when it ends.'; + el.appendChild(note); + el.querySelectorAll('button[data-extend]').forEach(b => b.addEventListener('click', async () => { + const rr = r.rates || {}; const durs = rr.featuredDurations || [1, 2, 7]; const per = rr.featuredPerDay || 40; + const d = await IAP.ask({ title: 'Extend featured run', text: 'How many more days? Choose ' + durs.join(', ') + '. ' + per + ' credits a day, charged now. The days are added after the current run, or start today if it has ended.', type: 'number', placeholder: 'e.g. ' + durs[durs.length - 1], ok: 'Extend' }); + if (!d) return; + try { const x = await api('/api/my/campaigns/' + b.dataset.extend + '/extend', { days: Number(d) }); + IAP.status('Extended ' + x.days + ' day' + (x.days === 1 ? '' : 's') + ' for ' + x.cost + ' credits' + (x.resumed ? '. The run is live again.' : '.'), 'ok'); + await loadCampaigns(); loadDashboard(); + } catch (e) { IAP.status(e.message, 'bad'); } + })); + el.querySelectorAll('button[data-morevisits]').forEach(b => b.addEventListener('click', async () => { + const per = (r.rates && r.rates.visitCostPerVisit) || 3; + const n = await IAP.ask({ title: 'Buy more visits', text: 'How many more verified visits? ' + per + ' credits each, charged now like the original pack.', type: 'number', placeholder: 'e.g. 50', ok: 'Buy visits' }); + if (!n) return; + try { const x = await api('/api/my/campaigns/' + b.dataset.morevisits + '/topup', { credits: Math.floor(Number(n)) * per }); + IAP.status('Added ' + x.visits + ' visits (' + x.added + ' credits)' + (x.reactivated ? '. The pack is open again.' : '.'), 'ok'); + await loadCampaigns(); loadDashboard(); + } catch (e) { IAP.status(e.message, 'bad'); } + })); el.querySelectorAll('button[data-camp]').forEach(b => b.addEventListener('click', async () => { try { await api('/api/my/campaigns/' + b.dataset.camp + '/' + b.dataset.act); await loadCampaigns(); } catch (e) { IAP.status(e.message, 'bad'); } })); el.querySelectorAll('button[data-topup]').forEach(b => b.addEventListener('click', async () => { - const n = await IAP.ask({ title: 'Add credits', text: 'How many credits to add to this campaign? More credits buy more views.', type: 'number', placeholder: 'e.g. 100', ok: 'Add credits' }); + const n = await IAP.ask({ title: 'Add credits', text: 'How many credits to add to this campaign? More credits buy more views. They are set aside now and spend as views are delivered; anything unspent comes back when the campaign ends.', type: 'number', placeholder: 'e.g. 100', ok: 'Add credits' }); if (!n) return; try { const r = await api('/api/my/campaigns/' + b.dataset.topup + '/topup', { credits: Number(n) }); IAP.status('Added ' + r.added + ' credits' + (r.reactivated ? ' — campaign is live again.' : '.'), 'ok'); @@ -1307,7 +1331,8 @@ function schedChips(c) { const now = Date.now(); let s = ''; if (c.starts && c.starts > now) s += ' starts ' + fmtWhen(c.starts) + ''; - if (c.expires && c.type !== 'featured') s += ' ' + (c.expires > now ? 'ends ' : 'ended ') + fmtWhen(c.expires) + ''; + if (c.expires && c.type === 'featured') { const day = new Date(c.expires - 1).toLocaleDateString([], { month: 'short', day: 'numeric' }); s += ' ' + (c.expires > now ? 'runs through ' : 'ran through ') + day + ''; } + else if (c.expires) s += ' ' + (c.expires > now ? 'ends ' : 'ended ') + fmtWhen(c.expires) + ''; return s; } function geoLine(rows) { diff --git a/public/my.html b/public/my.html index 12e23d3..660f14d 100644 --- a/public/my.html +++ b/public/my.html @@ -1021,7 +1021,7 @@ - + diff --git a/server.js b/server.js index bbddb7e..57110c1 100644 --- a/server.js +++ b/server.js @@ -2429,6 +2429,15 @@ const server = http.createServer(async (req, res) => { const r = await ads.topUpCampaign(s.email, memberId, m[1], b.credits); return json(res, r.error ? 400 : 200, r); } + m = /^\/api\/my\/campaigns\/(\d+)\/extend$/.exec(p); // featured links: book more days (flat, charged now) + if (m && req.method === 'POST') { + const s = await auth.fromRequest(req); + if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' }); + const memberId = await auth.refreshMemberId(s); + const b = await readBody(req); + const r = await ads.extendFeatured(s.email, memberId, m[1], b.days); + return json(res, r.error ? 400 : 200, r); + } m = /^\/api\/my\/campaigns\/(\d+)\/(pause|resume)$/.exec(p); if (m && req.method === 'POST') { const s = await auth.fromRequest(req);