Skip to content

forgesworn/402-announce

Repository files navigation

402-announce

Nostr: npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2

Announce HTTP 402 services on Nostr for decentralised discovery. Supports both L402 and x402 payment protocols.

CI Coverage MIT Licence

Publishes kind 31402 parameterised replaceable events so that AI agents (and any Nostr client) can discover paid APIs without a central registry.

Live Dashboard — see every service announcing on the network right now.

How it works

flowchart LR
    A[Your API] -->|"npm install<br/>402-announce"| B[402-announce]
    B -->|"kind 31402<br/>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
Loading

Your API publishes a service announcement to Nostr relays. AI agents (via 402-mcp) discover it, pay the invoice, and consume the API. No central registry required.

Quick start

npm install 402-announce
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):

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/ for runnable scripts.

How it fits together

flowchart LR
    subgraph "Your Server"
        API[Your API]
        TB[toll-booth<br/>L402 paywall]
        AN[402-announce]
    end

    subgraph "Nostr Network"
        R1[(Relay 1)]
        R2[(Relay 2)]
    end

    subgraph "AI Agent"
        MCP[402-mcp<br/>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
Loading
  1. 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 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 for that)
  • Does not subscribe to or search for announcements (use 402-mcp for that)
  • Does not handle payments or token verification

API

announceService(config): Promise<Announcement>

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

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
Loading

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.

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
Loading

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)
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:

{
  "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 L402 middleware — any API becomes a toll booth in minutes
toll-booth-announce Bridge — announce toll-booth services with one function call
satgate Production L402 gateway with Lightning and Cashu support
402-mcp MCP server for AI agents to discover, pay, and consume 402 APIs
Live Dashboard See every service announcing on the network

Licence

MIT

About

Announce HTTP 402 services on Nostr for decentralised discovery. Kind 31402 parameterised replaceable events.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors