> **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/patterns/nsite-publishing.md` · snapshot 2026-10-10
>
> Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate.

# Pattern: publishing a client as an nsite (NIP-5A)

> **Read this before writing a line of deploy code.** Five nsites have been
> published from this machine and every one of them cost a fresh round of
> rediscovery. The failures are always silent: the site returns HTTP 200 and
> looks broken, or returns 404 and looks un-deployed. Nothing tells you why.
>
> Verified against the live network **2026-09-21**. Spec read from
> `nostr-protocol/nips` `5A.md` at that date. Measurements in §4 were taken
> with throwaway keys against the real servers on that date.

---

## 0. The short version

```bash
# The workspace has a deploy script. Use it; don't write a fifth one.
~/Documents/nostr-dev/bin/nsite-deploy.py \
    --dir  dist \
    --keys ~/Documents/nostr-dev/secrets/<your-site>/keys.json \
    --title "Your Site" \
    --description "One sentence."
```

Six rules that cover ~every failure ever seen here:

1. **Kind 15128, no `d` tag.** 34128 is dead; a `d` tag on a root site gets the
   event rejected by the one relay that matters.
2. **The events must land on `wss://relay.nsite.lol`.** No gateway serves a
   site whose manifest it cannot find.
3. **Put `https://nostr.download` first in the `server` list.**
   `blossom.primal.net` serves every `text/*` asset as `text/plain` (§4),
   which silently kills stylesheets and service workers.
4. **Publish under a dedicated key, never the owner's.** 15128 is replaceable
   per pubkey — one deploy under the wrong key destroys whatever site that
   identity already served.
5. **Ship a `/404.html`.** The spec makes it the gateway's mandatory fallback;
   without it every deep link is a bare 404.
6. **Verify by fetching the gateway**, not by counting relay OKs. A deploy
   reports success and still 404s.

---

## 1. The event model, as the spec stands today

