402-announce 0.0.1 → 1.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Crypto Donkey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # 402-announce
2
+
3
+ Announce HTTP 402 services on Nostr for decentralised discovery. Supports both L402 and x402 payment protocols.
4
+
5
+ [![MIT Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](./LICENSE)
6
+
7
+ Publishes **kind 31402** parameterised replaceable events so that AI agents (and any Nostr client) can discover paid APIs without a central registry.
8
+
9
+ ## How it works
10
+
11
+ ```mermaid
12
+ flowchart LR
13
+ A[Your API] -->|"npm install<br/>402-announce"| B[402-announce]
14
+ B -->|"kind 31402<br/>signed event"| C[(Nostr Relays)]
15
+ C -->|subscribe & filter| D[402-mcp]
16
+ D -->|discover + pay| A
17
+
18
+ style B fill:#f59e0b,color:#000
19
+ style C fill:#8b5cf6,color:#fff
20
+ style D fill:#3b82f6,color:#fff
21
+ ```
22
+
23
+ Your API publishes a service announcement to Nostr relays. AI agents (via [402-mcp](https://github.com/TheCryptoDonkey/402-mcp)) discover it, pay the invoice, and consume the API. No central registry required.
24
+
25
+ ## Quick start
26
+
27
+ ```bash
28
+ npm install 402-announce
29
+ ```
30
+
31
+ ```typescript
32
+ import { announceService } from '402-announce'
33
+
34
+ const handle = await announceService({
35
+ secretKey: '64-char-hex-nostr-secret-key',
36
+ relays: ['wss://relay.damus.io', 'wss://relay.primal.net'],
37
+ identifier: 'jokes-api',
38
+ name: 'Jokes API',
39
+ url: 'https://jokes.example.com',
40
+ about: 'A joke-telling service behind an L402 paywall',
41
+ pricing: [
42
+ { capability: 'get_joke', price: 1, currency: 'sats' },
43
+ ],
44
+ paymentMethods: ['bitcoin-lightning-bolt11', 'bitcoin-cashu'],
45
+ topics: ['comedy', 'ai'],
46
+ capabilities: [
47
+ { name: 'get_joke', description: 'Returns a random joke' },
48
+ ],
49
+ version: '1.0.0',
50
+ })
51
+
52
+ console.log('Published event:', handle.eventId)
53
+ console.log('From pubkey:', handle.pubkey)
54
+
55
+ // Later, when shutting down:
56
+ handle.close()
57
+ ```
58
+
59
+ ## Announce flow
60
+
61
+ ```mermaid
62
+ flowchart TD
63
+ A[announceService config] --> B{Validate}
64
+ B -->|"secretKey: 64-char hex"| C[Derive pubkey]
65
+ B -->|"relays: wss:// URLs"| C
66
+ C --> D[Build kind 31402 event]
67
+ D --> E[Sign with secret key]
68
+ E --> F[Zeroise key bytes]
69
+ F --> G["Connect relays (parallel, 10s timeout)"]
70
+ G --> H{Each relay}
71
+ H -->|success| I[Publish event]
72
+ H -->|failure| J[Log warning, continue]
73
+ I --> K["Return { eventId, pubkey, close() }"]
74
+ J --> K
75
+ ```
76
+
77
+ ## Event format
78
+
79
+ Each announcement is a **kind 31402** parameterised replaceable event. The combination of `pubkey` + `d` tag uniquely identifies a listing — publishing again with the same values updates the existing listing.
80
+
81
+ ```mermaid
82
+ graph TB
83
+ subgraph "Kind 31402 Event"
84
+ direction TB
85
+ subgraph Tags
86
+ D["d: jokes-api"]
87
+ N["name: Jokes API"]
88
+ U["url: https://jokes.example.com"]
89
+ AB["about: A joke-telling service"]
90
+ PMI1["pmi: bitcoin-lightning-bolt11"]
91
+ PMI2["pmi: bitcoin-cashu"]
92
+ P["price: get_joke, 1, sats"]
93
+ T["t: comedy"]
94
+ end
95
+ subgraph Content["Content (JSON)"]
96
+ CAP["capabilities: [{ name, description }]"]
97
+ VER["version: 1.0.0"]
98
+ end
99
+ end
100
+
101
+ style Tags fill:#1e293b,color:#e2e8f0
102
+ style Content fill:#1e293b,color:#e2e8f0
103
+ ```
104
+
105
+ ### Tags
106
+
107
+ | Tag | Required | Description | Example |
108
+ |-----------|----------|----------------------------------------------|-----------------------------------|
109
+ | `d` | yes | Unique identifier for this listing | `jokes-api` |
110
+ | `name` | yes | Human-readable service name | `Jokes API` |
111
+ | `url` | yes | HTTP endpoint for the 402 service | `https://jokes.example.com` |
112
+ | `about` | yes | Short description | `A joke-telling service` |
113
+ | `pmi` | yes | Payment method identifier (repeatable) | `bitcoin-lightning-bolt11` |
114
+ | `price` | yes | Capability pricing (repeatable) | `get_joke`, `1`, `sats` |
115
+ | `t` | no | Topic tag for search/filtering (repeatable) | `comedy` |
116
+ | `picture` | no | Icon URL | `https://example.com/icon.png` |
117
+
118
+ ### Content
119
+
120
+ The event content is a JSON object with optional fields:
121
+
122
+ ```json
123
+ {
124
+ "capabilities": [
125
+ { "name": "get_joke", "description": "Returns a random joke" }
126
+ ],
127
+ "version": "1.0.0"
128
+ }
129
+ ```
130
+
131
+ ## API
132
+
133
+ ### `announceService(config): Promise<Announcement>`
134
+
135
+ High-level function that builds, signs, and publishes the event to multiple relays.
136
+
137
+ **Returns** an `Announcement` handle:
138
+ - `eventId` — the published Nostr event ID
139
+ - `pubkey` — the Nostr pubkey derived from your secret key
140
+ - `close()` — disconnect from all relays
141
+
142
+ ### `buildAnnounceEvent(secretKey, config): VerifiedEvent`
143
+
144
+ Lower-level function that builds and signs the event without publishing. Useful if you manage relay connections yourself.
145
+
146
+ ### Config options
147
+
148
+ | Field | Type | Required | Description |
149
+ |------------------|-------------------|----------|------------------------------------------------|
150
+ | `secretKey` | `string` | yes | 64-char hex Nostr secret key |
151
+ | `relays` | `string[]` | yes | Relay URLs (`wss://` or `ws://`) |
152
+ | `identifier` | `string` | yes | Unique listing ID (Nostr `d` tag) |
153
+ | `name` | `string` | yes | Human-readable service name |
154
+ | `url` | `string` | yes | HTTP endpoint for the service |
155
+ | `about` | `string` | yes | Short description |
156
+ | `pricing` | `PricingDef[]` | yes | Per-capability pricing |
157
+ | `paymentMethods` | `string[]` | yes | Accepted payment methods |
158
+ | `picture` | `string` | no | Icon URL |
159
+ | `topics` | `string[]` | no | Topic tags for filtering |
160
+ | `capabilities` | `CapabilityDef[]` | no | Capability details (stored in event content) |
161
+ | `version` | `string` | no | Service version (stored in event content) |
162
+
163
+ ## How it fits together
164
+
165
+ ```mermaid
166
+ flowchart LR
167
+ subgraph "Your Server"
168
+ API[Your API]
169
+ TB[toll-booth<br/>L402 paywall]
170
+ AN[402-announce]
171
+ end
172
+
173
+ subgraph "Nostr Network"
174
+ R1[(Relay 1)]
175
+ R2[(Relay 2)]
176
+ end
177
+
178
+ subgraph "AI Agent"
179
+ MCP[402-mcp<br/>discovery + payment]
180
+ end
181
+
182
+ API --> TB
183
+ TB -->|"protects"| API
184
+ AN -->|"kind 31402"| R1
185
+ AN -->|"kind 31402"| R2
186
+ R1 -->|"subscribe"| MCP
187
+ R2 -->|"subscribe"| MCP
188
+ MCP -->|"HTTP + L402 token"| TB
189
+
190
+ style TB fill:#ef4444,color:#fff
191
+ style AN fill:#f59e0b,color:#000
192
+ style MCP fill:#3b82f6,color:#fff
193
+ ```
194
+
195
+ 1. **[toll-booth](https://github.com/TheCryptoDonkey/toll-booth)** wraps your API with an L402 paywall
196
+ 2. **402-announce** publishes a kind 31402 event describing the service, pricing, and payment methods
197
+ 3. **[402-mcp](https://github.com/TheCryptoDonkey/402-mcp)** discovers the announcement, pays the invoice, and calls your API
198
+
199
+ ## Security
200
+
201
+ - Secret key bytes are zeroised immediately after signing (in both `announceService` and `buildAnnounceEvent`)
202
+ - Never log or persist the secret key — pass it in and let the library handle cleanup
203
+ - Relay connections use a 10-second timeout to prevent hanging
204
+ - Individual relay failures are logged as warnings but don't reject the promise
205
+
206
+ ## What it does
207
+
208
+ - Builds and signs kind 31402 Nostr events
209
+ - Publishes to one or more Nostr relays in parallel
210
+ - Zeroises secret key bytes after use
211
+ - Degrades gracefully when individual relays fail
212
+ - Provides a `close()` handle for clean disconnection
213
+
214
+ ## What it does not do
215
+
216
+ - Does not run an L402 paywall (use [toll-booth](https://github.com/TheCryptoDonkey/toll-booth) for that)
217
+ - Does not subscribe to or search for announcements (use [402-mcp](https://github.com/TheCryptoDonkey/402-mcp) for that)
218
+ - Does not handle payments or token verification
219
+
220
+ ## Ecosystem
221
+
222
+ | Package | Purpose |
223
+ |---------|---------|
224
+ | [toll-booth](https://github.com/TheCryptoDonkey/toll-booth) | L402 middleware — any API becomes a toll booth in minutes |
225
+ | [satgate](https://github.com/TheCryptoDonkey/satsgate) | Production L402 gateway with Lightning and Cashu support |
226
+ | [402-mcp](https://github.com/TheCryptoDonkey/402-mcp) | MCP server for AI agents to discover, pay, and consume 402 APIs |
227
+
228
+ ## Licence
229
+
230
+ [MIT](./LICENSE)
@@ -0,0 +1,13 @@
1
+ import type { AnnounceConfig, Announcement } from './types.js';
2
+ /**
3
+ * Publish a kind 31402 L402 service announcement to Nostr relays.
4
+ *
5
+ * Connects to each relay, publishes the signed event, and returns an
6
+ * {@link Announcement} handle whose `close()` method disconnects all relays.
7
+ *
8
+ * Relay failures are logged but do not reject the promise -- the function
9
+ * degrades gracefully. A warning is emitted if *no* relay accepted the event.
10
+ *
11
+ * @throws If the relay list is empty or contains invalid URLs.
12
+ */
13
+ export declare function announceService(config: AnnounceConfig): Promise<Announcement>;
@@ -0,0 +1,70 @@
1
+ import { getPublicKey } from 'nostr-tools/pure';
2
+ import { Relay } from 'nostr-tools/relay';
3
+ import { buildAnnounceEvent } from './event.js';
4
+ import { hexToBytes } from './utils.js';
5
+ /**
6
+ * Publish a kind 31402 L402 service announcement to Nostr relays.
7
+ *
8
+ * Connects to each relay, publishes the signed event, and returns an
9
+ * {@link Announcement} handle whose `close()` method disconnects all relays.
10
+ *
11
+ * Relay failures are logged but do not reject the promise -- the function
12
+ * degrades gracefully. A warning is emitted if *no* relay accepted the event.
13
+ *
14
+ * @throws If the relay list is empty or contains invalid URLs.
15
+ */
16
+ export async function announceService(config) {
17
+ const { relays, secretKey } = config;
18
+ if (!/^[0-9a-f]{64}$/i.test(secretKey)) {
19
+ throw new Error('secretKey must be a 64-character hex string');
20
+ }
21
+ if (relays.length === 0) {
22
+ throw new Error('At least one relay URL is required');
23
+ }
24
+ for (const url of relays) {
25
+ if (!/^wss?:\/\//i.test(url)) {
26
+ throw new Error(`Invalid relay URL: ${url} — must start with wss:// or ws://`);
27
+ }
28
+ }
29
+ // Derive pubkey (zeroises sk bytes internally)
30
+ const skBytes = hexToBytes(secretKey);
31
+ const pubkey = getPublicKey(skBytes);
32
+ skBytes.fill(0);
33
+ // Build and sign the event
34
+ const event = buildAnnounceEvent(secretKey, config);
35
+ // Connect to relays in parallel and publish
36
+ const connectedRelays = [];
37
+ let accepted = 0;
38
+ const results = await Promise.allSettled(relays.map(async (url) => {
39
+ const relay = await Promise.race([
40
+ Relay.connect(url),
41
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Relay connection timeout: ${url}`)), 10_000)),
42
+ ]);
43
+ connectedRelays.push(relay);
44
+ await relay.publish(event);
45
+ accepted++;
46
+ }));
47
+ for (const result of results) {
48
+ if (result.status === 'rejected') {
49
+ console.warn(`[402-announce] Failed to publish:`, result.reason);
50
+ }
51
+ }
52
+ if (accepted === 0) {
53
+ console.warn('[402-announce] No relays accepted the event');
54
+ }
55
+ return {
56
+ eventId: event.id,
57
+ pubkey,
58
+ close() {
59
+ for (const relay of connectedRelays) {
60
+ try {
61
+ relay.close();
62
+ }
63
+ catch {
64
+ // Ignore close errors
65
+ }
66
+ }
67
+ },
68
+ };
69
+ }
70
+ //# sourceMappingURL=announce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce.js","sourceRoot":"","sources":["../src/announce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAsB;IAC1D,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAA;IAEpC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAChE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,oCAAoC,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;IAED,+CAA+C;IAC/C,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;IACrC,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;IACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAEf,2BAA2B;IAC3B,MAAM,KAAK,GAAG,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAEnD,4CAA4C;IAC5C,MAAM,eAAe,GAAiC,EAAE,CAAA;IACxD,IAAI,QAAQ,GAAG,CAAC,CAAA;IAEhB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAC/B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAClB,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC/B,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAChF;SACF,CAAC,CAAA;QACF,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3B,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAC1B,QAAQ,EAAE,CAAA;IACZ,CAAC,CAAC,CACH,CAAA;IAED,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAA;IAC7D,CAAC;IAED,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,EAAE;QACjB,MAAM;QACN,KAAK;YACH,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;gBACpC,IAAI,CAAC;oBACH,KAAK,CAAC,KAAK,EAAE,CAAA;gBACf,CAAC;gBAAC,MAAM,CAAC;oBACP,sBAAsB;gBACxB,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { AnnounceConfig } from './types.js';
2
+ import type { VerifiedEvent } from 'nostr-tools/pure';
3
+ /**
4
+ * Build and sign a kind 31402 Nostr event announcing an L402 service.
5
+ *
6
+ * The secret key bytes are zeroised after signing.
7
+ *
8
+ * @param secretKeyHex - 64-character hex-encoded Nostr secret key
9
+ * @param config - Service announcement configuration
10
+ * @returns Signed Nostr event ready for relay publication
11
+ */
12
+ export declare function buildAnnounceEvent(secretKeyHex: string, config: Omit<AnnounceConfig, 'relays'>): VerifiedEvent;
package/build/event.js ADDED
@@ -0,0 +1,58 @@
1
+ import { finalizeEvent } from 'nostr-tools/pure';
2
+ import { L402_ANNOUNCE_KIND } from './types.js';
3
+ import { hexToBytes } from './utils.js';
4
+ /**
5
+ * Build and sign a kind 31402 Nostr event announcing an L402 service.
6
+ *
7
+ * The secret key bytes are zeroised after signing.
8
+ *
9
+ * @param secretKeyHex - 64-character hex-encoded Nostr secret key
10
+ * @param config - Service announcement configuration
11
+ * @returns Signed Nostr event ready for relay publication
12
+ */
13
+ export function buildAnnounceEvent(secretKeyHex, config) {
14
+ if (!/^[0-9a-f]{64}$/i.test(secretKeyHex)) {
15
+ throw new Error('secretKey must be a 64-character hex string');
16
+ }
17
+ const sk = hexToBytes(secretKeyHex);
18
+ try {
19
+ const tags = [
20
+ ['d', config.identifier],
21
+ ['name', config.name],
22
+ ['url', config.url],
23
+ ['about', config.about],
24
+ ];
25
+ if (config.picture) {
26
+ tags.push(['picture', config.picture]);
27
+ }
28
+ for (const pm of config.paymentMethods) {
29
+ tags.push(['pmi', pm]);
30
+ }
31
+ for (const p of config.pricing) {
32
+ tags.push(['price', p.capability, String(p.price), p.currency]);
33
+ }
34
+ if (config.topics) {
35
+ for (const topic of config.topics) {
36
+ tags.push(['t', topic]);
37
+ }
38
+ }
39
+ const contentObj = {};
40
+ if (config.capabilities) {
41
+ contentObj.capabilities = config.capabilities;
42
+ }
43
+ if (config.version) {
44
+ contentObj.version = config.version;
45
+ }
46
+ const event = finalizeEvent({
47
+ kind: L402_ANNOUNCE_KIND,
48
+ tags,
49
+ content: JSON.stringify(contentObj),
50
+ created_at: Math.floor(Date.now() / 1000),
51
+ }, sk);
52
+ return event;
53
+ }
54
+ finally {
55
+ sk.fill(0);
56
+ }
57
+ }
58
+ //# sourceMappingURL=event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.js","sourceRoot":"","sources":["../src/event.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAIvC;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,YAAoB,EACpB,MAAsC;IAEtC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAChE,CAAC;IAED,MAAM,EAAE,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IACnC,IAAI,CAAC;QACH,MAAM,IAAI,GAAe;YACvB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;YACxB,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC;YACrB,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC;YACnB,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;SACxB,CAAA;QAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;QACxC,CAAC;QAED,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;YACvC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAA;QACxB,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;QACjE,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAClC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;YACzB,CAAC;QACH,CAAC;QAED,MAAM,UAAU,GAA4B,EAAE,CAAA;QAC9C,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACxB,UAAU,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAA;QAC/C,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,UAAU,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;QACrC,CAAC;QAED,MAAM,KAAK,GAAG,aAAa,CACzB;YACE,IAAI,EAAE,kBAAkB;YACxB,IAAI;YACJ,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;YACnC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;SAC1C,EACD,EAAE,CACH,CAAA;QAED,OAAO,KAAK,CAAA;IACd,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { announceService } from './announce.js';
2
+ export { buildAnnounceEvent } from './event.js';
3
+ export { L402_ANNOUNCE_KIND } from './types.js';
4
+ export type { AnnounceConfig, Announcement, PricingDef, CapabilityDef } from './types.js';
package/build/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { announceService } from './announce.js';
2
+ export { buildAnnounceEvent } from './event.js';
3
+ export { L402_ANNOUNCE_KIND } from './types.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,56 @@
1
+ /** Pricing for a specific capability. */
2
+ export interface PricingDef {
3
+ /** Capability name (e.g. 'chat', 'get_joke', 'route') */
4
+ capability: string;
5
+ /** Price amount */
6
+ price: number;
7
+ /** Currency unit (e.g. 'sats') */
8
+ currency: string;
9
+ }
10
+ /** Capability with description (optional extended metadata). */
11
+ export interface CapabilityDef {
12
+ name: string;
13
+ description: string;
14
+ /** Optional JSON Schema describing the capability's input parameters. */
15
+ schema?: unknown;
16
+ /** Optional JSON Schema describing the capability's output. */
17
+ outputSchema?: unknown;
18
+ }
19
+ /** Configuration for announceService(). */
20
+ export interface AnnounceConfig {
21
+ /** Nostr secret key (64-char hex) for signing events */
22
+ secretKey: string;
23
+ /** Nostr relay URLs to publish to (wss:// or ws://) */
24
+ relays: string[];
25
+ /** Unique identifier for this service listing (d tag). Same pubkey + identifier = same listing. */
26
+ identifier: string;
27
+ /** Human-readable service name */
28
+ name: string;
29
+ /** HTTP URL where the L402 service is accessible */
30
+ url: string;
31
+ /** Short description of what the service does */
32
+ about: string;
33
+ /** Optional icon URL */
34
+ picture?: string;
35
+ /** Pricing for capabilities */
36
+ pricing: PricingDef[];
37
+ /** Accepted payment methods (e.g. ['bitcoin-lightning-bolt11', 'bitcoin-cashu']) */
38
+ paymentMethods: string[];
39
+ /** Optional topic tags for search/filtering (e.g. ['ai', 'inference']) */
40
+ topics?: string[];
41
+ /** Optional capability details (goes in content field) */
42
+ capabilities?: CapabilityDef[];
43
+ /** Optional service version (goes in content field) */
44
+ version?: string;
45
+ }
46
+ /** Handle returned by announceService() for cleanup. */
47
+ export interface Announcement {
48
+ /** Published event ID */
49
+ eventId: string;
50
+ /** Nostr pubkey derived from the secret key */
51
+ pubkey: string;
52
+ /** Close relay connections. Synchronous. */
53
+ close(): void;
54
+ }
55
+ /** The Nostr event kind used for L402 service announcements. */
56
+ export declare const L402_ANNOUNCE_KIND = 31402;
package/build/types.js ADDED
@@ -0,0 +1,3 @@
1
+ /** The Nostr event kind used for L402 service announcements. */
2
+ export const L402_ANNOUNCE_KIND = 31402;
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA0DA,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAA"}
@@ -0,0 +1 @@
1
+ export declare function hexToBytes(hex: string): Uint8Array;
package/build/utils.js ADDED
@@ -0,0 +1,8 @@
1
+ export function hexToBytes(hex) {
2
+ const bytes = new Uint8Array(hex.length / 2);
3
+ for (let i = 0; i < hex.length; i += 2) {
4
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
5
+ }
6
+ return bytes;
7
+ }
8
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACtD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC"}
package/llms-full.txt ADDED
@@ -0,0 +1,222 @@
1
+ # 402-announce — Full Reference
2
+
3
+ > Announce HTTP 402 services on Nostr for decentralised discovery.
4
+ > Supports both L402 and x402 payment protocols.
5
+ > Source: https://github.com/TheCryptoDonkey/402-announce
6
+
7
+ ## Overview
8
+
9
+ 402-announce is a TypeScript library that publishes kind 31402 Nostr events describing paid HTTP APIs. These events enable decentralised service discovery — AI agents and Nostr clients can find, evaluate, and pay for APIs without a central registry.
10
+
11
+ The library handles event construction, signing, and multi-relay publication. Secret key material is zeroised after use.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install 402-announce
17
+ ```
18
+
19
+ Requires Node.js >= 18. ESM only. Single runtime dependency: nostr-tools.
20
+
21
+ ## API Reference
22
+
23
+ ### announceService(config: AnnounceConfig): Promise<Announcement>
24
+
25
+ High-level function: builds a kind 31402 event, signs it, connects to relays in parallel, and publishes.
26
+
27
+ **Parameters:**
28
+
29
+ AnnounceConfig:
30
+ - secretKey (string, required): 64-character hex-encoded Nostr secret key
31
+ - relays (string[], required): Relay URLs (must start with wss:// or ws://)
32
+ - identifier (string, required): Unique listing ID, used as the Nostr `d` tag. Same pubkey + identifier = same listing.
33
+ - name (string, required): Human-readable service name
34
+ - url (string, required): HTTP endpoint where the 402 service is accessible
35
+ - about (string, required): Short description of the service
36
+ - pricing (PricingDef[], required): Per-capability pricing. Each entry: { capability, price, currency }
37
+ - paymentMethods (string[], required): Accepted payment method identifiers
38
+ - picture (string, optional): Icon URL
39
+ - topics (string[], optional): Topic tags for search/filtering
40
+ - capabilities (CapabilityDef[], optional): Capability details stored in event content
41
+ - version (string, optional): Service version stored in event content
42
+
43
+ PricingDef:
44
+ - capability (string): Capability name (e.g. "get_joke", "chat", "inference")
45
+ - price (number): Price amount
46
+ - currency (string): Currency unit (e.g. "sats", "USD")
47
+
48
+ CapabilityDef:
49
+ - name (string): Capability name
50
+ - description (string): Human-readable description
51
+
52
+ **Returns** Announcement:
53
+ - eventId (string): The published Nostr event ID
54
+ - pubkey (string): Nostr pubkey derived from the secret key
55
+ - close(): void — Disconnect from all connected relays
56
+
57
+ **Behaviour:**
58
+ - Validates secretKey (must be 64-char hex) and relay URLs (must be wss:// or ws://)
59
+ - Connects to all relays in parallel with a 10-second timeout per relay
60
+ - Individual relay failures are logged as warnings but do not reject the promise
61
+ - If no relay accepts the event, a warning is emitted (but the promise still resolves)
62
+ - Secret key bytes are zeroised after signing
63
+
64
+ **Throws:**
65
+ - If secretKey is not a 64-character hex string
66
+ - If relays array is empty
67
+ - If any relay URL does not start with wss:// or ws://
68
+
69
+ ### buildAnnounceEvent(secretKeyHex: string, config: Omit<AnnounceConfig, 'relays'>): VerifiedEvent
70
+
71
+ Lower-level function: builds and signs the kind 31402 event without publishing. Use this if you manage relay connections yourself.
72
+
73
+ **Returns** a nostr-tools VerifiedEvent ready for relay publication.
74
+
75
+ Secret key bytes are zeroised after signing (via try/finally).
76
+
77
+ ### Constants
78
+
79
+ - L402_ANNOUNCE_KIND = 31402 — The Nostr event kind for service announcements
80
+
81
+ ### Exported types
82
+
83
+ - AnnounceConfig — Configuration for announceService()
84
+ - Announcement — Handle returned by announceService()
85
+ - PricingDef — Pricing for a specific capability
86
+ - CapabilityDef — Capability with description
87
+
88
+ ## Event Format Specification
89
+
90
+ ### Kind 31402 — Parameterised Replaceable Event
91
+
92
+ A kind 31402 event is a Nostr parameterised replaceable event (NIP-33). This means:
93
+ - The combination of pubkey + kind + d-tag uniquely identifies a listing
94
+ - Publishing a new event with the same pubkey + kind + d-tag replaces the previous one
95
+ - This allows services to update their announcements (e.g. change pricing) without creating duplicates
96
+
97
+ ### Tag structure
98
+
99
+ Required tags:
100
+ ["d", "<identifier>"] — Unique listing identifier
101
+ ["name", "<service name>"] — Human-readable name
102
+ ["url", "<endpoint URL>"] — HTTP endpoint
103
+ ["about", "<description>"] — Short description
104
+ ["pmi", "<payment method>"] — Payment method identifier (one tag per method)
105
+ ["price", "<capability>", "<amount>", "<currency>"] — Pricing (one tag per capability)
106
+
107
+ Optional tags:
108
+ ["t", "<topic>"] — Topic tag for filtering (one tag per topic)
109
+ ["picture", "<icon URL>"] — Service icon
110
+
111
+ ### Content field
112
+
113
+ JSON object with optional fields:
114
+ {
115
+ "capabilities": [
116
+ { "name": "get_joke", "description": "Returns a random joke" }
117
+ ],
118
+ "version": "1.0.0"
119
+ }
120
+
121
+ ### Example event
122
+
123
+ {
124
+ "kind": 31402,
125
+ "pubkey": "ab1234...ef5678",
126
+ "created_at": 1710000000,
127
+ "tags": [
128
+ ["d", "jokes-api"],
129
+ ["name", "Jokes API"],
130
+ ["url", "https://jokes.example.com"],
131
+ ["about", "A joke-telling service behind an L402 paywall"],
132
+ ["pmi", "bitcoin-lightning-bolt11"],
133
+ ["pmi", "bitcoin-cashu"],
134
+ ["price", "get_joke", "1", "sats"],
135
+ ["t", "comedy"],
136
+ ["t", "ai"]
137
+ ],
138
+ "content": "{\"capabilities\":[{\"name\":\"get_joke\",\"description\":\"Returns a random joke\"}],\"version\":\"1.0.0\"}",
139
+ "id": "...",
140
+ "sig": "..."
141
+ }
142
+
143
+ ## Payment Method Identifiers
144
+
145
+ Known payment method identifiers for the `pmi` tag and `paymentMethods` config:
146
+
147
+ bitcoin-lightning-bolt11 — Lightning Network BOLT-11 invoices
148
+ bitcoin-cashu — Cashu ecash tokens
149
+ bitcoin-onchain — On-chain Bitcoin
150
+ x402-evm — x402 protocol on EVM chains
151
+ x402-solana — x402 protocol on Solana
152
+
153
+ ## Integration Patterns
154
+
155
+ ### Producer side: toll-booth + 402-announce
156
+
157
+ A typical server setup protects an API with toll-booth and announces it with 402-announce:
158
+
159
+ ```typescript
160
+ import { createTollBooth } from 'toll-booth'
161
+ import { announceService } from '402-announce'
162
+
163
+ // 1. Protect your API with a paywall
164
+ const booth = createTollBooth({ /* payment config */ })
165
+ app.use('/api', booth.middleware)
166
+
167
+ // 2. Announce it on Nostr
168
+ const handle = await announceService({
169
+ secretKey: process.env.NOSTR_SECRET_KEY,
170
+ relays: ['wss://relay.damus.io'],
171
+ identifier: 'my-api',
172
+ name: 'My API',
173
+ url: 'https://api.example.com',
174
+ about: 'A paid API service',
175
+ pricing: [{ capability: 'query', price: 10, currency: 'sats' }],
176
+ paymentMethods: ['bitcoin-lightning-bolt11'],
177
+ })
178
+
179
+ // 3. Clean up on shutdown
180
+ process.on('SIGTERM', () => handle.close())
181
+ ```
182
+
183
+ ### Consumer side: 402-mcp
184
+
185
+ AI agents use 402-mcp to discover and consume announced services:
186
+
187
+ 1. 402-mcp subscribes to kind 31402 events on configured relays
188
+ 2. Agent searches for services by topic, capability, or payment method
189
+ 3. 402-mcp handles L402 payment negotiation automatically
190
+ 4. Agent receives the API response
191
+
192
+ The agent never needs to know about Nostr or payment protocols — 402-mcp abstracts it all.
193
+
194
+ ### Updating a listing
195
+
196
+ Because kind 31402 is a parameterised replaceable event, calling announceService() again with the same identifier and pubkey replaces the previous listing. Use this to update pricing, add capabilities, or change the endpoint URL.
197
+
198
+ ## Security Considerations
199
+
200
+ - **Key zeroisation**: Secret key bytes (Uint8Array) are filled with zeros immediately after signing, in both announceService() and buildAnnounceEvent(). This limits the window during which key material exists in memory.
201
+ - **Key handling**: Pass the secret key as a hex string. The library converts to bytes, signs, and zeroises. Never log or persist the key yourself.
202
+ - **Relay trust**: Relays are untrusted infrastructure. Events are cryptographically signed — relay operators cannot forge announcements. However, relays can choose not to store or serve your events.
203
+ - **Connection timeouts**: Relay connections time out after 10 seconds to prevent indefinite hangs.
204
+ - **Graceful degradation**: If some relays fail, the event is still published to the others. A warning is emitted only if no relay accepts the event.
205
+
206
+ ## Error Handling
207
+
208
+ The library throws synchronously for invalid configuration:
209
+ - secretKey not 64-char hex → Error
210
+ - Empty relays array → Error
211
+ - Invalid relay URL (not wss:// or ws://) → Error
212
+
213
+ Relay-level failures during publication are handled gracefully:
214
+ - Connection timeouts → warning logged, other relays still tried
215
+ - Publish rejections → warning logged, other relays still tried
216
+ - All relays fail → warning logged, promise resolves with eventId from the signed event
217
+
218
+ ## Source
219
+
220
+ Repository: https://github.com/TheCryptoDonkey/402-announce
221
+ Licence: MIT
222
+ Runtime dependency: nostr-tools
package/llms.txt ADDED
@@ -0,0 +1,36 @@
1
+ # 402-announce
2
+
3
+ > Announce HTTP 402 services on Nostr for decentralised discovery
4
+
5
+ ## What it does
6
+
7
+ 402-announce publishes kind 31402 parameterised replaceable Nostr events that describe paid HTTP APIs. AI agents and Nostr clients discover these announcements to find services they can pay for and consume — no central registry needed.
8
+
9
+ ## Key concepts
10
+
11
+ - **Kind 31402**: Nostr event kind for L402/x402 service announcements. Parameterised replaceable — same pubkey + identifier updates the existing listing.
12
+ - **Payment methods**: Services declare accepted payment methods via `pmi` tags (e.g. `bitcoin-lightning-bolt11`, `bitcoin-cashu`).
13
+ - **Pricing**: Per-capability pricing via `price` tags (capability name, amount, currency).
14
+ - **Decentralised discovery**: Events are published to standard Nostr relays. Any client can subscribe and filter.
15
+
16
+ ## API surface
17
+
18
+ Two exports:
19
+
20
+ - `announceService(config)` — build, sign, and publish to relays. Returns `{ eventId, pubkey, close() }`.
21
+ - `buildAnnounceEvent(secretKey, config)` — build and sign without publishing. Returns a `VerifiedEvent`.
22
+
23
+ ## Ecosystem position
24
+
25
+ ```
26
+ toll-booth (paywall) → protects your API
27
+ 402-announce (this) → tells the world your API exists
28
+ 402-mcp (discovery) → AI agents find, pay, and consume your API
29
+ ```
30
+
31
+ ## Details
32
+
33
+ For full API reference, event format, integration patterns, and examples see:
34
+ - llms-full.txt (comprehensive reference)
35
+ - README.md (human-oriented documentation with diagrams)
36
+ - Source: https://github.com/TheCryptoDonkey/402-announce
package/package.json CHANGED
@@ -1,10 +1,60 @@
1
1
  {
2
2
  "name": "402-announce",
3
- "version": "0.0.1",
4
- "description": "Announce HTTP 402 services on Nostr for decentralised discovery. Kind 31402 parameterised replaceable events.",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "description": "Announce HTTP 402 services (L402 and x402) on Nostr for decentralised discovery. Kind 31402 parameterised replaceable events.",
5
6
  "license": "MIT",
7
+ "main": "./build/index.js",
8
+ "types": "./build/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./build/index.js",
12
+ "types": "./build/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "build",
17
+ "LICENSE",
18
+ "README.md",
19
+ "llms.txt",
20
+ "llms-full.txt"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "test": "vitest run",
25
+ "typecheck": "tsc --noEmit",
26
+ "prepublishOnly": "npm run typecheck && npm run build && npm test"
27
+ },
28
+ "dependencies": {
29
+ "nostr-tools": "^2.12.0"
30
+ },
31
+ "devDependencies": {
32
+ "@semantic-release/changelog": "^6.0.3",
33
+ "@semantic-release/git": "^10.0.1",
34
+ "@semantic-release/github": "^12.0.6",
35
+ "@types/node": "^25.5.0",
36
+ "semantic-release": "^25.0.3",
37
+ "typescript": "^5.7.0",
38
+ "vitest": "^4.0.0"
39
+ },
40
+ "keywords": [
41
+ "l402",
42
+ "x402",
43
+ "402",
44
+ "nostr",
45
+ "discovery",
46
+ "lightning",
47
+ "cashu",
48
+ "mcp",
49
+ "ai-agents",
50
+ "machine-payments",
51
+ "http-402"
52
+ ],
6
53
  "repository": {
7
54
  "type": "git",
8
- "url": "git+https://github.com/TheCryptoDonkey/402-announce.git"
55
+ "url": "https://github.com/TheCryptoDonkey/402-announce.git"
56
+ },
57
+ "engines": {
58
+ "node": ">=18"
9
59
  }
10
60
  }