diff --git a/ads.js b/ads.js
index 0a199c2..0bd62ea 100644
Binary files a/ads.js and b/ads.js differ
diff --git a/chatbot.js b/chatbot.js
index 0e1ac5f..87c3ffd 100644
--- a/chatbot.js
+++ b/chatbot.js
@@ -81,6 +81,7 @@ FACTS:
- HOLDING TANK (Members > My line > Holding tank card): free members who joined with no sponsor wait there; a member who has switched on payouts AND bought their own $20+ package can Adopt one (first come, max 2 open adoptions, 7-day window; if the person never links a wallet or buys, they fall back into the tank; a person can be adopted twice at most). Adopting sets the sponsor, opens a chat and emails the member; their first purchase then binds to the adopter on-chain. Members can also "Release to tank" one of their own free referrals (pay it forward), but NOT someone they adopted less than 3 days ago: an adoption is a commitment, and a dropped adoption still counts toward that person's two-adoption lifetime limit. Releases are posted to the feed and Telegram like pickups. Admin sees the tank under Members.
- DAILY CLAIM STREAK: finishing the daily ad set and claiming pays 5 credits on day 1, 7 on day 2, 10 from day 3, and 25 on every 7th consecutive day; miss a day and it restarts. After the set, verified visits (up to 20 a day, 1 credit each) keep earning, and the credits are meant to be spent on a campaign.
- BLOG (public, instantadpay.com/blog): Marty's coaching and teaching articles on building a line, advertising that pays, and daily habits; each article has its own page and can be shared; RSS at /blog/feed.xml. Members who want to write their own articles: not offered today.
+- ACHIEVEMENT BADGES ON TELEGRAM (2026-09-13): when a member unlocks Spark/Surge/Circuit/Nexus, their personalised badge image (username on the ribbon) is posted automatically to the team's Telegram payments topic and the main group, once per badge; the Overview badge card also has a 'Post to Telegram' button for badges unlocked earlier, and 'Share' downloads the image.
- HOLDING TANK ALERTS: when new members land in the tank, a note at the top of every member's Overview names them (usernames) and a post goes to the team's Telegram payments topic; adopt from My line > Holding tank (your own $20 package required).
- LEGACY WELCOME (former Faucet Wave / Tier One Ads members): they join through instantadpay.com/from/faucetwave or instantadpay.com/from/tieroneads and, if their email is on the legacy list, welcome-back credits are added automatically at signup (former advertisers 500, former earners 150; once per person; credits, not POL). They land in the holding tank like any member who joins without a sponsor.
- PROMO CODES: partner site owners get a reusable code; a member redeems it on a join link (?promo=CODE) or in the Overview box "Have a promo code?" and receives free ad credits (amount set per code by the admin, one use per account; codes can cap uses or expire). Credits, not POL.
diff --git a/public/assets/my.js b/public/assets/my.js
index 23757e2..dd6a5b5 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -87,17 +87,58 @@
return '
'
+ ''
+ '
' + b.label + '
' + b.sub + '
'
- + (got ? '' : '
π locked
') + '
';
+ + (got ? ' ' : '
π locked
') + '';
}).join('');
strip.querySelectorAll('[data-badge]').forEach(btn =>
btn.addEventListener('click', () => shareBadge(btn.dataset.badge, d)));
- // celebrate anything newly granted this load
+ strip.querySelectorAll('[data-badgepost]').forEach(btn =>
+ btn.addEventListener('click', () => postBadge(btn.dataset.badgepost, d, btn)));
+ fetch('/api/my/badge-posted').then(r => r.json()).then(r => (r.posted || []).forEach(k => { const b = strip.querySelector('[data-badgepost="' + k + '"]'); if (b) { b.textContent = 'Posted β'; b.disabled = true; } })).catch(() => {});
+ // celebrate anything newly granted this load, and post the branded badge to the team channels (Marty, 2026-09-13)
if (d.milestonesGranted && d.milestonesGranted.length) {
const total = d.milestonesGranted.reduce((s, g) => s + g.credited, 0);
const names = d.milestonesGranted.map(g => (BADGES.find(b => b.key === g.key) || {}).label).filter(Boolean).join(', ');
IAP.status('π Achievement unlocked: ' + names + ' β +' + total + ' bonus credits!', 'ok');
+ d.milestonesGranted.forEach((g, i) => setTimeout(() => postBadge(g.key, d, strip.querySelector('[data-badgepost="' + g.key + '"]')), 800 + i * 1500));
}
}
+ // draw the badge with the member's name on the ribbon; cb(canvas) or cb(null)
+ function composeBadge(key, d, cb) {
+ const b = BADGES.find(x => x.key === key); if (!b) return cb(null);
+ const who = d.username ? d.username : d.memberId ? 'member #' + d.memberId : '';
+ const img = new Image();
+ img.onload = () => {
+ const c = document.createElement('canvas'); c.width = img.width; c.height = img.height;
+ const x = c.getContext('2d');
+ x.drawImage(img, 0, 0);
+ if (who) {
+ x.textAlign = 'center'; x.textBaseline = 'middle';
+ x.font = 'bold ' + Math.round(c.width * 0.055) + 'px Sora, "Segoe UI", sans-serif';
+ const y = c.height * (b.ribbonY || 0.75);
+ x.lineJoin = 'round'; x.lineWidth = Math.max(3, Math.round(c.width * 0.008));
+ x.strokeStyle = 'rgba(4,20,15,.9)'; x.strokeText(who, c.width / 2, y);
+ x.fillStyle = '#ffd15c'; x.fillText(who, c.width / 2, y);
+ }
+ cb(c);
+ };
+ img.onerror = () => cb(null);
+ img.src = b.img;
+ }
+ async function postBadge(key, d, btn) {
+ if (btn && btn.disabled) return;
+ if (btn) { btn.disabled = true; btn.textContent = 'Postingβ¦'; }
+ composeBadge(key, d, c => {
+ if (!c) { if (btn) { btn.disabled = false; btn.textContent = 'Post to Telegram'; } return; }
+ c.toBlob(async bl => {
+ try {
+ const r = await (await fetch('/api/my/badge-post?key=' + encodeURIComponent(key), { method: 'POST', headers: { 'Content-Type': 'image/jpeg' }, body: bl })).json();
+ if (r.error) throw new Error(r.error);
+ if (btn) { btn.textContent = 'Posted β'; btn.disabled = true; }
+ if (!r.already) IAP.status('Your badge is posted in the team channels.', 'ok');
+ } catch (e) { if (btn) { btn.disabled = false; btn.textContent = 'Post to Telegram'; } IAP.status(e.message || 'Could not post the badge.', 'bad'); }
+ }, 'image/jpeg', 0.86);
+ });
+ }
// compose a shareable badge image on a canvas (zero-dep) and download it
// composite the ornate badge template with the member's @username on the ribbon
function shareBadge(key, d) {
@@ -296,6 +337,7 @@
// site-wide activity (not about me): joins, tank, adoptions, purchases, payouts, qualifications
if (ev.type === 'Joined') { communityToast('π ' + ev.name + ' just joined' + (ev.tank ? ' and is waiting for a sponsor in the holding tank' : '')); liveRefresh(); return; }
if (ev.type === 'Adopted') { communityToast('π€ ' + ev.sponsor + ' picked up ' + ev.member + ' from the holding tank'); liveRefresh(); return; }
+ if (ev.type === 'Badge') { communityToast('π ' + ev.member + ' unlocked ' + ev.label); return; }
if (ev.type === 'Released') { communityToast('πͺ£ ' + ev.sponsor + ' returned ' + ev.member + ' to the holding tank'); liveRefresh(); return; }
const mine = MYID && (ev.recipientId === MYID || ev.toId === MYID || ev.sponsorId === MYID || ev.skippedId === MYID || ev.buyerId === MYID);
if (!mine) {
diff --git a/public/my.html b/public/my.html
index 31508eb..37f8930 100644
--- a/public/my.html
+++ b/public/my.html
@@ -915,7 +915,7 @@
-
+