402-mcp 4.2.2 → 4.3.1

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 (76) hide show
  1. package/README.md +45 -16
  2. package/build/config.d.ts +6 -0
  3. package/build/config.js +41 -10
  4. package/build/config.js.map +1 -1
  5. package/build/fetch/resilient-fetch.js +15 -4
  6. package/build/fetch/resilient-fetch.js.map +1 -1
  7. package/build/fetch/transport.d.ts +1 -0
  8. package/build/fetch/transport.js +37 -14
  9. package/build/fetch/transport.js.map +1 -1
  10. package/build/http-security.d.ts +13 -0
  11. package/build/http-security.js +38 -0
  12. package/build/http-security.js.map +1 -0
  13. package/build/index.js +184 -119
  14. package/build/index.js.map +1 -1
  15. package/build/spend-tracker.d.ts +31 -4
  16. package/build/spend-tracker.js +102 -17
  17. package/build/spend-tracker.js.map +1 -1
  18. package/build/store/cashu-tokens.d.ts +19 -0
  19. package/build/store/cashu-tokens.js +32 -0
  20. package/build/store/cashu-tokens.js.map +1 -1
  21. package/build/store/credentials.d.ts +5 -0
  22. package/build/store/credentials.js +1 -0
  23. package/build/store/credentials.js.map +1 -1
  24. package/build/store/pending-payments.d.ts +36 -0
  25. package/build/store/pending-payments.js +88 -0
  26. package/build/store/pending-payments.js.map +1 -0
  27. package/build/tools/buy-credits.d.ts +9 -0
  28. package/build/tools/buy-credits.js +40 -3
  29. package/build/tools/buy-credits.js.map +1 -1
  30. package/build/tools/config.d.ts +5 -0
  31. package/build/tools/config.js.map +1 -1
  32. package/build/tools/confirm.d.ts +12 -0
  33. package/build/tools/confirm.js +27 -0
  34. package/build/tools/confirm.js.map +1 -0
  35. package/build/tools/fetch.d.ts +7 -1
  36. package/build/tools/fetch.js +255 -140
  37. package/build/tools/fetch.js.map +1 -1
  38. package/build/tools/nostr-subscribe.d.ts +5 -2
  39. package/build/tools/nostr-subscribe.js +0 -2
  40. package/build/tools/nostr-subscribe.js.map +1 -1
  41. package/build/tools/pay.d.ts +9 -0
  42. package/build/tools/pay.js +65 -8
  43. package/build/tools/pay.js.map +1 -1
  44. package/build/tools/reconcile.d.ts +33 -0
  45. package/build/tools/reconcile.js +111 -0
  46. package/build/tools/reconcile.js.map +1 -0
  47. package/build/tools/redeem-cashu.js +1 -1
  48. package/build/tools/redeem-cashu.js.map +1 -1
  49. package/build/tools/register-widgets.js +5 -5
  50. package/build/tools/register-widgets.js.map +1 -1
  51. package/build/tools/search.js +25 -7
  52. package/build/tools/search.js.map +1 -1
  53. package/build/tools/untrusted.d.ts +3 -0
  54. package/build/tools/untrusted.js +18 -0
  55. package/build/tools/untrusted.js.map +1 -0
  56. package/build/wallet/cashu.js +101 -21
  57. package/build/wallet/cashu.js.map +1 -1
  58. package/build/wallet/lnurlcash.js +25 -7
  59. package/build/wallet/lnurlcash.js.map +1 -1
  60. package/build/wallet/nwc.js +51 -3
  61. package/build/wallet/nwc.js.map +1 -1
  62. package/build/wallet/types.d.ts +17 -0
  63. package/build/widgets/index.js +2 -1
  64. package/build/widgets/index.js.map +1 -1
  65. package/build/widgets/paths.d.ts +7 -0
  66. package/build/widgets/paths.js +11 -0
  67. package/build/widgets/paths.js.map +1 -0
  68. package/build/x402/parse.d.ts +15 -3
  69. package/build/x402/parse.js +23 -9
  70. package/build/x402/parse.js.map +1 -1
  71. package/build/x402/payment.js +11 -6
  72. package/build/x402/payment.js.map +1 -1
  73. package/llms-full.txt +72 -34
  74. package/llms.txt +49 -22
  75. package/package.json +3 -3
  76. package/src/widgets/dist/service-directory.html +11 -2
