<!-- BUILD-FOR-AI · part 3 -->
setTimeout(ok, 500)); // let a few frames draw
        return { t: Date.now(), text: String(text), jpg: await p.screenshot({ type: 'jpeg', quality: 80 }) };
      })()]);
      cache.set(place + ':' + look, view);
      const mb = (await page.metrics()).JSHeapUsedSize / 1048576; // memory cap (JS heap): recycle
      if (mb &gt; MAX_MB) await stop();
      return view;
    } catch (e) { await stop(); throw e; } // a hung page is recycled
    finally { clearTimeout(timer); }
  });
  queue = job.catch(() =&gt; {});
  return job;
}

function limited(ip) { // N new renders per minute per IP
  const now = Date.now(), a = (hits.get(ip) || []).filter(t =&gt; now - t &lt; 60e3);
  a.push(now); hits.set(ip, a); return a.length &gt; PER_MIN;
}
const esc = s =&gt; String(s).replace(/[&amp;&lt;&gt;&quot;]/g, c =&gt; ({ '&amp;': '&amp;amp;', '&lt;': '&amp;lt;', '&gt;': '&amp;gt;', '&quot;': '&amp;quot;' })[c]);

const app = express();
app.get(['/see', '/see.txt', '/see.jpg'], async (req, res) =&gt; {
  const place = String(req.query.go || 'start'), look = String(req.query.look || 'ahead');
  if (!Object.hasOwn(PLACES, place) || !LOOKS.includes(look))
    return res.status(400).type('text').send('unknown; try ?go=' + Object.keys(PLACES).join('|') + '&amp;look=' + LOOKS.join('|'));
  let view = cache.get(place + ':' + look);
  if (!view || Date.now() - view.t &gt; CACHE_MS) {
    if (limited(req.ip)) return res.status(429).set('Retry-After', '30').type('text').send('slow down, try again in 30 s');
    const fresh = render(place, look);
    const quick = new Promise(ok =&gt; setTimeout(ok, 8000)); // never hang: answer within 8 s
    view = await Promise.race([fresh, quick]).catch(() =&gt; null) || view; // else the last (stale) view
  }
  if (!view) return res.status(503).set('Retry-After', '15').type('text').send('warming up, refresh in 15 s');
  const links = Object.keys(PLACES).map(p =&gt; [p, 'go=' + p]).concat(LOOKS.map(l =&gt; [l, 'go=' + place + '&amp;look=' + l]));
  if (req.path === '/see.jpg') return res.type('jpeg').send(view.jpg);
  if (req.path === '/see.txt') return res.type('text').send(view.text + '\n\nPicture: /see.jpg?go=' + place +
    '&amp;look=' + look + '\nActions:\n' + links.map(([n, q]) =&gt; '  ' + n + ': /see.txt?' + q).join('\n') + '\n');
  res.type('html').send('&lt;!doctype html&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;title&gt;View: ' + esc(place) + '&lt;/title&gt;' +
    '&lt;img src=&quot;/see.jpg?go=' + place + '&amp;amp;look=' + look + '&quot; style=&quot;max-width:100%&quot; alt=&quot;' + esc(view.text) + '&quot;&gt;' +
    '&lt;p&gt;' + esc(view.text) + '&lt;/p&gt;&lt;ul&gt;' + links.map(([n, q]) =&gt; '&lt;li&gt;&lt;a href=&quot;/see?' + q.replace('&amp;', '&amp;amp;') + '&quot;&gt;' + (q.includes('look') ? 'Look ' : 'Go to ') + n + '&lt;/a&gt;&lt;/li&gt;').join('') + '&lt;/ul&gt;');
});
app.listen(8080, '127.0.0.1', () =&gt; console.log('AI view on http://127.0.0.1:8080/see'));</code></pre>
<p class="cap">Install with <code>npm i express puppeteer-core</code> and point <code>CHROME</code> at a Chromium on the server. It listens on 127.0.0.1:8080; let your web server forward <code>/see</code>, <code>/see.txt</code> and <code>/see.jpg</code> to it.</p>
</section>
<section><h2>Test it with curl</h2>
<p>Test it the way an AI uses it: no JavaScript, just requests.</p>
<pre><code># 1. the text view (what a text-only AI reads)
curl -s &quot;http://127.0.0.1:8080/see.txt?go=castle&amp;look=left&quot;

# 2. the picture (what an image-reading AI looks at)
curl -s -o castle.jpg -w &quot;%{http_code} %{content_type} %{size_download}\n&quot; \
  &quot;http://127.0.0.1:8080/see.jpg?go=castle&quot;

# 3. the whitelist holds: a URL or unknown place gets 400
curl -s -w &quot; %{http_code}\n&quot; &quot;http://127.0.0.1:8080/see?go=http://example.com&quot;

# 4. the cache works: a repeat view should take well under 1 s
curl -s -o /dev/null -w &quot;%{time_total}\n&quot; &quot;http://127.0.0.1:8080/see?go=castle&quot;

# 5. the links work: list every href on the HTML view
curl -s &quot;http://127.0.0.1:8080/see?go=castle&quot; | grep -o 'href=&quot;[^&quot;]*&quot;'</code></pre>
</section>
<section><h2>Checklist</h2>
<ul class="check">
<li>The page loads and makes sense with JavaScript turned off (try <code>curl</code>).</li>
<li>Every view has a picture, a text readout and action links.</li>
<li>Every action is a plain <code>&lt;a href&gt;</code> link with a short query, like <code>?go=castle&amp;look=left</code>.</li>
<li>Unknown places, URLs and paths get a 400. The browser cannot leave your site.</li>
<li>One browser, one page, one render at a time (a queue).</li>
<li>A cache of 30 to 60 seconds per place, and the last good view is kept.</li>
<li>A per-IP rate limit that answers 429 with Retry-After.</li>
<li>A render timeout, and a &quot;warming up&quot; answer instead of a hung request.</li>
<li>The browser closes when idle and restarts on demand.</li>
<li>A