Sponsor attribution: last touch (30-day cookie), locked at account creation; 'Joining under' line at the code step; chatbot answer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-10 15:49:40 -05:00
parent 4addf7718e
commit 04e42e94ff
4 changed files with 19 additions and 8 deletions
+9 -6
View File
@@ -489,9 +489,12 @@ const server = http.createServer(async (req, res) => {
const u = new URL(req.url, 'http://x');
const p = u.pathname;
// -- join links: /join/<memberId or share code> — 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.
// -- join links: /join/<memberId or share code> — LAST-touch cookie (Marty,
// 2026-09-10): the link a visitor opened most recently is the sponsor shown
// and used, and it locks the moment the account is created (accounts.ensure
// never changes an existing account's sponsor; the contract binds the buyer at
// their first purchase). 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,20})$/.exec(p);
if (m && (req.method === 'GET' || req.method === 'HEAD')) {
// lead-capture page: email first, wallet later. ?v=<angle> picks the hook
@@ -501,9 +504,9 @@ const server = http.createServer(async (req, res) => {
const angle = String(u.searchParams.get('v') || '').toLowerCase();
const ang = JOIN_ANGLES[angle] || null;
if (req.method === 'GET') coach.recordView(tok, ang ? angle : '', req.headers.referer); // link stats per angle + source
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; // 30 days: whoever brings them back gets the credit
const set = [];
if (!cookies['iap.sponsor']) set.push('iap.sponsor=' + tok + cookieTail);
set.push('iap.sponsor=' + tok + cookieTail); // last touch wins
if (ang) set.push('iap.angle=' + angle + cookieTail);
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
@@ -628,7 +631,7 @@ 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 ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution
const ref = parseCookies(req)['iap.sponsor'] || ''; // last-touch attribution, locked at account creation
const r = await accounts.signup(b.email, b.password, ref);
if (r.error) return json(res, 400, r);
// sponsor is notified once the new member picks a username (onboarding),