// Syndicate member campaigns to AdRevLinks as popup ads. // // The AdRevLinks database is NOT reachable from this server (port 3306 on that box is // closed to us), so we talk to a narrow authenticated endpoint that runs there and does // the insert locally. It exposes exactly five actions and no general query surface, so a // leaked key can only create or remove popup campaigns. // // FEATURE-FLAGGED: inert unless ADREVLNKS_BRIDGE_URL and ADREVLNKS_BRIDGE_KEY are set. // // Two things learned from the Network Ad Space integration, deliberately repeated here: // // 1. DELIVERY IS READ, NEVER DERIVED. `status` returns the rotator's own per-country view // counters. We never compute "served" from a number that a stop or cancel could // overwrite, which is exactly what charged members for undelivered impressions there. // // 2. EVERY WRITE IS IDEMPOTENT. Each campaign carries ref "iap:". A retry after a // timeout returns the existing campaign instead of booking a second one. // // Capacity note (2026-09-18): that server serves roughly 5,500 popup impressions a day in // TOTAL, shared by every active campaign, and its rotator favours campaigns with the least // delivered. So syndicating everything would starve what is already running, including // Marty's own ads. Hence MAX_VIEWS_PER_CAMPAIGN and MIN_CREDITS below. Cost is not the // reason for the caps - Marty owns the platform and treats the placements as free - the // reason is that inventory there is finite and shared. 'use strict'; const https = require('https'); const { URL } = require('url'); // Tier 1 only. These are the sole countries with a configured popup price on that server; // anything else is refused by the bridge rather than silently accepted. const TIER1 = ['US', 'CA', 'GB', 'AU', 'NZ']; const WORLDWIDE = ['all']; const MIN_CREDITS = Number(process.env.ADREVLNKS_MIN_CREDITS || 1000); // below this, not worth a slot const MAX_VIEWS_PER_CAMPAIGN = Number(process.env.ADREVLNKS_MAX_VIEWS || 50000); const TRAFFIC_SOURCE = Number(process.env.ADREVLNKS_TRAFFIC_SOURCE || 1); const USER_ID = Number(process.env.ADREVLNKS_USER_ID || 2); function enabled() { return !!(process.env.ADREVLNKS_BRIDGE_URL && process.env.ADREVLNKS_BRIDGE_KEY); } function call(action, payload, timeoutMs) { return new Promise((resolve, reject) => { if (!enabled()) return reject(new Error('adrevlnks-disabled')); const u = new URL(process.env.ADREVLNKS_BRIDGE_URL); const body = JSON.stringify(Object.assign({ action }, payload || {})); const req = https.request({ hostname: u.hostname, path: u.pathname + u.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'X-Bridge-Key': process.env.ADREVLNKS_BRIDGE_KEY }, timeout: timeoutMs || 25000 }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch (e) {} if (!j) return reject(new Error('adrevlnks ' + res.statusCode + ': ' + d.slice(0, 120))); if (j.error) return reject(new Error(j.error)); resolve(j); }); }); req.on('error', reject); req.on('timeout', () => req.destroy(new Error('adrevlnks timeout'))); req.end(body); }); } const refFor = campaignId => 'iap:' + Number(campaignId); // How many popup views a campaign's credits are worth. 1 credit = 1 view, capped so a // single large campaign cannot swallow the whole server's daily inventory. function viewsFor(credits) { const v = Math.max(0, Math.floor(Number(credits) || 0)); return Math.min(v, MAX_VIEWS_PER_CAMPAIGN); } // Push a campaign. Returns null when it is not worth syndicating, rather than throwing, // so the caller can treat "too small" and "disabled" the same quiet way. async function push(campaign, opts) { if (!enabled()) return null; const credits = Number((campaign.budget || 0) - (campaign.spent || 0) - (campaign.accrued || 0)); if (credits < MIN_CREDITS) return null; const views = viewsFor(credits); if (views < 1000) return null; const title = String(campaign.name || campaign.title || 'InstantAdPay').slice(0, 250); const url = String(campaign.targetUrl || campaign.target_url || '').trim(); if (!/^https?:\/\//i.test(url)) return null; const countries = (opts && opts.worldwide) ? WORLDWIDE : TIER1; return await call('create_popup', { title, url, views, countries, traffic_source: TRAFFIC_SOURCE, user_id: USER_ID, ref: refFor(campaign.id), activate: !(opts && opts.paused) }); } // Real delivery, straight from the rotator's counters. Never derived. async function readServed(campaignId) { if (!enabled()) return null; try { return await call('status', { ref: refFor(campaignId) }); } catch (e) { if (/not found/i.test(e.message)) return null; throw e; } } async function pause(campaignId) { return enabled() ? call('pause', { ref: refFor(campaignId) }) : null; } async function resume(campaignId) { return enabled() ? call('resume', { ref: refFor(campaignId) }) : null; } async function remove(campaignId) { if (!enabled()) return null; try { return await call('delete', { ref: refFor(campaignId) }); } catch (e) { if (/not found/i.test(e.message)) return null; throw e; } } module.exports = { enabled, push, readServed, pause, resume, remove, viewsFor, refFor, TIER1, WORLDWIDE, MIN_CREDITS, MAX_VIEWS_PER_CAMPAIGN };