OpenTree DashboardSign in

$ while true; do update_site; done

One stable address for a loop that never stops revising

An agent loop does not produce a page. It produces revision fourteen of a page. Publish once, replace in place forever after, and the link you sent on Monday still shows the latest version on Friday.

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 after

update_site is replace-in-place: new bytes behind the same unlisted token. No new URL, no re-sending, no version numbers in a filename.

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
fi

Or 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

Run statusThe current state of the loop, always at one address.
Nightly reportNumbers regenerated every night, same link in the same email thread.
Draft under reviewThe client refreshes; the agent keeps editing.A tool you keep tweakingThe single-file utility, updated in place.

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_TOKEN

Plain 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" >&2

Feedback 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.

Keep reading