Free members refer from day one: share codes with late chain binding

Every account gets a share code at signup; /join/<code> 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 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 13:54:45 -05:00
parent 9dc5989aa3
commit dea279eed0
8 changed files with 112 additions and 49 deletions
+32 -8
View File
@@ -16,6 +16,18 @@ function load() {
try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {} try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {}
if (!db || !db.v) db = { v: 2, byEmail: {}, byAddress: {}, joins: 0 }; 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.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() { function save() {
try { try {
@@ -45,21 +57,24 @@ const normEmail = e => String(e || '').trim().toLowerCase();
const normAddr = a => String(a || '').trim().toLowerCase(); const normAddr = a => String(a || '').trim().toLowerCase();
// ---- email accounts (the normal join path) ---- // ---- email accounts (the normal join path) ----
function signup(email, password, sponsorId) { function signup(email, password, sponsorRef) {
const e = normEmail(email); const e = normEmail(email);
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' }; 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 (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.' }; if (db.byEmail[e]) return { error: 'That email already has an account. Log in instead.' };
const code = genCode();
db.byEmail[e] = { db.byEmail[e] = {
email: e, email: e,
pass: hashPassword(password), 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, address: null,
created: Date.now() created: Date.now()
}; };
db.byCode[code] = e;
db.joins += 1; db.joins += 1;
save(); save();
return { ok: true, account: publicView(db.byEmail[e]) }; return { ok: true, created: true, account: publicView(db.byEmail[e]) };
} }
function login(email, password) { function login(email, password) {
const e = normEmail(email); const e = normEmail(email);
@@ -70,15 +85,23 @@ function login(email, password) {
} }
// Passwordless path: a verified email code proves ownership, so the account // Passwordless path: a verified email code proves ownership, so the account
// may exist with no password at all. // may exist with no password at all.
function ensure(email, sponsorId) { function ensure(email, sponsorRef) {
const e = normEmail(email); const e = normEmail(email);
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' }; if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
let created = false;
if (!db.byEmail[e]) { 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; db.joins += 1;
created = true;
save(); 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 byEmail(email) { const a = db.byEmail[normEmail(email)]; return a ? publicView(a) : null; }
function byAddress(address) { function byAddress(address) {
@@ -104,8 +127,9 @@ function linkWallet(email, address) {
} }
function publicView(a) { 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; } 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 };
+7 -3
View File
@@ -5,10 +5,11 @@
IAP.$('contractLink').href = c.explorer + '/address/' + c.contract; IAP.$('contractLink').href = c.explorer + '/address/' + c.contract;
const sp = await (await fetch('/api/sponsor')).json(); const sp = await (await fetch('/api/sponsor')).json();
if (sp.sponsorId) { if (sp.invited) {
const el = IAP.$('sponsorLine'); const el = IAP.$('sponsorLine');
el.hidden = false; 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() { async function loadLadder() {
@@ -41,7 +42,10 @@
await IAPWallet.signIn(); await IAPWallet.signIn();
} }
IAP.status('Confirm the purchase in your wallet…'); 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.'); 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.status('Purchase settled on-chain. Credits are yours, payouts delivered. Watch it on the ledger.', 'ok');
IAP.refreshNavWallet(); IAP.refreshNavWallet();
+11 -7
View File
@@ -34,21 +34,25 @@
$('campaignCard').hidden = !me.memberId; $('campaignCard').hidden = !me.memberId;
if (me.memberId) loadCampaigns(); 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) { if (me.memberId) {
const bc = me.buyerCount || 0; const bc = me.buyerCount || 0;
$('qualLine').innerHTML = '<b>' + bc + '</b> qualifying buyer(s) referred<br>' $('qualLine').innerHTML = '<b>' + bc + '</b> qualifying buyer(s) referred<br>'
+ (bc >= 5 ? '<span class="badge">Level 3 unlocked: full three-level earnings</span>' + (bc >= 5 ? '<span class="badge">Level 3 unlocked: full three-level earnings</span>'
: bc >= 2 ? '<span class="badge">Level 2 unlocked</span> · ' + (5 - bc) + ' more for level 3' : bc >= 2 ? '<span class="badge">Level 2 unlocked</span> · ' + (5 - bc) + ' more for level 3'
: (2 - bc) + ' more buyer(s) of $20+ unlock level 2'); : (2 - bc) + ' more buyer(s) of $20+ unlock level 2');
$('inviteLine').textContent = location.origin + '/join/' + me.memberId;
$('copyInvite').hidden = false;
loadActivity(); loadActivity();
} else { } 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.'; $('qualLine').innerHTML = 'Share your link now. Then switch on payouts (free, above) '
$('inviteLine').textContent = me.address + '<b>before your people start buying</b>: the contract locks each buyer to their sponsor at '
? 'Activate payouts above and your invite link appears here.' + 'their first purchase, and payments only route to wallets that are switched on.';
: 'Link a wallet and switch on payouts to get your invite link.';
$('copyInvite').hidden = true;
} }
} }
+3 -3
View File
@@ -5,7 +5,7 @@
<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="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=20260904f"> <link rel="stylesheet" href="/assets/site.css?v=20260904g">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
@@ -129,7 +129,7 @@
<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=20260904f"></script> <script src="/assets/common.js?v=20260904g"></script>
<script src="/assets/contract.js?v=20260904f"></script> <script src="/assets/contract.js?v=20260904g"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -5,7 +5,7 @@
<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="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.">
<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=20260904f"> <link rel="stylesheet" href="/assets/site.css?v=20260904g">
</head> </head>
<body> <body>
@@ -255,7 +255,7 @@
<h3>Free membership includes</h3> <h3>Free membership includes</h3>
<ul class="checks"> <ul class="checks">
<li>A member account and the live ledger</li> <li>A member account and the live ledger</li>
<li>Your personal referral link, once payouts are switched on</li> <li>Your personal referral link, working from day one</li>
<li>Earnings from day one on your referrals' package purchases</li> <li>Earnings from day one on your referrals' package purchases</li>
<li>Access to the member dashboard</li> <li>Access to the member dashboard</li>
</ul> </ul>
@@ -345,8 +345,8 @@
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260904f"></script> <script src="/assets/common.js?v=20260904g"></script>
<script src="/assets/wallet.js?v=20260904f"></script> <script src="/assets/wallet.js?v=20260904g"></script>
<script src="/assets/home.js?v=20260904f"></script> <script src="/assets/home.js?v=20260904g"></script>
</body> </body>
</html> </html>
+3 -3
View File
@@ -5,7 +5,7 @@
<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="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=20260904f"> <link rel="stylesheet" href="/assets/site.css?v=20260904g">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
@@ -25,7 +25,7 @@
<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=20260904f"></script> <script src="/assets/common.js?v=20260904g"></script>
<script src="/assets/ledger.js?v=20260904f"></script> <script src="/assets/ledger.js?v=20260904g"></script>
</body> </body>
</html> </html>
+8 -7
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>My account | InstantAdPay</title> <title>My account | 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=20260904f"> <link rel="stylesheet" href="/assets/site.css?v=20260904g">
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
@@ -71,9 +71,10 @@
</div> </div>
<div class="card"> <div class="card">
<h3>Your invite link</h3> <h3>Your invite link, live from day one</h3>
<p class="muted small">Share it anywhere. Everyone who joins through it becomes part of your line. <p class="muted small">Share it anywhere, starting now. Everyone who joins through it becomes part of
You earn 50 percent of every ad package they ever buy, plus levels 2 and 3 of their teams as you qualify.</p> 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.</p>
<p class="mono" id="inviteLine">…</p> <p class="mono" id="inviteLine">…</p>
<button class="btn small sec" id="copyInvite" hidden>Copy link</button> <button class="btn small sec" id="copyInvite" hidden>Copy link</button>
</div> </div>
@@ -130,8 +131,8 @@
<footer><div>InstantAdPay · <a href="/ledger">live ledger</a></div></footer> <footer><div>InstantAdPay · <a href="/ledger">live ledger</a></div></footer>
</div> </div>
<script src="/assets/common.js?v=20260904f"></script> <script src="/assets/common.js?v=20260904g"></script>
<script src="/assets/wallet.js?v=20260904f"></script> <script src="/assets/wallet.js?v=20260904g"></script>
<script src="/assets/my.js?v=20260904f"></script> <script src="/assets/my.js?v=20260904g"></script>
</body> </body>
</html> </html>
+43 -13
View File
@@ -85,6 +85,32 @@ function isAdmin(req) {
const h = req.headers.authorization || ''; const h = req.headers.authorization || '';
return h === 'Bearer ' + ADMIN_PASSWORD; 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) ---- // ---- live feed (SSE) ----
const feedClients = new Set(); const feedClients = new Set();
@@ -99,14 +125,16 @@ const server = http.createServer(async (req, res) => {
const u = new URL(req.url, 'http://x'); const u = new URL(req.url, 'http://x');
const p = u.pathname; const p = u.pathname;
// -- join links: /join/<sponsorId> — first-touch attribution cookie // -- join links: /join/<memberId or share code> — first-touch cookie.
let m = /^\/join\/(\d{1,9})$/.exec(p); // 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') { if (m && req.method === 'GET') {
const sid = Number(m[1]); const tok = m[1].toLowerCase();
const cookies = parseCookies(req); const cookies = parseCookies(req);
const headers = { Location: '/' }; const headers = { Location: '/' };
if (!cookies['iap.sponsor']) { 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)); res.writeHead(302, baseHeaders(headers));
return res.end(); return res.end();
@@ -133,10 +161,9 @@ const server = http.createServer(async (req, res) => {
return; return;
} }
if (p === '/api/sponsor' && req.method === 'GET') { if (p === '/api/sponsor' && req.method === 'GET') {
const sid = Number(parseCookies(req)['iap.sponsor']) || 0; const tok = parseCookies(req)['iap.sponsor'] || '';
let sponsor = null; const sponsorId = await resolveSponsorToken(tok);
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, { ref: tok, sponsorId, invited: !!tok });
return json(res, 200, { sponsorId: sponsor ? sid : 0 });
} }
if (p === '/api/stats' && req.method === 'GET') { if (p === '/api/stats' && req.method === 'GET') {
let members = 0; try { members = await chain.memberCount(); } catch (e) {} 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) // out only at purchase / payout-activation time and gets linked then)
if (p === '/api/signup' && req.method === 'POST') { if (p === '/api/signup' && req.method === 'POST') {
const b = await readBody(req); const b = await readBody(req);
const sid = Number(parseCookies(req)['iap.sponsor']) || 0; // first-touch attribution const ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution
const r = accounts.signup(b.email, b.password, sid); const r = accounts.signup(b.email, b.password, ref);
if (r.error) return json(res, 400, r); if (r.error) return json(res, 400, r);
nudgeReferrer(ref);
const token = auth.mintSession({ email: r.account.email }); const token = auth.mintSession({ email: r.account.email });
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) }); 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 (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.' }); if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' });
emailCodes.delete(e); emailCodes.delete(e);
const sid = Number(parseCookies(req)['iap.sponsor']) || 0; const ref = parseCookies(req)['iap.sponsor'] || '';
const r = accounts.ensure(e, sid); // first touch wins; existing accounts unchanged const r = accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r); if (r.error) return json(res, 400, r);
if (r.created) nudgeReferrer(ref);
let memberId = 0; let memberId = 0;
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} } 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 }); 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 }); if (!s) return json(res, 200, { signedIn: false });
const memberId = await auth.refreshMemberId(s); const memberId = await auth.refreshMemberId(s);
const acct = (s.email && accounts.byEmail(s.email)) || (s.address && accounts.byAddress(s.address)) || null; 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, const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
address: s.address || (acct && acct.address) || null, memberId, 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) { if (memberId) {
try { try {
const mm = await chain.member(memberId); const mm = await chain.member(memberId);