高級
NIP-90 資料販賣機
Nostr 資料販賣機 (DVM) 標準,實現去中心化的 AI 計算和服務市場。
18 分鐘
概述
NIP-90 定義了「資料販賣機」(Data Vending Machines, DVM)標準, 建立一個去中心化的計算服務市場。客戶發布任務請求,服務提供者競爭完成任務並獲得報酬。 這是 Nostr 上的「Money in, data out」模式。
AI 計算市場
DVM 常用於 AI 相關服務:文字翻譯、圖片生成、語音轉文字、內容摘要等。 服務提供者可以運行 GPU 或 API 來處理任務並收取費用。
事件類型
NIP-90 保留了 kind 5000-7000:
| Kind 範圍 | 類型 | 說明 |
|---|---|---|
| 5000-5999 | 任務請求 | 客戶發布的任務 |
| 6000-6999 | 任務結果 | 服務者返回的結果(kind = 請求 + 1000) |
| 7000 | 任務回饋 | 狀態更新、報價、錯誤 |
常見任務類型
| Kind | 任務類型 | 說明 |
|---|---|---|
| 5000 | 文字生成 | LLM 文字生成 |
| 5001 | 文字轉語音 | TTS 語音合成 |
| 5002 | 翻譯 | 多語言翻譯 |
| 5003 | 語音轉文字 | STT 語音識別 |
| 5100 | 圖片生成 | AI 繪圖 |
| 5250 | 內容摘要 | 文章/影片摘要 |
任務請求 (Kind 5xxx)
{`{
"kind": 5002,
"content": "",
"tags": [
["i", "Hello, how are you?", "text"],
["param", "language", "zh-TW"],
["output", "text/plain"],
["relays", "wss://relay.damus.io", "wss://nos.lol"],
["bid", "1000"]
],
...
}`} 標籤說明
i- 輸入資料(可多個)param- 任務參數output- 期望的輸出格式relays- 監聽結果的中繼器bid- 願意支付的最高金額(毫聰)p- 指定服務提供者(選填)
輸入類型
| 類型 | 格式 | 說明 |
|---|---|---|
| text | ["i", "內容", "text"] | 直接文字輸入 |
| url | ["i", "https://...", "url"] | 遠端資源 URL |
| event | ["i", "<event-id>", "event", "<relay>"] | Nostr 事件 |
| job | ["i", "<job-id>", "job"] | 前一個任務的輸出 |
任務結果 (Kind 6xxx)
{`{
"kind": 6002,
"pubkey": "",
"content": "你好,你好嗎?",
"tags": [
["request", "<原始請求JSON>"],
["e", "", ""],
["p", ""],
["amount", "500", ""]
],
...
}`} 任務回饋 (Kind 7000)
服務提供者用來更新任務狀態:
{`{
"kind": 7000,
"content": "",
"tags": [
["status", "processing", "正在處理中..."],
["e", "", ""],
["p", ""]
],
...
}`} 狀態類型
| 狀態 | 說明 |
|---|---|
| payment-required | 需要先付款才能繼續 |
| processing | 任務正在處理中 |
| success | 任務完成 |
| partial | 部分結果(可能含預覽) |
| error | 處理失敗 |
付款流程
1
客戶發布請求
包含 bid 標籤表示願意支付的金額
2
服務者回應報價
發布 kind 7000 帶 payment-required 狀態和 BOLT11 發票
3
客戶付款
支付 BOLT11 發票或 Zap 任務事件
4
服務者交付結果
發布 kind 6xxx 結果事件
風險模式
服務提供者可自行決定風險模式:先付款後交付、先交付後收款、 或根據客戶信譽決定。這是一個自由市場。
實作
發布任務請求(客戶端)
{`import { finalizeEvent } from 'nostr-tools';
interface JobRequest {
kind: number;
inputs: Array<{
data: string;
type: 'text' | 'url' | 'event' | 'job';
relay?: string;
marker?: string;
}>;
params?: Record;
output?: string;
bid?: number;
serviceProvider?: string;
}
// 建立任務請求
function createJobRequest(
privateKey: Uint8Array,
job: JobRequest,
relays: string[]
) {
const tags: string[][] = [];
// 添加輸入
for (const input of job.inputs) {
const tag = ['i', input.data, input.type];
if (input.relay) tag.push(input.relay);
if (input.marker) tag.push(input.marker);
tags.push(tag);
}
// 添加參數
if (job.params) {
for (const [key, value] of Object.entries(job.params)) {
tags.push(['param', key, value]);
}
}
// 添加輸出格式
if (job.output) {
tags.push(['output', job.output]);
}
// 添加中繼器
tags.push(['relays', ...relays]);
// 添加出價
if (job.bid) {
tags.push(['bid', job.bid.toString()]);
}
// 指定服務提供者
if (job.serviceProvider) {
tags.push(['p', job.serviceProvider]);
}
return finalizeEvent({
kind: job.kind,
created_at: Math.floor(Date.now() / 1000),
tags,
content: '',
}, privateKey);
}
// 翻譯請求範例
const translateJob = createJobRequest(
privateKey,
{
kind: 5002,
inputs: [
{ data: 'Hello, how are you today?', type: 'text' },
],
params: {
language: 'zh-TW',
},
output: 'text/plain',
bid: 1000, // 1000 毫聰
},
['wss://relay.damus.io']
);
// 圖片生成請求範例
const imageJob = createJobRequest(
privateKey,
{
kind: 5100,
inputs: [
{ data: 'A cute cat sitting on a rainbow', type: 'text' },
],
params: {
size: '1024x1024',
style: 'anime',
},
output: 'image/png',
bid: 5000,
},
['wss://relay.damus.io']
);`} 監聽任務結果
{`import { SimplePool } from 'nostr-tools';
const pool = new SimplePool();
interface JobResult {
status: 'processing' | 'success' | 'error' | 'payment-required' | 'partial';
content?: string;
amount?: number;
bolt11?: string;
error?: string;
}
// 監聽任務回應
function subscribeToJobResults(
jobRequestId: string,
relays: string[],
onResult: (result: JobResult) => void,
onFeedback: (feedback: JobResult) => void
) {
// 監聽結果和回饋
return pool.subscribeMany(
relays,
[
{ kinds: [6000, 6001, 6002, 6003, 6100, 6250], '#e': [jobRequestId] },
{ kinds: [7000], '#e': [jobRequestId] },
],
{
onevent: (event) => {
if (event.kind === 7000) {
// 任務回饋
const statusTag = event.tags.find(t => t[0] === 'status');
const amountTag = event.tags.find(t => t[0] === 'amount');
onFeedback({
status: statusTag?.[1] as any || 'processing',
content: event.content || statusTag?.[2],
amount: amountTag ? parseInt(amountTag[1]) : undefined,
bolt11: amountTag?.[2],
});
} else {
// 任務結果
const amountTag = event.tags.find(t => t[0] === 'amount');
onResult({
status: 'success',
content: event.content,
amount: amountTag ? parseInt(amountTag[1]) : undefined,
bolt11: amountTag?.[2],
});
}
},
}
);
}
// 使用範例
const sub = subscribeToJobResults(
jobRequestId,
['wss://relay.damus.io'],
(result) => {
console.log('任務完成:', result.content);
if (result.bolt11) {
console.log('請付款:', result.bolt11);
}
},
(feedback) => {
if (feedback.status === 'payment-required') {
console.log('需要付款:', feedback.bolt11);
} else if (feedback.status === 'processing') {
console.log('處理中:', feedback.content);
} else if (feedback.status === 'error') {
console.log('錯誤:', feedback.content);
}
}
);`} 處理任務(服務提供者)
{`// 服務提供者:監聽任務請求
function startDVMService(
servicePrivateKey: Uint8Array,
supportedKinds: number[],
relays: string[],
processJob: (job: Event) => Promise
) {
const servicePubkey = getPublicKey(servicePrivateKey);
return pool.subscribeMany(
relays,
[{ kinds: supportedKinds }],
{
onevent: async (jobRequest) => {
try {
// 發送處理中狀態
const processingFeedback = finalizeEvent({
kind: 7000,
created_at: Math.floor(Date.now() / 1000),
tags: [
['status', 'processing', '正在處理您的請求...'],
['e', jobRequest.id],
['p', jobRequest.pubkey],
],
content: '',
}, servicePrivateKey);
await pool.publish(relays, processingFeedback);
// 處理任務
const result = await processJob(jobRequest);
// 發送結果
const resultKind = jobRequest.kind + 1000;
const jobResult = finalizeEvent({
kind: resultKind,
created_at: Math.floor(Date.now() / 1000),
tags: [
['request', JSON.stringify(jobRequest)],
['e', jobRequest.id],
['p', jobRequest.pubkey],
['amount', '500'], // 收費 500 毫聰
],
content: result,
}, servicePrivateKey);
await pool.publish(relays, jobResult);
} catch (error) {
// 發送錯誤狀態
const errorFeedback = finalizeEvent({
kind: 7000,
created_at: Math.floor(Date.now() / 1000),
tags: [
['status', 'error', error.message],
['e', jobRequest.id],
['p', jobRequest.pubkey],
],
content: '',
}, servicePrivateKey);
await pool.publish(relays, errorFeedback);
}
},
}
);
}
// 啟動翻譯服務
startDVMService(
servicePrivateKey,
[5002], // 翻譯任務
['wss://relay.damus.io'],
async (job) => {
const input = job.tags.find(t => t[0] === 'i')?.[1];
const lang = job.tags.find(t => t[0] === 'param' && t[1] === 'language')?.[2];
// 呼叫翻譯 API
const translated = await translateAPI(input, lang);
return translated;
}
);`} 任務串接
可以將前一個任務的輸出作為下一個任務的輸入,實現複雜的工作流程:
{`// 範例:播客轉錄 → 摘要 → 翻譯
// 1. 語音轉文字
const transcribeJob = createJobRequest(privateKey, {
kind: 5003,
inputs: [{ data: 'https://example.com/podcast.mp3', type: 'url' }],
output: 'text/plain',
}, relays);
// 發布並等待結果...
const transcribeJobId = transcribeJob.id;
// 2. 摘要(使用前一個任務的輸出)
const summarizeJob = createJobRequest(privateKey, {
kind: 5250,
inputs: [{ data: transcribeJobId, type: 'job' }],
params: { length: 'short' },
output: 'text/plain',
}, relays);
// 3. 翻譯摘要
const translateJob = createJobRequest(privateKey, {
kind: 5002,
inputs: [{ data: summarizeJob.id, type: 'job' }],
params: { language: 'zh-TW' },
output: 'text/plain',
}, relays);`} 加密請求
敏感資料可以使用 NIP-04 加密:
{`import * as nip04 from 'nostr-tools/nip04';
// 建立加密的任務請求
async function createEncryptedJobRequest(
privateKey: Uint8Array,
serviceProviderPubkey: string,
job: JobRequest,
relays: string[]
) {
// 加密輸入資料
const encryptedContent = await nip04.encrypt(
privateKey,
serviceProviderPubkey,
JSON.stringify({
inputs: job.inputs,
params: job.params,
})
);
const tags: string[][] = [
['p', serviceProviderPubkey],
['encrypted'],
['relays', ...relays],
];
if (job.output) tags.push(['output', job.output]);
if (job.bid) tags.push(['bid', job.bid.toString()]);
return finalizeEvent({
kind: job.kind,
created_at: Math.floor(Date.now() / 1000),
tags,
content: encryptedContent,
}, privateKey);
}`} 服務發現
服務提供者可以使用 NIP-89 發布能力:
{`{
"kind": 31990,
"content": "{\"name\":\"AI 翻譯服務\",\"about\":\"支援 50+ 語言\"}",
"tags": [
["d", "translation-service"],
["k", "5002"],
["t", "translation"],
["t", "ai"]
],
...
}`} 取消任務
{`// 取消任務請求
function cancelJob(
privateKey: Uint8Array,
jobRequestId: string
) {
return finalizeEvent({
kind: 5, // NIP-09 刪除事件
created_at: Math.floor(Date.now() / 1000),
tags: [['e', jobRequestId]],
content: 'Job cancelled',
}, privateKey);
}`} 最佳實踐
- 合理出價:根據任務複雜度設定 bid
- 監控狀態:訂閱 kind 7000 追蹤進度
- 處理超時:設定合理的等待時間
- 驗證結果:確認結果來自可信的服務者
- 加密敏感資料:使用 encrypted 標籤保護隱私
相關 NIP
參考資源
已複製連結