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 +21 -0
- package/README.md +230 -0
- package/build/announce.d.ts +13 -0
- package/build/announce.js +70 -0
- package/build/announce.js.map +1 -0
- package/build/event.d.ts +12 -0
- package/build/event.js +58 -0
- package/build/event.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +56 -0
- package/build/types.js +3 -0
- package/build/types.js.map +1 -0
- package/build/utils.d.ts +1 -0
- package/build/utils.js +8 -0
- package/build/utils.js.map +1 -0
- package/llms-full.txt +222 -0
- package/llms.txt +36 -0
- package/package.json +53 -3
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
|
+
[](./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"}
|
package/build/event.d.ts
ADDED
|
@@ -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"}
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -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"}
|
package/build/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA0DA,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAA"}
|
package/build/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
package/build/utils.js
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
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": "
|
|
55
|
+
"url": "https://github.com/TheCryptoDonkey/402-announce.git"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18"
|
|
9
59
|
}
|
|
10
60
|
}
|