Learn-skills.dev nostr-dvms
Build Nostr Data Vending Machine (DVM) services and clients using NIP-90. Use when implementing AI/compute service providers (kinds 5000-5999 job requests, 6000-6999 job results, kind 7000 feedback), creating DVM job request clients with payment handling, chaining DVM jobs, handling encrypted DVM params, or publishing NIP-89 service provider discovery events for DVMs.
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/accolver/skill-maker/nostr-dvms" ~/.claude/skills/neversight-learn-skills-dev-nostr-dvms && rm -rf "$T"
data/skills-md/accolver/skill-maker/nostr-dvms/SKILL.mdNostr Data Vending Machines (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.
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
Workflow
1. Determine Your Role
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 |
2. Choose the Job Kind
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.
3. Build the Job Request (Customer Side)
Construct a kind 5000-5999 event:
{ "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
i tag:
| 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 paybid
— where service providers should publish responsesrelays
— preferred service provider pubkey (others MAY still respond)p
— topic tags for categorizationt
4. Handle Job Feedback (Kind 7000)
Service providers send feedback events to communicate status:
{ "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:
payment-required is a hard gate — the SP will NOT proceed until
paid. Other statuses are informational.
The
content field MAY contain partial results (e.g., a sample of processed
output) for any feedback status.
5. Publish Job Results (Service Provider Side)
Result kind = request kind + 1000 (e.g., 5001 → 6001):
{ "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 stringrequest
— references the job request event IDe
— the customer's pubkey (so they can find the result)p
Important: The
request tag value is the entire job request event
serialized as a JSON string, not just the event ID.
6. Handle Payments
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 tag
SPs MUST use
payment-required feedback to block until paid. They SHOULD NOT
silently wait for payment without signaling.
7. Implement Job Chaining
Chain jobs by using the
job input type — the output of one job becomes the
input of the next:
{ "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"]
8. Add Encrypted Parameters (Optional)
For privacy, encrypt
i and param tags using NIP-04 with the service
provider's pubkey:
- Collect all
andi
tags into a JSON arrayparam - Encrypt with NIP-04 (customer's private key + SP's public key)
- Put encrypted payload in
fieldcontent - Add
tag and["encrypted"]
tag["p", "<sp-pubkey>"] - Remove plaintext
andi
tags from the eventparam
{ "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"].
9. Publish Service Provider Discovery (NIP-89)
Advertise DVM capabilities with a kind:31990 handler announcement:
{ "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 discoverabilityt
: JSON withcontent
andname
fields (like kind:0 metadata)about
10. Handle Cancellation
Customers cancel jobs by publishing a kind:5 deletion request:
{ "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.
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 eventrequest - Result includes
tag referencing the job request event IDe - Result includes
tag with customer's pubkeyp - Feedback events use kind:7000 with valid status values
-
feedback includespayment-required
tag 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 kindk - Cancellation uses kind:5 with
tag referencing the job requeste
Example: Complete Summarization DVM Service Provider
Scenario: Build a service provider that handles kind:5001 summarization requests.
Subscribe to job requests
const sub = relay.subscribe([{ kinds: [5001] }]);
Process a request
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)], ], }); }
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 tag in result | Clients can't verify the result matches their request | Include full stringified job request event |
tag contains event ID instead of full event | Spec requires the complete event JSON as a string | Use |
Using without tag | Customer has no way to pay | Always include |
Forgetting tag in result/feedback | 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 , result must too |
Using wrong input-type in tag | SP can't resolve the input data | =raw, =fetch, =nostr lookup, =chain |
Chaining with type instead of | SP treats it as a static event, not a job output | Use for chaining |
| Not monitoring for kind:5 cancellations | Wastes compute on cancelled jobs | Subscribe to kind:5 events tagging active jobs |
NIP-89 announcement missing tag | Clients can't discover which kinds the DVM supports | Include for each supported kind |
Key Principles
-
Result kind = request kind + 1000 — This is the fundamental mapping. kind:5001 always produces kind:6001. No exceptions.
-
The
tag 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 use
feedback 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 the
input type — Notjob
. Theevent
type tells the SP to wait for and use the output of a previous DVM job, not just read a static event.job