@@ -1 +1 @@
1
- {"version":3,"file":"payment.js","sourceRoot":"","sources":["../../src/x402/payment.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEjD,8FAA8F;AAC9F,MAAM,UAAU,wBAAwB,CAAC,SAAwB;IAM/D,MAAM,QAAQ,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAA;IAEhD,MAAM,IAAI,GAA4B;QACpC,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,MAAM;QAChB,QAAQ,EAAE,SAAS,CAAC,QAAQ;QAC5B,OAAO,EAAE,SAAS,CAAC,OAAO;QAC1B,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE;QACpC,SAAS,EAAE,SAAS,CAAC,SAAS;QAC9B,GAAG,CAAC,SAAS,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,OAAO,EAAE,sBAAsB,SAAS,CAAC,SAAS,IAAI,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,OAAO,SAAS,CAAC,OAAO,IAAI;cAC3G,WAAW,SAAS,CAAC,QAAQ,IAAI;cACjC,mEAAmE;KACxE,CAAA;IAED,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,IAAI,CAAC,OAAiB;KAChC,CAAA;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzC,CAAC"}
1
+ {"version":3,"file":"payment.js","sourceRoot":"","sources":["../../src/x402/payment.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE5C,8FAA8F;AAC9F,MAAM,UAAU,wBAAwB,CAAC,SAAwB;IAM/D,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,CAAA;IAC3C,wEAAwE;IACxE,8DAA8D;IAC9D,MAAM,KAAK,GAAG,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC;QAC5C,CAAC,CAAC,GAAG,SAAS,CAAC,SAAS,IAAI,KAAK,EAAE;QACnC,CAAC,CAAC,IAAI,SAAS,CAAC,SAAS,6BAA6B,KAAK,uBAAuB,CAAA;IAEpF,MAAM,IAAI,GAA4B;QACpC,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,MAAM;QAChB,QAAQ,EAAE,SAAS,CAAC,QAAQ;QAC5B,OAAO,EAAE,SAAS,CAAC,OAAO;QAC1B,KAAK;QACL,SAAS,EAAE,SAAS,CAAC,SAAS;QAC9B,GAAG,CAAC,SAAS,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,YAAY,EAAE,sIAAsI;QACpJ,OAAO,EAAE,qBAAqB,KAAK,OAAO,SAAS,CAAC,OAAO,IAAI;cAC3D,WAAW,SAAS,CAAC,QAAQ,yBAAyB;cACtD,mEAAmE;KACxE,CAAA;IAED,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,IAAI,CAAC,OAAiB;KAChC,CAAA;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzC,CAAC"}
package/llms-full.txt CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What is 402-mcp?
6
6
 
7
- 402-mcp is an MCP (Model Context Protocol) server that gives AI agents economic agency — the ability to discover, pay for, and consume any L402 or x402 payment-gated API without human registration, API keys, or middlemen.
7
+ 402-mcp is an MCP (Model Context Protocol) server that gives AI agents economic agency: the ability to discover, pay for, and consume L402 and other Lightning or ecash payment-gated APIs without human registration, API keys, or middlemen.
8
8
 
9
9
  It implements the client side of the L402 protocol: when an API responds with HTTP 402 (Payment Required), 402-mcp automatically parses the challenge, pays the Lightning invoice, stores the credential, and retries the request — all within a single tool call from the agent's perspective.
10
10
 
@@ -80,7 +80,7 @@ src/
80
80
  bolt11.ts # Decode BOLT11 invoices (amount extraction)
81
81
  challenge-cache.ts # Cache parsed challenges for l402-pay
82
82
  x402/
83
- parse.ts # Detect X-Payment-Required: x402 header; parse EVM challenge
83
+ parse.ts # Experimental custom x402 format: X-Payment-Required: x402 header, EVM challenge body
84
84
  payment.ts # Format payment request with EIP-681 deeplink for human payment
85
85
  xcashu/
86
86
  parse.ts # Detect X-Cashu header; parse NUT-18 payment request
@@ -151,18 +151,23 @@ tests/ # Mirrors src/ structure
151
151
 
152
152
  **Parameters:**
153
153
  - `url` (string, required) — the endpoint to fetch
154
+ - `urls` (string[], optional): all transport URLs from `l402-search`, tried in preference order
155
+ - `pubkey` (string, optional): service pubkey from `l402-search`, used to share a credential across the service's transports
154
156
  - `method` (string, optional) — HTTP method (default: GET)
155
157
  - `headers` (object, optional) — additional headers
156
158
  - `body` (string, optional) — request body
157
- - `paymentMethod` (string, optional) — force a specific payer method
159
+ - `autoPay` (boolean, optional, default false): pay a challenge within the limits
160
+ - `maxCostSats` (number, optional): most this call may pay; lowers `MAX_AUTO_PAY_SATS`, never raises it
161
+ - `txHash` (string, optional): transaction hash for the experimental x402 format
158
162
 
159
163
  **Behaviour:**
160
- 1. Checks for existing credential for the origin
164
+ 1. Uses a stored credential only if it is bound to every origin the request may reach
161
165
  2. Makes the HTTP request (with credential if available)
162
- 3. If 402 received: parses challenge, checks amount against `MAX_AUTO_PAY_SATS` and rolling spend cap
163
- 4. If within budget: pays invoice, stores credential, retries request
164
- 5. If over budget: returns the price and asks for human approval
165
- 6. Returns response body, status, headers, and remaining credit balance
166
+ 3. If 402 received and `autoPay` is set: refuses if an earlier payment to the service is unresolved, then checks the price against `MAX_AUTO_PAY_SATS`, `maxCostSats` and the rolling spend caps
167
+ 4. If within the limits: pays, stores the credential bound to the request's origins, retries
168
+ 5. Otherwise: returns the price and `paymentHash` unpaid, and caches the challenge for `l402-pay`
169
+ 6. A payment whose outcome is unknown is reported as an error and recorded for `l402-reconcile`; it never falls through to another rail
170
+ 7. Returns response body (in UNTRUSTED CONTENT markers), status, headers, and remaining credit balance
166
171
 
167
172
  **When to use:** Primary tool for consuming L402 APIs. Handles the entire flow.
168
173
 
@@ -170,19 +175,44 @@ tests/ # Mirrors src/ structure
170
175
 
171
176
  ### l402-pay
172
177
 
173
- **Purpose:** Pay a specific Lightning invoice.
178
+ **Purpose:** Pay a challenge that `l402-fetch` or `l402-discover` returned.
174
179
 
175
180
  **Parameters:**
176
- - `invoice` (string, required) — BOLT11 invoice to pay
177
- - `paymentMethod` (string, optional) — force a specific payer method
178
- - `challengeId` (string, optional) — ID from a previous `l402-discover` to associate the payment
181
+ - `paymentHash` (string): payment hash of the cached challenge
182
+ - `invoice`, `macaroon` (string, optional): only for an invoice that did not come from a challenge; the human must approve it through MCP elicitation, or it is refused
183
+ - `method` (string, optional): `nwc`, `cashu`, `lnurlcash` or `human`
179
184
 
180
185
  **Returns:**
181
- - `paid` (boolean)
182
- - `preimage` (string) — payment proof
186
+ - `paid` (boolean) and `paymentState` (`paid`, `failed`, `unknown` or `blocked`)
187
+ - `credentialsStored` (boolean)
183
188
  - `method` (string) — which wallet paid
184
189
 
185
- **When to use:** When you have an invoice from a non-L402 source, or want explicit control.
190
+ For human wallets with a toll-booth payment page, polls for settlement for up to 120 seconds.
191
+
192
+ ---
193
+
194
+ ### l402-reconcile
195
+
196
+ **Purpose:** Resolve payments whose outcome is unknown.
197
+
198
+ **Parameters:**
199
+ - `paymentHash` (string, optional): omit to list unresolved payments
200
+ - `preimage` (string, optional): settles the payment if it hashes to `paymentHash`
201
+ - `abandon` (boolean, optional): stop tracking it; requires the human's approval through MCP elicitation
202
+
203
+ **Behaviour:** Asks the wallet that made the payment (NWC `lookup_invoice`, the Cashu mint for a reserved melt, or the LNURLcash verify URL). Settled with a matching preimage clears it and stores the L402 credential; definitely failed clears it and releases reserved Cashu proofs. While any is unresolved for a service, auto-pay to it is paused.
204
+
205
+ ---
206
+
207
+ ### l402-fetch-preview
208
+
209
+ **Purpose:** Show what an endpoint costs without paying. Its dependencies include no wallet or spend tracker, so it cannot spend. Widget hosts render the result as a payment confirmation dialog.
210
+
211
+ ---
212
+
213
+ ### Widget tools
214
+
215
+ `l402-service-directory` (searchable service directory) and `l402-wallet-dashboard` (wallet status, limits, credentials) are MCP Apps widgets for hosts that support them.
186
216
 
187
217
  ---
188
218
 
@@ -235,6 +265,8 @@ tests/ # Mirrors src/ structure
235
265
  - `topics` — topic tags
236
266
  - `capabilities` — array of `{ name, description }`
237
267
 
268
+ Names, descriptions and capability text are wrapped in UNTRUSTED CONTENT markers. Topic filters are sent to the relays; the payment method and query text are matched locally, because relays index single-letter tags only.
269
+
238
270
  **When to use:** When the agent needs a paid API but doesn't know the URL. Searches the decentralised Nostr network for kind 31402 service announcements. Services may advertise multiple transport URLs (clearnet, .onion, Handshake); pass the `urls` array and `pubkey` to `l402-fetch` for automatic transport selection and fallback.
239
271
 
240
272
  ---
@@ -246,7 +278,7 @@ tests/ # Mirrors src/ structure
246
278
  **Parameters:**
247
279
  - `url` (string, required) — toll-booth server URL
248
280
  - `amountSats` (number, optional) — specific tier to purchase
249
- - `paymentMethod` (string, optional) — force a specific payer method
281
+ - `method` (string, optional): `nwc`, `cashu` or `human`
250
282
 
251
283
  **Behaviour:** Fetches available tiers, purchases the specified tier, stores the credential with the credit balance.
252
284
 
@@ -282,7 +314,7 @@ The L402 protocol is an extension of HTTP 402 (Payment Required):
282
314
 
283
315
  ## Supported 402 Protocol Variants
284
316
 
285
- `l402-fetch` detects and handles five HTTP 402 challenge variants. Detection order in source: LNURLcash, x402, IETF Payment, L402, with xCashu tried immediately after LNURLcash.
317
+ `l402-fetch` detects and handles four HTTP 402 challenge variants, plus an experimental x402 format. Detection order in source: LNURLcash, x402, IETF Payment, L402, with xCashu tried immediately after LNURLcash.
286
318
 
287
319
  | Protocol | Detection | Credential sent | Autonomous? |
288
320
  |----------|-----------|----------------|-------------|
@@ -290,7 +322,7 @@ The L402 protocol is an extension of HTTP 402 (Payment Required):
290
322
  | **IETF Payment** (`draft-ryan-httpauth-payment-01`) | `WWW-Authenticate: Payment id=… method=lightning …` | `Authorization: Payment <base64url-json>` | Yes (NWC or Cashu) |
291
323
  | **LNURLcash** (LUD-25) | `X-LNURLcash: lnurlcashreq1<base64url>` | `X-LNURLcash: <bearer note URL>` | Yes (note store only) |
292
324
  | **xCashu** (NUT-18) | `X-Cashu: creqA<base64url>` | `X-Cashu: <cashuB-token>` | Yes (Cashu wallet only) |
293
- | **x402** | `X-Payment-Required: x402` + JSON body | Human presents EIP-681 deeplink | No — on-chain EVM; surfaced to human |
325
+ | **x402** (experimental, custom format) | `X-Payment-Required: x402` + JSON body | `X-Payment: <tx hash>` after the human pays | No: payment details shown to the human. Not the x402 specification, whose servers send a base64 `PAYMENT-REQUIRED` header |
294
326
 
295
327
  LNURLcash is tried before the others because a bearer note is already money in hand: the note URL goes straight into the retry header and the server settles it by rotating the note. A note worth exactly the price is used as it is; otherwise one is split at the mint and the change stays in the store. The chosen note is held pending until the outcome is known, so nothing else can spend it in the meantime. xCashu uses coin selection via `wallet.send(amount, proofs, { includeFees: true })` and restores change proofs to the token store. IETF Payment decodes the base64url charge request to extract the BOLT11 invoice, then pays it through the normal wallet stack and wraps the preimage in a JSON credential.
296
328
 
@@ -305,20 +337,17 @@ Wallets are tried in order: NWC → Cashu → LNURLcash → Human. The first wal
305
337
  | LNURLcash | Fully autonomous | `LNURLCASH_NOTES` file with LUD-25 bearer notes | ~2-6 seconds |
306
338
  | Human | Requires human | None — always available as fallback | Minutes |
307
339
 
308
- The agent can force a specific method per-call using the `paymentMethod` parameter.
340
+ The agent can force a specific method on `l402-pay` and `l402-buy-credits` with the `method` parameter.
309
341
 
310
342
  ## Safety and Spend Limits
311
343
 
312
344
  ### Per-payment cap
313
345
 
314
- `MAX_AUTO_PAY_SATS` (default: 1000) caps any single payment. If an invoice exceeds this:
315
- - The agent is told the price and asked to seek human approval
316
- - The human wallet (QR code) is used for the actual payment
317
- - The credential is stored normally after payment
346
+ `MAX_AUTO_PAY_SATS` (default: 1000) caps any single payment. A dearer challenge is not paid from any configured wallet, and the human wallet does not take over: the price and invoice go back to the agent, and whether it asks the human is up to the agent. `maxCostSats` on `l402-fetch` lowers the cap for one call. `0` disables auto-pay.
318
347
 
319
348
  ### Rolling spend cap
320
349
 
321
- `MAX_SPEND_PER_MINUTE_SATS` (default: 10000) caps total spend in any 60-second window. This prevents:
350
+ `MAX_SPEND_PER_MINUTE_SATS` (default: 10000) caps total spend in any 60-second window, and `MAX_SPEND_PER_DAY_SATS` (default: 5000) in any 24 hours; the daily window is kept in `~/.402-mcp/spend-ledger.json` so a restart does not reset it. `0` in either blocks auto-pay. The budget set on the NWC connection is the real hard limit. These prevent:
322
351
  - Rapid successive small payments exceeding a total budget
323
352
  - Runaway loops where the agent keeps buying credits
324
353
  - Both caps use atomic `tryRecord()` to prevent TOCTOU race conditions
@@ -333,7 +362,7 @@ All outbound HTTP requests pass through an SSRF guard:
333
362
  ### Credential Security
334
363
 
335
364
  - Credentials encrypted with AES-256-GCM
336
- - Encryption key stored in OS keychain (macOS Keychain, Windows Credential Vault, Linux Secret Service) when available
365
+ - Encryption key stored in the OS keychain when available (macOS through the `security` command-line tool; elsewhere through the optional `keytar` module)
337
366
  - Falls back to file-based key at `~/.402-mcp/encryption.key` with a console warning
338
367
  - NWC capability and response events authenticated with NIP-44 v2 and signatures
339
368
  - Returned NWC preimages verified against the challenged BOLT11 payment hash
@@ -352,13 +381,16 @@ npx 402-mcp
352
381
 
353
382
  ### HTTP
354
383
 
355
- StreamableHTTP transport for network access. Useful for remote agents, multi-client setups, or deployment behind a reverse proxy.
384
+ StreamableHTTP transport, stateless. Prefer stdio wherever the client can start a local process: the HTTP transport can spend from the wallet.
356
385
 
357
386
  ```bash
358
- TRANSPORT=http PORT=3402 npx 402-mcp
387
+ openssl rand -hex 32 > ~/.402-mcp/http.token && chmod 600 ~/.402-mcp/http.token
388
+ TRANSPORT=http HTTP_AUTH_TOKEN_FILE=~/.402-mcp/http.token PORT=3402 npx 402-mcp
359
389
  ```
360
390
 
361
391
  HTTP transport includes:
392
+ - Bearer token required on every request (`HTTP_AUTH_TOKEN_FILE`, a private 0600 file); the server refuses to start in http mode without one
393
+ - DNS rebinding protection: only requests whose `Host` header is a loopback name with the port, the bound address, or listed in `HTTP_ALLOWED_HOSTS` are answered
362
394
  - Rate limiting: 100 requests per 60 seconds per IP
363
395
  - Security headers: CSP, X-Frame-Options, X-Content-Type-Options, Cache-Control
364
396
  - Loopback-only binding by default (`BIND_ADDRESS=127.0.0.1`)
@@ -372,11 +404,14 @@ HTTP transport includes:
372
404
  | `NWC_URI_FILE` | — | Path to a private 0600 file containing the NWC bearer URI. Raw NWC_URI is refused. |
373
405
  | `CASHU_TOKENS` | — | Path to Cashu token store file. Must be within home directory. |
374
406
  | `MAX_AUTO_PAY_SATS` | 1000 | Per-payment safety cap (satoshis). 0 disables autonomous payments. |
375
- | `MAX_SPEND_PER_MINUTE_SATS` | 10000 | Rolling 60-second spend cap (satoshis). |
407
+ | `MAX_SPEND_PER_MINUTE_SATS` | 10000 | Rolling 60-second spend cap (satoshis). 0 blocks auto-pay. |
408
+ | `MAX_SPEND_PER_DAY_SATS` | 5000 | Rolling 24-hour spend cap (satoshis), persisted across restarts. 0 blocks auto-pay. |
376
409
  | `CREDENTIAL_STORE` | `~/.402-mcp/credentials.json` | Path to encrypted credential store. Must be within home directory. |
377
410
  | `TRANSPORT` | `stdio` | Transport mode: `stdio` or `http`. |
378
411
  | `PORT` | 3402 | HTTP server port (when `TRANSPORT=http`). |
379
412
  | `BIND_ADDRESS` | `127.0.0.1` | HTTP bind address. Warning emitted for non-loopback addresses. |
413
+ | `HTTP_AUTH_TOKEN_FILE` | - | Private 0600 file holding the bearer token (32+ characters). Required when `TRANSPORT=http`. Raw HTTP_AUTH_TOKEN is refused. |
414
+ | `HTTP_ALLOWED_HOSTS` | - | Extra `Host` header values to accept (comma-separated), e.g. a reverse proxy's name. |
380
415
  | `CORS_ORIGIN` | disabled | CORS origin for HTTP transport. |
381
416
  | `FETCH_TIMEOUT_MS` | 30000 | HTTP request timeout in milliseconds. |
382
417
  | `FETCH_MAX_RETRIES` | 2 | Maximum retry attempts for failed requests. Money-mutating POSTs use 0. |
@@ -388,7 +423,7 @@ HTTP transport includes:
388
423
  | `TRANSPORT_PREFERENCE` | `onion,hns,https,http` | Preferred transport order for multi-URL services. |
389
424
  | `TOR_PROXY` | — | SOCKS5 proxy for .onion requests only (e.g. `socks5h://127.0.0.1:9050`). |
390
425
  | `SOCKS_PROXY` | — | SOCKS5 proxy for every paid-API request, names resolved by the proxy. Set this or `TOR_PROXY`, not both. Wallet and relay traffic is not proxied. |
391
- | `HNS_GATEWAY_URL` | `https://query.hdns.io/` | DNS-over-HTTPS gateway for Handshake (HNS) name resolution. |
426
+ | `HNS_GATEWAY_URL` | `https://query.hdns.io/` | DNS-over-HTTPS resolver for Handshake (HNS) names. Any host name ordinary DNS cannot find is sent here. |
392
427
 
393
428
  ## Multi-Transport Endpoints
394
429
 
@@ -416,7 +451,7 @@ Handshake names are resolved via DNS-over-HTTPS (RFC 8484 wire format) through t
416
451
 
417
452
  ### Credential Portability
418
453
 
419
- Credentials are keyed by service **pubkey** (from the kind 31402 event), not by URL origin. A macaroon obtained via a Tor hidden service works over clearnet, and vice versa. This is essential for transport fallback — the agent doesn't need to re-authenticate when switching transports.
454
+ Credentials are keyed by service **pubkey** (from the kind 31402 event) and bound to the transport origins they were bought through. A macaroon obtained via a Tor hidden service works over the service's clearnet URL, and vice versa, when both were in the `urls` list at purchase. A credential is never sent to an origin it is not bound to, whatever pubkey the request names.
420
455
 
421
456
  ## Ecosystem
422
457
 
@@ -424,7 +459,7 @@ Credentials are keyed by service **pubkey** (from the kind 31402 event), not by
424
459
  |---------|------|-----|
425
460
  | **402-mcp** | MCP client — AI agents discover, pay, and consume L402 APIs | https://github.com/forgesworn/402-mcp |
426
461
  | **toll-booth** | L402 payment middleware for any Node.js server | https://github.com/forgesworn/toll-booth |
427
- | **satgate** | Pay-per-token AI inference proxy (built on toll-booth) | https://github.com/forgesworn/satgate |
462
+ | **satgate** | Pay-per-token AI inference proxy (built on toll-booth) | https://github.com/TheCryptoDonkey/satgate |
428
463
  | **402-announce** | Publish L402 services on Nostr for decentralised discovery | https://github.com/forgesworn/402-announce |
429
464
 
430
465
  ## Integration Patterns
@@ -468,7 +503,10 @@ Credentials are keyed by service **pubkey** (from the kind 31402 event), not by
468
503
  ### Agent with no wallet (human-in-the-loop)
469
504
 
470
505
  ```
471
- 1. l402-fetch(url) → 402 received, amount shown
472
- 2. Agent presents QR code to user → Human pays via wallet app
473
- 3. Poll detects payment → Credential stored, data returned
506
+ 1. l402-fetch(url, autoPay) → 402 received; payment page URL or QR shown
507
+ 2. Human pays via wallet app
508
+ 3. l402-pay(paymentHash) → polls the toll-booth payment page, stores the credential
509
+ 4. l402-fetch(url) → data returned
510
+
511
+ Settlement can only be confirmed where the server reports it (toll-booth).
474
512
  ```
package/llms.txt CHANGED
@@ -1,8 +1,8 @@
1
1
  # 402-mcp
2
2
 
3
- > 402-mcp is an MCP server that gives AI agents economic agency — the ability to discover, pay for, and consume any L402 or x402 payment-gated API without human registration, API keys, or middlemen. It speaks the L402 protocol natively, carries its own payment methods, and makes autonomous purchasing decisions within human-set safety limits.
3
+ > 402-mcp is an MCP server that gives AI agents economic agency: the ability to discover, pay for, and consume L402 and other Lightning or ecash payment-gated APIs without human registration, API keys, or middlemen. It speaks the L402 protocol natively, carries its own payment methods, and makes autonomous purchasing decisions within human-set safety limits.
4
4
 
5
- 402-mcp is protocol-loyal: it works with any L402-compliant server (toll-booth, Aperture, or any conforming implementation), not just a specific vendor's stack. Bonus features activate automatically when talking to a toll-booth server. Three payer methods are supported — NWC (Nostr Wallet Connect), Cashu ecash, and human-in-the-loop — tried in that priority order so the agent picks the most autonomous option available.
5
+ 402-mcp is protocol-loyal: it works with any L402-compliant server (toll-booth, Aperture, or any conforming implementation), not just a specific vendor's stack. Bonus features activate automatically when talking to a toll-booth server. Four payer methods are supported (NWC (Nostr Wallet Connect), Cashu ecash, LNURLcash bearer notes, and human-in-the-loop), tried in that priority order so the agent picks the most autonomous option available.
6
6
 
7
7
  Agents can also discover paid APIs without knowing URLs upfront. The `l402-search` tool queries Nostr relays for kind 31402 service announcements — a decentralised registry of L402-enabled services. Discovered services may advertise multiple transport endpoints (clearnet HTTPS, Tor .onion, Handshake HNS domains). The agent automatically selects the best available transport and falls back through alternatives on connection failure — .onion endpoints need `TOR_PROXY` or `SOCKS_PROXY`; clearnet and HNS need no configuration.
8
8
 
@@ -14,12 +14,15 @@ Run instantly with no configuration required:
14
14
  npx 402-mcp
15
15
  ```
16
16
 
17
- For HTTP transport (useful for remote agents or multi-client setups):
17
+ Use stdio wherever the client can start a local process. The HTTP transport can spend from your wallet, so it refuses to start without a bearer token, and only answers requests whose `Host` header it recognises:
18
18
 
19
19
  ```bash
20
- TRANSPORT=http npx 402-mcp
20
+ openssl rand -hex 32 > ~/.402-mcp/http.token && chmod 600 ~/.402-mcp/http.token
21
+ TRANSPORT=http HTTP_AUTH_TOKEN_FILE=~/.402-mcp/http.token npx 402-mcp
21
22
  ```
22
23
 
24
+ Clients send `Authorization: Bearer <token>`. It listens on `127.0.0.1` only by default. To reach it from another machine, put it behind a TLS reverse proxy and add the proxy's host name to `HTTP_ALLOWED_HOSTS`; never expose it directly.
25
+
23
26
  ### Claude Desktop / Cursor
24
27
 
25
28
  Add to your MCP configuration:
@@ -45,25 +48,29 @@ Add to your MCP configuration:
45
48
  |----------|---------|-------------|
46
49
  | `NWC_URI_FILE` | — | Private 0600 file containing the Nostr Wallet Connect bearer URI |
47
50
  | `CASHU_TOKENS` | — | Path to Cashu token store file |
48
- | `MAX_AUTO_PAY_SATS` | 1000 | Safety cap; payments above this require human confirmation |
49
- | `MAX_SPEND_PER_MINUTE_SATS` | 10000 | Rolling 60-second spend cap across all payments |
51
+ | `LNURLCASH_NOTES` | - | Path to LNURLcash (LUD-25) bearer note store file |
52
+ | `MAX_AUTO_PAY_SATS` | 1000 | Most a single automatic payment may cost. Dearer challenges are not paid; they go back to the agent. `0` disables auto-pay |
53
+ | `MAX_SPEND_PER_MINUTE_SATS` | 10000 | Automatic spend allowed in any rolling 60 seconds. `0` blocks all auto-pay |
54
+ | `MAX_SPEND_PER_DAY_SATS` | 5000 | Automatic spend allowed in any rolling 24 hours, persisted across restarts. `0` blocks all auto-pay |
50
55
  | `CREDENTIAL_STORE` | `~/.402-mcp/credentials.json` | Persistent macaroon/credential storage |
51
56
  | `TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |
52
57
  | `PORT` | 3402 | HTTP server port (when `TRANSPORT=http`) |
58
+ | `HTTP_AUTH_TOKEN_FILE` | - | Private 0600 file holding the bearer token; required when `TRANSPORT=http` |
59
+ | `HTTP_ALLOWED_HOSTS` | - | Extra `Host` header values the HTTP transport accepts (comma-separated) |
53
60
  | `TRANSPORT_PREFERENCE` | `onion,hns,https,http` | Preferred transport order for multi-URL services |
54
61
  | `TOR_PROXY` | — | SOCKS5 proxy for .onion requests only (e.g. `socks5h://127.0.0.1:9050`) |
55
62
  | `SOCKS_PROXY` | — | SOCKS5 proxy for every paid-API request, names resolved by the proxy. Set this or `TOR_PROXY`, not both |
56
- | `HNS_GATEWAY_URL` | `https://query.hdns.io/` | DNS-over-HTTPS gateway for Handshake name resolution |
63
+ | `HNS_GATEWAY_URL` | `https://query.hdns.io/` | DNS-over-HTTPS resolver for Handshake names; any host name ordinary DNS cannot find is looked up here |
57
64
 
58
65
  ## Key Concepts
59
66
 
60
67
  - **L402** — an HTTP payment protocol where a server responds to unauthenticated requests with HTTP 402, a Lightning invoice, and a macaroon. The client pays the invoice, then sends `Authorization: L402 <macaroon>:<preimage>` to access the API.
61
68
  - **Economic agency** — the agent discovers a service, reads the price, pays the invoice, and retrieves the data, all within a single tool call. No human registration required.
62
- - **Safety cap** — `MAX_AUTO_PAY_SATS` sets the maximum any single autonomous payment can be. Above this limit, the agent asks for human approval. A rolling 60-second spend cap (`MAX_SPEND_PER_MINUTE_SATS`) prevents runaway spending.
69
+ - **Spending limits**: `MAX_AUTO_PAY_SATS` is the most any single automatic payment can be; a dearer challenge is returned to the agent unpaid. Rolling 60-second and persisted 24-hour caps (`MAX_SPEND_PER_MINUTE_SATS`, `MAX_SPEND_PER_DAY_SATS`) stop runaway spending. The budget on the NWC connection itself is the real hard limit.
63
70
  - **Credential store** — paid macaroons are persisted locally and encrypted with AES-256-GCM. Subsequent requests to the same server reuse credentials rather than paying again.
64
71
  - **Credit balance** — many L402 servers (including toll-booth) grant a credit balance after payment. 402-mcp tracks and caches that balance so the agent can reason about remaining capacity.
65
72
  - **Service discovery** — agents can search for L402 services on Nostr relays by querying kind 31402 announcements, finding paid APIs by topic, capability, or payment method.
66
- - **Multi-transport** — discovered services may advertise multiple endpoints (HTTPS, Tor .onion, Handshake HNS). The agent selects the best transport by preference (default: onion > hns > https > http), filters by capability (e.g. .onion requires a Tor proxy), and falls back automatically on connection failure. Credentials are keyed by service pubkey, not URL origin, so a macaroon obtained via one transport works on any other.
73
+ - **Multi-transport**: discovered services may advertise multiple endpoints (HTTPS, Tor .onion, Handshake HNS). The agent selects the best transport by preference (default: onion > hns > https > http), filters by capability (e.g. .onion requires a Tor proxy), and falls back automatically on connection failure. Credentials are keyed by service pubkey and bound to the transport origins they were bought through, so a macaroon obtained via one advertised transport works on the others, and is never sent to an origin it was not bought from.
67
74
 
68
75
  ## Core L402 Tools
69
76
 
@@ -77,13 +84,21 @@ Introspect the agent's payment capabilities and current configuration. Returns w
77
84
 
78
85
  Probe an endpoint to discover its pricing without making a payment. Returns the invoice amount in satoshis, the macaroon challenge, and whether the server is a toll-booth instance (which enables extension tools). Use this before committing to a purchase so the agent can reason about value.
79
86
 
87
+ ### `l402-fetch-preview`
88
+
89
+ Show what an endpoint (or each of a service's transport URLs) costs without paying: protocol, price and which wallet would pay. Cannot spend by construction. Widget hosts render it as a payment confirmation dialog.
90
+
80
91
  ### `l402-fetch`
81
92
 
82
- Make an HTTP request to an L402-protected endpoint. Handles the full payment flow automatically: detects 402 responses, pays the invoice if the amount is within the `MAX_AUTO_PAY_SATS` budget and the rolling spend cap, stores the credential, and retries the request. Returns the response body on success. This is the primary tool for consuming L402 APIs.
93
+ Make an HTTP request to an L402-protected endpoint. With `autoPay: true` it detects the 402, pays if the price is within `MAX_AUTO_PAY_SATS`, the optional per-call `maxCostSats` and the rolling spend caps, stores the credential, and retries. Without `autoPay` the challenge comes back with its price and `paymentHash`. Response bodies are wrapped in UNTRUSTED CONTENT markers. This is the primary tool for consuming L402 APIs.
83
94
 
84
95
  ### `l402-pay`
85
96
 
86
- Pay a specific Lightning invoice directly. Tries payment methods in priority order: NWC, Cashu, human-in-the-loop. Use this when you have an invoice from a source other than `l402-fetch`, or when you want explicit control over the payment step.
97
+ Pay a challenge that `l402-fetch` or `l402-discover` returned, by its `paymentHash`, using the wallet priority NWC, Cashu, LNURLcash, human. An invoice that did not come from such a challenge is paid only if the human approves it in the client (MCP elicitation); otherwise it is refused.
98
+
99
+ ### `l402-reconcile`
100
+
101
+ List payments whose outcome is unknown, or resolve one by asking the wallet that made it (NWC `lookup_invoice`, the Cashu mint, or the LNURLcash verify URL). While any is unresolved for a service, auto-pay to that service is paused.
87
102
 
88
103
  ### `l402-credentials`
89
104
 
@@ -95,7 +110,11 @@ Check the cached credit balance for a specific server URL. Returns the number of
95
110
 
96
111
  ### `l402-search`
97
112
 
98
- Search for L402 services on Nostr relays. Queries kind 31402 announcement events and returns matching services with their names, URLs, pricing, capabilities, and supported payment methods. Filter by query text, topics (e.g. "ai", "data"), or payment method. Services may advertise multiple transport URLs (clearnet, .onion, HNS); pass the returned URLs array to `l402-fetch` for automatic transport selection and fallback. This enables agents to find paid APIs without knowing URLs upfront — a decentralised alternative to API directories.
113
+ Search for L402 services on Nostr relays. Queries kind 31402 announcement events (topic filters are sent to the relays; the payment method and query text are matched locally) and returns matching services with their names, URLs, pubkey, pricing, capabilities, and supported payment methods. Filter by query text, topics (e.g. "ai", "data"), or payment method: the rail an announcement lists first in each `pmi` tag (`l402`, `cashu`, `xcashu`, `lnurlcash`, `x402`, `payment`). A Cashu service may list `cashu`, `xcashu` or both. Services may advertise multiple transport URLs (clearnet, .onion, HNS); pass the first as `url`, the whole array as `urls` and the service's `pubkey` to `l402-fetch`, for automatic transport selection and fallback with credentials shared across them. This enables agents to find paid APIs without knowing URLs upfront: a decentralised alternative to API directories. Announcement text is wrapped in UNTRUSTED CONTENT markers.
114
+
115
+ ## Widget Tools
116
+
117
+ For hosts that support MCP Apps: `l402-service-directory` opens a searchable directory of services from `l402-search`, and `l402-wallet-dashboard` shows wallet status, limits and stored credentials. `l402-fetch-preview` has a payment confirmation widget.
99
118
 
100
119
  ## toll-booth Extension Tools
101
120
 
@@ -111,30 +130,36 @@ Redeem Cashu ecash tokens directly with the toll-booth server, bypassing the Lig
111
130
 
112
131
  ## Payment Methods
113
132
 
114
- Three payer methods are tried in priority order:
133
+ Four payer methods are tried in priority order:
115
134
 
116
135
  1. **NWC (Nostr Wallet Connect)** — fully autonomous. The agent pays invoices directly from a connected Lightning wallet via the NWC protocol. Configure with `NWC_URI_FILE`; raw bearer values are refused. No human interaction required.
117
136
 
118
137
  2. **Cashu** — fully autonomous. The agent melts ecash tokens to pay invoices. Configure with `CASHU_TOKENS` pointing to a token store file. No Lightning node required on the agent side.
119
138
 
120
- 3. **Human-in-the-loop** — fallback when no autonomous payment method is configured or when a payment exceeds the `MAX_AUTO_PAY_SATS` cap. The agent presents a payment QR code and polls for settlement. Control returns to the agent once the human has paid.
139
+ 3. **LNURLcash**: fully autonomous. The agent melts LUD-25 bearer notes to pay invoices. Configure with `LNURLCASH_NOTES`.
140
+
141
+ 4. **Human-in-the-loop**: used when no autonomous payment method is configured. The agent presents a payment QR code or payment page, and `l402-pay` confirms settlement where the server reports it (toll-booth does). It does not take over above `MAX_AUTO_PAY_SATS`: a dearer challenge is simply returned to the agent unpaid.
121
142
 
122
143
  The agent can override the payment method per-call, or you can configure only the methods you want available.
123
144
 
124
- `l402-fetch` handles five HTTP 402 challenge variants transparently:
145
+ `l402-fetch` handles four HTTP 402 challenge variants, plus an experimental x402 format:
125
146
 
126
147
  - **L402** (`WWW-Authenticate: L402`) — the primary protocol; Lightning invoice paid via the wallet stack.
127
148
  - **IETF Payment** (`WWW-Authenticate: Payment`, `draft-ryan-httpauth-payment-01`) — alternative Lightning path; builds a base64url credential after payment.
128
149
  - **LNURLcash** (`X-LNURLcash: lnurlcashreq1…`, LUD-25): autonomous bearer-note payment; hands over a note worth exactly the price, splitting one at the mint if needed. Tried first: no Lightning hop and no swap at the mint.
129
150
  - **xCashu** (`X-Cashu: creqA…`, NUT-18) — autonomous ecash payment; selects and sends the exact token amount without a Lightning round-trip.
130
- - **x402** (`X-Payment-Required: x402`) — on-chain EVM payment; surfaces an EIP-681 deeplink to the human (no autonomous settlement).
151
+ - **x402** (`X-Payment-Required: x402`), experimental: a custom format, not the x402 specification (whose servers send a base64 `PAYMENT-REQUIRED` header), so real x402 services are not supported. Payment details are shown to the human, who pays from their own wallet; nothing is paid autonomously.
131
152
 
132
153
  ## Safety Model
133
154
 
134
- Two safety primitives protect against runaway spending:
155
+ These limits apply to every automatic payment:
156
+
157
+ 1. **`MAX_AUTO_PAY_SATS`** caps any single payment. A dearer challenge is returned to the agent unpaid; whether the agent then asks the human is up to the agent.
158
+ 2. **`maxCostSats`** on `l402-fetch` lowers that cap for one call, so a price shown in a preview is binding.
159
+ 3. **`MAX_SPEND_PER_MINUTE_SATS`** and **`MAX_SPEND_PER_DAY_SATS`** cap total spend over rolling 60-second and 24-hour windows; the daily window survives restarts. `0` blocks auto-pay.
160
+ 4. **Unknown outcomes** pause auto-pay to that service until `l402-reconcile` resolves them.
135
161
 
136
- 1. **`MAX_AUTO_PAY_SATS`** — caps any single autonomous payment. Above this threshold, the agent must ask for human approval.
137
- 2. **`MAX_SPEND_PER_MINUTE_SATS`** — rolling 60-second window cap across all payments. Prevents rapid successive payments from exceeding a total budget even if each individual payment is below the per-payment cap.
162
+ All of these are enforced in software by this process. The budget set on the NWC connection in the wallet is the real hard limit.
138
163
 
139
164
  The human stays in control of three things: the safety caps, which payment methods are available, and credential visibility. The agent operates within those boundaries, not around them.
140
165
 
@@ -142,9 +167,11 @@ The human stays in control of three things: the safety caps, which payment metho
142
167
 
143
168
  - **SSRF protection** — all outbound requests pass through an SSRF guard that blocks private/internal IP ranges by default. Handshake names are resolved via DNS-over-HTTPS with the resolved IP validated against blocked ranges. .onion addresses bypass DNS and route through the configured SOCKS proxy. With `SOCKS_PROXY`, no host name is resolved locally; IP literals and local names are still refused. Wallet and relay traffic is not proxied. Set `SSRF_ALLOW_PRIVATE=true` only for local development.
144
169
  - **Credential encryption** — stored credentials are encrypted with AES-256-GCM. The encryption key is stored in the OS keychain when available, falling back to a file-based key with a warning.
145
- - **Settlement proof** — NWC responses are authenticated with NIP-44 v2 and signed events, then the returned preimage is verified against the challenged BOLT-11 payment hash before storage.
170
+ - **Settlement proof**: NWC responses are authenticated with NIP-44 v2 and signed events. NWC and Cashu preimages are verified against the invoice's payment hash; a payment reported without a matching preimage is treated as an unknown outcome, not as unpaid.
171
+ - **Prompt-injection hardening**: third-party text is marked as untrusted, and `l402-pay` only pays invoices from challenges this process received unless the human approves.
172
+ - **Credential binding**: stored credentials are sent only to the origins they were obtained from.
146
173
  - **Spend tracking** — atomic `tryRecord()` prevents TOCTOU race conditions in concurrent spend checks.
147
- - **HTTP transport hardening** — rate limiting (100 req/60s per IP), security headers (CSP, X-Frame-Options, nosniff), and loopback-only binding by default.
174
+ - **HTTP transport hardening**: bearer token required (`HTTP_AUTH_TOKEN_FILE`), DNS rebinding protection through a `Host` allowlist, rate limiting (100 req/60s per IP), security headers (CSP, X-Frame-Options, nosniff), and loopback-only binding by default.
148
175
 
149
176
  ## Ecosystem
150
177
 
@@ -154,7 +181,7 @@ https://github.com/forgesworn/toll-booth
154
181
 
155
182
  **satgate** demonstrates the AI inference use case: a proxy that gates access to AI model endpoints (Ollama, vLLM, llama.cpp) behind L402 payments. Agents use 402-mcp to discover pricing, pay per-request or buy credit tiers, and consume inference capacity without pre-provisioned API keys.
156
183
 
157
- https://github.com/forgesworn/satgate
184
+ https://github.com/TheCryptoDonkey/satgate
158
185
 
159
186
  **402-announce** publishes HTTP 402 services on Nostr as kind 31402 events, making them discoverable by `l402-search`. Operators run it alongside their L402 server to announce capabilities, pricing, and payment methods to the decentralised network.
160
187
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "402-mcp",
3
- "version": "4.2.2",
3
+ "version": "4.3.1",
4
4
  "mcpName": "dev.forgesworn/402-mcp",
5
- "description": "L402 + x402 client MCP - AI agents discover, pay for, and consume any payment-gated API",
5
+ "description": "L402 client MCP: AI agents discover, pay for and consume Lightning and ecash payment-gated APIs (x402 support is experimental)",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "402-mcp": "./build/index.js"
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@cashu/cashu-ts": "^3.6.1",
40
- "@forgesworn/nwc-kit": "0.1.7",
40
+ "@forgesworn/nwc-kit": "0.1.8",
41
41
  "@modelcontextprotocol/ext-apps": "^1.3.1",
42
42
  "@modelcontextprotocol/sdk": "^1.26.0",
43
43
  "@scure/base": "^2.0.0",
@@ -161,6 +161,15 @@ function el(tag, attrs, ...children) {
161
161
  root.replaceChildren(el("div", { className: "status-text" }, text));
162
162
  }
163
163
 
164
+ // l402-search delimits announcement text as untrusted for the agent. The
165
+ // card shows the text itself; it is rendered as text, never as markup.
166
+ function plain(value) {
167
+ const m = typeof value === "string"
168
+ ? /^\[UNTRUSTED CONTENT from [^\]\n]*\]\n([\s\S]*)\n\[END UNTRUSTED CONTENT\]$/.exec(value)
169
+ : null;
170
+ return m ? m[1] : value;
171
+ }
172
+
164
173
  function renderServices(services, query) {
165
174
  const searchInput = el("input", {
166
175
  className: "search-box",
@@ -205,8 +214,8 @@ function el(tag, attrs, ...children) {
205
214
  });
206
215
  },
207
216
  },
208
- el("div", { className: "service-name" }, svc.name || "Unnamed Service"),
209
- svc.about ? el("div", { className: "service-desc" }, svc.about) : null,
217
+ el("div", { className: "service-name" }, plain(svc.name) || "Unnamed Service"),
218
+ svc.about ? el("div", { className: "service-desc" }, plain(svc.about)) : null,
210
219
  chips,
211
220
  );
212
221
  list.appendChild(card);