# 402-announce **Nostr:** [`npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`](https://njump.me/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2) Announce HTTP 402 services on Nostr for decentralised discovery. Supports both L402 and x402 payment protocols. [![CI](https://github.com/forgesworn/402-announce/actions/workflows/ci.yml/badge.svg)](https://github.com/forgesworn/402-announce/actions/workflows/ci.yml) [![Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)](./README.md) [![MIT Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](./LICENSE) [![GitHub Sponsors](https://img.shields.io/github/sponsors/TheCryptoDonkey?logo=githubsponsors&color=ea4aaa&label=Sponsor)](https://github.com/sponsors/TheCryptoDonkey) Publishes **kind 31402** parameterised replaceable events so that AI agents (and any Nostr client) can discover paid APIs without a central registry. **[Live Dashboard](https://402.pub)** — see every service announcing on the network right now. ## How it works ```mermaid flowchart LR A[Your API] -->|"npm install
402-announce"| B[402-announce] B -->|"kind 31402
signed event"| C[(Nostr Relays)] C -->|subscribe & filter| D[402-mcp] D -->|discover + pay| A style B fill:#f59e0b,color:#000 style C fill:#8b5cf6,color:#fff style D fill:#3b82f6,color:#fff ``` Your API publishes a service announcement to Nostr relays. AI agents (via [402-mcp](https://github.com/forgesworn/402-mcp)) discover it, pay the invoice, and consume the API. No central registry required. ## Quick start ```bash npm install 402-announce ``` ```typescript import { announceService } from '402-announce' const handle = await announceService({ secretKey: '64-char-hex-nostr-secret-key', relays: ['wss://relay.damus.io', 'wss://relay.primal.net'], identifier: 'jokes-api', name: 'Jokes API', urls: ['https://jokes.example.com'], about: 'A joke-telling service behind an L402 paywall', pricing: [ { capability: 'get_joke', price: 1, currency: 'sats' }, ], paymentMethods: [['l402', 'lightning'], ['cashu'], ['xcashu']], topics: ['comedy', 'ai'], capabilities: [ { name: 'get_joke', description: 'Returns a random joke' }, ], version: '1.0.0', }) console.log('Published event:', handle.eventId) console.log('From pubkey:', handle.pubkey) // Later, when shutting down: handle.close() ``` **Multi-transport example** (clearnet + Tor + Handshake — same service, different access paths): ```typescript const handle = await announceService({ secretKey: '64-char-hex-nostr-secret-key', relays: ['wss://relay.damus.io', 'wss://relay.primal.net'], identifier: 'jokes-api', name: 'Jokes API', urls: [ 'https://jokes.example.com', // clearnet 'http://jokesxyz...onion', // Tor hidden service 'https://jokes.example.hns', // Handshake domain ], about: 'A joke-telling service behind an L402 paywall', pricing: [{ capability: 'get_joke', price: 1, currency: 'sats' }], paymentMethods: [['l402', 'lightning']], topics: ['comedy', 'ai'], }) ``` Each URL becomes a separate `url` tag in the kind 31402 event. Clients try them in order and use whichever they can reach. `urls` accepts 1–10 entries; any parseable URL is valid. See [`examples/`](./examples) for runnable scripts. ## How it fits together ```mermaid flowchart LR subgraph "Your Server" API[Your API] TB[toll-booth
L402 paywall] AN[402-announce] end subgraph "Nostr Network" R1[(Relay 1)] R2[(Relay 2)] end subgraph "AI Agent" MCP[402-mcp
discovery + payment] end API --> TB TB -->|"protects"| API AN -->|"kind 31402"| R1 AN -->|"kind 31402"| R2 R1 -->|"subscribe"| MCP R2 -->|"subscribe"| MCP MCP -->|"HTTP + L402 token"| TB style TB fill:#ef4444,color:#fff style AN fill:#f59e0b,color:#000 style MCP fill:#3b82f6,color:#fff ``` 1. **[toll-booth](https://github.com/forgesworn/toll-booth)** wraps your API with an L402 paywall 2. **402-announce** publishes a kind 31402 event describing the service, pricing, and payment methods 3. **[402-mcp](https://github.com/forgesworn/402-mcp)** discovers the announcement, pays the invoice, and calls your API ## What it does - Builds and signs kind 31402 Nostr events - Publishes to one or more Nostr relays in parallel - Zeroises secret key bytes after use - Degrades gracefully when individual relays fail - Provides a `close()` handle for clean disconnection ## What it does not do - Does not run an L402 paywall (use [toll-booth](https://github.com/forgesworn/toll-booth) for that) - Does not subscribe to or search for announcements (use [402-mcp](https://github.com/forgesworn/402-mcp) for that) - Does not handle payments or token verification ## API ### `announceService(config): Promise` High-level function that builds, signs, and publishes the event to multiple relays. **Returns** an `Announcement` handle: - `eventId` — the published Nostr event ID - `pubkey` — the Nostr pubkey derived from your secret key - `close()` — disconnect from all relays ### `buildAnnounceEvent(secretKey, config): VerifiedEvent` Lower-level function that builds and signs the event without publishing. Useful if you manage relay connections yourself. ### Config options | Field | Type | Required | Description | |------------------|-------------------|----------|------------------------------------------------| | `secretKey` | `string` | yes | 64-char hex Nostr secret key | | `relays` | `string[]` | yes | Relay URLs (`wss://` or `ws://`) | | `identifier` | `string` | yes | Unique listing ID (Nostr `d` tag) | | `name` | `string` | yes | Human-readable service name | | `urls` | `string[]` | yes | HTTP endpoints for the service (1–10 entries, any parseable URL) | | `about` | `string` | yes | Short description | | `pricing` | `PricingDef[]` | yes | Per-capability pricing | | `paymentMethods` | `string[][]` | yes | Accepted payment methods (each entry is `[rail, ...params]`) | | `picture` | `string` | no | Icon URL | | `topics` | `string[]` | no | Topic tags for filtering | | `capabilities` | `CapabilityDef[]` | no | Capability details (stored in event content) | | `version` | `string` | no | Service version (stored in event content) | ## Announce flow ```mermaid flowchart TD A[announceService config] --> B{Validate} B -->|"secretKey: 64-char hex"| C[Derive pubkey] B -->|"relays: wss:// URLs"| C C --> D[Build kind 31402 event] D --> E[Sign with secret key] E --> F[Zeroise key bytes] F --> G["Connect relays (parallel, 10s timeout)"] G --> H{Each relay} H -->|success| I[Publish event] H -->|failure| J[Log warning, continue] I --> K["Return { eventId, pubkey, close() }"] J --> K ``` ## Event format 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. ```mermaid graph TB subgraph "Kind 31402 Event" direction TB subgraph Tags D["d: jokes-api"] N["name: Jokes API"] U1["url: https://jokes.example.com"] U2["url: https://jokesapi.example.onion (optional)"] AB["about: A joke-telling service"] PMI1["pmi: l402, lightning"] PMI2["pmi: cashu"] P["price: get_joke, 1, sats"] T["t: comedy"] end subgraph Content["Content (JSON)"] CAP["capabilities: [{ name, description }]"] VER["version: 1.0.0"] end end style Tags fill:#1e293b,color:#e2e8f0 style Content fill:#1e293b,color:#e2e8f0 ``` ### Tags | Tag | Required | Description | Example | |-----------|----------|----------------------------------------------|-----------------------------------| | `d` | yes | Unique identifier for this listing | `jokes-api` | | `name` | yes | Human-readable service name | `Jokes API` | | `url` | yes | HTTP endpoint (one tag per URL; repeatable) | `https://jokes.example.com` | | `about` | yes | Short description | `A joke-telling service` | | `pmi` | yes | Payment method identifier (repeatable, multi-element) | `l402`, `lightning` | | `price` | yes | Capability pricing (repeatable) | `get_joke`, `1`, `sats` | | `t` | no | Topic tag for search/filtering (repeatable) | `comedy` | | `picture` | no | Icon URL | `https://example.com/icon.png` | ### Recognised Payment Method Rails Each `paymentMethods` entry is an array where the first element is the rail identifier and subsequent elements are rail-specific parameters. | Rail | Example | Description | |------|---------|-------------| | `l402` | `['l402', 'lightning']` | L402 protocol (Lightning BOLT-11 invoices) | | `cashu` | `['cashu']` | Cashu ecash (generic) | | `xcashu` | `['xcashu']` | Cashu ecash via NUT-24 (X-Cashu header) | | `lnurlcash` | `['lnurlcash', 'mint.example']` | LUD-25 bearer note presented in an X-LNURLcash header (subsequent elements are the accepted mint hosts) | | `x402` | `['x402', 'base', 'usdc', '0xabc...']` | x402 stablecoin payments (chain, token, receiver address) | | `payment` | `['payment', 'lightning']` | IETF Payment protocol | ### Content The event content is a JSON object with optional fields: ```json { "capabilities": [ { "name": "get_joke", "description": "Returns a random joke" } ], "version": "1.0.0" } ``` ## Multiple URLs vs multiple events This distinction is important for operators: **Multiple URLs in one event** — use `urls: ['...', '...']` when the URLs represent the **same service** on different transports (clearnet, Tor, Handshake). The pricing, credentials, and macaroon signing key are identical. Clients pick whichever URL they can reach. This is for censorship resistance and redundancy. **Separate kind 31402 events** — publish a new event (different `identifier`) when you have **genuinely different services**: different pricing tiers per transport, different capabilities, or services that operate independently. A single event should describe one logical service. In short: same service + different network paths → one event with multiple `url` tags. Different services → separate events. ## Security - Secret key bytes are zeroised immediately after signing (in both `announceService` and `buildAnnounceEvent`) - Never log or persist the secret key — pass it in and let the library handle cleanup - Relay connections use a 10-second timeout to prevent hanging - Individual relay failures are logged as warnings but don't reject the promise ## Ecosystem | Package | Purpose | |---------|---------| | [toll-booth](https://github.com/forgesworn/toll-booth) | L402 middleware — any API becomes a toll booth in minutes | | [toll-booth-announce](https://github.com/forgesworn/toll-booth-announce) | Bridge — announce toll-booth services with one function call | | [satgate](https://github.com/forgesworn/satsgate) | Production L402 gateway with Lightning and Cashu support | | [402-mcp](https://github.com/forgesworn/402-mcp) | MCP server for AI agents to discover, pay, and consume 402 APIs | | [Live Dashboard](https://402.pub) | See every service announcing on the network | ## Part of the ForgeSworn Toolkit [ForgeSworn](https://forgesworn.dev) builds open-source cryptographic identity, payments, and coordination tools for Nostr. | Library | What it does | |---------|-------------| | [nsec-tree](https://github.com/forgesworn/nsec-tree) | Deterministic sub-identity derivation | | [ring-sig](https://github.com/forgesworn/ring-sig) | SAG/LSAG ring signatures on secp256k1 | | [range-proof](https://github.com/forgesworn/range-proof) | Pedersen commitment range proofs | | [canary-kit](https://github.com/forgesworn/canary-kit) | Coercion-resistant spoken verification | | [spoken-token](https://github.com/forgesworn/spoken-token) | Human-speakable verification tokens | | [toll-booth](https://github.com/forgesworn/toll-booth) | L402 payment middleware | | [geohash-kit](https://github.com/forgesworn/geohash-kit) | Geohash toolkit with polygon coverage | | [nostr-attestations](https://github.com/forgesworn/nostr-attestations) | NIP-VA verifiable attestations | | [dominion](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control | | [nostr-veil](https://github.com/forgesworn/nostr-veil) | Privacy-preserving Web of Trust | ## Licence [MIT](./LICENSE)