slate-sh
v0.1.0
Published
The blackboard from the multi-agent papers, as a URL. Shared key-value memory for agent fleets: set, get, wait, compare-and-swap.
Maintainers
Readme
# machine A
curl -X PUT -d 'migration done, artifacts in /shared/out' https://slate.legible.sh/swarm-7f3a/status
# machine B — blocks until the key exists, then prints it
curl 'https://slate.legible.sh/swarm-7f3a/status?wait=300'The hosted API at slate.legible.sh is in soft launch. To run the identical API yourself,
npx slate-sh servestarts it locally; every example below works withhttp://localhost:4183in place ofhttps://slate.legible.sh.
Agents on different machines — or the same machine, different sessions — have no shared memory. Claude finishes a migration at 2am and the notes die with the session. Two workers grab the same task because neither could see the other. The multi-agent papers solved this decades ago with a blackboard; every existing "free KV over curl" service either died, grew a signup form, or never had the verbs that make storage into coordination. Slate is those verbs: watch (long-poll), wait (barriers), CAS (leases and locks) — over the dumbest possible HTTP surface.
The whole API
| Verb | Path | Does |
|---|---|---|
| PUT | /{topic}/{key} | Set. Body = value (≤ 64 KB text/JSON). X-TTL: secs to expire, If-Match: rev for CAS (412 on mismatch), If-None-Match: * for create-only (412 if it exists — that's a lock). → {key, rev, expires?} |
| GET | /{topic}/{key} | Value as stored, plus X-Rev and X-Expires headers. 404 if absent/expired. |
| GET | /{topic}/{key}?wait=30&rev=N | Long-poll: returns the moment the rev differs from N (immediately if it already does), 304 on timeout. Without rev on a missing key: waits for the key to exist. |
| DELETE | /{topic}/{key} | Remove. If-Match: rev for a guarded delete. |
| GET | /{topic} | {topic, keys: [{key, rev, size, expires?}]} — names and metadata, never values. |
| GET | /{topic}/sse | Server-sent events: set / delete / expire, JSON data {op, key, rev}, heartbeat comments every 25 s. |
| GET | /README.md · /llms.txt | Meta, never token-gated: this document as text/markdown, and an llms.txt index. GET / with Accept: text/markdown also returns the README. |
Fine print: POST works as an alias for PUT (so bare curl -d works). Topics match [a-zA-Z0-9_-]{1,64} and are created by first use; keys also allow dots ([a-zA-Z0-9_.-]{1,256}; sse is reserved). Revisions are per-key monotonic integers that survive delete — a rev you saw is never reused. Errors are JSON {"error": "...", "code": "...", "hint": "..."} with honest status codes — the hint is the correct next request, so you never have to leave the response to fix your call. Browsers get HTML pages at / and /{topic}; curl gets text and JSON. All limits live in src/limits.mjs. On the hosted instance keys expire 30 days after last write by default; self-hosted keys live forever unless you say otherwise.
Teach your agent
Paste this into your CLAUDE.md / AGENTS.md and your agents have shared memory:
## Shared memory (slate)
Shared KV across sessions/machines. Base URL: https://slate.legible.sh — our topic: <unguessable-string>.
- write: curl -X PUT -d 'value' https://slate.legible.sh/TOPIC/key (add -H 'X-TTL: 3600' to expire)
- read: curl https://slate.legible.sh/TOPIC/key (404 if absent; X-Rev header = revision)
- wait: curl 'https://slate.legible.sh/TOPIC/key?wait=60' (blocks until key exists; &rev=N = until it changes past N; 304 on timeout)
- lock: curl -X PUT -H 'If-None-Match: *' -H 'X-TTL: 60' -d me https://slate.legible.sh/TOPIC/lock
(200 = you hold the lease for 60s, 412 = someone else does; DELETE with -H 'If-Match: REV' to release)
- CAS: curl -X PUT -H 'If-Match: REV' -d 'new' https://slate.legible.sh/TOPIC/key (412 = lost the race, re-read and retry)
- list: curl https://slate.legible.sh/TOPIC events: curl https://slate.legible.sh/TOPIC/sse
Values ≤ 64 KB text/JSON. Topics are created by first use; the unguessable name is the access control.Recipes
These three patterns are the product.
A lease-lock in two curls
Exactly one of N workers wins; a crash can hold the lock for at most the TTL.
# acquire: create-only + 60s TTL — 200 means you got it, 412 means someone holds it
curl -s -X PUT -H 'If-None-Match: *' -H 'X-TTL: 60' -d "$HOSTNAME" https://slate.legible.sh/swarm-7f3a/leader
# → {"key":"leader","rev":4,"expires":"2026-07-06T18:32:11.000Z"}
# release early (guarded by your rev — you can never delete a successor's lease)
curl -s -X DELETE -H 'If-Match: 4' https://slate.legible.sh/swarm-7f3a/leaderRenew by re-PUTting with If-Match: <your rev> before the TTL runs out. If the holder dies, the key expires and the next If-None-Match: * wins.
A barrier
Any number of processes block until one of them says go.
# every waiter, anywhere:
curl -s 'https://slate.legible.sh/swarm-7f3a/deploy-done?wait=300' # blocks (304 after 300s of nothing)
# the one that finishes the work:
curl -s -X PUT -d 'ok: build 1042' https://slate.legible.sh/swarm-7f3a/deploy-doneEvery waiter wakes with the value. For repeating rounds, wait with &rev=N to only wake on the next write.
A cross-session handoff
Claude session A (your laptop, 2am) finishes and leaves notes. Session B (CI box, 9am) picks them up — different machine, no shared filesystem, no human ferrying context.
# session A, before it dies:
curl -s -X PUT -H 'X-TTL: 86400' --data-binary @handoff-notes.md https://slate.legible.sh/proj-x9k2/handoff
# session B, whenever it starts (waits up to 5 min if A is still working):
curl -s 'https://slate.legible.sh/proj-x9k2/handoff?wait=300'Self-hosting
npx slate-sh serve # http://localhost:4183, in-memory
npx slate-sh serve --data-dir ./data # survives restarts (JSONL, replayed & compacted on boot)
npx slate-sh serve --token s3cret # every data route needs Authorization: Bearer s3cret (or ?token=)Or clone and run: git clone … && cd slate && npm start. Zero dependencies; there is nothing to install.
Flags: --port (default 4183), --host (default 0.0.0.0), --data-dir, --token, --default-ttl <secs> (what the hosted instance sets to 30 days), --base-url (absolute URLs in pages). With --token set, everything under /{topic} requires the token — reads included, because a KV store's contents are sensitive. Only the root usage page stays public.
CLI
The CLI is sugar over the same HTTP API — curl remains the contract. It resolves the server from --url, then $SLATE_URL, then https://slate.legible.sh; auth from --token or $SLATE_TOKEN.
slate set swarm-7f3a status 'phase one done' # or: echo big-value | slate set t k -
slate set swarm-7f3a leader me --if-absent --ttl 60 # the lease, as a one-liner
slate set swarm-7f3a counter 2 --if-rev 1 # CAS
slate get swarm-7f3a status
slate get swarm-7f3a handoff --wait 300 # the barrier (exit code 2 on timeout)
slate ls swarm-7f3a
slate watch swarm-7f3a # tab-separated: op key rev
slate rm swarm-7f3a leader --if-rev 4
slate serve --port 4183 --data-dir ./dataPro
Planned for the hosted instance — capacity and guarantees, never the verbs. Self-host stays complete forever.
- Bigger values — past the 64 KB anonymous cap.
- No (or longer) TTL — anonymous keys expire 30 days after last write; paid topics keep data as long as you pay for it.
- Snapshots and export — point-in-time dumps of a topic, delivered as JSONL.
- Per-topic read/write tokens — real auth for names you want to publish, not just obscure.
- Replication — hosted topics on more than one machine.
Straight talk
- An unguessable topic name is the security model. Same as ntfy.sh: anyone who has the name has the data. Mint topics like
swarm-$(openssl rand -hex 6), nevertest. Listings don't include values, and there is no topic enumeration — but for anything genuinely sensitive, self-host with--token. - It's a blackboard, not a database. 64 KB values, no queries, no multi-key transactions. CAS is per-key. If you need joins, you need a database.
- Leases are leases. A TTL lock can expire while its holder is still alive-but-slow. Use the rev as a fencing token (
If-Matchon every write the lease protects) and renew before expiry. - Long-poll wakes on writes, not deletes. A
?wait=waiter sleeps through a DELETE and wakes on the next PUT. Watch deletes on the SSE stream if you care about them. - Single node. Persistence is a synchronous JSONL append; replay-and-compact on boot. No replication in the open-source server — that honesty is why the Pro section exists.
- The hosted instance forgets. 30 days after the last write, anonymous keys expire. That's the deal that keeps a free anonymous store economically sane and abuse-resistant.
License
MIT.
slate is one of the legible primitives — small servers that give infrastructurally naked agents the verbs they're missing: gate (4180) — ntfy tells you things. gate asks you things. · bigred (4181) — The big red button for your agent fleet. · trail (4182) — The flight recorder for agent runs. · slate (4183) — The blackboard from the multi-agent papers, as a URL. · relay (4184) — The work queue your agents can provision themselves. · mutex (4185) — flock(1) for agents that live on different machines. · quorum (4186) — Coordination for agents that do not share a parent process. · meter (4187) — The kill-brake for agent spend. 429-as-a-service. · stash (4188) — ntfy moves signals. stash moves bytes. · tally (4189) — StatHat reborn as ntfy. Three months too late — or right on time.
