From dea279eed09050525b53fe1528bdecf3d49edc78 Mon Sep 17 00:00:00 2001 From: martbost Date: Fri, 4 Sep 2026 13:54:45 -0500 Subject: [PATCH] Free members refer from day one: share codes with late chain binding Every account gets a share code at signup; /join/ attributes first-touch site-side and resolves to the referrer's CURRENT on-chain id at the referral's buy time, so activating any time before your people buy locks the line to you. Joining through a code emails the referrer an activate-payouts nudge. Buy flow re-resolves the sponsor at click time. Copy updated across home and members; assets bumped to v=20260904g. Co-Authored-By: Claude Fable 5 --- accounts.js | 40 ++++++++++++++++++++++++------- public/assets/home.js | 10 +++++--- public/assets/my.js | 18 ++++++++------ public/contract.html | 6 ++--- public/index.html | 10 ++++---- public/ledger.html | 6 ++--- public/my.html | 15 ++++++------ server.js | 56 +++++++++++++++++++++++++++++++++---------- 8 files changed, 112 insertions(+), 49 deletions(-) diff --git a/accounts.js b/accounts.js index 6ab9912..bbb538a 100644 --- a/accounts.js +++ b/accounts.js @@ -16,6 +16,18 @@ function load() { try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {} if (!db || !db.v) db = { v: 2, byEmail: {}, byAddress: {}, joins: 0 }; if (db.v === 1) { db.v = 2; db.byEmail = db.byEmail || {}; } // early rehearsal file + if (!db.byCode) db.byCode = {}; + // every account carries a share code from day one (backfill older records) + for (const a of Object.values(db.byEmail)) { + if (!a.code) { a.code = genCode(); db.byCode[a.code] = a.email; } + else if (!db.byCode[a.code]) db.byCode[a.code] = a.email; + } +} +function genCode() { + let c; + do { c = crypto.randomBytes(5).toString('base64url').replace(/[-_]/g, '').slice(0, 7).toLowerCase(); } + while (!c || c.length < 6 || (db.byCode && db.byCode[c]) || /^\d+$/.test(c)); + return c; } function save() { try { @@ -45,21 +57,24 @@ const normEmail = e => String(e || '').trim().toLowerCase(); const normAddr = a => String(a || '').trim().toLowerCase(); // ---- email accounts (the normal join path) ---- -function signup(email, password, sponsorId) { +function signup(email, password, sponsorRef) { const e = normEmail(email); if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' }; if (String(password || '').length < 8) return { error: 'Password needs at least 8 characters.' }; if (db.byEmail[e]) return { error: 'That email already has an account. Log in instead.' }; + const code = genCode(); db.byEmail[e] = { email: e, pass: hashPassword(password), - sponsorId: Number(sponsorId) || 0, // first touch, written on-chain at first purchase + sponsorRef: String(sponsorRef || ''), // first touch; resolved to a chain id at buy time + code, address: null, created: Date.now() }; + db.byCode[code] = e; db.joins += 1; save(); - return { ok: true, account: publicView(db.byEmail[e]) }; + return { ok: true, created: true, account: publicView(db.byEmail[e]) }; } function login(email, password) { const e = normEmail(email); @@ -70,15 +85,23 @@ function login(email, password) { } // Passwordless path: a verified email code proves ownership, so the account // may exist with no password at all. -function ensure(email, sponsorId) { +function ensure(email, sponsorRef) { const e = normEmail(email); if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' }; + let created = false; if (!db.byEmail[e]) { - db.byEmail[e] = { email: e, pass: null, sponsorId: Number(sponsorId) || 0, address: null, created: Date.now() }; + const code = genCode(); + db.byEmail[e] = { email: e, pass: null, sponsorRef: String(sponsorRef || ''), code, address: null, created: Date.now() }; + db.byCode[code] = e; db.joins += 1; + created = true; save(); } - return { ok: true, account: publicView(db.byEmail[e]) }; + return { ok: true, created, account: publicView(db.byEmail[e]) }; +} +function byCode(code) { + const e = db.byCode[String(code || '').toLowerCase()]; + return e ? publicView(db.byEmail[e]) : null; } function byEmail(email) { const a = db.byEmail[normEmail(email)]; return a ? publicView(a) : null; } function byAddress(address) { @@ -104,8 +127,9 @@ function linkWallet(email, address) { } function publicView(a) { - return { email: a.email, sponsorId: a.sponsorId || 0, address: a.address || null, created: a.created }; + return { email: a.email, sponsorRef: a.sponsorRef || String(a.sponsorId || '') || '', + code: a.code || null, address: a.address || null, created: a.created }; } function count() { return Object.keys(db.byEmail).length; } -module.exports = { init, signup, login, ensure, byEmail, byAddress, linkWallet, count }; +module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, linkWallet, count }; diff --git a/public/assets/home.js b/public/assets/home.js index 5c18c54..e30021d 100644 --- a/public/assets/home.js +++ b/public/assets/home.js @@ -5,10 +5,11 @@ IAP.$('contractLink').href = c.explorer + '/address/' + c.contract; const sp = await (await fetch('/api/sponsor')).json(); - if (sp.sponsorId) { + if (sp.invited) { const el = IAP.$('sponsorLine'); el.hidden = false; - el.textContent = 'You were invited by member #' + sp.sponsorId + '. Your purchases pay their team, and your own link will do the same for you.'; + el.textContent = (sp.sponsorId ? 'You were invited by member #' + sp.sponsorId + '.' : 'You arrived through a member’s invite.') + + ' Your purchases pay their team, and your own link will do the same for you.'; } async function loadLadder() { @@ -41,7 +42,10 @@ await IAPWallet.signIn(); } IAP.status('Confirm the purchase in your wallet…'); - const r = await IAPWallet.buy(Number(btn.dataset.id), sp.sponsorId || 0, btn.dataset.cost); + // resolve the sponsor at buy time: a code referrer who activated since + // page load still gets locked in + const spNow = await (await fetch('/api/sponsor')).json(); + const r = await IAPWallet.buy(Number(btn.dataset.id), spNow.sponsorId || 0, btn.dataset.cost); if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.'); IAP.status('Purchase settled on-chain. Credits are yours, payouts delivered. Watch it on the ledger.', 'ok'); IAP.refreshNavWallet(); diff --git a/public/assets/my.js b/public/assets/my.js index b0d8325..c28f88e 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -34,21 +34,25 @@ $('campaignCard').hidden = !me.memberId; if (me.memberId) loadCampaigns(); + // the share link works from day one; codes resolve to your chain id later + if (me.refCode || me.memberId) { + $('inviteLine').textContent = location.origin + '/join/' + (me.refCode || me.memberId); + $('copyInvite').hidden = false; + } else { + $('inviteLine').textContent = 'Sign in with your email to get your link.'; + $('copyInvite').hidden = true; + } if (me.memberId) { const bc = me.buyerCount || 0; $('qualLine').innerHTML = '' + bc + ' qualifying buyer(s) referred
' + (bc >= 5 ? 'Level 3 unlocked: full three-level earnings' : bc >= 2 ? 'Level 2 unlocked · ' + (5 - bc) + ' more for level 3' : (2 - bc) + ' more buyer(s) of $20+ unlock level 2'); - $('inviteLine').textContent = location.origin + '/join/' + me.memberId; - $('copyInvite').hidden = false; loadActivity(); } else { - $('qualLine').textContent = 'Level 1 pays the moment you are on-chain. Referrals who buy packages of $20 or more unlock levels 2 and 3.'; - $('inviteLine').textContent = me.address - ? 'Activate payouts above and your invite link appears here.' - : 'Link a wallet and switch on payouts to get your invite link.'; - $('copyInvite').hidden = true; + $('qualLine').innerHTML = 'Share your link now. Then switch on payouts (free, above) ' + + 'before your people start buying: the contract locks each buyer to their sponsor at ' + + 'their first purchase, and payments only route to wallets that are switched on.'; } } diff --git a/public/contract.html b/public/contract.html index 0ee2007..6df0384 100644 --- a/public/contract.html +++ b/public/contract.html @@ -5,7 +5,7 @@ The contract | InstantAdPay - +
@@ -129,7 +129,7 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
- - + + diff --git a/public/index.html b/public/index.html index b6a32ca..5feb56a 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ InstantAdPay: advertise and earn, locked in code - + @@ -255,7 +255,7 @@

Free membership includes

  • A member account and the live ledger
  • -
  • Your personal referral link, once payouts are switched on
  • +
  • Your personal referral link, working from day one
  • Earnings from day one on your referrals' package purchases
  • Access to the member dashboard
@@ -345,8 +345,8 @@ - - - + + + diff --git a/public/ledger.html b/public/ledger.html index 22be73c..c8d662b 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -5,7 +5,7 @@ Live ledger | InstantAdPay - +
@@ -25,7 +25,7 @@
InstantAdPay · how it works · contract source ↗
- - + + diff --git a/public/my.html b/public/my.html index d0c235a..f17c013 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ My account | InstantAdPay - +
@@ -71,9 +71,10 @@
-

Your invite link

-

Share it anywhere. Everyone who joins through it becomes part of your line. - You earn 50 percent of every ad package they ever buy, plus levels 2 and 3 of their teams as you qualify.

+

Your invite link, live from day one

+

Share it anywhere, starting now. Everyone who joins through it becomes part of + your line, and you earn 50 percent of every ad package they ever buy, plus levels 2 and 3 as you + qualify. Just switch on payouts before your people start buying so every payment locks to you.

…

@@ -130,8 +131,8 @@ - - - + + + diff --git a/server.js b/server.js index 7bdbd96..7898da9 100644 --- a/server.js +++ b/server.js @@ -85,6 +85,32 @@ function isAdmin(req) { const h = req.headers.authorization || ''; return h === 'Bearer ' + ADMIN_PASSWORD; } +// A sponsor token is a numeric chain id or a site share code. Codes resolve +// to the referrer's CURRENT chain id, so activation any time before the +// referral's first purchase still locks the line to them. +async function resolveSponsorToken(tok) { + const t = String(tok || '').trim().toLowerCase(); + if (!t) return 0; + if (/^\d+$/.test(t)) return Number(t); + const acct = accounts.byCode(t); + if (!acct || !acct.address) return 0; + try { return await chain.memberIdByAccount(acct.address); } catch (e) { return 0; } +} +// The moment someone joins through a code, nudge its owner to activate. +function nudgeReferrer(ref) { + try { + const t = String(ref || '').trim().toLowerCase(); + if (!t || /^\d+$/.test(t) || !mailer.hasKey()) return; + const owner = accounts.byCode(t); + if (!owner || !owner.email || owner.address) return; // already activated-ready + mailer.send(owner.email, 'Someone just joined through your InstantAdPay link', + 'Good news: a new member just signed up through your share link.\n\n' + + 'One thing to do so you never miss a payment: sign in and switch on payouts ' + + '(one free wallet step). The contract locks each buyer to their sponsor at their ' + + 'first purchase, so have payouts on before your people start buying.\n\n' + + 'https://instantadpay.com/my\n\nInstantAdPay').catch(e => console.error('nudge failed', e.message)); + } catch (e) {} +} // ---- live feed (SSE) ---- const feedClients = new Set(); @@ -99,14 +125,16 @@ const server = http.createServer(async (req, res) => { const u = new URL(req.url, 'http://x'); const p = u.pathname; - // -- join links: /join/ — first-touch attribution cookie - let m = /^\/join\/(\d{1,9})$/.exec(p); + // -- join links: /join/ — first-touch cookie. + // Codes resolve LATE (at buy time) to whatever chain id the referrer + // has by then, so free members refer from day one. + let m = /^\/join\/([A-Za-z0-9]{1,16})$/.exec(p); if (m && req.method === 'GET') { - const sid = Number(m[1]); + const tok = m[1].toLowerCase(); const cookies = parseCookies(req); const headers = { Location: '/' }; if (!cookies['iap.sponsor']) { - headers['Set-Cookie'] = `iap.sponsor=${sid}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; + headers['Set-Cookie'] = `iap.sponsor=${tok}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; } res.writeHead(302, baseHeaders(headers)); return res.end(); @@ -133,10 +161,9 @@ const server = http.createServer(async (req, res) => { return; } if (p === '/api/sponsor' && req.method === 'GET') { - const sid = Number(parseCookies(req)['iap.sponsor']) || 0; - let sponsor = null; - if (sid) { try { const mm = await chain.member(sid); if (mm.account !== '0x' + '0'.repeat(40)) sponsor = { id: sid }; } catch (e) {} } - return json(res, 200, { sponsorId: sponsor ? sid : 0 }); + const tok = parseCookies(req)['iap.sponsor'] || ''; + const sponsorId = await resolveSponsorToken(tok); + return json(res, 200, { ref: tok, sponsorId, invited: !!tok }); } if (p === '/api/stats' && req.method === 'GET') { let members = 0; try { members = await chain.memberCount(); } catch (e) {} @@ -147,9 +174,10 @@ const server = http.createServer(async (req, res) => { // out only at purchase / payout-activation time and gets linked then) if (p === '/api/signup' && req.method === 'POST') { const b = await readBody(req); - const sid = Number(parseCookies(req)['iap.sponsor']) || 0; // first-touch attribution - const r = accounts.signup(b.email, b.password, sid); + const ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution + const r = accounts.signup(b.email, b.password, ref); if (r.error) return json(res, 400, r); + nudgeReferrer(ref); const token = auth.mintSession({ email: r.account.email }); return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) }); } @@ -191,9 +219,10 @@ const server = http.createServer(async (req, res) => { if (rec.tries > 6) { emailCodes.delete(e); return json(res, 400, { error: 'Too many tries. Request a fresh code.' }); } if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' }); emailCodes.delete(e); - const sid = Number(parseCookies(req)['iap.sponsor']) || 0; - const r = accounts.ensure(e, sid); // first touch wins; existing accounts unchanged + const ref = parseCookies(req)['iap.sponsor'] || ''; + const r = accounts.ensure(e, ref); // first touch wins; existing accounts unchanged if (r.error) return json(res, 400, r); + if (r.created) nudgeReferrer(ref); let memberId = 0; if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} } const token = auth.mintSession({ email: r.account.email, address: r.account.address, memberId }); @@ -234,9 +263,10 @@ const server = http.createServer(async (req, res) => { if (!s) return json(res, 200, { signedIn: false }); const memberId = await auth.refreshMemberId(s); const acct = (s.email && accounts.byEmail(s.email)) || (s.address && accounts.byAddress(s.address)) || null; + const sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']); const out = { signedIn: true, email: s.email || (acct && acct.email) || null, address: s.address || (acct && acct.address) || null, memberId, - sponsorId: (acct && acct.sponsorId) || Number(parseCookies(req)['iap.sponsor']) || 0 }; + refCode: (acct && acct.code) || null, sponsorId }; if (memberId) { try { const mm = await chain.member(memberId);