NIP-5A "Static Websites (nsites)", `draft` `optional`. Reference gateway:
[hzrd149/nsite-gateway](https://github.com/hzrd149/nsite-gateway), which is what
runs `nsite.lol`.

### 1.1 Kinds

| Kind | What | Class | Rule |
|---|---|---|---|
| **15128** | Root site | replaceable, one per pubkey | **MUST NOT** carry a `d` tag |
| **35128** | Named site ("subdomain") | addressable | **MUST** carry `d`, matching `^[a-z0-9-]{1,13}$`, not ending in `-` |
| **5128** | Manifest snapshot | regular, immutable | MUST carry exactly one aggregate `x` and one `a` |
| 34128 | *Legacy* per-path event | addressable | **dead.** `relay.nsite.lol` refuses it |
| 10002 | NIP-65 relay list | replaceable | how a gateway finds the manifest |
| 10063 | BUD-03 Blossom server list | replaceable | how a gateway finds the blobs |

The 13-character cap on a named site's `d` is not arbitrary: the canonical
DNS label is `<pubkeyB36><dTag>`, `pubkeyB36` eats 50 of the 63 characters a
label allows.

### 1.2 Manifest tags

| Tag | Form | Required |
|---|---|---|
| `path` | `["path", "/abs/path.ext", "<sha256-hex>"]` | **yes, one or more** |
| `x` | `["x", "<sha256-hex>", "aggregate"]` | optional, **recommended** (mandatory on 5128) |
| `d` | site identifier | 35128 only |
| `server` | `["server", "https://blossom…"]` | no — but see §4, it is load-bearing |
| `title` | human-readable title | no |
| `description` | **`description`, not `summary`** | no |
| `source` | `https://` or `nostr://` git URL | no |
| `app` | `["app", "<kind>:<pubkey>:<d>", "<relay>"]` | no (new — see §9) |
| `a` / `A` | copy lineage | copied sites only |

There is **no `relay` tag in the spec.** `nsyte` writes them and two of the
scripts on this machine copied that; it is harmless but it is not a spec tag,
and it is not how a gateway finds you — kind 10002 is.

`content` is empty in practice. The manifest is replaceable: **every deploy
republishes the complete file list.** There is no incremental update.

### 1.3 The aggregate hash

Deterministic identity for a site *version*, independent of tag order:

1. Take every `path` tag.
2. Emit one line per tag, exactly `<sha256hash> <absolute-path>\n`.
3. Sort the lines ascending lexicographically.
4. Concatenate as UTF-8, SHA-256 it, lowercase hex.

```bash
nak req -k 15128 -a <pubkey> --limit 1 wss://relay.nsite.lol \
  | jq -r '.tags[] | select(.[0] == "path") | "\(.[2]) \(.[1])"' | sort | sha256sum
```

Two manifests are "the same site" iff their aggregate hashes match — which is
what makes a copied or mirrored nsite verifiable. Emit it. `bin/nsite-deploy.py`
does; `nsyte 0.27` and `nak nsite` do not.

### 1.4 How a gateway resolves `<npub>.nsite.lol`

1. **Parse the left-most DNS label.** Valid `npub` → root site. `^v[0-9a-z]{50}$`
   → snapshot. `^[0-9a-z]{50}[a-z0-9-]{1,13}$` → named site. Else: not found.
2. **Find the manifest** on its own relays (`relay.nsite.lol`) *and* on the
   relays in the author's kind 10002, which it looks up via `LOOKUP_RELAYS`
   (default `wss://user.kindpag.es`, `wss://purplepag.es`).
3. **Match the path.** Exact `path`-tag match; a request not ending in a
   filename gets `index.html` appended (`/` → `/index.html`,
   `/blog/` → `/blog/index.html`). No match, or no manifest at all →
   **MUST fall back to `/404.html`**.
4. **Fetch the blob** by sha256: manifest `server` tags first, then the
   author's 10063, then the gateway's own fallbacks. Nothing anywhere → 404.
   The gateway SHOULD verify the blob's sha256 against the `path` tag.
5. **Serve it**, forwarding the Blossom server's `Content-Type` and
   `Content-Length`. **This forwarding is the whole ballgame — see §4.**

Two consequences worth internalising:

- The manifest does **not** have to be on `relay.nsite.lol` if your 10002 is
  discoverable on the lookup relays and points somewhere the gateway can read.
  Publishing to `relay.nsite.lol` is simply the path that has no moving parts.
- The gateway never executes anything. An nsite is static files talking to
  relays and Blossom from the browser. No server code, ever.

### 1.5 `relay.nsite.lol` policy (probed 2026-09-21)

> "This is a relay for nsite.lol, it only accepts kind 10002, 10063, 15128,
> 35128, and 5128 events."

strfry 1.1.0, `max_message_length: 131072` (128 KB ≈ 1,000+ `path` tags),
`max_limit: 5000`, no `restricted_writes`. It is public-write for those kinds
and those only — it will not hold your app's content events.

---

## 2. Four ways to deploy, and when to pick each

All four were run against the local rehearsal stack (§6) on the same four-file
directory, 2026-09-21. What each one actually puts in the kind 15128:

| | `bin/nsite-deploy.py` | `nak nsite` | `nsyte` | `ngit nsite` |
|---|---|---|---|---|
| `path` tags | ✅ | ✅ | ✅ | ✅ |
| `server` tags | ✅ | ✅ | ✅ | ✅ |
| `title` | ✅ | ❌ no flag | ✅ | ✅ |
| `description` | ✅ | ✅ | ✅ | ✅ |
| `source` | ✅ | ✅ | — | ✅ |
| aggregate `x` | ✅ | ❌ | ❌ | ? |
| `/404.html` fallback | ✅ | ❌ | ✅ `--fallback` | ✅ `--fallback` |
| SPA route aliases | ✅ `--route` | ❌ | ❌ | ❌ |
| kind 10002 + 10063 | ✅ | ❌ | flags exist but silently no-op in `--no-config -i` | via 10063 discovery |
| forbidden-key guard | ✅ | ❌ | ❌ | ❌ |
| verifies the gateway | ✅ | ❌ | ❌ | ❌ |
| per-server upload tolerance | ✅ | ❌ aborts on first failure | ✅ | ? |
| standalone directory | ✅ | ✅ | ✅ | ❌ **needs an ngit repo** |
| bunker signing | ❌ local key only | ✅ | ✅ `nbunksec` | ✅ `nbunksec`/`--signer` |



### 2.1 `bin/nsite-deploy.py` — the workspace default

`~/Documents/nostr-dev/bin/nsite-deploy.py`. Distilled from the four scripts
that Idle StarFighter, the Nostr Workshop Board, the Signer Login Tester and
Cerebrate each grew independently, plus everything in this document. It walks
a directory, uploads to Blossom with per-server tolerance, emits 15128 (with
the aggregate `x`) + 10063 + 10002, refuses forbidden pubkeys, aliases SPA
routes, and **verifies against the live gateway before it claims success.**

```bash
bin/nsite-deploy.py --dir dist --keys <keys.json> \
    --title "T" --description "D" \
    [--source nostr://npub…/repo] \
    [--route /about --route /help] \
    [--dry-run]
```

Use it. If it is missing something your site needs, fix it there — not in a
fifth copy.

### 2.2 `nak nsite` — built in, and nobody noticed

**`nak` has had an `nsite` subcommand this whole time.**

```bash
nak nsite upload --root -s https://nostr.download --sec <key> dist/ \
    wss://relay.nsite.lol wss://nos.lol wss://relay.damus.io
nak nsite download <npub> ./out
```

> **Why every agent missed it, and why that is fixed.** There used to be two
> `nak` binaries here, and the one that won on `PATH` (`~/.local/bin/nak`,
> v0.18.3, Feb 2026) had no `nsite` command — so `nak nsite` looked like it did
> not exist. Resolved 2026-09-21: `nak` is now v0.20.7 at `~/go/bin/nak`, it is
> the only one on `PATH`, and `nak nsite` works. The old binary is parked at
> `~/.local/bin/nak.v0.18.3.bak`. See [`known-issues.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/known-issues.md) §7.1.

It is genuinely good: it signs Blossom auth properly, and it sets each blob's
`Content-Type` from the file extension via Go's `mime.TypeByExtension` (verified
— `.css`→`text/css`, `.js`→`text/javascript`, `.wasm`→`application/wasm`).

Its limits, read from the source:

- **It aborts the whole deploy on the first failed upload to any server.** With
  flaky public Blossom that is a mid-deploy abort. This is the main reason the
  hand-rolled scripts exist.
- No `--title` flag, so no `title` tag.
- No aggregate `x` tag.
- No 10002 / 10063 — publish those yourself with `nak event`.
- No forbidden-key guard. Nothing stops you replacing the owner's site.

Good for a throwaway or a local rehearsal, and the only one-liner that takes a
bunker. For a release, use §2.1.

### 2.3 `nsyte` — the third-party CLI

`~/.deno/bin/nsyte` (0.27.0 installed; **0.28.1 upstream as of 2026-09-08**).
Deno; `deno install -A -f -g -n nsyte jsr:@nsyte/cli`.

```bash
nsyte deploy dist -r wss://relay.nsite.lol,wss://nos.lol -s https://nostr.download \
    --sec <hex|nsec|bunker://…|nbunksec…> --fallback dist/404.html -i
nsyte debug <npub> --relays …      # the diagnostic when a site 404s
nsyte list -p <npub> -r wss://relay.nsite.lol
```

Worth knowing:

- `--dry-run` / `--dry-run-show-kinds` previews the exact events with no network.
- `--sec` auto-detects nsec / hex / `bunker://` / `nbunksec`; `nsyte bunker` and
  `nsyte ci` manage NIP-46 signers for CI.
- **`--publish-relay-list` / `--publish-server-list` publish nothing in
  `--no-config -i` mode** (observed). Publish 10002/10063 yourself.
- **Never use `--publish-relay-list` / `--publish-server-list` /
  `--publish-profile` with the owner's key** — they overwrite the owner's real
  10002, 10063 and kind 0, which their phone depends on.
- `nsyte run` cannot do a fully-local gateway rehearsal: its profile-relay
  lookup is hardcoded to `user.kindpag.es` / `purplepag.es`, so a local-only key
  resolves to nothing. It also injects a CSP with `connect-src 'self' wss: https:`,
  which blocks plain `ws://localhost` relays — don't debug that as an app bug.

### 2.4 `ngit nsite` — new in ngit 3

`ngit` 3.0.2 grew a full NIP-5A publisher:

```bash
ngit nsite publish dist --id blog --title "…" --description "…" \
    --source nostr://npub…/repo --fallback dist/404.html \
    --blossom-server https://nostr.download --relay wss://relay.nsite.lol \
    --nbunksec <…>            # or -n <nsec>, or --signer <npub|alias>
```

It is the most complete of the three third-party options: `--fallback` does the
`/404.html` mapping the spec requires, `--id` switches to a named 35128, it
reads nsyte-compatible `.nsite/config.json`, it discovers Blossom servers from
the author's kind 10063 when you don't override, and it signs via nsec, bunker
(`nbunksec`) or a configured signer.

**Its one constraint: it must run inside an ngit repository.** Outside one it
fails with `no nostr git remotes or git config "nostr.repo" value`. That is a
sensible design — the site belongs to the repo, and `--source` writes itself —
but it means it is not a drop-in for a standalone `dist/`. Reach for it when the
site *is* the front end of a GRASP repo; use §2.1 otherwise.

While you are here: ngit 3 also added `ngit release` (software applications,
releases and assets — the Zapstore side, see
[`zapstore-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/zapstore-publishing.md)), `ngit container` (OCI
images over Nostr + Blossom), `ngit ci`, and `ngit pr` replacing the old
`ngit list` / `ngit fetch`.

---

## 3. Identity: the rule that has no exceptions

**Kind 15128 is replaceable per pubkey.** Publishing a root site under an
identity that already has one *irreversibly replaces it*. There is no merge, no
warning, no undo.

The owner's key `5ea4648045bb1ff222655ddd36e6dceddc43590c26090c486bef38ef450da5bd`
(`npub1t6jxfqz9…sqvksrw`, "Constant") **already serves a live root nsite**
(Archon-Web, kind 15128). It also has a real kind 10002, a real
Amethyst-maintained 10063, and a real kind 0. All four are destroyable by a
careless deploy.

So, per the workspace identity convention (`CLAUDE.md` § "Persistent identities"):

```bash
mkdir -p ~/Documents/nostr-dev/secrets/<site> && chmod 700 ~/Documents/nostr-dev/secrets/<site>
nak key generate > ~/Documents/nostr-dev/secrets/<site>/keys.json
chmod 600 ~/Documents/nostr-dev/secrets/<site>/keys.json
# then a README.md beside it and configs/identities/<site>.json with the public half
```

and put a **hard abort on forbidden pubkeys in the deploy script** — the owner's
key always, plus any key that is public by design (the Workshop Board's embedded
workshop key is in its `FORBIDDEN` set for exactly this reason: if it could
publish the site, anyone holding the app could replace the site).

If the owner's own identity must sign, use a **named site (35128, `d` ≤ 13 chars)**
via their bunker, and never the `--publish-*` flags. A root site under the owner
key is forbidden.

---

## 4. Blossom Content-Type: the measurement that explains everything

The gateway forwards whatever `Content-Type` the Blossom server reports. The
server records that type **at upload, keyed by sha256** — so a wrong type can
only be fixed by changing the file's bytes. Re-uploading identical content
re-uses the existing blob and its existing wrong type.

Measured 2026-09-21, uploading the same three files from a fresh key via
`nak blossom upload`, then reading back what each server serves:

| Server | `.css` | `.js` | `.wasm` | GET |
|---|---|---|---|---|
| **`https://nostr.download`** | `text/css; charset=utf-8` ✅ | `text/javascript; charset=utf-8` ✅ | `application/wasm` ✅ | 200 direct |
| `https://blossom.primal.net` | **`text/plain`** ❌ | **`text/plain`** ❌ | `application/wasm` ✅ | 302 → 200 |
| `https://blossom.band` | upload **400** | upload **400** | upload **415** | 404 |
| `https://azzamo.media` | upload **415** | upload **415** | upload **415** | — |
| `http://localhost:8081` (ngit-relay) | `text/css; charset=utf-8` ✅ | `text/javascript; charset=utf-8` ✅ | `application/wasm` ✅ | 200 |

Read that carefully, because it corrects the folklore:

- **`nak blossom upload` was never the problem.** It sends the correct
  `Content-Type` from the file extension, and the local server and
  `nostr.download` both honour it.
- **`blossom.primal.net` rewrites every `text/*` type to `text/plain`.** It is
  not "unreliable" and it is not content-sniffing — it is deterministic:
  `text/*` is flattened, `application/*` passes through. A browser refuses a
  stylesheet served as `text/plain`, and refuses to register a service worker
  served as `text/plain`. The page renders, completely unstyled, looking like
  it loaded fine.
- **`blossom.band` now rejects fresh-key uploads outright** (400 for text, 415
  for wasm), not just `.wasm` as previously recorded. Do not list it.
- **`azzamo.media` (= `blossom.azzamo.media`) cannot host an nsite.** It is the
  owner's media server and first in their kind 10063, it accepts uploads from
  any key, and it preserves image types faithfully — but it refuses by MIME with
  HTTP 415: `text/html` and `text/javascript` "not allowed for security
  reasons", `text/css`, `text/plain` and `application/wasm` "not supported".
  Since an nsite is almost entirely those types, it must never appear in a
  `server` tag. Use it for media; see [`../sources/blossom.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/blossom.md).
- `cdn.satellite.earth` returns 500 on both upload and GET (still down) — and
  it is the **second entry in the owner's live 10063**, so their Amethyst media
  has a dead fallback in it.
- `inner.sebastix.social` is reachable again (401 on `/upload`, 404 on unknown
  sha — i.e. a healthy BUD-01/02 server). Note its **root path** is the API;
  `…/blossom/` is an HTML catch-all that returns 200 for everything and would
  false-positive every dedup check. Its relay side is `restricted_writes: true`
  with `max_event_tags: 14`, so it cannot hold a manifest of more than ~11 files.

**What follows operationally:**

1. `SERVERS = ["https://nostr.download", "https://blossom.primal.net"]`, in that
   order, and **abort if the first one fails** for any file. Primal is a mirror
   for availability, never the type-of-record. The canonical list lives in
   `configs/blossom.json` → `web_assets`.
2. Inlining CSS and JS into `index.html` remains a good belt-and-braces move —
   it removes the failure mode entirely — but now you know *why*, and that it is
   only necessary because of server #2.
3. `.wasm` is safe everywhere that accepts it, including at 9 MB. Still ship the
   `fetch → arrayBuffer → WebAssembly.instantiate` fallback so a host with the
   wrong type can't break boot.
4. There are **no COOP/COEP headers** from nsite.lol ⇒ no `SharedArrayBuffer`,
   no wasm threads. Ever. Single-threaded Go/TinyGo/wasm-bindgen is fine.
5. No CSP from the gateway; CORS is `*` on gateway and blob GETs alike, so the
   app may fetch relays and blobs cross-origin without a proxy.

---

## 5. The failure table

| Symptom | Cause | Fix |
|---|---|---|
| `Site not found` / 404 at the root | Manifest never reached `relay.nsite.lol`, or reached it and was rejected | Publish 15128 there; check the OK message, not the relay count |
| Deploy reports "3/4 relays accepted", site still 404s | The one that refused was `relay.nsite.lol` — usually because the root event carried a `d` tag (`"Root site events must not include a 'd' tag"`) | Drop the `d`. It belongs to 35128 |
| Page loads completely unstyled, no console error | Stylesheet served `text/plain` by `blossom.primal.net` (§4) | Inline the CSS, or make sure `nostr.download` holds it and is listed first |
| Service worker silently never registers | Same, for the SW script | Same |
| Every deep link 404s | No `/404.html` path in the manifest | Add a `path` tag for `/404.html` pointing at index.html's sha; alias the known routes too |
| Deep link renders but returns HTTP 404 | You copied index.html to 404.html — that *renders* but the status is still 404 | Add extra `path` tags for the real routes, same sha, no extra upload → 200 |
| Redeploy doesn't show up | Gateway `cache-control: max-age=3600` | Hash-suffix asset filenames so only `/index.html` is a mutable path; hard-refresh; wait |
| Deploy hangs forever on a `nak` call | `nak` reads stdin | `stdin=subprocess.DEVNULL` on every call — but you cannot pass both `stdin=` and `input=`, so set it only when nothing is piped |
| Site was live, now 404 | Someone published a 15128 under the same pubkey | This is why §3 exists |
| `nsite.lol` returns 502 | Gateway outage; it is a real SPOF | Try another gateway (`<npub>.nsite-host.com`); the manifest is on relays, any implementation can serve it. A service worker precaching the bundle makes this survivable |
| Legacy site 404s | Published as 34128, or as 35128 with an empty `d` | Republish as a 15128 root |

---

## 6. Rehearsing locally

| Service | Endpoint | nsite behaviour |
|---|---|---|
| `nostr-rs-relay` | `ws://localhost:8080` | **accepts** 15128 / 10063 / 10002 — use this |
| khatru GRASP relay | `ws://localhost:8081` | **rejects** 15128 (`"event does not relate to a stored repository"`) — git-scoped, unusable here |
| Blossom (ngit-relay) | `http://localhost:8081` | BUD-01/02 work; **preserves the uploaded Content-Type**, wasm included |

**Level 1 — the app.** `python3 -m http.server 8088 -d dist` or
`nsyte serve -d dist`. Playwright + headless Chromium are installed.

**Level 2 — the pipeline.** Rehearse the whole publish against local infra with
a throwaway key:

```bash
nostr-rs-relay --config ~/Documents/nostr-dev/relays/nostr-rs-relay.config.toml &
KEY=$(nak key generate)
bin/nsite-deploy.py --dir dist --sec "$KEY" \
    --relay ws://localhost:8080 --server http://localhost:8081 --no-verify
nak req -k 15128 -a $(nak key public "$KEY") ws://localhost:8080 | jq '.tags|length'
```

This proves the walk, the hashing, the upload with MIME, the manifest shape and
the relay accept. Everything except the gateway.

**Level 3 — the gateway.** `nsyte run` cannot do this locally (§2.3). Either run
`hzrd149/nsite-gateway` yourself with `LOOKUP_RELAYS`/`NOSTR_RELAYS` pointed at
`ws://localhost:8080`, or accept Levels 1+2 and do the real check read-only on
nsite.lol at release time.

---

## 7. Definition of done

A deploy is not done when the relays said OK. It is done when:

- [ ] `curl -sI https://<npub>.nsite.lol/` → **200**, and the right `content-type`
- [ ] `curl -sI …/<your stylesheet or main js>` → the **correct** type, not `text/plain`
- [ ] `curl -so /dev/null -w '%{http_code}' …/<a deep route>` → **200**, not 404
- [ ] `curl -sI …/definitely-not-a-real-path` → serves your `/404.html`
- [ ] The page renders and does its job in a real browser (Playwright is installed)
- [ ] `nak req -k 15128 -a <pubkey> wss://relay.nsite.lol` returns the event
- [ ] The aggregate `x` in the event matches the one computed from its own `path` tags
- [ ] A `configs/identities/<site>.json` exists with the public half and the
      `secrets_location` pointer, and `git status` does not show the secret
- [ ] `nsite-manifest.json` is committed next to the source, so the next deploy
      is a diff rather than an archaeology exercise

---

## 8. Sites published from this machine

All five verified HTTP 200 on 2026-09-21.

| Site | npub | Source |
|---|---|---|
| Signer Login Tester | `npub138kdqjl645xz6rvuw92m2v4us0shhpxezgqqz9zrrj2jdstakvus86qzyh` | `~/Production Environment/Signer-Login-Tester/` |
| Nostr Workshop Board | `npub1jnrsdqxyyegg7xz8tl6u5zv5lccex393dz28uekzmdjjs9fjw4ysfch4a4` | `~/Production Environment/Nostr-Workshop-Leaderboard/` |
| Cerebrate | `npub1qldvgjekvjzur0vjjffe2m7k4y575k879ypr6ke08tfnxshrn33q0ghyac` | `~/Production Environment/Cerebrate/` |
| Idle StarFighter | `npub1kemgxdffeejmk0xf3ja7lhmxucp4cqzzx4jcvrv4l3ucua3j40gsfgteus` | `~/Production Environment/Idle StarFighter/` |
| Earthtree Media (demo) | `npub143ac528xxw9gtftammcf9dn7yhpcdy6k36824u85009wkwsx45cq57ev6h` | `~/Production Environment/EarthTreeMedia-Nostr/` |
| Nostr Agent Onboarding | `npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe` | `~/Production Environment/Nostr-Agent-Onboarding/` |

Plus the owner's own `npub1t6jxfqz9…` → Archon-Web. **Do not touch that key.**

The Earthtree site was recorded as 404 after the 34128→15128 transition; it was
republished with `nsyte` and is live. The note is stale.

---

## 9. What changed in the spec since the last local write-up

The previous in-depth pass on this machine was `Facebook-Museum/research/nsite-procedure.md`
(2026-07-07) — still the best long-form account of *how it was figured out*, and
the source of much of this document. NIP-5A has moved since:

- **`app` tags.** `["app", "<kind>:<pubkey>:<d>", "<relay>"]` links a manifest to
  an upstream app descriptor — deliberately generic so it can point at NIP-89
  (31990) *or* NIP-82/Zapstore (32267). This is the seam between an nsite and
  [`patterns/zapstore-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/zapstore-publishing.md): the same app can be a web nsite and a
  Zapstore listing, cross-referenced.
- **Copied nsites.** `a` (immediate parent) + `A` (lineage origin) let anyone pin
  someone else's site under their own namespace at a known state. Kind and `d`
  may change across a copy; `a`/`A` are the stable references. This is credible
  exit for a website.
- **Snapshots formalised.** 5128 now has MUSTs: copy the parent's `path` tags,
  exactly one matching aggregate `x`, exactly one `a`, and copy `A` if present.
- **The aggregate hash is specified**, with the exact line format and sort order,
  and is what makes two manifests provably the same site.
- **Canonical addressing moved to a single DNS label** (`<pubkeyB36><dTag>`,
  `v<snapshotIdB36>`) to dodge wildcard-certificate limits, and the spec's
  example host is now `nsite-host.com`.
- **Blob verification**: the host SHOULD check the served blob's sha256 against
  the `path` tag and treat a mismatch as not-found.
- `34128` is now listed in the NIPs registry explicitly as **deprecated**.

## Related

- `bin/nsite-deploy.py` — the shared implementation
- [`known-issues.md` §7](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/known-issues.md) — the nak-binary split, the primal
  MIME rewrite, blossom.band's rejections
- [`patterns/going-public.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/going-public.md) — the wider "mirror local
  artifacts to public infrastructure" recipe; nsite is its step 4
- [`patterns/go-client-baseline.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/go-client-baseline.md) — if the client
  you are publishing is Go/wasm
- [`sources/blossom.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/blossom.md) — BUD mechanics
- `~/Production Environment/Facebook-Museum/research/nsite-procedure.md` — the
  2026-07-07 primary research this distils
