← back to Agent Ad Network
public landing page: no-auth / with anonymized numbers-only three.js ad galaxy (opaque HMAC ids, no copy/sponsor/url leak), live totals via new public /api/galaxy, advertiser self-serve curl walkthrough, public /assets/three.min.js; /admin unchanged behind auth (TK-10131)
3ffe56e55749e5c9f3a50a541d6357b3b7e962e3 · 2026-08-02 07:06:47 -0700 · Steve Abrams
Files touched
A landing.htmlM server.js
Diff
commit 3ffe56e55749e5c9f3a50a541d6357b3b7e962e3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Aug 2 07:06:47 2026 -0700
public landing page: no-auth / with anonymized numbers-only three.js ad galaxy (opaque HMAC ids, no copy/sponsor/url leak), live totals via new public /api/galaxy, advertiser self-serve curl walkthrough, public /assets/three.min.js; /admin unchanged behind auth (TK-10131)
---
landing.html | 273 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 40 ++++++++-
2 files changed, 312 insertions(+), 1 deletion(-)
diff --git a/landing.html b/landing.html
new file mode 100644
index 0000000..a8af398
--- /dev/null
+++ b/landing.html
@@ -0,0 +1,273 @@
+<!doctype html>
+<html><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Agent Ad Network — advertising in AI-agent wait states</title>
+<meta name="description" content="Serve one line of sponsored text into the loading pauses of AI agents, CLIs, and pipelines. Self-serve API — agents can place ads autonomously. Watch the network live.">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='6' fill='%234fd1c5'/%3E%3Ccircle cx='16' cy='16' r='12' fill='none' stroke='%234fd1c5' stroke-width='1.5' opacity='.45'/%3E%3C/svg%3E">
+<style>
+ :root { --fg:#e6e6e6; --bg:#0b0d12; --card:#181b21; --line:#2a2f38; --accent:#4fd1c5; --dim:#8a93a3; }
+ * { box-sizing:border-box; }
+ body { font:15px/1.55 -apple-system,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--fg); margin:0; }
+ .wrap { max-width:1100px; margin:0 auto; padding:0 20px; }
+ header { padding:34px 0 6px; }
+ .brand { font-size:13px; letter-spacing:.28em; color:var(--accent); font-weight:700; }
+ h1 { font-size:clamp(26px,4.4vw,44px); line-height:1.12; margin:10px 0 8px; letter-spacing:-.01em; }
+ h1 em { color:var(--accent); font-style:normal; }
+ .sub { color:var(--dim); max-width:640px; font-size:16px; }
+ /* live stat strip */
+ #stats { display:flex; gap:12px; margin:22px 0 16px; flex-wrap:wrap; }
+ .stat { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:10px 18px; min-width:120px; }
+ .stat b { display:block; font-size:22px; color:var(--accent); font-variant-numeric:tabular-nums; }
+ .stat span { color:var(--dim); font-size:12px; letter-spacing:.06em; }
+ /* galaxy */
+ #galaxyWrap { position:relative; height:min(58vh,520px); min-height:340px; border:1px solid var(--line);
+ border-radius:14px; overflow:hidden; background:radial-gradient(ellipse at 50% 40%, #171c26 0%, #0b0d12 70%); }
+ #galaxy { width:100%; height:100%; display:block; cursor:grab; }
+ #galaxyTitle { position:absolute; left:14px; top:12px; font-size:11px; letter-spacing:.14em; color:var(--dim); z-index:2; }
+ #galaxyLive { color:#5ee6a8; animation:blink 2s infinite; } @keyframes blink { 50% { opacity:.25; } }
+ #galaxyTip { position:absolute; pointer-events:none; background:#181b21ee; border:1px solid var(--line);
+ border-radius:6px; padding:6px 10px; font-size:12px; display:none; max-width:240px; z-index:3; }
+ #galaxyTip b { color:var(--accent); }
+ #galaxyLegend { position:absolute; left:12px; bottom:10px; font-size:11px; color:var(--dim); z-index:2; }
+ #galaxyLegend i { display:inline-block; width:8px; height:8px; border-radius:99px; margin:0 4px 0 10px; }
+ .galaxyNote { color:var(--dim); font-size:12px; margin:8px 2px 0; }
+ /* sections */
+ section { margin:44px 0; }
+ h2 { font-size:20px; margin:0 0 14px; }
+ .cols { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:14px; }
+ .card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:16px 18px; }
+ .card h3 { margin:0 0 6px; font-size:14px; color:var(--accent); letter-spacing:.04em; }
+ .card p { margin:0; color:#aab2c0; font-size:14px; }
+ pre { background:#101216; border:1px solid var(--line); border-radius:10px; padding:14px 16px;
+ overflow-x:auto; font:12.5px/1.6 ui-monospace,Menlo,monospace; color:#cdd6e4; margin:10px 0 0; }
+ pre b { color:var(--accent); font-weight:600; }
+ .cta { display:inline-block; background:var(--accent); color:#08211e; font-weight:700; border-radius:8px;
+ padding:11px 22px; text-decoration:none; margin-top:14px; }
+ .steps { counter-reset:s; }
+ .step { position:relative; padding-left:34px; margin:18px 0; }
+ .step::before { counter-increment:s; content:counter(s); position:absolute; left:0; top:1px; width:22px; height:22px;
+ border-radius:99px; background:var(--card); border:1px solid var(--accent); color:var(--accent);
+ font-size:12px; font-weight:700; display:flex; align-items:center; justify-content:center; }
+ a { color:var(--accent); }
+ footer { border-top:1px solid var(--line); margin-top:50px; padding:22px 0 34px; color:var(--dim); font-size:13px; }
+</style></head><body>
+<div class="wrap">
+<header>
+ <div class="brand">AGENT AD NETWORK</div>
+ <h1>Advertising in <em>AI-agent wait states</em>.</h1>
+ <p class="sub">Every loading pause inside an AI agent, CLI, or pipeline is a moment of undivided
+ human attention. We fill it with one reviewed line of sponsored text. Below: the live network,
+ updating in real time.</p>
+</header>
+
+<div id="stats"></div>
+
+<div id="galaxyWrap">
+ <div id="galaxyTitle">LIVE NETWORK <span id="galaxyLive">● LIVE</span> — hubs are advertisers, orbiting nodes are ads. drag to spin, wheel to zoom</div>
+ <canvas id="galaxy"></canvas>
+ <div id="galaxyTip"></div>
+ <div id="galaxyLegend">
+ <i style="background:#5ee6a8"></i>active <i style="background:#5ec6e6"></i>pending
+ <i style="background:#e6d05e"></i>paused <i style="background:#b06ee6"></i>exhausted <i style="background:#e65e5e"></i>rejected
+ </div>
+</div>
+<p class="galaxyNote">Anonymized public view — numbers only. Every pulse you see is a real impression being served right now.</p>
+
+<section>
+ <h2>How it works</h2>
+ <div class="cols">
+ <div class="card"><h3>WAIT STATES ARE ATTENTION</h3>
+ <p>When an agent runs a slow step — a deploy, a scrape, a build — its human is watching
+ the terminal. One line of text there outperforms a banner nobody sees.</p></div>
+ <div class="card"><h3>ONE GET SERVES AN AD</h3>
+ <p>Integrations call <code>GET /ad</code> and print the line. Fail-open by design:
+ no eligible ad → empty response, the host tool never breaks or slows down.</p></div>
+ <div class="card"><h3>AGENTS PLACE ADS THEMSELVES</h3>
+ <p>The demand side is a plain JSON API — so an AI agent can register, set a budget,
+ and launch a campaign autonomously. Every ad is human-reviewed before it serves.</p></div>
+ </div>
+</section>
+
+<section>
+ <h2>Place an ad in 60 seconds</h2>
+ <div class="steps">
+ <div class="step">Register (returns your API key):
+<pre>curl -X POST <b>https://ads.agentabrams.com/api/advertisers</b> \
+ -H 'Content-Type: application/json' \
+ -d '{"name":"Your Company","email":"you@example.com"}'</pre></div>
+ <div class="step">Submit your ad — headline, one-line body, target URL, CPM bid and budget:
+<pre>curl -X POST <b>https://ads.agentabrams.com/api/ads</b> \
+ -H 'x-api-key: YOUR_KEY' -H 'Content-Type: application/json' \
+ -d '{"headline":"Ship faster","body":"The tool your agent wishes it had.",
+ "url":"https://example.com","keywords":["deploy","ci"],
+ "cpm_usd":2,"budget_usd":25}'</pre></div>
+ <div class="step">It goes live once approved. Track it any time:
+<pre>curl <b>https://ads.agentabrams.com/api/ads</b> -H 'x-api-key: YOUR_KEY'</pre></div>
+ </div>
+</section>
+
+<section>
+ <h2>Show ads in your agent or CLI</h2>
+ <p class="sub">Supply side is one request — plain text or JSON. Context keywords boost matching ads.</p>
+<pre>curl '<b>https://ads.agentabrams.com/ad</b>?context=git,deploy'
+[ad] Ship faster — The tool your agent wishes it had. → https://ads.agentabrams.com/c/…</pre>
+</section>
+
+<footer>
+ Agent Ad Network · an <a href="https://agentabrams.com">Agent Abrams</a> experiment ·
+ network numbers on this page are live and real · <a href="/admin">operator login</a>
+</footer>
+</div>
+
+<script src="/assets/three.min.js"></script>
+<script>
+const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+const num = n => Number(n||0).toLocaleString();
+
+async function load() {
+ const g = await fetch('/api/galaxy').then(r=>r.json()).catch(()=>null);
+ if (!g) return;
+ const t = g.totals || {};
+ document.getElementById('stats').innerHTML = [
+ ['advertisers', t.advertisers], ['ads live', (g.statuses||{}).active||0],
+ ['impressions served', t.impressions], ['clicks', t.clicks]
+ ].map(([k,v]) => `<div class="stat"><b>${num(v)}</b><span>${k.toUpperCase()}</span></div>`).join('');
+ try { galaxyUpdate(g.nodes || []); } catch (e) { console.warn('viz:', e); }
+}
+
+/* ============ three.js galaxy — anonymized: numbers-only tooltips ============ */
+let G = null;
+const STATUS_COLOR = { active:0x5ee6a8, pending:0x5ec6e6, paused:0xe6d05e, exhausted:0xb06ee6, rejected:0xe65e5e };
+function galaxyInit() {
+ if (typeof THREE === 'undefined') throw new Error('three.js unavailable');
+ const wrap = document.getElementById('galaxyWrap'), cv = document.getElementById('galaxy');
+ const renderer = new THREE.WebGLRenderer({ canvas: cv, antialias: true, alpha: true });
+ const scene = new THREE.Scene();
+ scene.fog = new THREE.FogExp2(0x0b0d12, 0.010);
+ const camera = new THREE.PerspectiveCamera(58, 1, 0.1, 500);
+ camera.position.set(0, 14, 46);
+ scene.add(new THREE.AmbientLight(0xffffff, 0.55));
+ const key = new THREE.PointLight(0x4fd1c5, 1.1, 200); key.position.set(0, 30, 30); scene.add(key);
+ const world = new THREE.Group(); scene.add(world);
+
+ const starGeo = new THREE.BufferGeometry(), starPos = new Float32Array(900*3);
+ for (let i=0;i<900;i++){ const r = 60 + Math.random()*80, t = Math.random()*Math.PI*2, ph = Math.acos(2*Math.random()-1);
+ starPos[i*3] = r*Math.sin(ph)*Math.cos(t); starPos[i*3+1] = r*Math.cos(ph)*0.6; starPos[i*3+2] = r*Math.sin(ph)*Math.sin(t); }
+ starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
+ world.add(new THREE.Points(starGeo, new THREE.PointsMaterial({ color:0x3a4560, size:0.5, transparent:true, opacity:0.85 })));
+
+ const gc = document.createElement('canvas'); gc.width = gc.height = 64;
+ const g2 = gc.getContext('2d'), grad = g2.createRadialGradient(32,32,2,32,32,30);
+ grad.addColorStop(0,'rgba(255,255,255,.9)'); grad.addColorStop(1,'rgba(255,255,255,0)');
+ g2.fillStyle = grad; g2.fillRect(0,0,64,64);
+ const glowTex = new THREE.CanvasTexture(gc);
+
+ const nodes = new THREE.Group(); world.add(nodes);
+ G = { renderer, scene, camera, world, nodes, glowTex, wrap, cv,
+ adMesh: new Map(), lastImp: new Map(), pulses: [], hubs: new Map(),
+ drag: null, targetRotY: 0, rotY: 0, rotX: 0.12, targetRotX: 0.12, zoom: 46, t: 0 };
+
+ cv.addEventListener('pointerdown', e => { G.drag = { x:e.clientX, y:e.clientY }; cv.style.cursor='grabbing'; });
+ addEventListener('pointerup', () => { G.drag = null; cv.style.cursor='grab'; });
+ addEventListener('pointermove', e => {
+ if (G.drag) { G.targetRotY += (e.clientX-G.drag.x)*0.005; G.targetRotX = Math.max(-0.6, Math.min(1.2, G.targetRotX + (e.clientY-G.drag.y)*0.003)); G.drag = { x:e.clientX, y:e.clientY }; }
+ hover(e);
+ });
+ cv.addEventListener('wheel', e => { e.preventDefault(); G.zoom = Math.max(16, Math.min(90, G.zoom + e.deltaY*0.05)); }, { passive:false });
+
+ const ray = new THREE.Raycaster(), ptr = new THREE.Vector2(), tip = document.getElementById('galaxyTip');
+ function hover(e) {
+ const r = cv.getBoundingClientRect();
+ if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) { tip.style.display='none'; return; }
+ ptr.x = ((e.clientX-r.left)/r.width)*2-1; ptr.y = -((e.clientY-r.top)/r.height)*2+1;
+ ray.setFromCamera(ptr, camera);
+ const hit = ray.intersectObjects([...G.adMesh.values()].map(o=>o.mesh))[0];
+ if (hit) { const d = hit.object.userData;
+ tip.innerHTML = `<b>${esc(d.status)}</b> ad · ${num(d.impressions)} impressions · ${num(d.clicks)} clicks`;
+ tip.style.display = 'block';
+ tip.style.left = Math.min(e.clientX-r.left+14, r.width-250)+'px'; tip.style.top = (e.clientY-r.top+10)+'px';
+ } else tip.style.display = 'none';
+ }
+
+ function resize() {
+ const w = wrap.clientWidth, h = wrap.clientHeight;
+ renderer.setSize(w, h, false); renderer.setPixelRatio(Math.min(2, window.devicePixelRatio||1));
+ camera.aspect = w/h; camera.updateProjectionMatrix();
+ }
+ addEventListener('resize', resize); resize();
+
+ (function tick() {
+ requestAnimationFrame(tick);
+ G.t += 0.016;
+ G.rotY += ((G.targetRotY + G.t*0.02) - G.rotY) * 0.06;
+ G.rotX += (G.targetRotX - G.rotX) * 0.08;
+ world.rotation.y = G.rotY; world.rotation.x = G.rotX;
+ camera.position.z += (G.zoom - camera.position.z) * 0.08;
+ for (const o of G.adMesh.values()) {
+ o.angle += o.speed;
+ o.mesh.position.set(o.hub.x + Math.cos(o.angle)*o.r, o.hub.y + Math.sin(o.angle*0.7)*1.6 + o.tilt*Math.sin(o.angle), o.hub.z + Math.sin(o.angle)*o.r);
+ o.glow.position.copy(o.mesh.position);
+ const s = 1 + Math.max(0, o.punch); o.punch *= 0.94;
+ o.mesh.scale.setScalar(s); o.glow.scale.setScalar(o.baseGlow*s);
+ if (o.line) { o.line.geometry.setFromPoints([new THREE.Vector3(o.hub.x,o.hub.y,o.hub.z), o.mesh.position]); }
+ }
+ for (const h of G.hubs.values()) h.mesh.rotation.y += 0.01;
+ for (let i=G.pulses.length-1;i>=0;i--) { const pu = G.pulses[i];
+ pu.mesh.scale.multiplyScalar(1.09); pu.mesh.material.opacity *= 0.90; pu.mesh.lookAt(camera.position);
+ if (pu.mesh.material.opacity < 0.02) { world.remove(pu.mesh); G.pulses.splice(i,1); } }
+ renderer.render(scene, camera);
+ })();
+}
+function galaxyUpdate(nodesData) {
+ if (!G) return;
+ const advs = [...new Set(nodesData.map(a => a.hub))];
+ advs.forEach((id, i) => {
+ if (G.hubs.has(id)) return;
+ const ang = (i / Math.max(advs.length,1)) * Math.PI*2, R = advs.length > 1 ? 16 : 0;
+ const pos = new THREE.Vector3(Math.cos(ang)*R, (Math.random()-0.5)*4, Math.sin(ang)*R);
+ const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(1.15, 0),
+ new THREE.MeshStandardMaterial({ color:0x4fd1c5, emissive:0x1f6e66, roughness:0.3, metalness:0.5, wireframe:true }));
+ mesh.position.copy(pos); G.world.add(mesh);
+ const glow = new THREE.Sprite(new THREE.SpriteMaterial({ map:G.glowTex, color:0x4fd1c5, transparent:true, opacity:0.5, depthWrite:false }));
+ glow.scale.setScalar(5); glow.position.copy(pos); G.world.add(glow);
+ G.hubs.set(id, { mesh, glow, pos });
+ });
+ for (const a of nodesData) {
+ const hub = (G.hubs.get(a.hub) || { pos:new THREE.Vector3(0,0,0) }).pos;
+ let o = G.adMesh.get(a.id);
+ const col = STATUS_COLOR[a.status] ?? 0x8a93a3;
+ const size = 0.5 + Math.log2((a.impressions||0)+2)*0.16;
+ if (!o) {
+ const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20),
+ new THREE.MeshStandardMaterial({ color:col, emissive:col, emissiveIntensity:0.55, roughness:0.35 }));
+ const glow = new THREE.Sprite(new THREE.SpriteMaterial({ map:G.glowTex, color:col, transparent:true, opacity:0.6, depthWrite:false }));
+ const line = new THREE.Line(new THREE.BufferGeometry(),
+ new THREE.LineBasicMaterial({ color:0x2f3a4a, transparent:true, opacity:0.55 }));
+ G.world.add(mesh); G.world.add(glow); G.world.add(line);
+ o = { mesh, glow, line, hub, angle: Math.random()*Math.PI*2, speed: 0.004 + Math.random()*0.006,
+ r: 5 + Math.random()*5, tilt: (Math.random()-0.5)*2, punch: 0, baseGlow: 3 };
+ G.adMesh.set(a.id, o);
+ }
+ o.mesh.geometry.dispose(); o.mesh.geometry = new THREE.SphereGeometry(size, 20, 20);
+ o.mesh.material.color.setHex(col); o.mesh.material.emissive.setHex(col);
+ o.glow.material.color.setHex(col); o.baseGlow = size*5;
+ o.mesh.userData = { status:a.status, impressions:a.impressions||0, clicks:a.clicks||0 };
+ const prev = G.lastImp.get(a.id);
+ if (prev != null && (a.impressions||0) > prev) {
+ o.punch = 0.9;
+ const ring = new THREE.Mesh(new THREE.RingGeometry(size*1.3, size*1.55, 40),
+ new THREE.MeshBasicMaterial({ color:col, transparent:true, opacity:0.85, side:THREE.DoubleSide, depthWrite:false }));
+ ring.position.copy(o.mesh.position); G.world.add(ring); G.pulses.push({ mesh: ring });
+ }
+ G.lastImp.set(a.id, a.impressions||0);
+ }
+ for (const [id, o] of G.adMesh) if (!nodesData.some(a => a.id === id)) {
+ G.world.remove(o.mesh); G.world.remove(o.glow); G.world.remove(o.line); G.adMesh.delete(id);
+ }
+}
+try { galaxyInit(); } catch (e) {
+ console.warn('galaxy disabled:', e);
+ document.getElementById('galaxyTitle').textContent = 'LIVE NETWORK — WebGL unavailable on this browser (numbers below are still live)';
+}
+load(); setInterval(load, 5000);
+</script></body></html>
diff --git a/server.js b/server.js
index 7442970..569af7f 100644
--- a/server.js
+++ b/server.js
@@ -83,6 +83,10 @@ const readEventsTail = (maxBytes = 1024 * 1024) => {
}
const id = (p, bytes = 6) => p + '_' + crypto.randomBytes(bytes).toString('hex');
+// opaque public tokens for the no-auth galaxy feed — per-process salt, so real
+// ids are unrecoverable; stability across polls is all the frontend needs.
+const GALAXY_SALT = crypto.randomBytes(16);
+const opaque = (s) => crypto.createHmac('sha256', GALAXY_SALT).update(String(s)).digest('hex').slice(0, 10);
const SEC_HEADERS = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
@@ -365,8 +369,42 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'GET' && p === '/healthz') return json(res, 200, { ok: true, ads: ads.length });
+ // ---------- public landing + anonymized galaxy feed (no auth) ----------
+ // Numbers only: opaque HMAC tokens replace real ad/advertiser ids (pending
+ // and rejected ads' ids must never leak), and no headline/sponsor/url/
+ // budget ever leaves this endpoint.
+ if (req.method === 'GET' && p === '/api/galaxy') {
+ if (rateLimited(req, res, 'galaxy', 120, 60000)) return;
+ const statuses = {};
+ for (const a of ads) statuses[a.status] = (statuses[a.status] || 0) + 1;
+ return json(res, 200, {
+ nodes: ads.map(a => ({
+ id: opaque(a.id), hub: opaque(a.advertiserId), status: a.status,
+ impressions: a.impressions || 0, clicks: a.clicks || 0
+ })),
+ statuses,
+ totals: {
+ advertisers: advertisers.length, ads: ads.length,
+ impressions: ads.reduce((s, a) => s + (a.impressions || 0), 0),
+ clicks: ads.reduce((s, a) => s + (a.clicks || 0), 0)
+ }
+ });
+ }
+ if (req.method === 'GET' && p === '/assets/three.min.js') {
+ res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'public, max-age=86400', ...SEC_HEADERS });
+ return res.end(fs.readFileSync(path.join(__dirname, 'assets', 'three.min.js')));
+ }
+ if (req.method === 'GET' && p === '/') {
+ if (rateLimited(req, res, 'landing', 240, 60000)) return;
+ res.writeHead(200, {
+ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', ...SEC_HEADERS,
+ 'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
+ });
+ return res.end(fs.readFileSync(path.join(__dirname, 'landing.html')));
+ }
+
// ---------- admin (basic auth) ----------
- if (p === '/' || p.startsWith('/admin')) {
+ if (p.startsWith('/admin')) {
if (rateLimited(req, res, 'admin', 240, 60000)) return;
if (!adminAuthed(req, res)) return;
if (p === '/admin/api/overview') {
← 2428013 live-viz dashboard: three.js ad galaxy (advertiser hubs, sta
·
back to Agent Ad Network
·
README: public landing surface + domain-ownership note (ads. eb5205f →