# Episode 18 --- MuseHub API **Working YouTube title:**\ **I Built A MuseHub Client In Bash (No SDK, No CLI, Just curl)** **Thumbnail thought:**\ `curl. one header. no muse binary.` **Target runtime:** \~8:00 ------------------------------------------------------------------------ ## \[0:00--0:20\] COLD OPEN **\[CAMERA --- Episode 17's closing line, on screen: "what you can actually build with an API this well-specified."\]** **GABRIEL:** Every episode so far has used the `muse` binary as the client. Today it's gone. No `muse hub` subcommands. Just `curl`, one signed header, and the raw HTTP API underneath everything else this season has been built on. **\[TITLE CARD --- fast\]** > MUSEHUB API **\[Music enters.\]** ------------------------------------------------------------------------ ## \[0:20--1:00\] THE API DESCRIBES ITSELF **\[TERMINAL\]** ``` text $ curl -sk https://localhost:1337/api/openapi.json | jq '.info, (.paths | length)' ``` ``` json { "title": "MuseHub API", "version": "0.2.0.dev3" } 239 ``` **GABRIEL VO:** Two hundred thirty-nine documented paths, generated live from the actual FastAPI route definitions --- not hand-written docs that can drift, like the ones we caught lagging in episode 16. If you're building a real integration, this schema is the actual, current contract. ------------------------------------------------------------------------ ## \[1:00--2:30\] SIGNING A REQUEST BY HAND, AND GETTING IT WRONG FIRST **\[TERMINAL\]** ``` text $ HEADER=$(muse sign header --method GET --path "/api/repos" --hub https://localhost:1337 --json | jq -r .header_value) $ curl -sk "https://localhost:1337/api/repos?limit=3" -H "Authorization: $HEADER" ``` ``` json { "detail": "Signature verification failed." } ``` **\[beat\]** **GABRIEL:** That's a real mistake, not staged. I signed `/api/repos` --- without the `?limit=3` I actually put in the URL. MSign signs the *exact* method, path, and query string; add a query parameter after signing and the signature no longer matches what the server sees. It rejects it, correctly. ``` text $ HEADER=$(muse sign header --method GET --path "/api/repos?limit=3" --hub ... --json | jq -r .header_value) $ curl -sk "https://localhost:1337/api/repos?limit=3" -H "Authorization: $HEADER" ``` ``` text gabriel/wire-episode17 gabriel/wire-demo gabriel/wire-episode17-demo ``` **GABRIEL VO:** Sign the full thing, and it's just... data. No `muse` process running on this end at all --- one signed header, one HTTP request. ------------------------------------------------------------------------ ## \[2:30--3:30\] FINDING THE REAL ENDPOINT FROM THE SCHEMA, NOT A GUESS **\[TERMINAL\]** ``` text $ curl -sk .../openapi.json | jq '.paths | keys[] | select(contains("issues"))' ``` ``` text "/api/repos/{repo_id}/issues" "/api/repos/{repo_id}/issues/{issue_number}" ... ``` **GABRIEL:** Notice: it's `repo_id`, not `owner/slug`. My first instinct --- guess a REST-y `/api/{owner}/{repo}/issues` path --- was wrong, and I only know that because I checked the schema instead of assuming. That's the actual point of a machine-readable contract: stop guessing. ------------------------------------------------------------------------ ## \[3:30--5:00\] A REAL WRITE, ZERO ABSTRACTION **\[TERMINAL\]** ``` text $ REPO_ID=$(curl ... | jq -r '.repos[] | select(.slug=="wire-episode17") | .repoId') $ HEADER=$(muse sign header --method POST --path "/api/repos/$REPO_ID/issues" \ --body-file issue.json --hub ... --json | jq -r .header_value) $ curl -sk -X POST ".../api/repos/$REPO_ID/issues" \ -H "Authorization: $HEADER" -H "Content-Type: application/json" \ --data-binary @issue.json ``` ``` json { "issueId": "sha256:b9928a...", "number": 2, "author": "gabriel", "state": "open" } ``` **GABRIEL VO:** A real issue, in the real database, created by a script that has never once called `muse`. This is what "build something on top of MuseHub" actually means --- not a metaphor, an actual bash script with one dependency: something that can compute an Ed25519 signature. ------------------------------------------------------------------------ ## \[5:00--6:00\] THE SIGNING PART DOESN'T HAVE TO BE MUSE EITHER **\[CAMERA\]** `muse sign header` is a convenience --- the CLI happens to hold your key. The actual requirement is just: compute this over the canonical message, using the private key you registered. ``` text $ muse sign curl --method GET --url ".../api/repos?limit=2" --hub ... ``` ``` text curl -X GET \ -H 'Authorization: MSign handle="gabriel" alg="ed25519" ts=... sig="..."' \ https://localhost:1337/api/repos?limit=2 ``` **GABRIEL:** Prints the whole command, ready to paste, ready to put in a script you hand off to someone who's never touched Muse. Anything that can produce an Ed25519 signature over that string --- Python, Node, Rust, whatever --- is a valid MuseHub client. The `muse` binary is the reference implementation, not a gate. ------------------------------------------------------------------------ ## \[6:00--6:40\] WHAT THIS BUYS YOU **\[CAMERA\]** Every write tool in Episode 16's MCP server, every `muse hub` subcommand this whole season, and the raw curl calls from the last five minutes all terminate at the exact same 239 routes, the exact same signature check, the exact same database. Three different front doors, one real building behind all of them. ------------------------------------------------------------------------ ## \[6:40--7:20\] OUT **\[TERMINAL --- fading to black\]** **GABRIEL VO:** Every episode this season has assumed one identity per person. Next: what happens when that gets more complicated --- organizations, attestations, an agent's genealogy back to the human who deployed it. **\[beat\]** That's next. **\[CUT TO BLACK\]** > `musehub.ai` ------------------------------------------------------------------------ # Production Notes Episode 18's spine is subtraction, not addition: every previous episode added a layer (CLI, MCP tools, wire format); this one strips everything back to curl and a signature to prove none of those layers were load-bearing magic. The failed signature at 1:00 has to stay in --- it's the most convincing three seconds in the episode, because it's a real mistake with a real, immediate, correct rejection, not a staged gotcha. ## Don't Skip The Schema-Discovery Beat The `/{owner}/{repo}/issues` guess being wrong (2:30) is small but important: it demonstrates the actual workflow for anyone building a real integration — check the schema, don't assume REST convention holds. Cutting it for pacing would remove the episode's only moment of "here's how you'd actually work with this if you were doing it for real," which is the whole premise. ## The Three-Front-Doors Framing Closes The Loop On The Season 6:00's beat --- MCP tools, `muse hub`, and raw curl all hitting the same 239 routes --- is deliberately a callback to episode 16. Make the connection explicit rather than assuming the audience remembers; that's the payoff for having built the MCP episode first. ## Everything Here Is Real Every curl call, the intentional failed signature, the schema lookup, and the created issue were run against the real local MuseHub API, not a mock. `wire-episode17`'s repo was reused from Episode 17 rather than creating a new throwaway --- intentional, since this episode's point is interoperating with what already exists, not building a new sandbox. Re-run `make-api-episode18-demo.sh` at record time; the exact `repoId`/`issueId` values will differ run to run, which is expected --- narrate the mechanism, not the exact hashes. ## The Seed The viewer arrives thinking: > **The CLI and the MCP server are both nice wrappers around > something else. What's actually underneath?** They should leave thinking: > **The wrapper was never the point. A hand-rolled bash script with > curl and one Ed25519 signature can do everything the official > client does. If I can build a client this thin, what happens when > the "client" isn't a person's script at all, but another > organization, or another agent, with its own identity to prove?** That's Episode 19.