Login ads as post-signin interstitials: Open Ad in new tab, timer here, Go to Dashboard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -38,7 +38,7 @@ const CANNED = [
|
||||
{ re: /((view|watch|see).{0,12}ads?|earn.{0,12}credits?|daily (set|ads|views))/i,
|
||||
a: 'In the Earn credits section of Members, each ad in the daily set opens full screen in its own tab, showing the advertiser\'s real site. A countdown runs while you watch (it pauses if you leave the tab), then you pass a quick click-the-icon check and the view counts. Finish the set, claim your daily credits, and spend them on your own banner or text campaigns. You never see your own ads, and viewer rewards are credits, never cash.' },
|
||||
{ re: /(credit|impression|cpm|what do i get|what am i buying)/i,
|
||||
a: 'Packages mint ad credits on-chain, and every new member also gets a small batch of welcome credits just for joining. One credit is one cent of ad delivery: banners, text ads, and login ads across the network, managed from the campaign manager in Members. Only your campaigns can spend your credits.' },
|
||||
a: 'Packages mint ad credits on-chain, and every new member also gets a small batch of welcome credits just for joining. One credit is one cent of ad delivery: banners, text ads, full-screen login ads, and solo ads across the network, managed from the campaign manager in Members. Only your campaigns can spend your credits.' },
|
||||
{ re: /(price|cost|package|how much is)/i,
|
||||
a: 'Packages run 5 to 250 dollars, priced in dollars and settled in POL at the live rate when you buy. The full ladder with live pricing is on the home page. Packages of $20 or more count toward qualification.' },
|
||||
{ re: /(safe|trust|rug|run away|company disappear)/i,
|
||||
@@ -52,7 +52,7 @@ function systemPrompt() {
|
||||
FACTS:
|
||||
- Free to join with email only (6-digit code sign-in, no passwords). Wallet appears only at purchase or payout activation. Every new member gets a small welcome batch of ad credits at signup (engine-side bonus credits, separate from on-chain purchased credits).
|
||||
- Ad packages: Micro $5/500 credits, Activation $20/2,000, Builder $50/5,500, Growth $100/12,000, Leader $250/32,500. Dollar-priced, settled in POL (Polygon) at the live Chainlink rate. 1 credit = 1 cent of ad delivery.
|
||||
- Live formats: display banners (per impression), text ads (per impression), login ads (per day). Coming: inbox ads, featured rotation with disclosed rotation size, verified-visit packs.
|
||||
- Live formats: display banners (per impression), text ads (per impression), full-screen LOGIN ADS (per day: right after a member signs in they land on a sponsor interstitial — they click "Open Ad", the advertiser's page opens in a NEW tab, a countdown runs on the interstitial, and at zero a "Go to dashboard" button appears. Just a CTA link is enough; an optional banner image can be the clickable creative. No framing requirement since it opens in its own tab), and solo ads. Coming: featured rotation with disclosed rotation size, verified-visit packs.
|
||||
- Members EARN credits by attention: in the Earn credits section of Members, each ad in the daily set opens FULL SCREEN in its own tab, showing the advertiser's real website. A countdown runs while you watch (it pauses if you leave the tab), then a quick human check (click the named icon) must be passed before the view counts. Finish the daily set, claim a small daily credit batch. Earned credits spend on banner and text campaigns; attention earns advertising, referrals earn money, and viewer rewards are never cash. Advertisers get real, verified visits to their site.
|
||||
- Onsite SOLO ADS are live: a solo ad is a full message (subject + up to 2000 characters of formatted text + your link) delivered into members' on-site Inbox (Members > Inbox). The composer in Campaigns > Solo ad has a rich-text editor (bold, headings, lists, links), lets you ATTACH one image (PNG/JPG/WebP/GIF, up to 3MB) or one video (MP4/WebM, up to 25MB), and adds a call-to-action button with a custom label that opens the target URL. Cost 5 credits per guaranteed delivery, minimum 10 deliveries (50 credits). Each member receives a given solo at most once, and never the sender's own. Readers earn 2 credits per real read (10-second dwell on the open message, up to 5 rewarded reads/day) — claimed right from the message. Compose one in Campaigns > Solo ad.
|
||||
- Campaign target URLs are checked the moment they are submitted: the page must be reachable and must ALLOW framing (no X-Frame-Options deny/sameorigin, no blocking CSP frame-ancestors), because surf views show the real site full screen. Frame-blocking or dead URLs are rejected with the exact reason; the fix is a landing page that allows framing. Login-ad targets skip the frame check (they are click-through only).
|
||||
|
||||
+52
-3
@@ -372,7 +372,7 @@
|
||||
});
|
||||
$('cType').addEventListener('change', () => {
|
||||
const t = $('cType').value;
|
||||
$('cImageRow').hidden = t === 'text' || t === 'solo';
|
||||
$('cImageRow').hidden = t !== 'banner'; // only banners carry a creative; login frames its URL
|
||||
$('cTitleRow').hidden = t !== 'text' && t !== 'solo';
|
||||
$('cBodyRow').hidden = t !== 'text';
|
||||
$('cSoloRow').hidden = t !== 'solo';
|
||||
@@ -644,6 +644,7 @@
|
||||
$('mcVerifyBtn').addEventListener('click', busy($('mcVerifyBtn'), async () => {
|
||||
await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value });
|
||||
IAP.status('You are in.', 'ok');
|
||||
await showLoginAd();
|
||||
await render();
|
||||
}));
|
||||
})();
|
||||
@@ -656,6 +657,7 @@
|
||||
$('loginBtn').addEventListener('click', busy($('loginBtn'), async () => {
|
||||
await api('/api/login', { email: $('liEmail').value, password: $('liPass').value });
|
||||
IAP.status('Logged in.', 'ok');
|
||||
await showLoginAd();
|
||||
await render();
|
||||
}));
|
||||
$('walletSigninLink').addEventListener('click', async e => {
|
||||
@@ -664,6 +666,7 @@
|
||||
IAP.status('Check your wallet for the free sign-in signature…');
|
||||
await IAPWallet.signIn();
|
||||
IAP.status('Signed in with your wallet.', 'ok');
|
||||
await showLoginAd();
|
||||
await render();
|
||||
} catch (err) { IAP.status((err && err.message) || String(err), 'bad'); }
|
||||
});
|
||||
@@ -692,9 +695,55 @@
|
||||
await render();
|
||||
});
|
||||
|
||||
// ad surfaces: login ads greet the sign-in screen; members see live
|
||||
// ── login ad interstitial (ClickBaitPays pattern): after a successful
|
||||
// sign-in the sponsor card appears; "Open Ad" opens the CTA link in a NEW
|
||||
// tab (a real, counted click) while the timer counts down on THIS page —
|
||||
// deliberately not paused, the member is expected to be in the ad tab.
|
||||
// At zero, "Go to dashboard" appears.
|
||||
async function showLoginAd() {
|
||||
try {
|
||||
const { ad } = await (await fetch('/api/ads/slot?type=login')).json();
|
||||
if (!ad || !ad.targetUrl) return;
|
||||
const gate = $('loginGate');
|
||||
const openBtn = $('lgOpen');
|
||||
const goBtn = $('lgContinue');
|
||||
const timer = $('lgTimer');
|
||||
$('lgCreative').innerHTML = ad.imageUrl
|
||||
? '<img src="' + ad.imageUrl + '" alt="sponsor ad">'
|
||||
: '<span class="lg-linkcard">' + (ad.title ? String(ad.title).replace(/[&<>]/g, '') : 'Visit today\'s sponsor') + '</span>';
|
||||
$('lgStatus').textContent = 'Login ad sponsor — click "Open Ad" to begin.';
|
||||
timer.hidden = true;
|
||||
goBtn.hidden = true;
|
||||
openBtn.disabled = false;
|
||||
gate.hidden = false;
|
||||
await new Promise(done => {
|
||||
openBtn.onclick = () => {
|
||||
window.open(ad.targetUrl, '_blank'); // the click relay counts it
|
||||
openBtn.disabled = true;
|
||||
$('lgStatus').textContent = 'Ad open in a new tab. View it and come back — the timer runs here.';
|
||||
let left = ad.dwell || 10;
|
||||
timer.hidden = false;
|
||||
timer.textContent = 'Time remaining: ' + left + 's';
|
||||
const t = setInterval(() => {
|
||||
left -= 1;
|
||||
if (left > 0) { timer.textContent = 'Time remaining: ' + left + 's'; return; }
|
||||
clearInterval(t);
|
||||
timer.textContent = 'Time is up';
|
||||
timer.classList.add('done');
|
||||
$('lgStatus').textContent = 'Thanks for the look. Your dashboard is ready.';
|
||||
goBtn.hidden = false;
|
||||
}, 1000);
|
||||
goBtn.onclick = () => done();
|
||||
};
|
||||
});
|
||||
gate.hidden = true;
|
||||
$('lgTimer').classList.remove('done');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ad surfaces: a banner greets the sign-in screen; members see live
|
||||
// banner + text placements inside the back office (they ARE the audience)
|
||||
IAP.adSlot('login', 'adSlotLogin');
|
||||
IAP.adSlot('banner', 'adSlotLogin');
|
||||
IAP.adSlot('banner', 'adSlotOverview');
|
||||
IAP.adSlot('text', 'adSlotSide');
|
||||
|
||||
|
||||
@@ -295,6 +295,21 @@ textarea{resize:vertical;font:inherit}
|
||||
.donut-legend{display:flex;flex-direction:column;gap:8px;font-size:13px}
|
||||
.donut-legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:8px}
|
||||
.donut-center{font-family:var(--disp);font-weight:800}
|
||||
/* ── login ad interstitial: sponsor opens in a new tab, timer runs here ── */
|
||||
.lgate{position:fixed;inset:0;z-index:90;display:flex;flex-direction:column;background:var(--ground)}
|
||||
.lgate-bar{display:flex;align-items:center;gap:14px;padding:10px 16px;background:var(--panel-solid);
|
||||
border-bottom:1px solid var(--line);flex-wrap:wrap}
|
||||
.lg-brand{font-family:var(--disp);font-weight:800;font-size:15px;white-space:nowrap}
|
||||
.lg-brand em{color:var(--mint);font-style:normal}
|
||||
.lgate-note{flex:1;min-width:140px}
|
||||
.lg-timer{font-weight:700;font-size:14px;border:1px solid var(--line-strong);border-radius:999px;
|
||||
padding:5px 14px;min-width:150px;text-align:center}
|
||||
.lg-timer.done{color:var(--mint);border-color:var(--mint)}
|
||||
.lgate-body{flex:1;display:grid;place-items:center;padding:24px;overflow:auto}
|
||||
.lgate-card{max-width:560px;text-align:center}
|
||||
.lgate-card img{max-width:100%;max-height:50vh;border-radius:12px;border:1px solid var(--line-strong)}
|
||||
#lgCreative .lg-linkcard{display:inline-block;background:var(--panel);border:1px solid var(--line-strong);
|
||||
border-radius:14px;padding:22px 28px;font-family:var(--disp);font-weight:700;font-size:18px;overflow-wrap:anywhere}
|
||||
/* ── solo composer: toolbar + contenteditable editor ── */
|
||||
.ed-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.ed-bar button{background:var(--panel);color:var(--ink);border:1px solid var(--line-strong);border-radius:8px;
|
||||
|
||||
+3
-1
@@ -221,7 +221,9 @@
|
||||
<div class="platecard">
|
||||
<div class="plate"><svg viewBox="0 0 24 24"><circle cx="12" cy="9" r="3.5"/><path d="M5 20c1.5-3.5 4-5 7-5s5.5 1.5 7 5"/><rect x="3" y="3" width="18" height="18" rx="3"/></svg></div>
|
||||
<h3>Login ads <span class="badge">live</span></h3>
|
||||
<p>Full attention at sign-in, priced per day. The member is already engaged before they do anything else.</p>
|
||||
<p>The moment a member signs in, your ad is the toll gate: they open your page in a fresh tab
|
||||
while a countdown holds their dashboard. Just a link is enough — add a banner if you have
|
||||
one. Priced per day.</p>
|
||||
</div>
|
||||
<div class="platecard">
|
||||
<div class="plate"><svg viewBox="0 0 24 24"><path d="M3 7l9 6 9-6"/><rect x="3" y="5" width="18" height="14" rx="2"/></svg></div>
|
||||
|
||||
+26
-6
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Member area | InstantAdPay</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905o">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905q">
|
||||
</head>
|
||||
<body class="bo-body">
|
||||
|
||||
@@ -48,6 +48,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── login ad interstitial: sponsor opens in a new tab, timer runs here ── -->
|
||||
<div id="loginGate" class="lgate" hidden>
|
||||
<div class="lgate-bar">
|
||||
<span class="lg-brand">Instant<em>AdPay</em></span>
|
||||
<span class="lgate-note small muted" id="lgStatus">Login ad sponsor — click "Open Ad" to begin.</span>
|
||||
<span class="lg-timer mono" id="lgTimer" hidden>…</span>
|
||||
<button class="btn small" id="lgContinue" type="button" hidden>Go to dashboard</button>
|
||||
</div>
|
||||
<div class="lgate-body">
|
||||
<div class="lgate-card">
|
||||
<p class="eyebrow">Today's login sponsor</p>
|
||||
<div id="lgCreative"></div>
|
||||
<p style="margin-top:18px"><button class="btn" id="lgOpen" type="button">Open Ad</button></p>
|
||||
<p class="small muted">The ad opens in a new tab. The timer counts down right here —
|
||||
when it hits zero, your dashboard button appears above.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── signed-in: back-office shell ──────────────────────── -->
|
||||
<div id="memberArea" class="bo" hidden>
|
||||
<aside class="bo-side" id="boSide">
|
||||
@@ -218,7 +237,8 @@
|
||||
</div>
|
||||
<p><input id="cTarget" placeholder="Target URL (https://…)" style="width:100%"></p>
|
||||
<p class="small muted" style="margin-top:-6px">Banner and text targets are shown full screen in the
|
||||
ad viewer, so the URL must allow framing — we check it the moment you submit.</p>
|
||||
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.</p>
|
||||
<p id="cImageRow"><input id="cImage" placeholder="Image URL (banner/login ads)" style="width:100%"></p>
|
||||
<p id="cTitleRow" hidden><input id="cTitle" placeholder="Headline (max 60)" style="width:100%"></p>
|
||||
<p id="cBodyRow" hidden><input id="cBody" placeholder="Ad text (max 140)" style="width:100%"></p>
|
||||
@@ -379,9 +399,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/common.js?v=20260905o"></script>
|
||||
<script src="/assets/wallet.js?v=20260905o"></script>
|
||||
<script src="/assets/my.js?v=20260905o"></script>
|
||||
<script src="/assets/chat.js?v=20260905o"></script>
|
||||
<script src="/assets/common.js?v=20260905q"></script>
|
||||
<script src="/assets/wallet.js?v=20260905q"></script>
|
||||
<script src="/assets/my.js?v=20260905q"></script>
|
||||
<script src="/assets/chat.js?v=20260905q"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Viewing ad — InstantAdPay</title>
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905o">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905q">
|
||||
<style>
|
||||
html,body{height:100%;margin:0;overflow:hidden}
|
||||
.vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
|
||||
@@ -43,6 +43,6 @@
|
||||
</div>
|
||||
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
|
||||
</div>
|
||||
<script src="/assets/view.js?v=20260905o"></script>
|
||||
<script src="/assets/view.js?v=20260905q"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -594,7 +594,11 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
// -- ad engine (spec §8b v1: banners, text, login ads)
|
||||
if (p === '/api/ads/slot' && req.method === 'GET') {
|
||||
const ad = await ads.serve(String(u.searchParams.get('type') || 'banner'));
|
||||
const t = String(u.searchParams.get('type') || 'banner');
|
||||
const ad = await ads.serve(t);
|
||||
// login ads: the member clicks "Open Ad" (a real, counted click into a
|
||||
// new tab) while the countdown runs on our interstitial page
|
||||
if (ad && t === 'login') ad.dwell = ads.rates().loginDwellSeconds || 10;
|
||||
return json(res, 200, { ad });
|
||||
}
|
||||
m = /^\/api\/ads\/click\/(\d+)$/.exec(p);
|
||||
@@ -619,7 +623,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const memberId = await auth.refreshMemberId(s); // 0 is fine: earned credits fund banner/text
|
||||
const b = await readBody(req);
|
||||
if (!['login', 'solo'].includes(String(b.type || ''))) { // surf views frame the target: catch frame-breakers at the door (login + solo are click-through only)
|
||||
if (!['login', 'solo'].includes(String(b.type || ''))) { // banner/text surf views frame the target: catch frame-breakers at the door (login opens a new tab, solo links are click-through)
|
||||
const fc = await frameCheck(b.targetUrl);
|
||||
if (!fc.ok) return json(res, 400, { error: fc.reason });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user