A server somewhere (a lab box, a cloud VM, a build machine) sends a line of data twice a second. Every open copy of your space draws it about a tenth of a second later, and buttons in the space can send presses back to the server. The space is never republished. Scripts, sensors and webhooks can stream the same way. Try it below; the server is pretend and runs in this tab.
starting…
$ npx spacesheep stream lab/my-box --system --load-test streaming to lab/my-box
A real one: a Linux server on Google Cloud streaming into lab-1, live (sign in to open it).
Anything that can print text or make a request can stream: a server reading its own CPU, a script reading a sensor, a job reporting progress. Here the source is a pretend server that prints one line of JSON twice a second.
Each line goes to a stream, a name like lab/my-box. The stream hands it to every open tab and keeps the last ten minutes for tabs that open later. Nothing is published.
The space names the stream once. Each new line reaches it about a tenth of a second after it was sent, and the space redraws at most once per screen refresh.
Buttons go the other way. The space sends a press, here run at your intensity, back through the stream. The source acts on it only if it knows that button, and it clamps the numbers itself, because anyone who can open the space can press. To keep a button for the people who can edit the space, add data-press="editors" to the page's ss-streams declaration: everyone else's press is refused, and the button can say why.
Any Linux or Mac with Node 20+. Two commands: one gives it a key, one starts the stream.
# on a laptop signed in to spacesheep: a key that can only send numbers, never shown
npx -y spacesheep@latest keys create --scope stream --name my-box | ssh my-box 'npx -y spacesheep@latest keys save'
# on the box: its own numbers, Run test buttons, kept running in the background
npx -y spacesheep@latest stream lab/my-box --system --load-test --service
--system reads the computer itself (per-core load, memory, temperatures). --load-test adds the buttons you just pressed, with nothing to install. --service keeps it running after you log out. A cloud server has no thermometer, so its temperatures are a model of its load, and the line says so.
Stream my server MY-BOX into a spacesheep space and make me a space that shows it live. From this computer (signed in to spacesheep), run: npx -y spacesheep@latest keys create --scope stream --name MY-BOX | ssh MY-BOX 'npx -y spacesheep@latest keys save' ssh MY-BOX 'npx -y spacesheep@latest stream lab/MY-BOX --system --load-test --service' Then publish a space that declares the stream lab/MY-BOX and draws its per-core load, load average and temperatures, with an intensity slider and a Run test button.
No CLI needed: a server can talk to the stream over HTTP. Send a reading with a POST, hear the space's buttons with a long poll, and answer each press. Here is a whole server, in two languages:
// server.mjs: stream this server's load into a space, and answer its Run test button
import os from "node:os";
const BASE = "https://spacesheep.dev/api/streams/lab/my-box";
const HEAD = { Authorization: "Bearer " + process.env.SPACESHEEP_KEY, "Content-Type": "application/json" };
// 1. Send a reading twice a second. Each POST is one line on the stream.
setInterval(() => {
const value = { load: os.loadavg(), mem_free_mb: Math.round(os.freemem() / 1048576) };
fetch(BASE, { method: "POST", headers: HEAD, body: JSON.stringify(value) }).catch(() => {});
}, 500);
// 2. Hear the space's buttons (a long poll), and answer every press.
const get = (url) => fetch(url, { headers: HEAD }).then((r) => r.json());
let { cursor } = await get(BASE + "/-/events?wait=0"); // start now: ignore old presses
while (true) {
const r = await get(BASE + "/-/events?after=" + cursor + "&wait=25000");
for (const ev of r.events) {
const known = ev.name === "run"; // only buttons you handle
if (known) startLoad(Math.min(100, Math.max(5, Number(ev.data?.intensity) || 50))); // clamp: anyone can press
await fetch(BASE + "/-/ack", { method: "POST", headers: HEAD,
body: JSON.stringify({ seq: ev.seq, status: known ? "started" : "refused" }) });
}
cursor = r.cursor;
}
# server.py: stream this server's load into a space, and answer its Run test button
import json, os, threading, time, urllib.request
BASE = "https://spacesheep.dev/api/streams/lab/my-box"
HEAD = {"Authorization": "Bearer " + os.environ["SPACESHEEP_KEY"], "Content-Type": "application/json"}
def call(url, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, headers=HEAD, method="POST" if data else "GET")
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read() or b"null")
# 1. Send a reading twice a second. Each POST is one line on the stream.
def send_readings():
while True:
call(BASE, {"load": os.getloadavg()})
time.sleep(0.5)
threading.Thread(target=send_readings, daemon=True).start()
# 2. Hear the space's buttons (a long poll), and answer every press.
cursor = call(BASE + "/-/events?wait=0")["cursor"] # start now: ignore old presses
while True:
r = call(BASE + "/-/events?after=%d&wait=25000" % cursor)
for ev in r["events"]:
known = ev["name"] == "run" # only buttons you handle
if known:
start_load(min(100, max(5, int((ev.get("data") or {}).get("intensity", 50))))) # clamp
call(BASE + "/-/ack", {"seq": ev["seq"], "status": "started" if known else "refused"})
cursor = r["cursor"]
startLoad is your own code: whatever a press should do on your server. Run it with a streams-only key: SPACESHEEP_KEY=ss_… node server.mjs. The space shows each reading about a tenth of a second after the POST, and a press answered with started comes back to the button that sent it.
Drop --system and give the CLI any program that prints one JSON line per reading:
#!/usr/bin/env python3
# read-sensors.py: each line it prints becomes one value on the stream
import json, time
while True:
temps = [read_chip_temp(i) for i in range(4)] # your sensor code here
print(json.dumps({"host": "chip-1", "temp_c": temps, "temp_source": "sensor"}), flush=True)
time.sleep(0.5)
npx -y spacesheep@latest stream lab/chip-1 --run "python3 read-sensors.py"
A webhook, a cloud function, a phone shortcut: one POST is one value. Use a streams-only key (npx spacesheep keys create --scope stream).
curl -X POST https://spacesheep.dev/api/streams/lab/orders \
-H "Authorization: Bearer $SPACESHEEP_KEY" -H "Content-Type: application/json" \
-d '{"orders_per_min": 42}'
Declare the stream in the head of the space's page. The declaration is also the access rule: a space can read only the streams it names, and only the people who can open the space see them.
<meta name="ss-streams" content="lab/my-box">
<script>
const s = ss.stream("lab/my-box");
s.draw((last, history, status) => {
// once per screen refresh, only when a new line arrived:
// last = the newest line, history = the last 600, status.lag_ms = source → screen
});
s.send("run", { intensity: 60, seconds: 30 }).then((r) => console.log(r.status)); // "started"
</script>
Anyone who can open the space can press its buttons. For a button that costs something, like a load test or a restart, keep it for the people who can edit the space: the owner, and whoever the Share panel lists as Can edit. Add data-press="editors" to the declaration. Everyone else's press is refused before it reaches the source, and status.can_press tells the page, so the button can be greyed out with a reason instead of failing on the click.
<meta name="ss-streams" content="lab/my-box" data-press="editors">
<script>
s.draw((last, history, status) => {
run.disabled = status.can_press === false; // null until the server answers
run.title = run.disabled ? "View only. Ask the owner for edit access." : "";
});
</script>
A data-ss-send button is greyed out for readers with no script at all. People who can edit the space can also change this rule, so give Can edit only to people you'd let press.
Agents already know this: the spacesheep skill teaches it, and the MCP tool streams shows them a stream's latest lines so they can see its exact shape before drawing it.
Your streams page shows where it stops: whether each source is sending (its last hour, minute by minute, with any gap and any value that was refused and why), who has a space open that reads it, and whether their copy drew what it got. A gap is the source; a page that got values and drew none is the page's own code, and the error is shown.
| Plan | Sending into a stream is part of Max. Anyone who can open the space can watch, on any plan or none. |
|---|---|
| Creating a stream | There's no create step. The first value makes it. |
| Who can see it | Only spaces that declare it, and only people who can open those spaces. |
| The sender's key | Streams-only: it can send to your streams and hear their buttons, nothing else. No deploys, no reads. |
| Buttons | The CLI runs only the buttons it was given, never a press from before it started; a source clamps every number it's sent. |
| Who can press | Anyone who can open the space, or with data-press="editors" on the declaration, only the people who can edit it. |
| Limits | 16 KB per value, 20 values a second per stream, the last 600 values or 10 minutes kept, 200 streams per account. |
| Speed | Source to screen measured at 74 ms on 2026-10-02, a Google Cloud server in Iowa to a browser in California. |
| Health | /me/streams: every stream's last hour, the tabs watching, and whether their pages drew it. |