@fangorn-network/westmarch 0.0.1-dev → 0.0.3-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/consume/apps.js +4 -2
  3. package/consume/corpora.js +1 -1
  4. package/consume/directory.js +1 -1
  5. package/consume/embed.js +1 -1
  6. package/consume/lint.js +1 -1
  7. package/consume/mcp-bin.js +6 -0
  8. package/consume/mcp.js +193 -34
  9. package/consume/ort-preload.js +25 -0
  10. package/consume/rank.js +1 -1
  11. package/consume/roles.js +6 -4
  12. package/consume/shard.js +1 -1
  13. package/consume/steam.js +1 -1
  14. package/consume/taste-doc.js +1 -1
  15. package/consume/taste.js +1 -1
  16. package/consume/tools.js +28 -5
  17. package/consume/ui.js +1 -1
  18. package/consume/view-tools.js +202 -0
  19. package/consume/x402.js +94 -0
  20. package/dist/ts/eval.js +244 -0
  21. package/docs/app-to-agent.md +595 -185
  22. package/package.json +24 -10
  23. package/publish/agent-card.js +12 -2
  24. package/publish/cohort.js +1 -1
  25. package/publish/demand.js +1 -1
  26. package/publish/enrich.js +1 -1
  27. package/publish/envelope.js +1 -1
  28. package/publish/eval.ts +250 -0
  29. package/publish/graph.js +1 -1
  30. package/publish/manifest.js +1 -1
  31. package/publish/reactions.js +4 -2
  32. package/publish/settle.js +1 -1
  33. package/publish/ship.js +462 -0
  34. package/publish/terms.js +1 -1
  35. package/publish/vault.js +1 -1
  36. package/publish/view-cli.js +39 -0
  37. package/publish/view.js +434 -0
  38. package/publish/westmarch.d.ts +35 -0
  39. package/site/agent.js +44 -0
  40. package/site/dist/assets/__vite-browser-external-CKnVkMRn.js +1 -0
  41. package/site/dist/assets/embed.worker-CQqmvS52.js +1 -0
  42. package/site/dist/assets/index-CaHGhiKK.js +8 -0
  43. package/site/dist/assets/ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm +0 -0
  44. package/site/dist/assets/preload-helper-uBIymjUX.js +1 -0
  45. package/site/dist/assets/roles-C9V_uDr_.js +1 -0
  46. package/site/dist/assets/roles-YDRjv_PY.js +1 -0
  47. package/site/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  48. package/site/dist/assets/transformers.web-BgUm7F_-.js +46 -0
  49. package/site/dist/assets/transformers.web-u9uSoh5E.js +37 -0
  50. package/site/dist/index.html +20 -0
  51. package/site/feed.js +62 -0
  52. package/site/store.js +87 -0
  53. package/site/worker.js +103 -0
@@ -1,169 +1,538 @@
1
- # From app to agent: registering and discovering a runtime-free backend
1
+ # From nothing to a running Fangorn app
2
2
 
3
- This walks one static site from nothing to "any agent can find it and use it":
3
+ This walks an app owner from an empty directory to a live app that people can use in a
4
+ browser and any agent can find, verify and query. By the end you have:
4
5
 
5
- 1. The site exposes WebMCP tools and an A2A agent card.
6
- 2. You claim a Fangorn app and point it at that card.
7
- 3. You register the card as an ERC-8004 agent, which makes it publicly searchable.
8
- 4. Someone who has never heard of it discovers it, verifies it, and drives it.
6
+ - **an app** claimed on Fangorn, owned by your wallet;
7
+ - **a schema** committed on chain: what your data's types are, which field is what, and
8
+ which relations are allowed between them;
9
+ - **your data** committed on chain under your namespace;
10
+ - **a view** of it: your committed data embedded and sharded as static files, so people
11
+ and agents can search it by meaning, updated with one command after each commit;
12
+ - **a static site** with no server: the view, and a page for people;
13
+ - **an agent card** bound to the app on chain, and registered as an ERC-8004 agent;
14
+ - **agents using it:** anyone running `fangorn-mcp` can `open-app` it by name.
9
15
 
