datspace

datspace.dev

The source of the network: how it's built, the contract every surface derives from, the whole ledger checked in your browser, and how it was built. The code is MIT licensed at github.com/discohead/datspace.

The shape

Two Workers and three Durable Objects, all in this repository, and patter on a Worker of its own. Every page is prerendered from the repository; the actors hold what changes.

Callers on the left; the Workers that answer them on the right.
datspace-web
apps/web. SvelteKit, prerendered from packages/content and served as static assets. It serves datspace.io, and the pages of datspace.ai, .dev, .fm, and .music through datspace-ai. It binds nothing.
datspace-ai
apps/ai. MCP at /mcp, the JSON API at /api/*, /llms.txt, /live.json, /ledger.json, and /admin/* behind a token. It holds the custom domains of every band but datspace.io, rewrites / on each to that band's page, and forwards every other page to datspace-web over the WEB service binding. It answers POST /decide on every host but datspace.ai. /mcp and /api/* allow 120 requests a minute per client.
Ledger
apps/ai/src/ledger.ts. A Durable Object in datspace-ai. It owns the append-only, hash-chained event log: deploy, agent.call, and message.received. It never stores arguments, IP addresses, user agents, or message contents.
Inbox
apps/ai/src/inbox.ts. A Durable Object in datspace-ai. It owns messages to Jared and the limits on sending them: 3 per sender per 24 hours, and 30 a day in all. A sender is a salted hash that changes daily, never an address. Mail goes out through a send_email binding.
Budget
apps/ai/src/budget.ts. A Durable Object in datspace-ai. It owns daily spending counters, one row per budget per UTC day. Today it caps datspace.fm's Clef Flash decisions at 20,000 a day, about a dollar.
patter
github.com/discohead/patter, AGPL-3.0. A separate Worker and repository with Durable Objects of its own. Its route on datspace.studio takes precedence over datspace-ai's custom domain.
datspace
apps/cli, on npm. The CLI calls the JSON API and sends x-datspace-client: cli/<version>, so the Ledger records its calls with surface cli.
CI
.github/workflows/deploy.yml. On every push to main: build, typecheck, and test; deploy datspace-web, then datspace-ai; wait until the Worker and the Ledger both report the new commit; record the deploy in the Ledger; then check accessibility, rendering, and answerability against production.

Nothing agent-facing calls a model; agents bring their own. The one model call a visitor can cause is datspace.fm's POST /decide: Clef Flash, a decision model on Workers AI, chooses the station's system, scale, and five rubric scores from options in packages/fm. It writes no text. Decisions are cached by a hash of the request; after the cache come a limit of 20 a minute per address and the Budget's 20,000 a day. Past either, or if Clef fails, the visitor's answers seed a local draw.

The contract

Seven tools, defined once in packages/contract/src/tools.ts: a name, a title, a one-sentence description, a zod input schema, and a run function that reads only a Source. MCP, the JSON API, and the CLI wrap the same definitions.

  • MCP: POST to https://datspace.ai/mcp with accept: application/json, text/event-stream. The server keeps no sessions, so a tools/call needs no handshake.
  • JSON: GET for the tools that only read, POST with a JSON body for transmit. Every response carries apiVersion, now 2026-10-30.
  • CLI: npx datspace, on Node 22 or later.

Input schemas are JSON Schema (draft 2020-12), as MCP's tools/list returns them. Each example call is checked against its tool's schema when this page is built.

list_stations

Returns every station on the datspace dial with its frequency, domain, status, and one-line summary.

Title
List stations
Read-only
Yes
JSON
GET https://datspace.ai/api/stations
MCP
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_stations","arguments":{}}}
CLI
npx datspace ls
Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

get_work

Returns one work's story fields (question, prototype, finding, future), links, and full markdown, by slug.

Title
Get work
Read-only
Yes
JSON
GET https://datspace.ai/api/work/datspace
MCP
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_work","arguments":{"slug":"datspace"}}}
CLI
npx datspace show datspace
Input schema
{
  "type": "object",
  "properties": {
    "slug": {
      "type": "string",
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
    }
  },
  "required": [
    "slug"
  ],
  "additionalProperties": false
}

search

Returns full-text matches across every work's title, summary, story, and body, each with a snippet.

Title
Search
Read-only
Yes
JSON
GET https://datspace.ai/api/search?q=Cloudflare
MCP
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"Cloudflare"}}}
CLI
npx datspace search Cloudflare
Input schema
{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "limit": {
      "default": 10,
      "type": "integer",
      "minimum": 1,
      "maximum": 20
    }
  },
  "required": [
    "query"
  ],
  "additionalProperties": false
}

get_graph

Returns the network's works and domains as nodes, and the edges between them.

Title
Get graph
Read-only
Yes
JSON
GET https://datspace.ai/api/graph
MCP
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_graph","arguments":{}}}
CLI
npx datspace graph
Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

get_profile

Returns Jared McFarland's structured CV and contact details.

Title
Get profile
Read-only
Yes
JSON
GET https://datspace.ai/api/profile
MCP
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_profile","arguments":{}}}
CLI
None
Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

get_ledger

Returns the most recent network events, such as deploys and tool calls, optionally of one type, with times and no arguments.

Title
Get ledger
Read-only
Yes
JSON
GET https://datspace.ai/api/ledger?limit=3&type=deploy
MCP
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_ledger","arguments":{"type":"deploy","limit":3}}}
CLI
None
Input schema
{
  "type": "object",
  "properties": {
    "limit": {
      "default": 20,
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "type": {
      "type": "string",
      "enum": [
        "deploy",
        "publish",
        "agent.call",
        "message.received"
      ]
    }
  },
  "additionalProperties": false
}

transmit

Delivers a message to Jared McFarland by email and returns its id; it needs a reply-to address, and each sender can send three a day.

Title
Transmit
Read-only
No
JSON
POST https://datspace.ai/api/transmit
content-type: application/json

{"reply_to":"you@example.com","body":"Hello"}
MCP
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"transmit","arguments":{"reply_to":"you@example.com","body":"Hello"}}}
CLI
npx datspace transmit --reply-to you@example.com --body Hello
Input schema
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "reply_to": {
      "type": "string",
      "format": "email",
      "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
    },
    "body": {
      "type": "string",
      "minLength": 1,
      "maxLength": 2000
    }
  },
  "required": [
    "reply_to",
    "body"
  ],
  "additionalProperties": false
}

The ledger

Every deploy and every tool call is an event in the Ledger. Each event's hash covers the one before it:

hash = SHA-256(prevHash + "\n" + canonicalJson({ seq, at, type, data }))

canonicalJson sorts object keys at every depth, and the first event's prevHash is 64 zeros. Changing any event changes its hash and every hash after it.

This page reads the whole log from /ledger.json, 100 events at a time, and recomputes every hash in your browser with Web Crypto, using the Ledger's own code from packages/contract/src/hash.ts. Reading /ledger.json isn't a tool call, so it adds nothing to the log. From a clone of the repository, AI_URL=https://datspace.ai pnpm verify:ledger checks the newest 100 events the same way.

Answerability

Questions a recruiter's agent would ask, each with the tool that answers it and the facts its answer must contain, from tests/answerability.yaml. No model is in the loop: each check is a substring or an error code. CI runs every case over MCP and over JSON after each deploy.

The button runs every case over JSON from this browser, the way any client calls datspace.ai, so each case adds an agent.call event to the ledger above. The transmit case gives an invalid address, so no message is sent.

The cases in tests/answerability.yaml
QuestionCallExpectsResult
What is on the dial?list_stations {}contains datspace, agent-native-interfaceNot run
What has Jared built for music?search {"query":"SuperCollider"}contains patter, datspace-fmNot run
Where can I hear something Jared made?get_work {"slug":"datspace-fm"}contains https://datspace.fm, Clef FlashNot run
What is Mixpanel Headless, and where is its code?get_work {"slug":"mixpanel-headless"}contains github.com/mixpanel/mixpanel-headless, May 21, 2026Not run
What has Jared built on Cloudflare?search {"query":"Cloudflare"}contains datspaceNot run
How do I reach Jared?get_profile {}contains jared@datspace.ioNot run
How do I run the datspace CLI?get_work {"slug":"agent-native-interface"}contains npx datspaceNot run
Where is the source code?get_work {"slug":"datspace"}contains github.com/discohead/datspaceNot run
How do the works connect across domains?get_graph {}contains datspace.io, datspace.ai, lives-onNot run
When was the network last deployed?get_ledger {"type":"deploy","limit":3}contains "type":"deploy", commitNot run
What happens when an agent asks for a work that doesn't exist?get_work {"slug":"no-such-work"}error not_foundNot run
Can an agent send Jared a message, and what does it need?transmit {"reply_to":"not-an-email","body":"Hello"}error invalid_inputNot run

How it was built

In phases. Each phase's goal, done-when checks, and status are from .planning/ROADMAP.md; its plan, summary, and notes are in .planning/phases. The architecture is in .planning/ARCHITECTURE.md.

Phase 01: Foundation

Status
Complete, 2026-10-22
Goal

A push to main deploys both Workers, and the Ledger records the commit.

Done when

After a push, CI is green; the web Worker's /index lists the placeholder stations; the ai Worker's /api/stations returns them with apiVersion; /api/ledger shows a deploy event with the pushed SHA; and any other path on the ai Worker is served by the web Worker.

Summary

A push to main now builds, tests, and deploys both Workers to workers.dev, and the Ledger records the commit in a hash chain anyone can verify.

Accomplishments

  • pnpm monorepo: TypeScript 6 strict with erasableSyntaxOnly, so Node 24 runs .ts files directly (the content build and scripts need no compiler).
  • @datspace/clock: GENESIS_MS, msPerBar, barAt, pure. 5 tests.
  • @datspace/contract: zod 4 schemas (each doubles as its type), the six launch tools as data, callTool (validates, never throws, returns { error: { code, message, fix } }), the Source interface with createStaticSource, canonicalJson, sha256Hex, ledgerHash, and API_VERSION. 21 tests.
  • @datspace/content: loads stations/*.md, profile.json, and clock.json; rejects missing fields, slug and file-name mismatches, duplicate slugs and frequencies, off-dial frequencies, and live stations closer than 3.2 MHz, naming file and field; writes .generated/content.json with content hashes and glyph seeds. 8 tests.
  • datspace-web: SvelteKit 3 and adapter-cloudflare 8, prerendered / placeholder and /index/, 1-bit base styles, self-hosted Geist and Geist Mono.
  • datspace-ai: the Ledger Durable Object (declared in the exports block, SQLite), GET /api/stations, GET /api/ledger, POST /admin/deploy, everything else forwarded to WEB. 11 tests in workerd, including concurrent appends, argument-bearing events rejected, and tamper detection.
  • CI: ci.yml on pull requests; deploy.yml on main deploys web, then ai, then records the deploy. First run green on every step.
  • pnpm verify:ledger: recomputes the production chain from the public /api/ledger; no token needed.

Files Created/Modified

  • Root: package.json, pnpm-workspace.yaml, tsconfig.base.json, .nvmrc, LICENSE, README.md, .gitignore, scripts/verify-ledger.ts
  • packages/{clock,contract,content}/**
  • apps/web/**, apps/ai/**
  • .github/workflows/{ci,deploy}.yml
  • CLAUDE.md (commands, SvelteKit 3 notes), .planning/ROADMAP.md (progress)

Decisions Made

  • The schema lives in contract, not content. content imports StationFrontmatter from contract, so the shape is defined once. Contract tests use a fixture rather than the built content, which would be a dependency cycle.
  • Ledger seq is assigned in code, not by AUTOINCREMENT, because the hash covers it. append runs inside blockConcurrencyWhile, since the Web Crypto await doesn't close the input gate and concurrent appends would otherwise claim the same seq.
  • Ledger event data is a flat record of scalars. Recursive JSON types broke RPC stub typing, and flat data also enforces "facts, not payloads."
  • The Ledger throws plain Errors with a readable message, because a ZodError loses its details crossing RPC.
  • Fonts: Geist and Geist Mono as the grotesk and mono sibling (OFL, true family, tabular figures). Provisional; Jared can swap them in app.css and +layout.svelte.
  • /index lives at /index/. /index prerenders to the root index.html, and Cloudflare's asset handling redirects /index to /. _redirects sends /index to /index/ with a 308.
  • @cloudflare/vitest-plugin replaces @cloudflare/vitest-pool-workers; Vitest is pinned to 4.1 as its docs specify.
  • workers.dev subdomain is datspace-web, set automatically on the first deploy. It's temporary until phase 06 attaches the domains.
  • DEPLOY_TOKEN was generated locally, set as a Worker secret and a GitHub secret, and written to the gitignored apps/ai/.dev.vars. It was never printed.

Issues Encountered

  • SvelteKit 3 moved config from svelte.config.js into the Vite plugin, renamed $lib to #lib, and requires tsconfig.json to extend $app/tsconfig. ARCHITECTURE's sketches predate this; CLAUDE.md now records it.
  • The prerender crawler silently skips /index because its output file collides with the root page. Fixed as above.
  • A new workers.dev subdomain takes a few minutes to get a TLS certificate; the first smoke tests failed with a handshake error and then passed.

Next Phase Readiness

Phase 02 (the tuner) can start. It reads content.json (frequencies, glyph seeds, clock) through #lib/server/content.ts, imports @datspace/clock, and replaces the placeholder /. Nothing in phase 02 touches the ai Worker.

Phase 02: The tuner

Status
Complete, 2026-10-22
Goal

The first ten seconds: static that resolves into work as you tune, per the tuner specification in ARCHITECTURE.

Done when

The dial works by drag, scroll, and keyboard with correct aria-valuetext; each station shows its own glyph and locks with its color; the σ and SNR readouts and the 1973 bar clock update; prefers-reduced-motion gets a still frame; no WebGL goes to /index; and the field holds 60 fps on a mid-range laptop.

Summary

datspace.io's front door works: 1-bit static that resolves into each station's glyph as you tune, and locks in the station's color.

Accomplishments

  • #lib/tuner/model.ts: tuning and quantizing, nearest live station, σ, lock, SNR, aria-valuetext, the #96.1 fragment, and stationPitch for phase 03's sound. 8 tests.
  • #lib/tuner/glyph.ts: a 24×24 glyph per station from its seed (sfc32, mirrored, two cellular-automaton passes, density held to 30–60% by regrowing, not patching), and a hue per station. 4 tests.
  • Field.svelte and field.glsl.ts: a GLSL3 ShaderMaterial on a clip-space plane. 3-CSS-px cells, pcg3d noise, on = s·(1 − σ) + n·1.6·σ > 0. Threlte renders on demand: a task advances the noise at 24 fps, and dial moves invalidate.
  • Tuner.svelte: drag anywhere, wheel and trackpad, click or drag on the scale, a role="slider" with arrows, PageUp/PageDown, Home and End, arrow keys anywhere when nothing has focus, Enter to open a locked station, the fragment kept with replaceState, the bar clock, separate hints for touch and fine pointers, and a <noscript> link to the index.
  • /s/<slug> stubs prerendered for every station; index rows link to them.

Verified (headless Chrome, locally and on workers.dev)

  • Keyboard from page load: 12 × → reaches 88.7 and reads "88.7 MHz, datspace"; End, Home, and PageUp work on the slider.
  • /#92.3 reloads locked on the agent-native interface; Enter opens /s/datspace.
  • requestAnimationFrame holds 60 fps idle and while dragging.
  • Reduced motion: the field is identical across 600 ms and re-rolls when the dial moves. Normal motion: the noise animates.
  • No WebGL (--disable-3d-apis) goes to /index/. No JavaScript shows the index link and keeps the slider markup.
  • No console errors or warnings.

Files Created/Modified

  • apps/web/src/lib/tuner/{model.ts,glyph.ts,field.glsl.ts,Field.svelte,Tuner.svelte}
  • apps/web/src/routes/{+page.svelte,+page.server.ts}, apps/web/src/routes/s/[slug]/*, apps/web/src/routes/index/+page.svelte
  • apps/web/{vite.config.ts,vitest.config.ts,package.json}, apps/web/test/*

Decisions Made

  • The receiver panel is solid black. Text over 1-bit noise is illegible, so the instruments sit on a black panel, and the glyph is centered in the space above it.
  • The frequency is quantized to 0.1 everywhere. That's what the shader, the lock, and the readout all use. It steps like a digital tuner; continuous σ while dragging wasn't worth two sources of truth.
  • Dragging the full screen width moves half the dial, and 30 wheel pixels make one step.
  • The fragment is #96.1 and isn't an element id. The prerender's missing-id check now ignores frequency-shaped ids; any other missing id still fails the build.
  • three r186 doesn't alias gl_FragColor for GLSL3, so the shader declares out vec4 fragColor.
  • The datspace glyph locks in acid green. That's the hue its seed produces. It's on the cliché list, but it's deterministic from the address; other stations land elsewhere on the wheel.

Issues Encountered

  • The Claude in Chrome extension wasn't connected, so checks ran through playwright-core driving the installed Chrome headless (a scratch install, outside the repo).
  • The first reduced-motion check compared whole screenshots, and the bar clock ticked between them; it now compares the field only.
  • 60 fps was measured on this MacBook, not a mid-range laptop. The shader is one hash and one texture read per pixel, so the risk is low, but check on slower hardware before launch.

Next Phase Readiness

Phase 03 builds the station page layout (replacing the stub), renders the markdown body, the about and colophon pages, the tape, the band selector, Tone.js sound (stationPitch is ready), and axe-core checks. The tuner exposes nothing phase 03 needs to change, except that sound needs σ and the station index, both already derived in Tuner.svelte.

Phase 03: datspace.io

Status
Complete, 2026-10-22
Goal

Everything behind the tuner: the index, station pages with the four beats, about, colophon, the tape, the band selector, and opt-in sound.

Done when

Every station is reachable by keyboard, with prefers-reduced-motion, and without WebGL; axe-core reports no violations on /, /index, and one station page; locking a station from the tuner opens its page.

Summary

datspace.io is live on its own domain with station pages, about, colophon, the tape, the band selector, and opt-in sound, and every deploy now passes an axe-core audit.

Accomplishments

  • Content: a Page schema and packages/content/pages/{about,colophon}.md, validated like stations. The colophon is drafted, including the exact dedication; about is a placeholder for Jared.
  • Markdown: marked renders station bodies and pages during prerender only; external links get rel="noopener".
  • Station page: the glyph as crisp SVG in the station's color, frequency and domain, title, summary, the four beats as a definition list, the body, links, a link to the work's record on datspace.ai (/api/work/<slug>, built in phase 04), and a link back to its position on the tuner.
  • Header and band selector on every page and the tuner: io and ai are live, and dev, fm, music, and studio are dimmed and dotted, with "not yet" for screen readers. The current band is inverted like a pressed receiver button. On the tuner the dial position rides along in the fragment.
  • The tape: each new lock goes to sessionStorage; /tape lists the visit in order, with "Email Jared" (a mailto: prefilled with the tape) and "Clear the tape". Nothing is sent anywhere.
  • Sound: a Sound toggle; Tone.js (341 KB) is imported only on the first press. Band-passed white noise swept 300 to 2,900 Hz across the dial with gain ∝ σ, a sine at stationPitch(i) with gain ∝ (1 − σ)², ramped, and muted while the tab is hidden.
  • Domains: datspace.io → datspace-web; datspace.ai, .dev, .fm, .music, .studio → datspace-ai, which 302s the unlit four to https://datspace.io/#<band>. WEB_URL and AI_URL now point at the real domains. This was phase 06 work, done early to take DNS off the launch-day path.
  • Accessibility: pnpm check:a11y runs axe-core (WCAG 2.1 A and AA) in Chrome on /, /index/, a station, /about, /colophon, and /tape, with and without reduced motion: 12 of 12 clean, locally, in CI, and on production. CI runs it after every deploy.

Verified on datspace.io

Keyboard tuning, fragment reload, Enter to open, 60 fps while dragging, reduced-motion still frame, tape recording two locks in order with the mailto carrying them, Tone.js absent until the Sound press, no console errors, the Ledger chain intact (5 deploys), and all six domains answering over HTTPS.

Decisions Made

  • Unlit domains attach to datspace-ai, not datspace-web (a change from ARCHITECTURE). datspace-web serves static assets before any Worker code runs, so it can't redirect / by host.
  • Drag starts only on the field. Capturing the pointer on the whole tuner swallowed clicks on the overlay's links and buttons, including Sound.
  • The bar clock moved into the panel as a third instrument beside σ and SNR, which freed the header for the bands and the site links.
  • The field keeps the glyph clear of the header as well as the panel, so it doesn't collide on phones.
  • The tuner page is 197 KB gzipped, almost all three.js. Inner pages load a few KB and none of it.

Issues Encountered

  • sd silently ignores multi-line patterns; perl or the Edit tool are used for those.
  • Mobile: the band selector overflowed and the bar count clipped; fixed with wrapping and a more specific mobile rule (the first attempt lost the cascade).

Next Phase Readiness

Phase 04 (datspace.ai) adds MCP, the remaining JSON routes (/api/work/:slug, .md, search, graph, profile), llms.txt, agent.call events, the /ai live view for the datspace.ai host, and the answerability suite. The station pages already link to /api/work/<slug>.

Phase 04: datspace.ai

Status
Complete, 2026-10-22
Goal

The agent-native interface: stateless MCP with six tools, the JSON API, llms.txt, and the public live view of agent calls.

Done when

The answerability suite passes in CI against MCP and JSON; every tool call appends an agent.call event with no arguments; Claude Code can add https://<ai>/mcp and answer "How do I reach Jared?" from it.

Summary

datspace.ai is a working agent interface: stateless MCP and a JSON API over the same six tools, llms.txt, a public live feed of agent calls, rate limits, and an answerability suite that passes 16 of 16 over MCP and JSON after every deploy. No model runs on our side.

Accomplishments

  • MCP at https://datspace.ai/mcp: createMcpHandler from agents 0.30.0 with @modelcontextprotocol/server 2.3.1, both pinned as that release requires. The six contract tools are registered with title, description, input and output schemas, and read-only annotations. Results carry structuredContent plus a JSON text block; contract errors come back as isError with { error: { code, message, fix } }.
  • JSON API: /api/stations, /api/work/:slug, /api/work/:slug.md, /api/search?q=&limit=, /api/graph, /api/profile, /api/ledger?limit=&type=. Each response carries apiVersion.
  • Logging: every tool call on MCP and JSON appends agent.call { surface, tool } via waitUntil; the CLI header sets the surface to cli. Tests prove the arguments never reach the Ledger.
  • Rate limits: a Workers rate-limit binding, 120 requests a minute per client per location, on /mcp and /api/*; 429 with a fix.
  • llms.txt: facts and links (MCP URL, CLI, every JSON endpoint, one line per station linking to its markdown, contact).
  • datspace.ai / serves the web Worker's /ai page: how to connect (including the Claude Code one-liner), the tools with their descriptions, and the live feed from /live.json, which isn't a tool and isn't logged, so watching doesn't add rows.
  • Contract additions: workToMarkdown, toolPath (the JSON route per tool, for the CLI and the suite), literal tool names so ToolName is the exact union, and a type filter on get_ledger.
  • Answerability suite: tests/answerability.yaml, eight recruiter questions including one error case, run by pnpm check:answers over MCP (@modelcontextprotocol/client) and JSON. It runs in CI after every deploy, alongside the axe-core audit (now including /ai).
  • A real agent: headless Claude Code with only datspace.ai's MCP answered "how do I reach Jared, and what's on the dial?" correctly.

Decisions Made

  • get_ledger takes type. "When was the network last deployed?" read the latest 20 events, and agent traffic pushed every deploy out of the window. Agents would hit the same wall.
  • Ledger.recent() takes an options object so fields can be added without breaking callers on older code.
  • Deploys wait for the Ledger. New code reaches a Durable Object minutes after a deploy (observed: at least 4 minutes, about 90 seconds, and 5 minutes). The ai Worker deploys with --var COMMIT:<sha>, the Ledger reports it through version(), and CI polls GET /admin/version until both match before recording the deploy and running checks.
  • The MCP handler is built per request with a closure over env; nothing uses notify.

Issues Encountered

  • The type filter passed every local test and failed in production. Sending the new options object proved the cause: production returned one event, which is what the old recent(limit) code does with an object. The old Ledger was still serving requests. Fixed by the wait step above; recorded in CLAUDE.md.
  • My first note in CLAUDE.md said "over 20 minutes" before I had measured; corrected to the observed times.
  • Tests needed a Host header on MCP requests (real clients always send one).
  • The live feed and the Ledger now include CI's own answerability calls: 16 per deploy, labeled with their surface like any other agent.

Next Phase Readiness

Phase 05 (CLI) builds on toolPath, the cli/<version> surface header, and the contract types. npx datspace ls must work on a clean machine, and the name is already held on npm at 0.0.0.

Phase 05: CLI

Status
Complete, 2026-10-22
Goal

npx datspace reads the network over JSON.

Done when

npx datspace ls works on a clean machine with no setup, and ls, show, search, graph, and hewitt all support --json.

Summary

npx datspace ls works on a clean machine: datspace 0.1.0 is on npm, built by GitHub Actions with signed provenance, staged through trusted publishing, and approved by Jared with 2FA.

Accomplishments

  • apps/cli: ls, show <slug>, search <query> [--limit n], graph, hewitt, help, --version; --json on every command; --api or DATSPACE_API to point elsewhere; bold only on a TTY without NO_COLOR. API errors print the message and the fix and exit 1; usage errors exit 2. 12 tests against a fake API.
  • One 8 KB file, no runtime dependencies. esbuild bundles the CLI with the contract's toolPath, imported through a new @datspace/contract/paths subpath so zod stays out. 3.7 KB packed.
  • The Ledger records the surface. Every request sends x-datspace-client: cli/<version>, so the Ledger and the live view show those calls as cli.
  • Release: cli-release.yml on cli-v* tags checks that the tag matches package.json, typechecks, tests, builds, strips the workspace devDependencies, and runs npm stage publish over OIDC. No npm token exists anywhere. Jared approves on npmjs.com with a passkey.
  • Verified: the packed tarball installed into an empty directory with an empty cache ran every command against production; after approval, npx --yes datspace ls with an empty cache and an empty HOME printed the dial. datspace@0.1.0 carries an SLSA v1 provenance attestation.

Decisions Made

  • Staged publishing, not direct. Jared's trusted publisher allows npm stage publish and npm dist-tag only, so every release waits for a human with 2FA. That's slower by one click and safer.
  • graph prints JSON even without --json, per ARCHITECTURE; a node-and-edge list has no better text form.
  • DATSPACE_API joins --api so local development doesn't need the flag on every call.

Issues Encountered

  • Jared's local npm (11.5.2) predates staged publishing (11.15), so approval happened on npmjs.com. CI upgrades npm before staging.
  • Packing locally with npm pkg delete devDependencies stripped them from the uncommitted package.json; restored before commit. CI does the same strip on its throwaway checkout only.

Next Phase Readiness

Phase 06 (launch) is content, production checks, the film, and the application. Every engineering done-when check in the roadmap now passes on production. What remains is Jared's writing, the four cuttable stations, and the film.

Phase 06: Launch

Status
In progress
Goal

Real content, every domain live, final checks on production, the film, and the application sent.

Done when

The launch stations, the about page, and the colophon are published; every check above passes on datspace.io and datspace.ai; the four unlit domains redirect to datspace.io (done in phase 03); the film is uploaded; the application links datspace.io, datspace.ai, the repo, and the film.

Files
QA.md

Phase 07: Bands

Status
Complete, 2026-10-30
Goal

Every domain on the band selector serves something real, and every station has its writing.

Done when

datspace.dev verifies the ledger in the browser and runs the answerability suite against production; datspace.fm plays a station chosen by Clef Flash from the vibe check, with the cache, rate limit, budget, and local fallback tested; datspace.music's sleeves play their own artwork; datspace.studio serves patter in listener mode, with prompting still behind Access; the six stations, about, colophon, and profile are written; and check:a11y, check:render, and the answerability suite pass in production.

Summary

October 29 to 30. Every domain on the band selector now serves something real, and every station is written.

What shipped

Band What's there
datspace.io Six stations, written in full: datspace (with the 1-bit studies on its page), the agent-native interface, Mixpanel Headless, aux, patter, and datspace.fm. About, the colophon, profile.json, and an essay, Actors all the way down, at /actors.
datspace.ai Unchanged surfaces; llms.txt lists the bands.
datspace.dev The shape, the contract (all seven tools with their schemas and calls on every surface), the ledger verified in the browser (454 events, seq 1 to 454, on October 30), answerability runnable against production, and the build log.
datspace.fm A generative station on SuperSonic. Three questions from a bank of 153 go to Clef Flash through POST /decide; ten SynthDefs play one of six systems, in time with the 1973 clock; a one-bit XY scope draws the voices. Since October 30 the scope is also an XY pad: two macros per system, under a pointer, a finger, or the keyboard, bend the listener's copy until they let go.
datspace.music DSM001 to DSM012, unpressed; each sleeve plays its glyph like a player-piano roll, in Tone.js after a press.
datspace.studio patter in listener mode (github.com/discohead/patter, AGPL-3.0): anyone can listen and turn knobs on their own copy; /agents, /api, and /login sit behind Access.

Numbers

  • 178 tests across the monorepo; patter has 163 of its own.
  • A Clef Flash decision reads about 1,390 input tokens: about $0.000053, 500 to 1,000 ms; cached repeats return in under 20 ms.
  • Calibration: dance-floor answers chose fold, a lake at 4 a.m. chose drone, punch cards chose bits, and "rain, a sound, the ocean" chose grain at 84%, a lock (σ 0.06).
  • SuperSonic's runtime is 2.1 MB, self-hosted; the SynthDefs are 13 KB.

What didn't work at first

  • Clef's own confidence ran 0.05 to 0.2 across six options, too low to use; σ became the runner-up's probability over the winner's.
  • The first scale criteria chose "harmonic" five times out of six. Rewording each option as a mood plus a familiar use spread the choices.
  • The scope drew the final mix, and stereo reverb turned every figure into a cloud. ds_master now writes the voices before the room to outputs 2 and 3, and the scope reads those.
  • The decision cache keyed on datspace.fm's host while requests arrived on other zones' hosts, so nothing cached; keys use the request's origin now.
  • Six dial slots for eight candidate works: the 1-bit studies (first on the cut list) moved onto the datspace station's page, and the essay became a page.

Changes outside this repository

  • patter's route moved from datspace.fm/* to datspace.studio/*, and its Access application now covers only datspace.studio/agents, /api, and /login (same AUD, Jared's email only, cookie path attribute off). Jared approved each step.
  • patter's listener rate limiter uses namespace 1003; datspace-ai uses 1001 and 1002.

Open

  • patter's host side hasn't been exercised through real Access yet: Jared signs in at datspace.studio/login and goes on air.
  • Takes patter recorded on datspace.fm live in that origin's IndexedDB in Jared's browser; datspace.studio can't see them.
  • The answerability case shape and check() exist twice, in tests/answerability.ts and apps/web/src/lib/dev/answerability.ts.
  • pnpm verify:ledger checks the newest 100 events through a logged tool call; it could page /ledger.json like datspace.dev does.

October 29 and 30: the other bands

datspace.dev, datspace.fm, datspace.music, and datspace.studio went live overnight. datspace.fm is a station that plays in the browser on SuperSonic, SuperCollider's scsynth compiled to WebAssembly, with a system chosen from a visitor's answers to a few questions. Choosing it is the one model call a visitor can cause, an exception Jared made on October 29 to the rule against inference: Clef Flash picks among options in the repository and writes no text, behind a cache, a limit per address, a daily budget, and a local fallback. datspace.music is a catalog with nothing pressed yet. datspace.studio is patter, Jared's synthesizer you talk to, in listener mode: anyone can watch and turn its knobs, and only Jared can prompt it, which is the only way its own model is ever called.

The cut line

If the schedule slips, cut in this order: Tone.js sound, the 1-bit-studies station, the essay Actors all the way down, the live view on datspace.ai, CLI search and graph, the tape.

Never cut: the tuner, the index, station pages, MCP, npx datspace ls, and the film.