Badge emblems, profile social links, and SEO/OG social previews

- Achievement badge share-image now draws a milestone emblem (⚡🎯⭐🏆) in the medal
- Profile: Facebook/X/YouTube/Instagram/TikTok/Telegram/LinkedIn/Website links, shown on the public bio page
- OG + Twitter Card tags on homepage/ledger/contract (hero banner as share image)
- Per-member OG tags server-injected into /wall/<username> so shared bio links preview with name/bio/avatar
- robots.txt + sitemap.xml; .txt/.xml MIME types

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 12:21:53 -05:00
parent b001df61b1
commit 3c0e94f20f
16 changed files with 178 additions and 52 deletions
+7 -5
View File
@@ -35,7 +35,7 @@ function newCode(taken) {
const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null, const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null,
username: a.username || null, memberId: a.memberId || 0, username: a.username || null, memberId: a.memberId || 0,
lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null, lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null,
avatarUrl: a.avatarUrl || null, bio: a.bio || null, avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials: a.socials || null,
address: a.address || null, created: a.created } : null; address: a.address || null, created: a.created } : null;
const USER_RE = /^[a-zA-Z0-9_]{3,20}$/; const USER_RE = /^[a-zA-Z0-9_]{3,20}$/;
const normUser = u => String(u || '').trim().toLowerCase(); const normUser = u => String(u || '').trim().toLowerCase();
@@ -108,11 +108,12 @@ const J = {
this.save(); this.save();
return { ok: true, account: pub(acct) }; return { ok: true, account: pub(acct) };
}, },
async setProfile(e, avatarUrl, bio) { async setProfile(e, avatarUrl, bio, socials) {
const acct = this.db.byEmail[e]; const acct = this.db.byEmail[e];
if (!acct) return { error: 'No such account.' }; if (!acct) return { error: 'No such account.' };
if (avatarUrl !== undefined) acct.avatarUrl = avatarUrl || null; if (avatarUrl !== undefined) acct.avatarUrl = avatarUrl || null;
if (bio !== undefined) acct.bio = bio || null; if (bio !== undefined) acct.bio = bio || null;
if (socials !== undefined) acct.socials = socials || null;
this.save(); this.save();
return { ok: true, account: pub(acct) }; return { ok: true, account: pub(acct) };
}, },
@@ -155,7 +156,7 @@ const J = {
const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code, const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code,
username: r.username, memberId: r.member_id || 0, username: r.username, memberId: r.member_id || 0,
lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url, lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url,
avatarUrl: r.avatar_url, bio: r.bio, avatarUrl: r.avatar_url, bio: r.bio, socials: r.socials,
address: r.address, created: Number(r.created) }) : null; address: r.address, created: Number(r.created) }) : null;
const D = { const D = {
async signup(e, password, ref) { async signup(e, password, ref) {
@@ -205,9 +206,10 @@ const D = {
await db.q('UPDATE accounts SET line_banner_url=?, line_target_url=? WHERE email=?', [bannerUrl || null, targetUrl || null, e]); await db.q('UPDATE accounts SET line_banner_url=?, line_target_url=? WHERE email=?', [bannerUrl || null, targetUrl || null, e]);
return { ok: true, account: await this.byEmail(e) }; return { ok: true, account: await this.byEmail(e) };
}, },
async setProfile(e, avatarUrl, bio) { async setProfile(e, avatarUrl, bio, socials) {
if (avatarUrl !== undefined) await db.q('UPDATE accounts SET avatar_url=? WHERE email=?', [avatarUrl || null, e]); if (avatarUrl !== undefined) await db.q('UPDATE accounts SET avatar_url=? WHERE email=?', [avatarUrl || null, e]);
if (bio !== undefined) await db.q('UPDATE accounts SET bio=? WHERE email=?', [bio || null, e]); if (bio !== undefined) await db.q('UPDATE accounts SET bio=? WHERE email=?', [bio || null, e]);
if (socials !== undefined) await db.q('UPDATE accounts SET socials=? WHERE email=?', [socials || null, e]);
return { ok: true, account: await this.byEmail(e) }; return { ok: true, account: await this.byEmail(e) };
}, },
async byMemberId(id) { async byMemberId(id) {
@@ -310,5 +312,5 @@ async function count() { return impl().count(); }
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername, module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername,
setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count, setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count,
setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t), setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t),
setProfile: (e, a, bio) => impl().setProfile(String(e || '').toLowerCase(), a, bio), setProfile: (e, a, bio, socials) => impl().setProfile(String(e || '').toLowerCase(), a, bio, socials),
byMemberId: id => impl().byMemberId(id) }; byMemberId: id => impl().byMemberId(id) };
+1
View File
@@ -83,6 +83,7 @@ async function bootstrap() {
await alterSafe('ALTER TABLE accounts ADD KEY idx_member (member_id)'); await alterSafe('ALTER TABLE accounts ADD KEY idx_member (member_id)');
await alterSafe('ALTER TABLE accounts ADD COLUMN avatar_url VARCHAR(500) NULL'); await alterSafe('ALTER TABLE accounts ADD COLUMN avatar_url VARCHAR(500) NULL');
await alterSafe('ALTER TABLE accounts ADD COLUMN bio VARCHAR(600) NULL'); await alterSafe('ALTER TABLE accounts ADD COLUMN bio VARCHAR(600) NULL');
await alterSafe('ALTER TABLE accounts ADD COLUMN socials VARCHAR(1200) NULL'); // JSON {platform:url}
await alterSafe('ALTER TABLE accounts ADD COLUMN line_banner_url VARCHAR(500) NULL'); await alterSafe('ALTER TABLE accounts ADD COLUMN line_banner_url VARCHAR(500) NULL');
await alterSafe('ALTER TABLE accounts ADD COLUMN line_target_url VARCHAR(500) NULL'); await alterSafe('ALTER TABLE accounts ADD COLUMN line_target_url VARCHAR(500) NULL');
await q(`CREATE TABLE IF NOT EXISTS daily_views ( await q(`CREATE TABLE IF NOT EXISTS daily_views (
+20 -13
View File
@@ -42,10 +42,10 @@
// achievement badges: the milestone ladder as unlockable medals + share-to-image // achievement badges: the milestone ladder as unlockable medals + share-to-image
const BADGES = [ const BADGES = [
{ key: 'payouts', label: 'Payouts On', sub: 'wallet linked', icon: '<circle cx="12" cy="12" r="8"/><path d="M9 12l2 2 4-4"/>' }, { key: 'payouts', label: 'Payouts On', sub: 'wallet linked', emblem: '⚡', icon: '<circle cx="12" cy="12" r="8"/><path d="M9 12l2 2 4-4"/>' },
{ key: 'firstBuyer', label: 'First Buyer', sub: 'first qualified referral', icon: '<path d="M6 7h12l-1 13H7z"/><path d="M9 7a3 3 0 0 1 6 0"/>' }, { key: 'firstBuyer', label: 'First Buyer', sub: 'first qualified referral', emblem: '🎯', icon: '<path d="M6 7h12l-1 13H7z"/><path d="M9 7a3 3 0 0 1 6 0"/>' },
{ key: 'level2', label: 'Level 2', sub: '2 qualifying buyers', icon: '<path d="M12 3l2.5 5 5.5.8-4 3.9.9 5.5L12 21l-4.9 2.6.9-5.5-4-3.9 5.5-.8z"/>' }, { key: 'level2', label: 'Level 2', sub: '2 qualifying buyers', emblem: '⭐', icon: '<path d="M12 3l2.5 5 5.5.8-4 3.9.9 5.5L12 21l-4.9 2.6.9-5.5-4-3.9 5.5-.8z"/>' },
{ key: 'level3', label: 'Level 3', sub: 'fully qualified', icon: '<path d="M5 4h14v5a7 7 0 0 1-14 0z"/><path d="M9 20h6M12 16v4"/><path d="M5 6H3v2a3 3 0 0 0 3 3M19 6h2v2a3 3 0 0 1-3 3"/>' } { key: 'level3', label: 'Level 3', sub: 'fully qualified', emblem: '🏆', icon: '<path d="M5 4h14v5a7 7 0 0 1-14 0z"/><path d="M9 20h6M12 16v4"/><path d="M5 6H3v2a3 3 0 0 0 3 3M19 6h2v2a3 3 0 0 1-3 3"/>' }
]; ];
let lastMilestones = []; let lastMilestones = [];
function renderBadges(d) { function renderBadges(d) {
@@ -81,16 +81,19 @@
x.strokeStyle = 'rgba(67,232,195,.5)'; x.lineWidth = 6; x.strokeRect(24, 24, 1152, 582); x.strokeStyle = 'rgba(67,232,195,.5)'; x.lineWidth = 6; x.strokeRect(24, 24, 1152, 582);
x.fillStyle = '#43e8c3'; x.font = 'bold 34px Sora, sans-serif'; x.fillText('InstantAdPay', 70, 100); x.fillStyle = '#43e8c3'; x.font = 'bold 34px Sora, sans-serif'; x.fillText('InstantAdPay', 70, 100);
x.fillStyle = '#8ba69c'; x.font = '22px Sora, sans-serif'; x.fillText('ACHIEVEMENT UNLOCKED', 70, 150); x.fillStyle = '#8ba69c'; x.font = '22px Sora, sans-serif'; x.fillText('ACHIEVEMENT UNLOCKED', 70, 150);
// medal circle // medal with the milestone emblem
x.beginPath(); x.arc(600, 320, 110, 0, Math.PI * 2); x.beginPath(); x.arc(600, 300, 112, 0, Math.PI * 2);
const mg = x.createRadialGradient(600, 290, 20, 600, 320, 110); const mg = x.createRadialGradient(600, 270, 20, 600, 300, 112);
mg.addColorStop(0, 'rgba(67,232,195,.45)'); mg.addColorStop(1, 'rgba(9,26,19,.9)'); mg.addColorStop(0, 'rgba(67,232,195,.5)'); mg.addColorStop(1, 'rgba(9,26,19,.92)');
x.fillStyle = mg; x.fill(); x.strokeStyle = '#43e8c3'; x.lineWidth = 5; x.stroke(); x.fillStyle = mg; x.fill(); x.strokeStyle = '#43e8c3'; x.lineWidth = 6; x.stroke();
x.save(); x.shadowColor = 'rgba(67,232,195,.6)'; x.shadowBlur = 24;
x.font = '90px "Segoe UI Emoji","Noto Color Emoji",sans-serif'; x.textAlign = 'center'; x.textBaseline = 'middle';
x.fillText(b.emblem || '', 600, 300); x.restore(); x.textBaseline = 'alphabetic';
x.fillStyle = '#eef7f3'; x.textAlign = 'center'; x.fillStyle = '#eef7f3'; x.textAlign = 'center';
x.font = 'bold 64px Sora, sans-serif'; x.fillText(b.label, 600, 500); x.font = 'bold 60px Sora, sans-serif'; x.fillText(b.label, 600, 485);
x.font = '26px Sora, sans-serif'; x.fillStyle = '#8ba69c'; x.fillText(b.sub, 600, 545); x.font = '25px Sora, sans-serif'; x.fillStyle = '#8ba69c'; x.fillText(b.sub, 600, 527);
const who = d.username ? '@' + d.username : d.memberId ? 'member #' + d.memberId : ''; const who = d.username ? '@' + d.username : d.memberId ? 'member #' + d.memberId : '';
if (who) { x.fillStyle = '#43e8c3'; x.font = 'bold 30px Sora, sans-serif'; x.fillText(who, 600, 590); } if (who) { x.fillStyle = '#43e8c3'; x.font = 'bold 30px Sora, sans-serif'; x.fillText(who, 600, 575); }
x.textAlign = 'left'; x.textAlign = 'left';
try { try {
c.toBlob(bl => { c.toBlob(bl => {
@@ -1228,10 +1231,13 @@
fillProfileDetails(a); fillProfileDetails(a);
} catch (e) {} } catch (e) {}
} }
const SOCIALS = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website'];
function fillProfileDetails(a) { function fillProfileDetails(a) {
if (!a) return; if (!a) return;
if (a.bio) $('pfBio').value = a.bio; if (a.bio) $('pfBio').value = a.bio;
if (a.avatarUrl) { const p = $('pfAvatarPrev'); p.src = a.avatarUrl; p.hidden = false; } if (a.avatarUrl) { const p = $('pfAvatarPrev'); p.src = a.avatarUrl; p.hidden = false; }
let soc = {}; try { soc = a.socials ? JSON.parse(a.socials) : {}; } catch (e) {}
for (const p of SOCIALS) if ($('soc-' + p)) $('soc-' + p).value = soc[p] || '';
if (a.username) { if (a.username) {
const link = location.origin + '/wall/' + a.username; const link = location.origin + '/wall/' + a.username;
$('pfBioLink').textContent = link; $('pfBioLink').textContent = link;
@@ -1255,7 +1261,8 @@
$('pfAvatarFile').value = ''; $('pfAvatarFile').value = '';
}); });
$('pfDetailsSave').addEventListener('click', busy2($('pfDetailsSave'), async () => { $('pfDetailsSave').addEventListener('click', busy2($('pfDetailsSave'), async () => {
const body = { bio: $('pfBio').value }; const body = { bio: $('pfBio').value, socials: {} };
for (const p of SOCIALS) body.socials[p] = ($('soc-' + p) && $('soc-' + p).value.trim()) || '';
if (pfAvatar) body.avatarUrl = pfAvatar; if (pfAvatar) body.avatarUrl = pfAvatar;
const r = await api('/api/my/profile-details', body); const r = await api('/api/my/profile-details', body);
IAP.status('Profile saved.', 'ok'); IAP.status('Profile saved.', 'ok');
+4
View File
@@ -373,6 +373,10 @@ textarea{resize:vertical;font:inherit}
.bio-head{display:flex;gap:22px;align-items:center;flex-wrap:wrap;padding:8px 0 4px} .bio-head{display:flex;gap:22px;align-items:center;flex-wrap:wrap;padding:8px 0 4px}
.bio-avatar{width:96px;height:96px;border-radius:50%;object-fit:cover;border:2px solid var(--mint);flex:0 0 auto} .bio-avatar{width:96px;height:96px;border-radius:50%;object-fit:cover;border:2px solid var(--mint);flex:0 0 auto}
.bio-meta{flex:1;min-width:220px} .bio-meta{flex:1;min-width:220px}
.bio-socials{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.bio-socials a{font-size:12px;font-weight:700;color:var(--mint);border:1px solid var(--line-strong);
border-radius:999px;padding:4px 12px;text-decoration:none}
.bio-socials a:hover{border-color:var(--mint)}
.bio-qr{flex:0 0 auto;background:var(--panel);border:1px solid var(--line-strong);border-radius:14px;padding:10px} .bio-qr{flex:0 0 auto;background:var(--panel);border:1px solid var(--line-strong);border-radius:14px;padding:10px}
.bio-qr img{display:block;border-radius:8px;background:#eef7f3} .bio-qr img{display:block;border-radius:8px;background:#eef7f3}
/* ── wall page ── */ /* ── wall page ── */
+8
View File
@@ -18,6 +18,14 @@
if (w.avatarUrl) { const av = IAP.$('bioAvatar'); av.src = w.avatarUrl; av.hidden = false; } if (w.avatarUrl) { const av = IAP.$('bioAvatar'); av.src = w.avatarUrl; av.hidden = false; }
IAP.$('bioText').textContent = w.bio || 'Building a team on InstantAdPay — join through this page and you\'re in my line.'; IAP.$('bioText').textContent = w.bio || 'Building a team on InstantAdPay — join through this page and you\'re in my line.';
if (w.qrUrl) IAP.$('bioQr').src = w.qrUrl; if (w.qrUrl) IAP.$('bioQr').src = w.qrUrl;
// social links
const SOC = { facebook: 'Facebook', twitter: 'X', youtube: 'YouTube', instagram: 'Instagram', tiktok: 'TikTok', telegram: 'Telegram', linkedin: 'LinkedIn', website: 'Website' };
const sc = IAP.$('bioSocials');
if (sc && w.socials && typeof w.socials === 'object') {
const links = Object.keys(SOC).filter(k => w.socials[k]).map(k =>
'<a href="' + String(w.socials[k]).replace(/"/g, '%22') + '" target="_blank" rel="noopener nofollow me">' + SOC[k] + '</a>');
sc.innerHTML = links.join('');
}
IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line'; IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line';
IAP.$('wallJoin').href = w.joinUrl; IAP.$('wallJoin').href = w.joinUrl;
// per-wall viewed-position memory (survives revisits; anonymous-friendly) // per-wall viewed-position memory (survives revisits; anonymous-friendly)
+15 -4
View File
@@ -4,8 +4,19 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>The contract | InstantAdPay</title> <title>The contract | InstantAdPay</title>
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself."> <meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
<link rel="canonical" href="https://instantadpay.com/contract">
<meta property="og:type" content="website"><meta property="og:site_name" content="InstantAdPay">
<meta property="og:url" content="https://instantadpay.com/contract">
<meta property="og:title" content="InstantAdPay — the contract, in plain language">
<meta property="og:description" content="What the settlement contract does, what nobody can change, and how to verify all of it yourself.">
<meta property="og:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="InstantAdPay — the contract, in plain language">
<meta name="twitter:description" content="Immutable 50-20-10 splits, no withdrawal button, verify it yourself.">
<meta name="twitter:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png">
<meta name="theme-color" content="#043b2f">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <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=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
@@ -129,8 +140,8 @@
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div> <div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
</footer> </footer>
</div> </div>
<script src="/assets/common.js?v=20260906k"></script> <script src="/assets/common.js?v=20260906l"></script>
<script src="/assets/contract.js?v=20260906k"></script> <script src="/assets/contract.js?v=20260906l"></script>
<script src="/assets/chat.js?v=20260906k"></script> <script src="/assets/chat.js?v=20260906l"></script>
</body> </body>
</html> </html>
+20 -6
View File
@@ -3,9 +3,23 @@
<head> <head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>InstantAdPay: advertise and earn, locked in code</title> <title>InstantAdPay: advertise and earn, locked in code</title>
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger."> <meta name="description" content="Advertise and earn instantly. Real ad packages with instant on-chain settlement — every purchase pays the sponsor line in the same Polygon transaction, verifiable by anyone on the live ledger. Seven ad formats, earn credits back for your attention.">
<link rel="canonical" href="https://instantadpay.com/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="InstantAdPay">
<meta property="og:url" content="https://instantadpay.com/">
<meta property="og:title" content="InstantAdPay — Advertise and Earn Instantly">
<meta property="og:description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction — verifiable on the live ledger. Seven ad formats, earn credits back for your attention.">
<meta property="og:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="InstantAdPay — Advertise and Earn Instantly">
<meta name="twitter:description" content="Instant on-chain ad payouts, verifiable on a public ledger. Seven ad formats. Earn credits back for your attention.">
<meta name="twitter:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png">
<meta name="theme-color" content="#043b2f">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <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=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
</head> </head>
<body> <body>
@@ -423,9 +437,9 @@
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906k"></script> <script src="/assets/common.js?v=20260906l"></script>
<script src="/assets/wallet.js?v=20260906k"></script> <script src="/assets/wallet.js?v=20260906l"></script>
<script src="/assets/home.js?v=20260906k"></script> <script src="/assets/home.js?v=20260906l"></script>
<script src="/assets/chat.js?v=20260906k"></script> <script src="/assets/chat.js?v=20260906l"></script>
</body> </body>
</html> </html>
+15 -4
View File
@@ -4,8 +4,19 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Live ledger | InstantAdPay</title> <title>Live ledger | InstantAdPay</title>
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line."> <meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
<link rel="canonical" href="https://instantadpay.com/ledger">
<meta property="og:type" content="website"><meta property="og:site_name" content="InstantAdPay">
<meta property="og:url" content="https://instantadpay.com/ledger">
<meta property="og:title" content="InstantAdPay — the live payout ledger">
<meta property="og:description" content="Watch real ad purchases split into instant on-chain payouts, live. Every line links to the raw transaction.">
<meta property="og:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="InstantAdPay — the live payout ledger">
<meta name="twitter:description" content="Real ad purchases splitting into instant on-chain payouts, live and verifiable.">
<meta name="twitter:image" content="https://instantadpay.com/banners/iap-hero-1200x630.png">
<meta name="theme-color" content="#043b2f">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <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=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
@@ -25,8 +36,8 @@
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div> <div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
</footer> </footer>
</div> </div>
<script src="/assets/common.js?v=20260906k"></script> <script src="/assets/common.js?v=20260906l"></script>
<script src="/assets/ledger.js?v=20260906k"></script> <script src="/assets/ledger.js?v=20260906l"></script>
<script src="/assets/chat.js?v=20260906k"></script> <script src="/assets/chat.js?v=20260906l"></script>
</body> </body>
</html> </html>
+17 -6
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Member area | InstantAdPay</title> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
</head> </head>
<body class="bo-body"> <body class="bo-body">
@@ -534,7 +534,18 @@
<button class="btn small sec" id="pfAvatarBtn" type="button">Upload avatar</button> <button class="btn small sec" id="pfAvatarBtn" type="button">Upload avatar</button>
<span class="small muted" id="pfAvatarInfo"></span></p> <span class="small muted" id="pfAvatarInfo"></span></p>
<p><textarea id="pfBio" rows="3" maxlength="600" placeholder="A short bio for your profile page…" style="width:100%"></textarea></p> <p><textarea id="pfBio" rows="3" maxlength="600" placeholder="A short bio for your profile page…" style="width:100%"></textarea></p>
<p><button class="btn" id="pfDetailsSave">Save profile</button> <p class="small muted" style="margin-bottom:4px">Social links (https URLs — shown on your public page):</p>
<div class="grid c2" style="gap:8px">
<input id="soc-facebook" placeholder="Facebook URL" style="width:100%">
<input id="soc-twitter" placeholder="X / Twitter URL" style="width:100%">
<input id="soc-youtube" placeholder="YouTube URL" style="width:100%">
<input id="soc-instagram" placeholder="Instagram URL" style="width:100%">
<input id="soc-tiktok" placeholder="TikTok URL" style="width:100%">
<input id="soc-telegram" placeholder="Telegram URL" style="width:100%">
<input id="soc-linkedin" placeholder="LinkedIn URL" style="width:100%">
<input id="soc-website" placeholder="Website URL" style="width:100%">
</div>
<p style="margin-top:12px"><button class="btn" id="pfDetailsSave">Save profile</button>
<a class="btn small sec" id="pfViewBio" target="_blank" rel="noopener" hidden>View my page</a></p> <a class="btn small sec" id="pfViewBio" target="_blank" rel="noopener" hidden>View my page</a></p>
</div> </div>
<div class="card"> <div class="card">
@@ -584,9 +595,9 @@
</div> </div>
</div> </div>
<script src="/assets/common.js?v=20260906k"></script> <script src="/assets/common.js?v=20260906l"></script>
<script src="/assets/wallet.js?v=20260906k"></script> <script src="/assets/wallet.js?v=20260906l"></script>
<script src="/assets/my.js?v=20260906k"></script> <script src="/assets/my.js?v=20260906l"></script>
<script src="/assets/chat.js?v=20260906k"></script> <script src="/assets/chat.js?v=20260906l"></script>
</body> </body>
</html> </html>
+5
View File
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /my
Disallow: /api/
Sitemap: https://instantadpay.com/sitemap.xml
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Shorts | InstantAdPay</title> <title>Shorts | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <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=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
<style> <style>
html,body{height:100%;margin:0;overflow:hidden;background:#000} html,body{height:100%;margin:0;overflow:hidden;background:#000}
.sh{position:fixed;inset:0;display:flex;flex-direction:column;background:#000;color:var(--ink,#e8fff7)} .sh{position:fixed;inset:0;display:flex;flex-direction:column;background:#000;color:var(--ink,#e8fff7)}
@@ -43,6 +43,6 @@
</div> </div>
</div> </div>
</div> </div>
<script src="/assets/shorts.js?v=20260906k"></script> <script src="/assets/shorts.js?v=20260906l"></script>
</body> </body>
</html> </html>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://instantadpay.com/</loc><changefreq>daily</changefreq><priority>1.0</priority></url>
<url><loc>https://instantadpay.com/ledger</loc><changefreq>always</changefreq><priority>0.8</priority></url>
<url><loc>https://instantadpay.com/contract</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
</urlset>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Transaction | InstantAdPay</title> <title>Transaction | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <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=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
</head> </head>
<body> <body>
<div id="nav"></div> <div id="nav"></div>
@@ -33,7 +33,7 @@
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p> <p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906k"></script> <script src="/assets/common.js?v=20260906l"></script>
<script src="/assets/tx.js?v=20260906k"></script> <script src="/assets/tx.js?v=20260906l"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Viewing ad — InstantAdPay</title> <title>Viewing ad — InstantAdPay</title>
<link rel="stylesheet" href="/assets/site.css?v=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
<style> <style>
html,body{height:100%;margin:0;overflow:hidden} 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)} .vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
@@ -43,6 +43,6 @@
</div> </div>
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe> <iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
</div> </div>
<script src="/assets/view.js?v=20260906k"></script> <script src="/assets/view.js?v=20260906l"></script>
</body> </body>
</html> </html>
+5 -3
View File
@@ -3,8 +3,9 @@
<head> <head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Banner wall | InstantAdPay</title> <title>Banner wall | InstantAdPay</title>
<!--OG-->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <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=20260906k"> <link rel="stylesheet" href="/assets/site.css?v=20260906l">
</head> </head>
<body> <body>
<div id="nav"></div> <div id="nav"></div>
@@ -16,6 +17,7 @@
<p class="eyebrow">Member profile</p> <p class="eyebrow">Member profile</p>
<h2 id="wallTitle" style="margin:.1em 0">Loading…</h2> <h2 id="wallTitle" style="margin:.1em 0">Loading…</h2>
<p id="bioText" class="lead" style="font-size:16px"></p> <p id="bioText" class="lead" style="font-size:16px"></p>
<div id="bioSocials" class="bio-socials"></div>
</div> </div>
<div class="bio-qr"><img id="bioQr" alt="Join QR code" width="150" height="150"><div class="small muted" style="text-align:center">scan to join</div></div> <div class="bio-qr"><img id="bioQr" alt="Join QR code" width="150" height="150"><div class="small muted" style="text-align:center">scan to join</div></div>
</div> </div>
@@ -34,7 +36,7 @@
</div> </div>
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906k"></script> <script src="/assets/common.js?v=20260906l"></script>
<script src="/assets/wall.js?v=20260906k"></script> <script src="/assets/wall.js?v=20260906l"></script>
</body> </body>
</html> </html>
+48 -4
View File
@@ -151,7 +151,7 @@ function siteConfig() {
const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript',
'.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp',
'.ico': 'image/x-icon', '.json': 'application/json', '.mp4': 'video/mp4', '.woff2': 'font/woff2', '.ico': 'image/x-icon', '.json': 'application/json', '.mp4': 'video/mp4', '.woff2': 'font/woff2',
'.gif': 'image/gif', '.webm': 'video/webm' }; '.gif': 'image/gif', '.webm': 'video/webm', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml; charset=utf-8' };
const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; media-src 'self' https: blob:; connect-src 'self'; font-src 'self' data: https://fonts.gstatic.com; form-action 'self'; frame-src https: http:"; const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; media-src 'self' https: blob:; connect-src 'self'; font-src 'self' data: https://fonts.gstatic.com; form-action 'self'; frame-src https: http:";
function baseHeaders(extra) { function baseHeaders(extra) {
return Object.assign({ 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff', return Object.assign({ 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff',
@@ -684,7 +684,18 @@ const server = http.createServer(async (req, res) => {
if (avatar && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(avatar)) if (avatar && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(avatar))
return json(res, 400, { error: 'Avatar must be an uploaded image or an https image URL.' }); return json(res, 400, { error: 'Avatar must be an uploaded image or an https image URL.' });
const bio = b.bio === undefined ? undefined : String(b.bio || '').trim().slice(0, 600); const bio = b.bio === undefined ? undefined : String(b.bio || '').trim().slice(0, 600);
const r = await accounts.setProfile(s.email, avatar, bio); // socials: {platform: url}; keep only known platforms with valid https urls
let socials;
if (b.socials !== undefined) {
const PLAT = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website'];
const clean = {};
for (const p of PLAT) {
const v = String((b.socials && b.socials[p]) || '').trim();
if (v && /^https:\/\/[^\s]+$/i.test(v) && v.length <= 200) clean[p] = v;
}
socials = Object.keys(clean).length ? JSON.stringify(clean) : null;
}
const r = await accounts.setProfile(s.email, avatar, bio, socials);
return json(res, r.error ? 400 : 200, r); return json(res, r.error ? 400 : 200, r);
} }
// -- line banner: the member's viral slot on welcome tours + their wall // -- line banner: the member's viral slot on welcome tours + their wall
@@ -737,8 +748,9 @@ const server = http.createServer(async (req, res) => {
.map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member', .map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member',
bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null })); bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null }));
const joinPath = '/join/' + (a.username || a.code); const joinPath = '/join/' + (a.username || a.code);
let socials = null; try { socials = a.socials ? JSON.parse(a.socials) : null; } catch (e) {}
return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0), return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0),
avatarUrl: a.avatarUrl || null, bio: a.bio || null, avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials,
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder }); joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder });
} }
// -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch // -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch
@@ -996,7 +1008,39 @@ const server = http.createServer(async (req, res) => {
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p); m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
if (m) return sendFile(res, path.join(UPLOADS_DIR, m[1])); if (m) return sendFile(res, path.join(UPLOADS_DIR, m[1]));
if (/^\/tx\/0x[0-9a-fA-F]{64}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'tx.html')); if (/^\/tx\/0x[0-9a-fA-F]{64}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'tx.html'));
if (/^\/wall\/[A-Za-z0-9_]{1,20}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'wall.html')); m = /^\/wall\/([A-Za-z0-9_]{1,20})$/.exec(p);
if (m) { // server-inject per-member OG tags so shared bio links preview correctly (crawlers don't run JS)
try {
const tok = m[1].toLowerCase();
let a = await accounts.byUsername(tok); if (!a) a = await accounts.byCode(tok);
let html = fs.readFileSync(path.join(PUBLIC_DIR, 'wall.html'), 'utf8');
if (a) {
const nm = a.username ? '@' + a.username : 'member #' + (a.memberId || 0);
const esc = t => String(t || '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const title = nm + ' on InstantAdPay';
const desc = a.bio ? esc(a.bio).slice(0, 200) : 'Join ' + nm + '’s line on InstantAdPay — instant on-chain ad payouts, free to join.';
const img = a.avatarUrl && /^https:/.test(a.avatarUrl) ? a.avatarUrl : 'https://instantadpay.com/banners/iap-hero-1200x630.png';
const url = 'https://instantadpay.com/wall/' + (a.username || a.code);
const og = [
'<meta property="og:type" content="profile">',
'<meta property="og:site_name" content="InstantAdPay">',
'<meta property="og:url" content="' + url + '">',
'<meta property="og:title" content="' + esc(title) + '">',
'<meta property="og:description" content="' + desc + '">',
'<meta property="og:image" content="' + esc(img) + '">',
'<meta name="twitter:card" content="summary_large_image">',
'<meta name="twitter:title" content="' + esc(title) + '">',
'<meta name="twitter:description" content="' + desc + '">',
'<meta name="twitter:image" content="' + esc(img) + '">',
'<meta name="description" content="' + desc + '">',
'<link rel="canonical" href="' + url + '">'
].join('\n');
html = html.replace('<title>Banner wall | InstantAdPay</title>', '<title>' + esc(title) + '</title>').replace('<!--OG-->', og);
}
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' }));
return res.end(html);
} catch (e) { return sendFile(res, path.join(PUBLIC_DIR, 'wall.html')); }
}
const safe = path.normalize(p).replace(/^([.\\/])+/, ''); const safe = path.normalize(p).replace(/^([.\\/])+/, '');
const file = path.join(PUBLIC_DIR, safe); const file = path.join(PUBLIC_DIR, safe);
if (file.startsWith(PUBLIC_DIR) && fs.existsSync(file) && fs.statSync(file).isFile()) return sendFile(res, file); if (file.startsWith(PUBLIC_DIR) && fs.existsSync(file) && fs.statSync(file).isFile()) return sendFile(res, file);