[object Object]

← 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

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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 →