402发通知
实验存储发现 现在有一个单独的,没有钥匙的API 对于公众付费
包和免费镜像掩蔽。 见 第1版合同, (中文(简体) ),
杰森·斯克马 财务报告和审定财务报表
已签名的互操作性固定装置。 。 。 .
buildStorageAnnouncement 为外部签名者创建未签名的事件;
acceptSignedStorageAnnouncement 核实准确的返回事件
selectStorageAnnouncements 保存已过期/已撤销的替换记录.
这些API不发布,联系存储节点或购买存储. 现有
已支付 API announceService 合同不变.
诺斯特: npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2
在Nostr上宣布HTTP 402服务 用于分散的发现. 支持L402和x402支付协议.
出版物 类型31402 参数化可替换的事件,以便AI代理(和任何诺斯特客户端)可以在没有中央注册的情况下发现付费API.
活盘 立刻在网络上公布.
如何运作
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
您的API 向 Nostr 中继器发布服务公告 。 人工智能特工(通过 402-mcp (中文(简体) )发现后支付发票,消费API. 无需中央登记处.
快开始
npm install 402-announce
402 通知只是无害环境管理。 它的船没有共同JS的建造及其 exports 地图没有 require 条件,所以 require('402-announce') 失败 ERR_PACKAGE_PATH_NOT_EXPORTED启用 import,或 await import('402-announce') 从普通JS.
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()
多种运输实例 (清网+Tor + 握手-相同服务,不同接入路径):
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'],
})
每个 URL 成为单独的 url 标记于 31402 类事件。 客户端按顺序尝试,并使用可以达到的任意方法.
urls 接受1至10个条目。 并不是每个可解析的 URL 被接受: 见 URL和中继验证。 。 。 .
见 examples/ gitHub 仓库中可运行脚本的目录。 它不包含在 npm 包中 .
如何融为一体
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
- 收费 用 L402 付费墙包裹您的 API
- 402发通知 发布描述服务、定价和支付方法的31402类事件
- 402-mcp (中文(简体) ) 发现通知,支付发票,并拨打你的API
它会做什么
- 构建和标志 31402 诺斯特事件
- 以平行方式发布给一个或多个 Nostr 中继器
- 使用后 Zeroise 密钥字节
- 当单个接力失败时(如果每次接力失败,仍能解决;见
announceService(中文(简体) ) - 规定 a
close()干净断开的控件
它不会做的
- 不运行 L402 付费墙( 使用) 收费 为此)
- 不订阅或搜索通知(使用 402-mcp (中文(简体) ) 为此)
- 不处理付款或符号核查
API 密码
announceService(config): Promise<Announcement>
高级功能,用于构建,签名,并发布事件给多个中继器.
回返 a 一个 Announcement 控件 :
eventId: 已签名的诺斯特事件的IDpubkey: 从您的密钥中衍生出来的诺斯特公钥close(): 断开所有中继器( 同步; 在关闭时调用, 以避免泄露 WebSockets)
拒绝 (这是一个 async 当配置无效时: a secretKey 这不是64个十六进制字符, 一个空接力列表, 超过50个接力, 一个不是的接力 URL ws:// 或者说 wss://,在私人或回放主机、服务中拥有证书或分数 urls 列表中没有公共 URL,或者任何 buildAnnounceEvent 验证失败.
中继失败永远不会拒绝. 每个中继器都与10秒的连接超时同时进行试验,并记录故障 console.warn如果每次中继失败, announceService 仍然决定与 eventId 和日志 [402-announce] No relays accepted the event编辑 Announcement handle不报告有多少继电器接受事件,所以警告是唯一的信号. 如果您需要知道至少一个中继存储了事件,请拨 buildAnnounceEvent 然后自己出版,检查每个 relay.publish() 结果.
buildAnnounceEvent(secretKey, config): VerifiedEvent
下层功能,用于构建和签名事件而不发布. 如果你自己管理中继连接,那就有用了. config 是一个 AnnounceConfig 无 relays。它会同步投放无效输入,不应用“至少一个公共URL”规则.
isPrivateHost(hostname): boolean
SSRF负责保护 announceService 适用于转发和服务 URL,导出为您自己使用。 它需要一个赤裸的主机名( 不是 URL) 然后返回 true 用于:
localhost, (中文(简体) ),*.localhost- IPv6 请检查url=值 (帮助)
::1, (中文(简体) ),::链接 - 本地fe80::/10,独特的本地fc00::/7,泰雷多语2001:0000::/32,和 IPv4-映射, IPv4-相容或 6to4 形式嵌入私人 IPv4 地址 - IPv4 请检查url=值 (帮助)
0.0.0.0/8, (中文(简体) ),10.0.0.0/8, (中文(简体) ),100.64.0.0/10, (中文(简体) ),127.0.0.0/8, (中文(简体) ),169.254.0.0/16, (中文(简体) ),172.16.0.0/12, (中文(简体) ),192.0.0.0/24, (中文(简体) ),192.0.2.0/24, (中文(简体) ),192.88.99.0/24, (中文(简体) ),192.168.0.0/16, (中文(简体) ),198.18.0.0/15, (中文(简体) ),198.51.100.0/24, (中文(简体) ),203.0.113.0/24财务报告和审定财务报表240.0.0.0/4 - 八进制,十六进制,单整数和两相或三相接的接头IPv4拼写法(例如:
0x7f000001, (中文(简体) ),2130706433, (中文(简体) ),127.1(中文(简体) )
它只检查字符串,从来不解决DNS. 它不标出多播地址, 例如 : 224.0.0.1。 。 。 .
常数和类型
L402_ANNOUNCE_KIND:事件的类型,31402AnnounceConfig, (中文(简体) ),Announcement, (中文(简体) ),PricingDef, (中文(简体) ),CapabilityDef: 纯类型导出
interface PricingDef {
capability: string // 1-64 characters, no control characters
price: number // finite and non-negative
currency: string // any non-blank string up to 32 characters, no control characters
}
interface CapabilityDef {
name: string // up to 128 characters
description: string // up to 4096 characters
endpoint?: string // starts with '/', 'http://' or 'https://'; up to 2048 characters
schema?: unknown // JSON Schema for the input
outputSchema?: unknown // JSON Schema for the output
}
currency 是一个自由形式的标签,而不是一个引用: 库不对照允许的值列表来检查,以及 'sats' 仅是整个文档中使用的价值。 客户是否理解另一个单位,取决于该客户. pricing 需要1至100个条目和 capabilities 最多100个。 整个事件内容可能达到最多64个KiB和20个等级的筑巢.
配置选项
| 外地 | 类型 | 需求 | 说明 |
|---|---|---|---|
secretKey | string | 对 | 64 Char 十六进制鼻音密钥 |
relays | string[] | 对 | 中继 URL (E)wss:// 或者说 ws://(中文(简体) ) |
identifier | string | 对 | 独一无二的上市ID( Nostr) d 标签) |
name | string | 对 | 可读服务名称 |
urls | string[] | 对 | 服务运输端点(1-10个独特的条目,每个条目最多可达2048个字符;一些方案被拒绝 announceService 需要一个公开的 URL,参见 URL和中继验证(中文(简体) ) |
about | string | 对 | 简短说明 |
pricing | PricingDef[] | 对 | 个人能力定价 |
paymentMethods | string[][] | 对 | 接受付款方法(每个条目为: [rail, ...params](中文(简体) ) |
picture | string | 没有 | 图标 URL |
topics | string[] | 没有 | 过滤的主题标签 |
capabilities | CapabilityDef[] | 没有 | 能力细节(存储于事件内容) |
version | string | 没有 | 服务版本( 存储于事件内容) |
URL和中继验证
两者 buildAnnounceEvent 财务报告和审定财务报表 announceService 拒绝一个 urls 条目:
- 无法解析
new URL(),大于2048个字符或包含控件字符 - 使用此方案
data:, (中文(简体) ),file:, (中文(简体) ),javascript:, (中文(简体) ),blob:或者说vbscript: - 复制另一个条目
buildAnnounceEvent 停止那里并接受私人主机,因为它只是串行. announceService 还需要至少一个公共 URL: a http: 或者说 https: 主机名的 URL isPrivateHost 不符, a .onion 主机名,或任何其他方案(后两个跳过主机检查). 其余的条目可能是私人的.
中继 URL 更为严格 : ws:// 或者说 wss:// 只有,没有嵌入式证书,没有私有或回回机主机,最多在去除后50个. A 类 ws:// 中继记录一个警告.
这些检查只读取主机名文本 。 没有DNS分辨率,因此DNS重新绑定没有被捕获;部署在网络层的Egress控制后.
存储通知 API
此 README 顶部描述的存储扩展是无密钥的: 它构建未签名的事件, 验证外部签名器返回的事件, 并在它们之间选择 。 它不发布、联系存储节点或购买存储,而是与 announceService。不要将存储描述传递给 announceService 或者说 buildAnnounceEvent。 。 。 .
导入路径 。 这6个功能从 402-announce/storage 子路径, 也来自套件根 :
import { buildStorageAnnouncement } from '402-announce/storage'
// or: import { buildStorageAnnouncement } from '402-announce'
子路径只包含六个功能和 ParsedStorageAnnouncement 类型。 常数( E) :STORAGE_PROFILE, (中文(简体) ), STORAGE_SERVICE_PATH, (中文(简体) ), STORAGE_MAX_EVENT_BYTES, (中文(简体) ), STORAGE_MAX_TTL_SECONDS, (中文(简体) ), STORAGE_DEFAULT_MAX_AGE_SECONDS说明类型(例如: StorageServiceDescription 财务报告和审定财务报表 StorageReadOptions)只从根导出.
| 函数 | 签署 | 目的 |
|---|---|---|
buildStorageAnnouncement | (value: unknown, createdAt?: number) => EventTemplate | 验证一个描述并返回一个外部签名符的未签名的 31402 事件 |
acceptSignedStorageAnnouncement | (value: unknown, requested: EventTemplate, options: StorageReadOptions) => ParsedStorageAnnouncement | 校验返回的事件, 并且只有在它与当前事件相同时才接受它 requested (同样善良, created_at,标签和内容) |
parseStorageAnnouncement | (value: unknown, options: StorageReadOptions) => ParsedStorageAnnouncement | 校验一个签名的事件, 并返回其中的描述和 a freshness 联 合 国 'current', (中文(简体) ), 'stale' 或者说 'expired' |
selectStorageAnnouncements | (values: readonly unknown[], options: StorageReadOptions) => ParsedStorageAnnouncement[] | 保持每个小酒鬼最新的有效记录 d 标记,包括已过期和已撤回的;无效的记录被跳过 |
validateStorageDescription | (value: unknown) => StorageServiceDescription | 严格验证和复制一个公开的 HTTP 描述 |
validateStorageEndpointBinding | (value: unknown, announcement: ParsedStorageAnnouncement, selectedOrigin: string, options: StorageReadOptions) => StorageServiceDescription | 对照签名的身份、列名和选定来源检查新获取的描述 |
StorageReadOptions 实值 { now: number; maxAgeSeconds?: number; clockSkewSeconds?: number },与 now 在Unix秒内。 函数抛出 Error('Invalid storage service profile.') 输入无效. selectStorageAnnouncements 跳过无效的记录取而代之,但仍为无效选项丢出或超过512个记录.
流量为构建,对外签名,接受,选择,再绑定到终点. 下面的例子使用一个丢弃的本地密钥作为外部签名器的站口(nostr-tools 是此软件包的依赖性 。 在生产中,密钥从未输入此代码 .
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'
import {
acceptSignedStorageAnnouncement,
buildStorageAnnouncement,
selectStorageAnnouncements,
validateStorageEndpointBinding,
} from '402-announce/storage'
const now = Math.floor(Date.now() / 1000)
const signerKey = generateSecretKey() // stand-in for an external signer
const description = {
profile: 'forgesworn-storage-service-v1',
service_pubkey: getPublicKey(signerKey),
identifier: 'open-shelter-node',
name: 'Open shelter node',
about: 'Best-effort mirror shelter for signed blobs.',
origins: ['https://storage.example/'],
max_blob_bytes: 1073741824,
observed_at: now,
expires_at: now + 7200,
state: 'published',
checkout: null,
services: [{
id: 'open-shelter',
type: 'free-mirror',
revision: 1,
availability: 'accepting',
operation: 'mirror',
authorisation: 'bud-11',
retention: 'best-effort',
eviction_policy: 'Guest mirrors can be evicted under storage pressure.',
}],
}
// 1. Build the unsigned event. No key or network access.
const requested = buildStorageAnnouncement(description)
// 2. Sign it externally. The signer must not change the template.
const signed = finalizeEvent(requested, signerKey)
// 3. Accept the returned event. Throws if the signature, freshness or template differ.
const accepted = acceptSignedStorageAnnouncement(signed, requested, { now })
// Publishing accepted.event to relays is a separate step that you perform.
// 4. Later, a reader selects the newest record per listing from relay results.
const listings = selectStorageAnnouncements([accepted.event], { now })
const [listing] = listings
// Keep `listings` as watermarks. Show only current, published, accepting services.
// 5. After separately fetching the node's description (GET /services/v1, here reusing
// `description`), check it matches the signed listing and the chosen origin.
const fetched = description
const current = validateStorageEndpointBinding(fetched, listing, 'https://storage.example/', { now })
console.log(current.identifier)
支付存储列表 checkout 对象和 paid-storage 服务。 见 第1版合同 每个字段和 签名固定装置 已支付、合并、撤回和过期的例子.
宣布流量
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
事件格式
每个通知都是 类型31402 参数化可替换事件。 组合 pubkey + 键 d 标记独有地识别列表——再次以相同值发布更新了现有的列表.
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
标记
| 标记 | 需求 | 说明 | 示例 |
|---|---|---|---|
d | 对 | 此列表的独特标识符 | jokes-api |
name | 对 | 可读服务名称 | Jokes API |
url | 对 | HTTP 端点( 每个 URL 一个标签; 可重复) | https://jokes.example.com |
about | 对 | 简短说明 | A joke-telling service |
pmi | 对 | 支付方法标识符(可重复、多要素) | l402, (中文(简体) ), lightning |
price | 对 | 能力定价(可重复) | get_joke, (中文(简体) ), 1, (中文(简体) ), sats |
t | 没有 | 搜索/过滤的主题标签( 可重复) | comedy |
picture | 没有 | 图标 URL | https://example.com/icon.png |
公认付款方法铁路
每个 paymentMethods 条目是第一个元素是铁路标识符,随后的元素是铁路专用参数的数组.
| 铁路 | 示例 | 说明 |
|---|---|---|
l402 | ['l402', 'lightning'] | L402协议(照明BOLT-11发票) |
cashu | ['cashu'] | 现金(普通) |
xcashu | ['xcashu'] | Cashu 电子现金通过 NUT-24(X-Cashu 头) |
lnurlcash | ['lnurlcash', 'mint.example'] | 在X-LNURLCASH标题中呈现的LUD-25无记名纸条(随后的元素是被接受的薄荷主机) |
x402 | ['x402', 'base', 'usdc', '0xabc...'] | x402 稳定币付款( 链、 令牌、 接收地址) |
payment | ['payment', 'lightning'] | IETF 支付协议 |
内容
事件内容是一个带有可选字段的JSON对象. 每种能力也可携带 endpoint, (中文(简体) ), schema 财务报告和审定财务报表 outputSchema (见 CapabilityDef 上文:
{
"capabilities": [
{ "name": "get_joke", "description": "Returns a random joke", "endpoint": "/api/joke" }
],
"version": "1.0.0"
}
多个 URL 对多个事件
这种区分对操作者很重要:
一次事件的多个 URL ——使用 urls: ['...', '...'] 当 URL 代表 同一服务 不同运输方式(清网、Tor、握手)。 定价、证书和马卡龙签名密钥相同。 客户端选择可以达到的URL 。 这是为了抵制审查 和冗余.
单独的31402事件 ——发布新活动(不同) identifier当你有 真正不同的服务:每辆运输不同的定价等级,不同的能力,或独立运营的服务. 单一事件应描述一种逻辑服务.
简而言之:同一服务+不同的网络路径 + 一个有多个事件 url 标签。 不同的服务——单独的活动.
警卫
- 密钥字节在签名后立即被零化( 在两个字节中)
announceService财务报告和审定财务报表buildAnnounceEvent(中文(简体) ) - 永远不要登录或坚持秘密密钥——让图书馆处理清理
- 中继连接使用10秒超时防止挂起
- 单个接力故障被记录为警告,但不要拒绝承诺. 如果每一个中继失败,
announceService仍然决定与eventId和日志[402-announce] No relays accepted the event - 以私有模式和回回模式检查中继和服务 URL(见 URL和中继验证); DNS 重新绑定没有被捕获
生态系统
| 软件包 | 目的 |
|---|---|
| 收费 | L402 中间软件——任何API在几分钟内成为收费站 |
| 罚单 | Bridge – 宣布一个功能呼叫的收费站服务 |
| 配置门 | 提供闪电和Cashu支助的生产L402网关 |
| 402-mcp (中文(简体) ) | MCP 服务器,供 AI 代理商发现、支付和消费 402 API |
| 活盘 | 查看网络上宣布的每个服务 |
ForgeSworn 工具包的一部分
福吉斯沃恩语Name 为诺斯特建立开源加密身份,支付和协调工具.
| 图书馆 | 它会做什么 |
|---|---|
| 树 | 决定性的子标识衍生 |
| 环西格 | 秒数256k1上的 SAG/LSAG 环签名 |
| 区域防护 | 佩德森承诺范围证明 |
| 金丝雀 | 胁迫性口语核查 |
| 手语 | 人称核查标志 |
| 收费 | L402 支付中间软件 |
| 土拨鼠 | 覆盖多边形的Geohash工具包 |
| 节点证明 | NIP-VA可核查的证明 |
| 统治权 | 基于 Epoch 的加密访问控制 |
| 鼻孔微风 | 隐私保护信任网 |