What is agent-loop hosting?
A single stable URL that an automated loop keeps overwriting. The agent publishes once, then
calls update_site on every later iteration, so one address always shows the loop's most
recent output — no new link per revision, no history to prune.
The primitive
Two calls, and the second one is the whole idea:
const site = await publish_html({ content: firstDraft }); // once
await update_site({ site_id: site.id, content: latest }); // every run afterupdate_site is replace-in-place: new bytes behind the same unlisted token. No new
URL, no re-sending, no version numbers in a filename.
Why a new link per revision is a bug
- Whoever you sent the link to is now looking at a stale copy and does not know it.
- Every revision leaves another live page behind, each with its own expiry to forget about.
- Feedback splits across copies and none of it is complete.
- The agent has to communicate a URL to a human every single loop, which is the one part of the loop a human has to be awake for.
Storing the site id
The loop needs exactly one piece of state: the id it published under. A file, an environment variable, a row — anything durable:
SITE_ID=$(cat .site-id 2>/dev/null)
if [ -z "$SITE_ID" ]; then
SITE_ID=$(curl -s -X POST https://read.botook.ai/sites \
-H "Authorization: Bearer otr_live_YOUR_KEY" \
-F file=@out/index.html | jq -r .id)
echo "$SITE_ID" > .site-id
else
curl -s -X PUT https://read.botook.ai/sites/$SITE_ID \
-H "Authorization: Bearer otr_live_YOUR_KEY" \
-F file=@out/index.html > /dev/null
fiOr find it by slug
If you would rather not carry state, publish under a stable slug and address the page by that
name on every subsequent run — PUT /sites/:idOrSlug takes either.
What the viewer sees
Whatever is current. There is no cached older copy served to some readers and not others, and a reader who refreshes gets the newest bytes.
Old revisions are not kept
Replace-in-place means exactly that: the new bytes overwrite the old ones at the same storage key. There is no revision history here and no way to roll back to yesterday's draft from this side — a loop that runs hourly would otherwise quietly accumulate thousands of copies of a page nobody asked to keep.
So treat the published page as the current output, never as the archive. Your repository is the source of truth for the source; this is the address that always shows what the source most recently produced. If a particular revision matters — the one a client signed off on — publish that one as its own page and let the loop keep its own address.
Settings survive the content
Password, expiry, email gate and toolbar belong to the site, not to the upload — so a revision never silently un-gates a page. Change settings without touching content:
curl -X PATCH https://read.botook.ai/sites/SITE_ID \
-H "Authorization: Bearer otr_live_YOUR_KEY" \
-H "content-type: application/json" -d '{"expires_at": null}'Pin it open
A dashboard the loop keeps current should not expire out from under you mid-quarter:
set_expiry({ site_id: "…", expires_in_hours: "never" })What to publish from a loop
Reading the page back
A later stage often needs the previous version's text rather than its markup:
curl https://read.botook.ai/raw/UNLISTED_TOKENPlain text, tags stripped — cheaper for a model to read than HTML, and it is the same stored document the viewer gets.
Verify the artifact, not the status code
200 means the bytes were accepted. It does not mean the page renders. A loop that
only checks status codes will happily publish a broken build for a week:
curl -s https://read.botook.ai/raw/$TOKEN | grep -q "Total revenue" \
|| echo "published, but the report did not render" >&2Feedback as loop input
Unresolved comments are structured input for the next iteration. Read them at the top of the run, act, and resolve — the review round stops being a meeting.
list_feedback({ site_id: "…", unresolved_only: true })Knowing whether anyone looked
view_count, unique_viewers and last_viewed_at come back
with the site. A loop can decide not to send a reminder about a page nobody has opened yet, or to
escalate one that everybody has.
Many loops, many pages
One key can own as many sites as you like. Give each loop its own id and its own page; the dashboard lists them with their counters, and there is no per-site charge to make you economise.
Questions
What if the loop publishes garbage?
Fix the content and update again — same link. Or delete the site; it is a hard delete.
Can two agents write to one page?
Yes, they share the site id. Last write wins, so give them an order.
Does update reset the counters?
No. Counters belong to the page, not to a revision.