nostr-dvms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNostr Data Vending Machines (NIP-90)
Nostr数据自动售货机(NIP-90)
Overview
概述
Build AI and compute services on Nostr using the Data Vending Machine protocol.
DVMs follow a simple pattern: customers publish job requests (kind 5000-5999),
service providers process them and return results (kind 6000-6999), with
optional feedback events (kind 7000) for status updates and payment negotiation.
使用数据自动售货机协议在Nostr上构建AI和计算服务。DVM遵循简单的运行模式:用户发布任务请求(5000-5999类),服务提供商处理请求并返回结果(6000-6999类),同时支持可选的反馈事件(7000类)用于状态更新和支付协商。
When to Use
适用场景
- Building a DVM service provider that processes job requests
- Creating a client that publishes DVM job requests and handles results
- Implementing payment flows for DVM services (bid, amount, bolt11)
- Chaining multiple DVM jobs (output of one feeds into another)
- Adding encrypted parameters to DVM requests for privacy
- Publishing NIP-89 handler announcements for DVM discoverability
- Handling DVM feedback states (processing, payment-required, error, partial)
Do NOT use when:
- Building general Nostr events (use nostr-event-builder)
- Implementing relay WebSocket logic
- Working with Lightning payments outside the DVM context
- 构建处理任务请求的DVM服务提供商
- 开发可发布DVM任务请求并处理结果的客户端
- 实现DVM服务的支付流程(出价、金额、bolt11)
- 串联多个DVM任务(前一个任务的输出作为后一个的输入)
- 为DVM请求添加加密参数保障隐私
- 发布NIP-89处理程序公告提升DVM可发现性
- 处理DVM反馈状态(处理中、需支付、错误、部分完成)
禁止使用场景:
- 构建通用Nostr事件(请使用nostr-event-builder)
- 实现中继WebSocket逻辑
- 处理DVM场景外的闪电网络支付
Workflow
工作流
1. Determine Your Role
1. 确定你的角色
Ask: "Am I building a service provider, a client, or both?"
| Role | Publishes | Subscribes To |
|---|---|---|
| Service Provider | kind:6xxx results | kind:5xxx requests |
| kind:7000 feedback | kind:5 cancellations | |
| kind:31990 NIP-89 info | ||
| Customer/Client | kind:5xxx requests | kind:6xxx results |
| kind:5 cancellations | kind:7000 feedback |
先明确:“我要构建的是服务提供商、客户端,还是两者都有?”
| 角色 | 发布内容 | 订阅内容 |
|---|---|---|
| 服务提供商 | kind:6xxx 结果 | kind:5xxx 请求 |
| kind:7000 反馈 | kind:5 取消请求 | |
| kind:31990 NIP-89 信息 | ||
| 消费者/客户端 | kind:5xxx 请求 | kind:6xxx 结果 |
| kind:5 取消请求 | kind:7000 反馈 |
2. Choose the Job Kind
2. 选择任务类型编号
Each DVM operation has a specific kind number. The result kind is always request
kind + 1000.
| Request Kind | Result Kind | Description |
|---|---|---|
| 5000 | 6000 | Text extraction |
| 5001 | 6001 | Summarization |
| 5002 | 6002 | Translation |
| 5003 | 6003 | Text generation |
| 5005 | 6005 | Content discovery/recommendation |
| 5050 | 6050 | Text-to-speech |
| 5100 | 6100 | Image generation |
| 5250 | 6250 | Event publishing |
See references/dvm-kinds.md for the full registry.
每个DVM操作都有对应的类型编号,结果类型编号始终为请求类型编号+1000。
| 请求类型编号 | 结果类型编号 | 描述 |
|---|---|---|
| 5000 | 6000 | 文本提取 |
| 5001 | 6001 | 内容摘要 |
| 5002 | 6002 | 翻译 |
| 5003 | 6003 | 文本生成 |
| 5005 | 6005 | 内容发现/推荐 |
| 5050 | 6050 | 语音合成 |
| 5100 | 6100 | 图像生成 |
| 5250 | 6250 | 事件发布 |
完整注册表请查看 references/dvm-kinds.md。
3. Build the Job Request (Customer Side)
3. 构建任务请求(消费者端)
Construct a kind 5000-5999 event:
json
{
"kind": 5001,
"content": "",
"tags": [
["i", "<data>", "<input-type>", "<relay>", "<marker>"],
["output", "<mime-type>"],
["relays", "wss://relay.example.com"],
["bid", "<msat-amount>"],
["t", "<topic-tag>"],
["p", "<preferred-service-provider-pubkey>"]
]
}Input types — the second element of the tag:
i| Type | Meaning | Example |
|---|---|---|
| Raw text data, no resolution needed | |
| URL to fetch content from | |
| Reference to a Nostr event by ID | |
| Output of a previous DVM job (for chaining) | |
Optional tags:
- — requested MIME type for the result (e.g.,
output)text/plain - — key/value parameters:
param["param", "lang", "es"] - — max millisats the customer will pay
bid - — where service providers should publish responses
relays - — preferred service provider pubkey (others MAY still respond)
p - — topic tags for categorization
t
构造一个5000-5999类的事件:
json
{
"kind": 5001,
"content": "",
"tags": [
["i", "<data>", "<input-type>", "<relay>", "<marker>"],
["output", "<mime-type>"],
["relays", "wss://relay.example.com"],
["bid", "<msat-amount>"],
["t", "<topic-tag>"],
["p", "<preferred-service-provider-pubkey>"]
]
}输入类型 —— 标签的第二个元素:
i| 类型 | 含义 | 示例 |
|---|---|---|
| 原始文本数据,无需解析 | |
| 可获取内容的URL | |
| 通过ID引用Nostr事件 | |
| 前一个DVM任务的输出(用于任务串联) | |
可选标签:
- —— 期望的结果MIME类型(例如
output)text/plain - —— 键值对参数:
param["param", "lang", "es"] - —— 消费者愿意支付的最大毫秒聪金额
bid - —— 服务提供商发布响应的中继地址
relays - —— 首选服务提供商的公钥(其他服务商仍可响应)
p - —— 用于分类的主题标签
t
4. Handle Job Feedback (Kind 7000)
4. 处理任务反馈(7000类)
Service providers send feedback events to communicate status:
json
{
"kind": 7000,
"content": "",
"tags": [
["status", "<status>", "<extra-info>"],
["amount", "<msat>", "<bolt11>"],
["e", "<job-request-id>", "<relay-hint>"],
["p", "<customer-pubkey>"]
]
}Status values:
| Status | Meaning | Action Required |
|---|---|---|
| SP requires payment before continuing | Customer must pay |
| SP is actively working on the job | Wait for result |
| SP could not process the job | Check extra-info for why |
| SP completed the job | Result incoming |
| SP has partial results (content may have samples) | More results coming |
Critical: is a hard gate — the SP will NOT proceed until
paid. Other statuses are informational.
payment-requiredThe field MAY contain partial results (e.g., a sample of processed
output) for any feedback status.
content服务提供商通过反馈事件同步状态:
json
{
"kind": 7000,
"content": "",
"tags": [
["status", "<status>", "<extra-info>"],
["amount", "<msat>", "<bolt11>"],
["e", "<job-request-id>", "<relay-hint>"],
["p", "<customer-pubkey>"]
]
}状态值说明:
| 状态 | 含义 | 所需操作 |
|---|---|---|
| 服务提供商需要先收到付款才能继续处理 | 消费者完成支付 |
| 服务提供商正在处理任务 | 等待结果返回 |
| 服务提供商无法处理该任务 | 查看额外信息了解错误原因 |
| 服务提供商已完成任务 | 即将返回结果 |
| 服务提供商已生成部分结果(content可能包含样本) | 后续会返回更多结果 |
关键说明: 是硬性门槛,未收到付款前服务提供商不会继续处理。其他状态仅为信息通知。
payment-required任意反馈状态的字段都可能包含部分结果(例如处理输出的样本)。
content5. Publish Job Results (Service Provider Side)
5. 发布任务结果(服务提供商端)
Result kind = request kind + 1000 (e.g., 5001 → 6001):
json
{
"kind": 6001,
"content": "<result-payload>",
"tags": [
["request", "<stringified-original-job-request-event>"],
["e", "<job-request-id>", "<relay-hint>"],
["i", "<original-input-data>"],
["p", "<customer-pubkey>"],
["amount", "<msat>", "<optional-bolt11>"]
]
}Required tags:
- — the FULL original job request event as a stringified JSON string
request - — references the job request event ID
e - — the customer's pubkey (so they can find the result)
p
Important: The tag value is the entire job request event
serialized as a JSON string, not just the event ID.
request结果类型编号 = 请求类型编号 + 1000(例如5001 → 6001):
json
{
"kind": 6001,
"content": "<result-payload>",
"tags": [
["request", "<stringified-original-job-request-event>"],
["e", "<job-request-id>", "<relay-hint>"],
["i", "<original-input-data>"],
["p", "<customer-pubkey>"],
["amount", "<msat>", "<optional-bolt11>"]
]
}必填标签:
- —— 完整的原始任务请求事件序列化后的JSON字符串
request - —— 关联任务请求的事件ID
e - —— 消费者的公钥(方便消费者检索到结果)
p
重要提示: 标签的值是完整序列化后的任务请求事件,而不仅仅是事件ID。
request6. Handle Payments
6. 处理支付
The payment model is flexible by design:
- Customer bids: Include in the request
["bid", "<msat>"] - SP quotes: Include in feedback/result
["amount", "<msat>", "<bolt11>"] - Customer pays: Either pay the bolt11 invoice OR zap the result event
Customer Service Provider
| |
|-- kind:5001 (bid: 5000) ---->|
| |
|<-- kind:7000 ----------------| status: payment-required
| amount: 3000, bolt11:... |
| |
|-- pay bolt11 or zap -------->|
| |
|<-- kind:7000 ----------------| status: processing
| |
|<-- kind:6001 ----------------| result + amount tagSPs MUST use feedback to block until paid. They SHOULD NOT
silently wait for payment without signaling.
payment-required支付模型在设计上具备灵活性:
- 消费者出价: 在请求中添加标签
["bid", "<msat>"] - 服务商报价: 在反馈/结果中添加标签
["amount", "<msat>", "<bolt11>"] - 消费者支付: 支付bolt11发票,或者给结果事件发送zap
消费者 服务提供商
| |
|-- kind:5001 (出价: 5000) --->|
| |
|<-- kind:7000 ----------------| 状态: payment-required
| 金额: 3000, bolt11:... |
| |
|-- 支付bolt11或发送zap ------>|
| |
|<-- kind:7000 ----------------| 状态: processing
| |
|<-- kind:6001 ----------------| 结果 + 金额标签服务提供商必须使用反馈来暂停流程直到收到付款,不应在未通知的情况下静默等待付款。
payment-required7. Implement Job Chaining
7. 实现任务串联
Chain jobs by using the input type — the output of one job becomes the
input of the next:
jobjson
{
"kind": 5001,
"content": "",
"tags": [
["i", "<translation-job-event-id>", "job"],
["param", "lang", "en"]
]
}The service provider for job #2 watches for the result of job #1, then processes
it. Payment timing is at the SP's discretion — they may wait for the customer to
zap job #1's result before starting job #2.
Chaining example — translate then summarize:
Step 1: Publish kind:5002 (translation)
["i", "https://article.com/post", "url"]
["param", "lang", "en"]
Step 2: Publish kind:5001 (summarization)
["i", "<step-1-event-id>", "job"]通过输入类型实现任务串联,前一个任务的输出作为后一个任务的输入:
jobjson
{
"kind": 5001,
"content": "",
"tags": [
["i", "<translation-job-event-id>", "job"],
["param", "lang", "en"]
]
}第二个任务的服务提供商会监听第一个任务的结果,获取后进行处理。支付时机由服务提供商自行决定,他们可以选择等待消费者支付完第一个任务的结果后再开始处理第二个任务。
串联示例 —— 翻译后摘要:
步骤1: 发布kind:5002(翻译任务)
["i", "https://article.com/post", "url"]
["param", "lang", "en"]
步骤2: 发布kind:5001(摘要任务)
["i", "<step-1-event-id>", "job"]8. Add Encrypted Parameters (Optional)
8. 添加加密参数(可选)
For privacy, encrypt and tags using NIP-04 with the service
provider's pubkey:
iparam- Collect all and
itags into a JSON arrayparam - Encrypt with NIP-04 (customer's private key + SP's public key)
- Put encrypted payload in field
content - Add tag and
["encrypted"]tag["p", "<sp-pubkey>"] - Remove plaintext and
itags from the eventparam
json
{
"kind": 5001,
"content": "<nip04-encrypted-payload>",
"tags": [
["p", "<service-provider-pubkey>"],
["encrypted"],
["output", "text/plain"],
["relays", "wss://relay.example.com"]
]
}The SP decrypts the content to recover the input parameters. If the request was
encrypted, the result MUST also be encrypted and tagged .
["encrypted"]为了保障隐私,可以使用NIP-04协议,用服务提供商的公钥加密和标签:
iparam- 将所有和
i标签整理为JSON数组param - 使用NIP-04加密(消费者私钥 + 服务提供商公钥)
- 将加密后的 payload 放入字段
content - 添加标签和
["encrypted"]标签["p", "<sp-pubkey>"] - 移除事件中明文的和
i标签param
json
{
"kind": 5001,
"content": "<nip04-encrypted-payload>",
"tags": [
["p", "<service-provider-pubkey>"],
["encrypted"],
["output", "text/plain"],
["relays", "wss://relay.example.com"]
]
}服务提供商会解密内容获取输入参数。如果请求是加密的,返回的结果也必须加密,并且添加标签。
["encrypted"]9. Publish Service Provider Discovery (NIP-89)
9. 发布服务提供商发现信息(NIP-89)
Advertise DVM capabilities with a kind:31990 handler announcement:
json
{
"kind": 31990,
"content": "{\"name\":\"My Summarizer DVM\",\"about\":\"AI-powered text summarization\"}",
"tags": [
["d", "<unique-identifier>"],
["k", "5001"],
["t", "summarization"],
["t", "ai"]
]
}- tag: the job request kind this DVM handles (e.g.,
k)5001 - tags: topic tags for discoverability
t - : JSON with
contentandnamefields (like kind:0 metadata)about
通过kind:31990处理程序公告来宣传DVM的能力:
json
{
"kind": 31990,
"content": "{\"name\":\"我的摘要DVM\",\"about\":\"AI驱动的文本摘要服务\"}",
"tags": [
["d", "<unique-identifier>"],
["k", "5001"],
["t", "summarization"],
["t", "ai"]
]
}- 标签:该DVM支持的任务请求类型编号(例如
k)5001 - 标签:用于提升可发现性的主题标签
t - :包含
content和name字段的JSON(类似kind:0元数据)about
10. Handle Cancellation
10. 处理任务取消
Customers cancel jobs by publishing a kind:5 deletion request:
json
{
"kind": 5,
"tags": [
["e", "<job-request-event-id>"],
["k", "5001"]
],
"content": "No longer needed"
}Service providers SHOULD monitor for kind:5 events tagging their active jobs and
stop processing if a cancellation is received.
消费者可以通过发布kind:5删除请求来取消任务:
json
{
"kind": 5,
"tags": [
["e", "<job-request-event-id>"],
["k", "5001"]
],
"content": "不再需要该任务"
}服务提供商应监听标记了其活跃任务的kind:5事件,收到取消请求后停止处理对应的任务。
Checklist
检查清单
- Job request uses correct kind (5000-5999) for the operation type
- Input tags use valid input-type (
i,text,url,event)job - Job result kind = request kind + 1000
- Result includes tag with full stringified job request event
request - Result includes tag referencing the job request event ID
e - Result includes tag with customer's pubkey
p - Feedback events use kind:7000 with valid status values
- feedback includes
payment-requiredtag with bolt11amount - Encrypted requests have tag and encrypted content
["encrypted"] - Encrypted results also use tag
["encrypted"] - Job chains use input type
["i", "<event-id>", "job"] - NIP-89 discovery uses kind:31990 with tag for supported job kind
k - Cancellation uses kind:5 with tag referencing the job request
e
- 任务请求使用了对应操作类型的正确编号(5000-5999)
- 输入标签使用了有效的输入类型(
i、text、url、event)job - 任务结果类型编号 = 请求类型编号 + 1000
- 结果包含标签,值为完整序列化的任务请求事件
request - 结果包含标签,关联任务请求的事件ID
e - 结果包含标签,值为消费者的公钥
p - 反馈事件使用kind:7000,并且状态值合法
- 反馈包含带bolt11的
payment-required标签amount - 加密请求带有标签且内容已加密
["encrypted"] - 加密结果也带有标签
["encrypted"] - 任务串联使用输入类型
["i", "<event-id>", "job"] - NIP-89发现信息使用kind:31990,带有标签说明支持的任务类型
k - 取消请求使用kind:5,带有标签关联任务请求
e
Example: Complete Summarization DVM Service Provider
示例:完整的摘要DVM服务提供商
Scenario: Build a service provider that handles kind:5001 summarization
requests.
场景: 构建一个处理kind:5001摘要请求的服务提供商。
Subscribe to job requests
订阅任务请求
typescript
const sub = relay.subscribe([{ kinds: [5001] }]);typescript
const sub = relay.subscribe([{ kinds: [5001] }]);Process a request
处理请求
typescript
async function handleJobRequest(event: NostrEvent) {
const customerPubkey = event.pubkey;
const jobId = event.id;
// 1. Send processing feedback
await publishEvent({
kind: 7000,
content: "",
tags: [
["status", "processing", "Starting summarization"],
["e", jobId, "wss://relay.example.com"],
["p", customerPubkey],
],
});
// 2. Extract input data
const inputTag = event.tags.find((t) => t[0] === "i");
const inputType = inputTag[2]; // "text", "url", "event", "job"
let inputData: string;
if (inputType === "text") {
inputData = inputTag[1];
} else if (inputType === "url") {
inputData = await fetch(inputTag[1]).then((r) => r.text());
} else if (inputType === "event") {
inputData = await fetchNostrEvent(inputTag[1], inputTag[3]);
}
// 3. Process the job
const summary = await summarize(inputData);
// 4. Publish result (kind = 5001 + 1000 = 6001)
await publishEvent({
kind: 6001,
content: summary,
tags: [
["request", JSON.stringify(event)],
["e", jobId, "wss://relay.example.com"],
["i", inputTag[1], inputTag[2]],
["p", customerPubkey],
["amount", "1000", generateBolt11(1000)],
],
});
}typescript
async function handleJobRequest(event: NostrEvent) {
const customerPubkey = event.pubkey;
const jobId = event.id;
// 1. 发送处理中反馈
await publishEvent({
kind: 7000,
content: "",
tags: [
["status", "processing", "开始生成摘要"],
["e", jobId, "wss://relay.example.com"],
["p", customerPubkey],
],
});
// 2. 提取输入数据
const inputTag = event.tags.find((t) => t[0] === "i");
const inputType = inputTag[2]; // "text", "url", "event", "job"
let inputData: string;
if (inputType === "text") {
inputData = inputTag[1];
} else if (inputType === "url") {
inputData = await fetch(inputTag[1]).then((r) => r.text());
} else if (inputType === "event") {
inputData = await fetchNostrEvent(inputTag[1], inputTag[3]);
}
// 3. 处理任务
const summary = await summarize(inputData);
// 4. 发布结果(类型 = 5001 + 1000 = 6001)
await publishEvent({
kind: 6001,
content: summary,
tags: [
["request", JSON.stringify(event)],
["e", jobId, "wss://relay.example.com"],
["i", inputTag[1], inputTag[2]],
["p", customerPubkey],
["amount", "1000", generateBolt11(1000)],
],
});
}Common Mistakes
常见错误
| Mistake | Why It Breaks | Fix |
|---|---|---|
| Result kind doesn't match request kind + 1000 | Clients can't correlate results to requests | kind:5001 → kind:6001, always add 1000 |
Missing | Clients can't verify the result matches their request | Include full stringified job request event |
| Spec requires the complete event JSON as a string | Use |
Using | Customer has no way to pay | Always include |
Forgetting | Customer can't find the result via subscription | Always tag the customer's pubkey |
| Encrypting request but not result | Leaks the output even though input was private | If request has |
Using wrong input-type in | SP can't resolve the input data | |
Chaining with | SP treats it as a static event, not a job output | Use |
| Not monitoring for kind:5 cancellations | Wastes compute on cancelled jobs | Subscribe to kind:5 events tagging active jobs |
NIP-89 announcement missing | Clients can't discover which kinds the DVM supports | Include |
| 错误 | 造成的问题 | 修复方案 |
|---|---|---|
| 结果类型编号不等于请求类型编号+1000 | 客户端无法关联结果和对应的请求 | kind:5001 → kind:6001,始终加1000 |
结果中缺少 | 客户端无法验证结果是否匹配自己的请求 | 包含完整序列化后的任务请求事件 |
| 规范要求值为完整的事件JSON字符串 | 使用 |
使用 | 消费者无法完成支付 | 始终添加 |
结果/反馈中忘记加 | 消费者无法通过订阅找到对应的结果 | 始终标记消费者的公钥 |
| 加密了请求但没有加密结果 | 即使输入是私密的,输出仍然会泄露 | 如果请求带有 |
| 服务提供商无法解析输入数据 | |
串联任务时使用 | 服务提供商会将其视为静态事件而非任务输出 | 串联时使用 |
| 未监听kind:5取消请求 | 在已取消的任务上浪费计算资源 | 订阅标记了活跃任务的kind:5事件 |
NIP-89公告缺少 | 客户端无法发现DVM支持的任务类型 | 为每个支持的任务类型添加 |
Key Principles
核心原则
-
Result kind = request kind + 1000 — This is the fundamental mapping. kind:5001 always produces kind:6001. No exceptions.
-
Thetag carries the full event — Not just the ID. The entire original job request event must be stringified and included so clients can verify the result matches their request without additional lookups.
request -
Payment is flexible, signaling is not — SPs can choose when to require payment, but they MUST usefeedback to signal it. Silent blocking creates a broken UX.
payment-required -
Encrypted in = encrypted out — If the job request uses encrypted params, the result MUST also be encrypted. Partial encryption leaks data.
-
Job chaining uses theinput type — Not
job. Theeventtype tells the SP to wait for and use the output of a previous DVM job, not just read a static event.job
-
结果类型编号 = 请求类型编号 + 1000 —— 这是基础映射规则,kind:5001永远返回kind:6001,没有例外。
-
标签包含完整事件 —— 而不只是ID。必须包含完整的原始任务请求事件序列化字符串,这样客户端无需额外查询就能验证结果是否匹配自己的请求。
request -
支付方式灵活,但通知规则固定 —— 服务提供商可以选择何时要求付款,但必须使用反馈来通知,静默阻塞会导致极差的用户体验。
payment-required -
加密入 → 加密出 —— 如果任务请求使用了加密参数,返回结果也必须加密,部分加密会导致数据泄露。
-
任务串联使用输入类型 —— 而非
job。event类型会告知服务提供商需要等待并使用前一个DVM任务的输出,而不是读取静态事件。job