10
- It runs on Arbitrum Sepolia. Steps 1–5 and 7 were run against the live chain (step 6's
11
- publish commands are from the CLI's help and have not been run for Kingsfoil yet); [Kingsfoil](#reference-kingsfoil) is the worked example, and its real values are
12
- at the end.
16
+ Where the data comes from is up to you. The guide starts from a JSON array of objects,
17
+ which anything can produce: a scraper, a database export, a converted spreadsheet.
13
18
 
14
19
  ```
15
- ERC-8004 identity registry ──(A2A endpoint)──┐
16
- (searchable on 8004scan) ▼
17
- https://your.site/.well-known/agent-card.json
18
- Fangorn AppRegistry ──(agent_uri)────────────▲ │
19
- AppAgentChanged log = the app directory │ card names appId, fromBlock,
20
- │ namespaces, views, WebMCP tools
21
- ▼
22
- https://your.site/tools-page ← a browser, or fangorn-mcp driving Chrome
23
- document.modelContext tools, static shards, no server
20
+ your data ─► data/graph.json ─► fangorn commit/push ─┐
21
+ schema/graph.json ─► fangorn commit/push ────────────┤ (namespace fangorn.schema)
22
+ ▼
23
+ chain ─► westmarch-view ─► site/view/
24
+ (embeds only what changed, applies the schema)
25
+ app/ (page) ─► vite build ─► site/ ◄── the page loads site/view
26
+ pipeline/agent-card.mjs ─► site/.well-known/agent-card.json (views: [site/view]) ─► deploy
27
+ │
28
+ fangorn app agent ─► ERC-8004 registration + AppRegistry agent_uri = card URL
29
+ │
30
+ fangorn-mcp: open-app my-app ─► reads the binding, verifies the card, searches the view
24
31
  ```
25
32
 
26
- ## What you need
33
+ **Two tiers, one format.**
34
+ - **Free, local:** `westmarch-view` (step 7). It turns your commits into a view on your
35
+ own machine or CI. It embeds on CPU, ~14 records/s, only what changed since the last
36
+ run, and you host the files with your site.
37
+ - **Hosted:** quickbeam watches apps on chain, embeds on a GPU, and serves the view
38
+ itself. It will also do things one app's files can't, such as state across apps. It
39
+ writes the same files, so moving an app to it means changing the `views` URL in its
40
+ card.
41
+
42
+ It runs on Arbitrum Sepolia.
43
+
44
+ ## The short way: one config, one command
45
+
46
+ `westmarch-ship` does every step below from one `app.json`, and re-running it is how you
47
+ update: each step checks the chain (or Cloudflare) first and acts only on a difference.
48
+
49
+ ```jsonc
50
+ // app.json
51
+ {
52
+ "app": "my-app", "name": "My App", "description": "What it holds, for people and agents.",
53
+ "tags": ["…"],
54
+ "site": { "project": "my-app", "account": "<cloudflare account id>" },
55
+ "types": { "my.thing.v1": { "description": "…",
56
+ "role_map": { "identity": "id", "title": "name", "temporal": "date", "media": "url",
57
+ "tags": ["category"], "text": ["summary"] },
58
+ "presentation": { "facets": ["category"] } } },
59
+ "relations": [],
60
+ "sources": [{ "namespace": "things", "command": ["python", "-m", "my_source"] }],
61
+ "fangorn": "fangorn", // the CLI that signs
62
+ "paid": { "price": "0.01", "network": "arbitrum-sepolia", "description": "the full record" }
63
+ }
64
+ ```
65
+
66
+ ```sh
67
+ npx westmarch-ship app.json # claim, join, schema, crawl + publish, view, site, card, deploy, agent
68
+ npx westmarch-ship app.json --dry-run # every step, printed, none done
69
+ npx westmarch-ship app.json --no-crawl # rebuild view, site and card from what is on chain
70
+ npx westmarch-ship app.json --replace # each source's crawl becomes its namespace's whole state
71
+ ```
72
+
73
+ What it does, in order (the long way, below, is the same steps by hand):
74
+
75
+ 1. **claim** the app on the first run, and record the claim block (the card's `fromBlock`);
76
+ 2. **join** it as a publisher, if the wallet has not;
77
+ 3. **commit the schema** (`types`, `relations`) to `fangorn.schema`, if the chain's differs;
78
+ 4. **run each source**: a command that publishes into `--namespace` when given `--publish`.
79
+ Every quickbeam scraper Source does. A source with `"each": "rows.json"` is a template,
80
+ one source per row (`{field}`, `{field|slug}`, `{args...}`), so adding a town, a feed or
81
+ an account is adding a row;
82
+ 5. **westmarch-view**: the app's commits → `.ship/site/view`, embedding only what changed.
83
+ A removal rewrites the domain, so a retracted record's bytes stop being served;
84
+ 6. **the site**: the stock page (search, plus a feed of what is coming up and what just
85
+ happened, by the `temporal` role and the first `facets` field), the agent card and
86
+ `_headers`. With `paid`, also `_worker.js`, which sells each record over x402; only
87
+ records whose sha256 a live row carries are for sale;
88
+ 7. **deploy** to Cloudflare Pages (`wrangler`; the project is created if missing);
89
+ 8. **register** the ERC-8004 agent and bind the card, if the chain points elsewhere.
90
+
91
+ State lives in `.ship/` next to `app.json`. In CI, cache `.ship/cache` and `.ship/stage`
92
+ and commit `.ship/state.json`; Quorum's `.github/workflows/ship.yml` is a working example
93
+ (daily cron, and on every push to its config).
94
+
95
+ **What was run:** all of it, for Quorum (`https://quorum-bua.pages.dev`, agent `421614:245`):
96
+ five towns' meetings crawled, published, embedded and deployed by `westmarch-ship`, then
97
+ searched and bought (x402, settled on Arbitrum Sepolia) from `fangorn-mcp` in a fresh
98
+ session. Kingsfoil, the reference at the end, predates `ship` and was built by hand.
99
+
100
+ ## Grading a change before it ships
101
+
102
+ New records are facts, and they always ship. What can make an app worse is its
103
+ **recipe**: how sources shape records, which fields `app.json`'s role maps embed, how a
104
+ view splits them. So a recipe change is graded against the recipe it replaces, over the
105
+ same records, before it merges.
106
+
107
+ ```sh
108
+ npx westmarch-ship app.json --local /tmp/base-view # a view from what the sources staged; no chain, no deploy
109
+ # …change app.json or a source…
110
+ npx westmarch-ship app.json --local /tmp/cand-view
111
+ npx westmarch-eval /tmp/cand-view --base /tmp/base-view # exit 1 = worse
112
+ ```
113
+
114
+ `--local` builds every record every time (a changed `text` role must reach them all) and
115
+ remembers vectors in `.ship/vectors.ndjson` by the exact text embedded, so the second
116
+ build embeds only what the change touched.
117
+
118
+ What is asked lives in `eval/golden.jsonl`, one check per line, as data rather than code:
119
+
120
+ ```jsonc
121
+ {"id":"plover","q":"Plover village board","expect":{"where":{"city":"Plover"}},"min":0.8} // share of the top 5 that fits
122
+ {"id":"wells","kind":"count","where":{"county":"Portage"},"expect":{"match":{"heading":"well"}}} // can a filter reach them at all
123
+ {"id":"addresses","kind":"records","expect":{"match":{"text":"/\\b\\d{2,6} [A-Z]\\w+ St\\b/"}},"max_pct":0.5,"hard":true}
124
+ ```
125
+
126
+ `expect` is a predicate over a row: `where` (the whole-value match agents filter with),
127
+ `match` (a field, or several joined by `|`, to a regex; case-insensitive unless written
128
+ `/re/flags`), `not`. Relevance is a predicate and not a list of record ids, because ids
129
+ are content hashes and a recipe change that re-parses records changes every one.
130
+ Without labels, it also searches 200 sampled rows by their own titles (`known@1`, `known@10`).
131
+
132
+ Worse is: mean precision or `known@10` down more than 0.05, any question the base
133
+ answered now answered by nothing, a `hard` record check rising, or any floor or ceiling
134
+ missed. Quorum's `.github/workflows/eval.yml` runs this on every pull request, from the
135
+ records and vectors its `ship.yml` leaves in the cache, with no secrets; make it a
136
+ required check on `main`, and put `eval/`, `.github/` and `app.json` under `CODEOWNERS`,
137
+ or a PR can change the gate along with what it gates.
138
+
139
+ What is not graded yet: a change to how a source *parses*. `--local` reads what the
140
+ sources last staged; it does not replay their parsing over the raw documents.
141
+
142
+ ## The long way, by hand
143
+
144
+ What `ship` does, one step at a time: for understanding it, or for doing one step differently.
145
+
146
+ ### What you need
27
147
 
28
- - Node 22, and the SDK/CLI: `npm i -g @fangorn-network/sdk@2026.9.22-dev` (gives `fangorn`)
29
- - A wallet on Arbitrum Sepolia with a little ETH. **It owns the app forever**; use the
148
+ - Node 22.
149
+ - The Fangorn CLI: `npm i -g @fangorn-network/sdk@2026.9.22-dev` (gives `fangorn`).
150
+ - A wallet on Arbitrum Sepolia with a little ETH. **It owns the app forever**, so use the
30
151
  wallet that will publish the data.
31
- - A Pinata JWT. ERC-8004 registration pins a registration file to IPFS.
32
- - A static host that serves `/.well-known/` and custom headers (Cloudflare Pages does both).
33
- - `@fangorn/westmarch` for the card generator and the discovery helpers.
152
+ - A Pinata JWT. Registering the ERC-8004 agent (step 11) pins a file to IPFS.
153
+ - A static host that serves `/.well-known/` and custom headers. Cloudflare Pages does
154
+ both, and is what the commands below use.
34
155
 
35
- Configure the CLI once:
156
+ Start the project:
36
157
 
37
158
  ```sh
38
- fangorn init # writes ~/.fangorn/config.json: key, Pinata JWT, gateway
39
- fangorn wallet # confirm which address will sign
159
+ mkdir my-app && cd my-app && npm init -y && npm pkg set type=module
160
+ npm i @fangorn-network/westmarch @huggingface/transformers vite
161
+ mkdir -p data schema pipeline app
162
+ ```
163
+
164
+ The Fangorn SDK and viem come with westmarch as peer dependencies. transformers embeds:
165
+ your records in `westmarch-view`, and the reader's query in their browser.
166
+
167
+ ## 1. Set up the CLI
168
+
169
+ ```sh
170
+ fangorn init # writes ~/.fangorn/config.json: key, Pinata JWT, gateway
171
+ fangorn wallet # confirm which address will sign
172
+ fangorn set-app my-app # the app every later command uses; --app <name> overrides it once
40
173
  ```
41
174
 
42
175
  > The config file wins over `ETH_PRIVATE_KEY` in the environment. If `fangorn wallet`
43
176
  > shows the wrong address, that's why.
44
177
 
45
- ## 1. Expose tools with WebMCP
178
+ ## 2. Claim the app
179
+
180
+ First, note the current block. It becomes the card's `fromBlock` (step 9), which is where
181
+ readers start scanning the chain for your app:
182
+
183
+ ```sh
184
+ node -e 'const {createPublicClient,http}=require("viem");const {arbitrumSepolia}=require("viem/chains");
185
+ createPublicClient({chain:arbitrumSepolia,transport:http()}).getBlockNumber().then(String).then(console.log)'
186
+ ```
187
+
188
+ Then claim the name:
189
+
190
+ ```sh
191
+ fangorn app info # "unclaimed", and the wallet you expect
192
+ fangorn app claim # first come, first served; permanent
193
+ ```
194
+
195
+ With no flags, `claim` uses placeholder terms. Publish real ones later with
196
+ `fangorn app terms <hash> <uri>`; anyone who joined under the old terms has to accept again.
197
+ `--fee <wei>` sets what joining costs other publishers.
198
+
199
+ ## 3. Become a publisher in it
200
+
201
+ Owning the app makes you its first publisher. You still need global standing:
202
+
203
+ ```sh
204
+ fangorn register # DataRegistry.register(), then joins this app
205
+ fangorn app info # DataRegistry: registered · This app: joined
206
+ ```
207
+
208
+ Without the join, every push reverts with `NotRegisteredForApp`.
209
+
210
+ ## 4. Your data, as a graph
211
+
212
+ A commit is a graph: vertices, each with an `id`, a `tag` (its type) and a `payload` (the
213
+ record), and optional edges between them. Start from rows, `data/rows.json`:
214
+
215
+ ```json
216
+ [
217
+ { "id": "oak", "name": "Oak", "species": "Quercus robur", "family": "Fagaceae", "region": "Europe",
218
+ "description": "Long-lived deciduous tree with lobed leaves and acorns; hard durable timber …" },
219
+ { "id": "baobab", "name": "Baobab", "species": "Adansonia digitata", "family": "Malvaceae", "region": "Africa",
220
+ "description": "Massive trunk stores water through the dry season; fruit pulp is rich in vitamin C." }
221
+ ]
222
+ ```
223
+
224
+ ```js
225
+ // pipeline/graph.mjs — data/rows.json → data/graph.json
226
+ import { readFileSync, writeFileSync } from "node:fs";
227
+ const rows = JSON.parse(readFileSync("data/rows.json", "utf8"));
228
+ writeFileSync("data/graph.json", JSON.stringify({
229
+ vertices: rows.map((r) => ({ id: r.id, tag: "my-app.tree.v1", payload: r })),
230
+ // edges: [{ rel: "grows_in", from: "oak", to: "europe" }] // between vertex ids, if you have them
231
+ }));
232
+ ```
233
+
234
+ What makes records work well:
235
+
236
+ - **A stable id.** The same thing keeps the same id across commits.
237
+ - **Text that says what the record is, in words.** This is what gets embedded, so it's
238
+ what "search by meaning" matches. A title alone is weak; a sentence or a paragraph is good.
239
+ - **Fields worth filtering or counting on**, such as a category, a region, or a date. Agents
240
+ use these with `where` and `count`.
241
+ - **One tag per kind of thing.** Version it (`.v1`) so a later shape can be a new type.
242
+
243
+ ## 5. Declare the schema
244
+
245
+ The schema says what your data is. You commit it to a reserved namespace,
246
+ `fangorn.schema`, in your app:
247
+ - **Types:** each is a vertex tagged `fangorn.type.v1`.
248
+ - **Relations:** the relations allowed between types are edges between those vertices.
249
+
250
+ `westmarch-view` (and hosted quickbeam) read it for every record they embed, so it decides:
251
+ - **what gets embedded:** the `text` role, plus title, subtitle and tags;
252
+ - **what readers see:** titles, links, labels;
253
+ - **what agents can do:** filters, counts, and choosing among views by their description.
254
+
255
+ `schema/graph.json`:
256
+
257
+ ```json
258
+ {
259
+ "vertices": [
260
+ { "id": "tree", "tag": "fangorn.type.v1", "payload": {
261
+ "tag": "my-app.tree.v1",
262
+ "description": "Tree species: what they are, where they grow, and what they are good for.",
263
+ "role_map": { "identity": "id", "title": "name", "subtitle": "species",
264
+ "tags": ["family", "region"], "text": ["description"] },
265
+ "presentation": { "externalUrl": { "my-app.tree.v1": "https://en.wikipedia.org/wiki/{species}" } } } }
266
+ ],
267
+ "edges": []
268
+ }
269
+ ```
270
+
271
+ ```sh
272
+ cd schema && fangorn repo init fangorn.schema \
273
+ && fangorn commit graph.json -m "schema v1" --replace && fangorn push && cd ..
274
+ ```
275
+
276
+ `fangorn repo init` tracks one namespace per directory (in `.fangorn/`), which is why the
277
+ schema is committed from its own `schema/` folder.
278
+
279
+ **`role_map`** says which field is what:
280
+
281
+ | role | means | used for |
282
+ |---|---|---|
283
+ | `identity` | the id field | `get`, links, updates |
284
+ | `text` | the field(s) whose words get embedded | search by meaning |
285
+ | `title`, `subtitle` | what a record is called | result lists, and embedded too |
286
+ | `tags` | categories | filters, `count`, and embedded too |
287
+ | `temporal`, `spatial`, `measures`, `relations`, `media` | dates, places, numbers, links to other records, playable media | sorting, facets, previews |
288
+
289
+ **Rules:**
290
+ - **Declared means declared.** A role you leave out stays empty; it isn't guessed.
291
+ - **Undeclared types are still guessed:** a type the schema doesn't mention gets a role
292
+ map inferred from its fields. That works for display and often fails for search: a
293
+ field called `desc` or `nct` isn't recognized, and a `price` can end up as the date.
294
+ Declare every type you publish.
295
+ - **Only the app owner's schema counts.** Any publisher can write a namespace called
296
+ `fangorn.schema`; only the owner's is read.
297
+ - **You can change it at any time** with another commit. A view takes the new role map and
298
+ description on its next `westmarch-view` run. **Records already embedded keep the text they were embedded
299
+ with** until they're re-embedded, so change the `text` role early.
300
+ - **Relations** (edges between type vertices, e.g. `{ "rel": "grows_in", "from": "tree",
301
+ "to": "region" }`) declare which edges the app allows. Nothing enforces them yet; they
302
+ are what publisher graphs will be validated against.
303
+
304
+ ## 6. Commit and push the data
305
+
306
+ ```sh
307
+ node pipeline/graph.mjs
308
+ fangorn repo init my-app # the data namespace, tracked in ./.fangorn
309
+ fangorn commit data/graph.json -m "first data" --replace # local; builds and uploads the commit
310
+ fangorn push # the on-chain transaction
311
+ ```
312
+
313
+ `--replace` makes the file the namespace's whole state, so records you removed are
314
+ removed. Without it, the commit adds to what's already there. `fangorn status` compares
315
+ your local tip with the one on chain.
316
+
317
+ ## 7. Publish the view
318
+
319
+ ```sh
320
+ npx westmarch-view --app my-app --namespace my-app --out site/view --from-block 311700000
321
+ # my-app: 1 publisher(s) since block 311700000
322
+ # …-my-app: +20 embedded, -0 removed, 20 live
323
+ ```
324
+
325
+ It reads the chain and writes files; it needs no key. For every publisher of each
326
+ `--namespace` in the app, it:
327
+ - reads their records;
328
+ - embeds the ones the view doesn't hold yet, with your schema's roles;
329
+ - marks the ones that were removed as deleted;
330
+ - writes a new shard;
331
+ - refits the coverage sketch once the view has doubled.
332
+
333
+ The view's own files are its state, so a second run with nothing new on chain changes
334
+ nothing, and it runs the same on a laptop, in CI, or from an agent. The first run
335
+ downloads the embedding model (131 MB, kept in `~/.cache/fangorn-mcp/models`, shared with
336
+ `fangorn-mcp`). Use `--from-block` with the block you noted in step 2.
337
+
338
+ ```
339
+ site/view/cdn/catalog every publisher's domain, with descriptions and coverage
340
+ site/view/cdn/domains/<app>-<publisher>-<ns>/manifest role map, embedder, shards with their sha256, tombstones
341
+ site/view/cdn/domains/<app>-<publisher>-<ns>/shards/shard-NNNN-<sha>.ndjson.gz
342
+ ```
343
+
344
+ Check it the way a reader will:
345
+
346
+ ```sh
347
+ (cd site && python3 -m http.server 8765 &)
348
+ node node_modules/@fangorn-network/westmarch/consume/lint.js http://127.0.0.1:8765/view
349
+ # …-my-app — 20 rows — nothing to fix
350
+ ```
351
+
352
+ `lint.js` reports three levels:
353
+ - **blocking:** readers can't find you (no coverage, wrong model);
354
+ - **findable, but not readable** (no text role);
355
+ - **readable, but a dead end** (nowhere for a hit to link to).
356
+
357
+ The fix for most findings is in the schema (step 5).
358
+
359
+ A view remembers which embedder made it, and `westmarch-view` refuses to add to a view
360
+ another one built. Vectors from two encoders don't compare. Pass `--rebake` to start
361
+ a view over.
46
362
 
47
- The site's agent surface is a function that registers tools on `document.modelContext`.
48
- Guard it, since most browsers don't have WebMCP yet:
363
+ ## 8. The page
364
+
365
+ A page for people, with the same search exposed to agents in the browser as WebMCP tools.
366
+ It reads the view: shards are checked against their manifests' sha256, the query is
367
+ embedded in the reader's tab, and nothing about the query leaves it.
49
368
 
50
369
  ```js
51
- // app/agent.js
52
- import { search, getRow } from "@fangorn/westmarch/tools";
370
+ // app/agent.js — the page's tools for agents in a browser. `ctx` holds the loaded rows.
371
+ import { search, getRow } from "@fangorn-network/westmarch/tools";
53
372
 
54
373
  const ok = (o) => ({ content: [{ type: "text", text: JSON.stringify(o) }] });
55
374
 
56
375
  export function registerAgent(ctx) {
57
376
  const mc = document.modelContext;
58
- if (!mc?.registerTool) return 0;
377
+ if (!mc?.registerTool) return; // most browsers have no WebMCP yet
378
+ mc.registerTool({
379
+ name: "search-trees",
380
+ description: "Search tree species by meaning: a question in plain words. Call this first.",
381
+ inputSchema: { type: "object", properties: { query: { type: "string" }, limit: { type: "number" } }, required: ["query"] },
382
+ execute: async ({ query, limit = 10 }) =>
383
+ ok(search(ctx.rows, query, ctx.roles, { qv: await ctx.queryVector(query), limit })),
384
+ });
59
385
  mc.registerTool({
60
- name: "search-rows",
61
- description: "Search the corpus by meaning. Call this first.",
62
- inputSchema: { type: "object", properties: { query: { type: "string" } } },
63
- execute: async ({ query }) => ok(search(ctx.rows(), query, ctx.roles(), { qv: await ctx.queryVector(query) })),
386
+ name: "get-tree",
387
+ description: "One tree species in full, by the id search returns.",
388
+ inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
389
+ execute: async ({ id }) => ok(getRow(ctx.rows, id, ctx.roles) ?? { error: `no tree ${id}` }),
64
390
  });
65
- // …more tools
66
391
  }
67
392
  ```
68
393
 
69
- Call it only once the data the tools read has loaded, not at module top level. A human
70
- clicks after the page has settled; an agent calls the moment a tool appears, and a tool
71
- that answers before its data exists answers wrong without erroring. (Kingsfoil's first
72
- search routed to zero areas until its tools waited for `routes.json`.)
394
+ ```js
395
+ // app/main.js
396
+ import { configure, loadShard } from "@fangorn-network/westmarch/shard";
397
+ import { rolesFrom, textOf } from "@fangorn-network/westmarch/roles";
398
+ import { search } from "@fangorn-network/westmarch/tools";
399
+ import { embedQuery } from "@fangorn-network/westmarch/embed";
400
+ import { registerAgent } from "./agent.js";
401
+
402
+ export const VIEW = new URL("view", location.href).href; // step 7
403
+
404
+ const ctx = { rows: [], roles: rolesFrom([]), queryVector: (q) => embedQuery(q).catch(() => null) };
405
+ configure({ onManifests: (ms) => { ctx.roles = rolesFrom(ms); }, rowText: (f) => textOf(f, ctx.roles) });
406
+ ctx.rows = await loadShard(VIEW); // checked against the manifests' sha256
407
+ registerAgent(ctx); // only now: a tool called before its data loads answers wrong, silently
408
+
409
+ // The human side: the same search, as a list.
410
+ const input = document.querySelector("input"), list = document.querySelector("ul");
411
+ input.placeholder = `Search ${ctx.rows.length} trees…`;
412
+ input.oninput = async () => {
413
+ const q = input.value;
414
+ const hits = search(ctx.rows, q, ctx.roles, { qv: await ctx.queryVector(q), limit: 10 });
415
+ if (q !== input.value) return; // a newer keystroke won
416
+ list.replaceChildren(...hits.map((h) => {
417
+ const li = document.createElement("li"), a = document.createElement("a");
418
+ a.textContent = h.title; if (h.url) a.href = h.url;
419
+ li.append(a, ` — ${h.subtitle ?? ""}`);
420
+ return li;
421
+ }));
422
+ };
423
+ ```
424
+
425
+ ```html
426
+ <!-- app/index.html -->
427
+ <!doctype html>
428
+ <meta charset="utf-8">
429
+ <meta name="viewport" content="width=device-width, initial-scale=1">
430
+ <title>My App</title>
431
+ <h1>My App</h1>
432
+ <input type="search" autofocus placeholder="Loading…">
433
+ <ul></ul>
434
+ <script type="module" src="./main.js"></script>
435
+ ```
73
436
 
74
437
  ```js
75
- const data = await loadIndex(); // whatever the tools read
76
- registerAgent(ctxOver(data));
438
+ // app/vite.config.js
439
+ export default {
440
+ base: "./",
441
+ // westmarch's embedder finds its worker by `new URL(…, import.meta.url)`;
442
+ // pre-bundling loses that and search silently drops to word matching.
443
+ optimizeDeps: { exclude: ["@fangorn-network/westmarch"] },
444
+ build: { outDir: "../site", emptyOutDir: false }, // keep site/view
445
+ };
77
446
  ```
78
447
 
79
- `@fangorn/westmarch/tools` holds verbs that work on any corpus (describe, search, browse,
80
- facet, get, neighbours). Add a domain verb only when the generic ones can't express it.
448
+ ```sh
449
+ (cd app && npx vite build)
450
+ ```
451
+
452
+ `@fangorn-network/westmarch/tools` also has `describe`, `browse`, `facet` and `neighbors`,
453
+ if the page needs more than search. `watchShard(fn)` tells the page when the view gains
454
+ shards, if it should update without a reload.
81
455
 
82
- ## 2. Generate the agent card
456
+ ## 9. The agent card
83
457
 
84
458
  A WebMCP tool only exists inside a loaded tab, so the card is the only machine-readable
85
- record that the tools exist. Don't write the tool list by hand. `captureTools` runs your
86
- register function against a recording stub, so the card lists exactly what the browser
87
- registers. No `execute` runs during capture.
459
+ record that the app, its tools and its view exist. The tool list isn't written by hand:
460
+ `captureTools` runs your register function against a recording stub, so the card lists
461
+ exactly what the page registers. No `execute` runs.
88
462
 
89
463
  ```js
90
- // pipeline/agent-card.mjs — run at build time, after the site is built
91
- import { writeFileSync, mkdirSync } from "node:fs";
92
- import { agentCard, captureTools } from "@fangorn/westmarch/agent-card";
464
+ // pipeline/agent-card.mjs — site/ → site/.well-known/agent-card.json + site/_headers
465
+ import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
466
+ import { agentCard, captureTools } from "@fangorn-network/westmarch/agent-card";
93
467
  import { registerAgent } from "../app/agent.js";
94
468
 
95
- const tools = await captureTools(() => registerAgent({}));
469
+ const SITE = "https://my-app.pages.dev";
470
+ const VIEW = `${SITE}/view`; // step 7; a hosted quickbeam view's URL goes here instead
471
+
96
472
  const card = agentCard({
97
473
  name: "My App",
98
- description: "What a stranger gets from this, in two sentences.",
99
- url: "https://my-app.pages.dev/tools.html", // the page that REGISTERS the tools, not just the site
100
- version: "2026-09-22",
101
- tools,
102
- tags: ["my-app"],
474
+ description: "Tree species: what they are, where they grow, and what they are good for. Search by meaning.",
475
+ url: `${SITE}/`, // the page that registers the tools
476
+ version: "1",
477
+ tools: await captureTools(() => registerAgent({})),
478
+ tags: ["trees"],
103
479
  fangorn: {
104
- app: "my-app", // the name you will claim in step 4
105
- fromBlock: 311671082, // see below
106
- namespaces: ["my-app"], // where your data is published
107
- views: ["https://my-app.pages.dev/v1"], // baked quickbeam views, if any
480
+ app: "my-app", // the name you claimed
481
+ fromBlock: 311700000, // the block you noted before claiming
482
+ namespaces: ["my-app"], // your data namespace(s)
483
+ views: [VIEW],
108
484
  },
109
485
  });
110
-
111
486
  mkdirSync("site/.well-known", { recursive: true });
112
487
  writeFileSync("site/.well-known/agent-card.json", JSON.stringify(card, null, 1));
113
- // Discovery runs in other sites' tabs: without CORS the browser won't hand over the card.
114
- writeFileSync("site/_headers", "/.well-known/agent-card.json\n Access-Control-Allow-Origin: *\n");
115
- ```
116
-
117
- The `fangorn` block becomes an A2A extension (`https://fangorn.network/a2a/app/v1`) holding
118
- the chain, the registry addresses (taken from the SDK, so they can't drift), your appId,
119
- `fromBlock`, `namespaces` and `views`. `agentCard` validates these fields the same way
120
- readers will, so a bad card fails your build, not someone else's discovery.
121
488
 
122
- **`fromBlock`** is where readers start scanning for your data. Set it to the current block
123
- **before** you claim the app:
124
-
125
- ```sh
126
- node -e 'const {createPublicClient,http}=require("viem");const {arbitrumSepolia}=require("viem/chains");
127
- createPublicClient({chain:arbitrumSepolia,transport:http()}).getBlockNumber().then(String).then(console.log)'
489
+ // The card is read from other origins, so it needs CORS, and so does the view, for
490
+ // agents and pages elsewhere. Bundles and shards are named by their digest, so they
491
+ // never change: tell caches so. (Pages allows one * per rule, hence a rule per domain.)
492
+ const immutable = " Cache-Control: public, max-age=31536000, immutable\n";
493
+ writeFileSync("site/_headers",
494
+ "/.well-known/agent-card.json\n Access-Control-Allow-Origin: *\n"
495
+ + "/view/*\n Access-Control-Allow-Origin: *\n"
496
+ + `/assets/*\n${immutable}`
497
+ + readdirSync("site/view/cdn/domains").map((d) => `/view/cdn/domains/${d}/shards/*\n${immutable}`).join(""));
128
498
  ```
129
499
 
130
- Too early is only slow: readers query logs 1000 blocks per call. Leave it at 0 and a reader
131
- makes hundreds of thousands of calls.
132
-
133
- **Hard-code these values once the app is claimed.** A rebuild that drops the extension
134
- publishes a card that every reader rejects.
135
-
136
- ## 3. Deploy and check the card
137
-
138
500
  ```sh
139
- npx wrangler pages deploy site --project-name my-app
140
- curl -sD - https://my-app.pages.dev/.well-known/agent-card.json -o /dev/null | grep -iE "^HTTP|content-type|access-control"
141
- # HTTP/2 200 · content-type: application/json · access-control-allow-origin: *
501
+ node pipeline/agent-card.mjs
142
502
  ```
143
503
 
144
- The card has to be live before step 5, because the CLI fetches it.
504
+ - **The `fangorn` block** becomes an A2A extension (`https://fangorn.network/a2a/app/v1`).
505
+ It holds the chain, the registry addresses (taken from the SDK, so they can't drift), your
506
+ appId, `fromBlock`, `namespaces` and `views`. `agentCard` validates them the same way
507
+ readers will, so a bad card fails your build, not someone else's discovery.
508
+ - **`views` is what agents search:** `fangorn-mcp` reads it directly, with no browser.
509
+ - **`url` is the page that registers the tools,** not just any page on the site.
510
+ - **`fromBlock`** only affects speed: readers query logs 1000 blocks per call, so an early
511
+ block is slow, and 0 means hundreds of thousands of calls.
512
+ - **Keep these values fixed.** A rebuild that drops the extension or changes `url`
513
+ publishes a card every reader rejects.
145
514
 
146
- ## 4. Claim the app
515
+ ## 10. Deploy and check
147
516
 
148
517
  ```sh
149
- fangorn --app my-app app info # "unclaimed", and the wallet you expect
150
- fangorn --app my-app app claim # first come, first served; permanent
518
+ npx wrangler pages deploy site --project-name my-app
519
+ curl -sD - https://my-app.pages.dev/.well-known/agent-card.json -o /dev/null | grep -iE "^HTTP|access-control"
520
+ # HTTP/2 200 · access-control-allow-origin: *
151
521
  ```
152
522
 
153
- With no flags, `claim` uses placeholder terms. Publish real ones later with
154
- `fangorn app terms <hash> <uri>`; anyone who joined under the old terms has to accept again.
155
- `--fee <wei>` sets a join fee.
523
+ Open `https://my-app.pages.dev/` and search: that's the app for people. The card must be
524
+ live before step 11, because the CLI fetches it.
156
525
 
157
- ## 5. Register the agent and bind the card
526
+ ## 11. Register the agent and bind the card
158
527
 
159
528
  ```sh
160
- fangorn --app my-app app agent https://my-app.pages.dev/.well-known/agent-card.json
161
- # Agent ID: 421614:226
529
+ fangorn app agent https://my-app.pages.dev/.well-known/agent-card.json
530
+ # Agent ID: 421614:…
162
531
  # Card: https://my-app.pages.dev/.well-known/agent-card.json
163
532
  # Tx: 0x…
164
533
  ```
165
534
 
166
- This command does two things:
535
+ This does two things:
167
536
 
168
537
  1. **ERC-8004.** It builds a registration file from the card (name, description, A2A
169
538
  endpoint = the card URL, trust = reputation, x402 only if a skill is tagged `x402`),
@@ -171,34 +540,122 @@ This command does two things:
171
540
  `0x8004A818BFB912233c491871b3d84c89A494BD9e`. That's the same address as on Ethereum
172
541
  and Base Sepolia. [8004scan](https://8004scan.io) indexes it within minutes.
173
542
  2. **Fangorn.** It calls `setAppAgentUri(cardUrl)`, which emits `AppAgentChanged`. That log
174
- is the app directory: westmarch's `listApps` reads it.
543
+ is the app directory: `list-apps` and `listApps` read it.
544
+
545
+ `--skip-register` does only the second step, e.g. to move an already-registered card to a
546
+ new URL.
547
+
548
+ ## 12. Use it as an agent would
549
+
550
+ Register `fangorn-mcp` once (see [Using Fangorn apps from an agent](#using-fangorn-apps-from-an-agent)),
551
+ then in any agent:
552
+
553
+ ```
554
+ open-app my-app → verified: true, data_tools: true
555
+ my-app__describe → the view's domains, with the schema's description
556
+ my-app__search "trees that survive fire" → cork oak, oak, redwood (ranked by meaning)
557
+ my-app__get "baobab" → the whole record
558
+ ```
559
+
560
+ `open-app` reads your app's binding from the registry, fetches the card, and checks that
561
+ the binding points back at exactly that URL. Only then does it add the tools.
562
+
563
+ ## 13. Update it
564
+
565
+ New or changed data is a commit, then the view, then a deploy:
175
566
 
176
- `--skip-register` does only the second step, e.g. to move a card that's already registered
177
- to a new URL.
567
+ ```sh
568
+ node pipeline/graph.mjs && fangorn commit data/graph.json -m "…" --replace && fangorn push
569
+ npx westmarch-view --app my-app --namespace my-app --out site/view --from-block 311700000
570
+ node pipeline/agent-card.mjs && npx wrangler pages deploy site --project-name my-app
571
+ ```
572
+
573
+ Only what changed is embedded and uploaded. The page and agents see it on their next
574
+ load, since the view URL, the card and the on-chain binding all stay the same. Run
575
+ `fangorn app agent` again only if the card moves. Anything that can run those three lines
576
+ can keep an app current: a cron job, a CI workflow, or an agent that just published.
577
+
578
+ ---
579
+
580
+ ## Using Fangorn apps from an agent
178
581
 
179
- ## 6. Publish data into the app
582
+ `fangorn-mcp` is one MCP server for every Fangorn app: a new app needs no new
583
+ registration. You can install it as a single file (Linux x64/arm64): no node, no install,
584
+ and search by meaning included.
180
585
 
181
- The owner is the app's first publisher, but still needs global standing:
586
+ ```sh
587
+ curl -fLo fangorn-mcp https://github.com/fangorn-network/westmarch/releases/latest/download/fangorn-mcp-linux-x64
588
+ chmod +x fangorn-mcp
589
+ claude mcp add fangorn -- "$PWD/fangorn-mcp"
590
+ ```
591
+
592
+ Or through npm, wherever node is:
182
593
 
183
594
  ```sh
184
- fangorn --app my-app register
185
- fangorn --app my-app repo init my-app
186
- fangorn --app my-app commit graph.json -m "first bake" --replace
187
- fangorn --app my-app push
595
+ claude mcp add fangorn -- npx -y -p @fangorn-network/westmarch -p @huggingface/transformers fangorn-mcp
188
596
  ```
189
597
 
190
- `graph.json` is `{ vertices: [{id, tag, payload}], edges?: [{rel, from, to}] }`.
191
- `westmarch/publish/graph.js` builds one from a file tree.
598
+ Without `-p @huggingface/transformers`, the install is ~500 MB lighter and `search` ranks by
599
+ words, not meaning. For any MCP client:
600
+
601
+ ```json
602
+ { "mcpServers": { "fangorn": { "command": "/path/to/fangorn-mcp" } } }
603
+ ```
192
604
 
193
- ## 7. Discover it
605
+ `bun build-bin.js` in westmarch builds the file (bun ≥ 1.2).
194
606
 
195
- Every route below ends in `discoverApp(cardUrl)`, which accepts a card only if:
607
+ It starts with three tools:
608
+ - **`list-apps`** reads the apps off the chain and keeps the ones whose cards verify.
609
+ - **`open-app <name>`** reads that app's binding from the registry (one call), verifies its
610
+ card, and adds the app's tools as `<name>__<tool>`.
611
+ - **`call-app-tool`** reaches the same tools, for clients that don't refresh their tool list.
612
+
613
+ **Two kinds of tools.**
614
+ - **Data tools:** an app whose card names `views` gets these at once, with no browser:
615
+ `describe`, `search`, `get`, `similar`, `count`, `browse`. westmarch runs them over the
616
+ app's shards, checked against their manifests' sha256. `search` reads the 3 views whose
617
+ coverage sketch best matches the query and names the next candidates.
618
+ - **Page tools:** `open-app` with `page: true` also opens `card.url` in a browser for the
619
+ page's own WebMCP tools. An app with no views gets only these.
620
+
621
+ **How fast** (Kingsfoil: 23 views, 60,760 trials):
622
+
623
+ | | time |
624
+ |---|---|
625
+ | `open-app` | 0.4–2 s |
626
+ | `describe` | under 5 ms |
627
+ | first `search`, caches warm | 0.4–1.8 s |
628
+ | later searches | ~0.2–0.5 s |
629
+ | first `search` from an empty machine (single file) | ~3.5 s, by words while the model downloads |
630
+ | page tools (browser) | 5–15 s to open, ~2 GB of memory |
631
+
632
+ On a new machine, the embedding model (131 MB, kept in `~/.cache/fangorn-mcp/models`)
633
+ downloads in the background. Until it lands, `search` ranks by words and says so in
634
+ `ranked_by`; then it switches to meaning. Shards are downloaded once and kept by content
635
+ hash.
636
+
637
+ **Where the browser comes from (page tools only).** Anything that speaks the Chrome DevTools
638
+ Protocol and has WebMCP will do: a headless Chromium service, a hosted browser, or the local
639
+ Chrome.
640
+
641
+ | setting | browser |
642
+ |---|---|
643
+ | `FANGORN_MCP_CDP=http://host:9222` (or `--cdp`) | a running browser's DevTools address |
644
+ | `FANGORN_MCP_CDP=ws://host:9222/devtools/browser/…` | a browser WebSocket endpoint, which is what hosted services hand out |
645
+ | neither | the local Chrome 150+ (`CHROME=/path` if it isn't `google-chrome`), headless (`--headed` to watch) |
646
+
647
+ A remote browser is never closed; only the tabs the server opened are. A local Chrome is
648
+ shared by every session on the machine and closes when the last one exits. Its profile is
649
+ `~/.cache/westmarch-mcp/`. `--from-block <n>` points the server at another deployment.
650
+
651
+ ## Discovering apps in code
652
+
653
+ Every route ends in `discoverApp(cardUrl)`, which accepts a card only if:
196
654
 
197
655
  - the card carries the Fangorn extension for **this** chain and **this** AppRegistry, and
198
656
  - `appAgentUri(card.appId)` on chain is **exactly** the card URL.
199
657
 
200
658
  The registry and the index are where you look. The on-chain binding is what you trust.
201
-
202
659
  `example/discover.mjs` runs all three routes:
203
660
 
204
661
  ```sh
@@ -207,13 +664,7 @@ node example/discover.mjs chain 311637349 # every app bound since this
207
664
  node example/discover.mjs search Kingsfoil # the public ERC-8004 index
208
665
  ```
209
666
 
210
- ```
211
- Kingsfoil https://kingsfoil.pages.dev/.well-known/agent-card.json
212
- app 0x3069…c8ca data from block 311671082 namespaces kingsfoil
213
- open https://kingsfoil.pages.dev — WebMCP tools: list-areas, search-trials, get-trial, …
214
- ```
215
-
216
- **From a card URL** (someone handed it to you):
667
+ **From a card URL:**
217
668
 
218
669
  ```js
219
670
  import { Fangorn, FangornConfig } from "@fangorn-network/sdk";
@@ -226,17 +677,13 @@ const { card, appId, fromBlock, namespaces } = await fangorn.discoverApp(cardUrl
226
677
  **From the chain** (every Fangorn app; no index, no server):
227
678
 
228
679
  ```js
229
- import { listApps } from "@fangorn/westmarch/apps";
680
+ import { listApps } from "@fangorn-network/westmarch/apps";
230
681
  const { apps, rejected } = await listApps(fangorn, { fromBlock: 311637349n });
231
682
  // apps: [{ name, appId, card, url, views, namespaces, fromBlock }]
232
683
  // rejected: [{ card, why }]: unreachable or not bound, reported rather than dropped
233
684
  ```
234
685
 
235
- `sourcesFromChain` in `@fangorn/westmarch/directory` turns that into `findCorpora` sources,
236
- so an agent can rank every app's corpora against a question without downloading a shard.
237
- In the example page, `?fromBlock=311637349` does the same thing in a browser.
238
-
239
- **From ERC-8004** (general-purpose agents that know nothing about Fangorn):
686
+ **From ERC-8004** (for general-purpose agents that know nothing about Fangorn):
240
687
 
241
688
  ```js
242
689
  const api = "https://8004scan.io/api/v1/agents";
@@ -245,57 +692,7 @@ const detail = await (await fetch(`${api}/421614/${items[0].token_id}`)).json();
245
692
  const found = await fangorn.discoverApp(detail.services.a2a.endpoint);
246
693
  ```
247
694
 
248
- Without the index, read it straight from the registry: `tokenURI(agentId)` returns the
249
- registration file's `ipfs://` URI, and its `A2A` endpoint is the card.
250
-
251
- ### Use it
252
-
253
- - **An agent:** open `card.url` in Chrome 150+ with `--enable-features=WebMCP` (headless
254
- works) and call the tools the card lists as `webmcp` skills. Queries run in that tab, and
255
- nothing reaches a server. WebMCP is not a network MCP server: the tools exist only inside
256
- a loaded tab, so an agent outside the browser (Claude Code, a script) needs something
257
- that drives the tab for it. `card.url` must be the page that registers the tools, not
258
- just the site's root. `fangorn-mcp` is that driver, for every app at once. Register it
259
- once:
260
-
261
- ```sh
262
- claude mcp add fangorn -- npx -y -p @fangorn/westmarch fangorn-mcp
263
- ```
264
-
265
- or, for any MCP client:
266
-
267
- ```json
268
- { "mcpServers": { "fangorn": {
269
- "command": "npx", "args": ["-y", "-p", "@fangorn/westmarch", "fangorn-mcp"],
270
- "env": { "FANGORN_MCP_CDP": "ws://your-browser:9222" } } } }
271
- ```
272
-
273
- Drop `env` to use the local Chrome.
274
-
275
- It starts with three tools. `list-apps` reads the apps off the chain and keeps the ones
276
- whose cards verify. `open-app kingsfoil` verifies that app's card, opens `card.url` in a
277
- tab of one shared Chrome with WebMCP on, and adds the page's tools as
278
- `kingsfoil__search-trials` and so on. `call-app-tool` reaches the same tools for clients
279
- that don't refresh their tool list. A new app needs no new registration: it shows up in
280
- `list-apps` once its card is bound on chain.
281
-
282
- **Where the browser comes from.** Anything that speaks the Chrome DevTools Protocol and
283
- has WebMCP will do: a headless Chromium service, a hosted browser, or the local Chrome.
284
-
285
- | setting | browser |
286
- |---|---|
287
- | `FANGORN_MCP_CDP=http://host:9222` (or `--cdp`) | a running browser's DevTools address |
288
- | `FANGORN_MCP_CDP=ws://host:9222/devtools/browser/…` | a browser WebSocket endpoint, which is what hosted services hand out |
289
- | neither | the local Chrome 150+ (`CHROME=/path` if it isn't `google-chrome`), headless (`--headed` to watch) |
290
-
291
- A remote browser is never closed. Only the tabs the server opened are. A local Chrome is
292
- shared by every session on the machine and closes when the last one exits. Its profile
293
- (`~/.cache/westmarch-mcp/`) keeps page downloads such as an embedding model.
294
- `--from-block <n>` points the server at another deployment.
295
-
296
- - **Without a browser:** the views are plain HTTP (`<view>/cdn/catalog`, then the shards it
297
- names). Point `westmarch/consume/shard.js` at `views[i]`.
298
- - **The committed data itself:**
695
+ **The committed data itself:**
299
696
 
300
697
  ```js
301
698
  fangorn.setAppId(appId);
@@ -312,27 +709,40 @@ const { contents } = await fangorn.readNamespace(timelines[0].owner, namespaces[
312
709
  wasn't changed after binding. The card is live HTTP, not content-addressed, and the
313
710
  binding is to the URL. That's deliberate: the card is mostly endpoints, which change, and
314
711
  pinning each version would cost a transaction for nothing. The data's integrity comes
315
- from the commits, not from the card.
712
+ from the commits and the shard digests, not from the card.
316
713
 
317
714
  ## Gotchas
318
715
 
319
716
  | symptom | cause |
320
717
  |---|---|
718
+ | push reverts `NotRegisteredForApp` | registered globally but not joined; `fangorn register` does both |
321
719
  | `not bound to app … on-chain agent_uri is …` | URLs must match exactly. A trailing slash, `http` vs `https`, or a different path counts as a different card. |
322
- | card rejected after a rebuild | the build dropped the `fangorn` block or changed `fromBlock`/`url`; hard-code them |
720
+ | card rejected after a rebuild | the build dropped the `fangorn` block or changed `fromBlock`/`url`; keep them fixed |
323
721
  | discovery works in Node, fails in a browser | no `Access-Control-Allow-Origin` on the card |
324
722
  | `listApps` takes forever | `fromBlock` is too early; the scan is 1000 blocks per RPC call |
723
+ | `app agent`: "ERC-8004 registration pins the registration file to IPFS" | no Pinata JWT in `fangorn init` |
724
+ | `app agent` signs as the wrong wallet | `~/.fangorn/config.json` beats `ETH_PRIVATE_KEY` |
725
+ | search on the page matches words, never meaning | the bundler pre-bundled westmarch; `optimizeDeps.exclude` it |
726
+ | an agent's first call returns empty, later calls work | the tools registered before their data loaded (step 8) |
727
+ | an agent opens the page and finds no tools | `card.url` is not the page that registers them |
728
+ | `search` ignores what records say, or titles are ids | the type isn't declared in the schema, so its roles were guessed; declare it (step 5) |
729
+ | a schema change didn't change search results | records keep the text they were embedded with; only newly embedded records use the new `text` role |
730
+ | the view is empty after a push | `westmarch-view` wasn't rerun, `--from-block` is after your commits, or the push hasn't landed (`fangorn status`) |
731
+ | `westmarch-view`: "vectors from two encoders are not comparable" | the view was built by another embedder (e.g. quickbeam); `--rebake`, or keep publishing it with that tool |
732
+ | `westmarch-view` runs for minutes before doing anything | `--from-block` is far too early; the log scan is 1000 blocks per call |
733
+ | lint: "nowhere to go" | no `presentation.externalUrl` in the schema, so a hit links nowhere |
734
+ | `fangorn-mcp`: "this browser has no WebMCP" | page tools only: the browser lacks `document.modelContext`; it needs Chrome 150+ with `--enable-features=WebMCP` |
735
+ | `fangorn-mcp`: "no browser: cannot launch google-chrome" | page tools only: no local Chrome; set `FANGORN_MCP_CDP` |
325
736
  | a log scan misses a transaction you just sent | fixed in the SDK (uncached head block); upgrade to ≥ 2026.9.22-dev |
326
737
  | IPFS reads return "switching to a service worker gateway" | ipfs.io stopped serving raw content; use a Pinata gateway |
327
- | `app agent` signs as the wrong wallet | `~/.fangorn/config.json` beats `ETH_PRIVATE_KEY` |
328
- | an agent opens the page and finds no tools | `card.url` is the site root, but the tools register on another page |
329
- | an agent's first call returns empty, later calls work | the tools registered before their data loaded (step 1) |
330
- | `fangorn-mcp`: "this browser has no WebMCP" | the browser at `FANGORN_MCP_CDP` (or the local Chrome) lacks `document.modelContext`; it needs Chrome 150+ with `--enable-features=WebMCP`, or a build with it on |
331
- | `fangorn-mcp`: "no browser: cannot launch google-chrome" | no local Chrome; set `FANGORN_MCP_CDP` |
332
738
  | the page isn't listed as a "Web" service on 8004scan | agent0-sdk has no web endpoint type; agents find the page through the card's `url` |
333
739
 
334
740
  ## Reference: Kingsfoil
335
741
 
742
+ Kingsfoil has 60,760 clinical trials in 23 views. It predates the schema and quickbeam
743
+ path: it embeds and bakes its own views (`kingsfoil/pipeline/bake.mjs`) and serves them
744
+ from its own site. That works, but every update is a manual rebake and redeploy.
745
+
336
746
  | | |
337
747
  |---|---|
338
748
  | site | https://kingsfoil.pages.dev |