Badge posts: manual Post to Telegram is admin-only; members' badges still post automatically on unlock

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-13 17:09:59 -05:00
parent 964a02a342
commit e63bc57a99
4 changed files with 13 additions and 8 deletions
+1 -1
View File
@@ -81,7 +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.
- 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; members cannot trigger posts themselves (the 'Post to Telegram' button is admin-only); '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.
+8 -5
View File
@@ -87,13 +87,16 @@
return '<div class="badge-a' + (got ? '' : ' locked') + '">'
+ '<img class="badge-img" src="' + b.img + '" alt="' + b.label + ' badge" loading="lazy">'
+ '<div class="bl">' + b.label + '</div><div class="bs">' + b.sub + '</div>'
+ (got ? '<button class="share" data-badge="' + b.key + '">Share</button> <button class="share" data-badgepost="' + b.key + '">Post to Telegram</button>' : '<div class="bs">🔒 locked</div>') + '</div>';
+ (got ? '<button class="share" data-badge="' + b.key + '">Share</button> <button class="share" data-badgepost="' + b.key + '" hidden>Post to Telegram</button>' : '<div class="bs">🔒 locked</div>') + '</div>';
}).join('');
strip.querySelectorAll('[data-badge]').forEach(btn =>
btn.addEventListener('click', () => shareBadge(btn.dataset.badge, d)));
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(() => {});
btn.addEventListener('click', () => postBadge(btn.dataset.badgepost, d, btn, true)));
fetch('/api/my/badge-posted').then(r => r.json()).then(r => { // the manual button is the admin's only (Marty, 2026-09-13)
if (r.canPost) strip.querySelectorAll('[data-badgepost]').forEach(b => { b.hidden = false; });
(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);
@@ -124,14 +127,14 @@
img.onerror = () => cb(null);
img.src = b.img;
}
async function postBadge(key, d, btn) {
async function postBadge(key, d, btn, manual) {
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();
const r = await (await fetch('/api/my/badge-post?key=' + encodeURIComponent(key) + (manual ? '&manual=1' : ''), { 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');
+1 -1
View File
@@ -915,7 +915,7 @@
<script src="/assets/common.js?v=20260913a"></script>
<script src="/assets/wallet.js?v=20260911a"></script>
<script src="/assets/promo.js?v=20260911a"></script>
<script src="/assets/my.js?v=20260913g"></script>
<script src="/assets/my.js?v=20260913h"></script>
<script src="/assets/chat.js?v=20260907l"></script>
</body>
</html>
+3 -1
View File
@@ -1293,6 +1293,8 @@ const server = http.createServer(async (req, res) => {
if (!held.includes(key)) return json(res, 400, { error: 'You have not unlocked that badge yet.' });
const log = badgeLog(); const mine = log[s.email] || {};
if (mine[key]) return json(res, 200, { ok: true, already: true });
// manual re-posts (the button) are Marty's only; members' badges go out automatically on unlock
if (u.searchParams.get('manual') === '1' && s.email !== ADMIN_EMAIL) return json(res, 403, { error: 'Badges post automatically when they unlock.' });
const a = await accounts.byEmail(s.email);
const who = a && a.username ? '@' + a.username : (a && a.memberId ? 'member #' + a.memberId : 'a member');
const link = a && a.username ? 'instantadpay.com/join/' + a.username : 'instantadpay.com';
@@ -1311,7 +1313,7 @@ const server = http.createServer(async (req, res) => {
if (p === '/api/my/badge-posted' && req.method === 'GET') { // which of my badges are already on Telegram
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, { posted: Object.keys(badgeLog()[s.email] || {}) });
return json(res, 200, { posted: Object.keys(badgeLog()[s.email] || {}), canPost: s.email === ADMIN_EMAIL });
}
if (p === '/api/my/tank/adopt' && req.method === 'POST') {
const s = await auth.fromRequest(req);