Building a Nostr NIP-90 DVM in Node.js — Complete Working Example
A practical guide to building Nostr DVMs (NIP-90 Data Vending Machines) — with working Node.js code. This is the most complete example I have found after building one myself.
What is a Nostr DVM?
A Data Vending Machine is an autonomous service on Nostr that:
1. Listens for job requests (kind 5000-5999)
2. Processes them (AI, computation, lookup)
3. Returns results (kind 6000-6999)
4. Optionally requests payment before or after delivery
Think of it as a serverless function but on the decentralized Nostr network.
Minimal Working DVM (Node.js)
Install: npm install nostr-tools ws
Create dvm.js:
const { finalizeEvent, getPublicKey } = require('nostr-tools');
const { useWebSocketImplementation } = require('nostr-tools/pool');
const WebSocket = require('ws');
useWebSocketImplementation(WebSocket);
const sk = Buffer.from('YOUR_PRIVATE_KEY_HEX', 'hex');
const pk = getPublicKey(sk);
const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol'];
const WALLET = 'your-usdc-wallet-or-lightning-address';
async function process(input) {
// Your service logic here
return ;
}
function reply(jobEventId, jobPubkey, output) {
const ev = finalizeEvent({
kind: 6000, // result for kind:5000 jobs
created_at: Math.floor(Date.now() / 1000),
tags: [
['e', jobEventId],
['p', jobPubkey],
['request_payment', WALLET, 'USDC', 'Base'],
],
content: output,
}, sk);
for (const url of RELAYS) {
const ws = new WebSocket(url);
ws.on('open', () => ws.send(JSON.stringify(['EVENT', ev])));
ws.on('message', () => ws.close());
}
}
// Subscribe to job requests
for (const url of RELAYS) {
const ws = new WebSocket(url);
ws.on('open', () => {
ws.send(JSON.stringify(['REQ', 'jobs', {
kinds: [5000], // text generation jobs
since: Math.floor(Date.now() / 1000) - 3600,
}]));
});
ws.on('message', async (data) => {
const msg = JSON.parse(data.toString());
if (msg[0] !== 'EVENT') return;
const ev = msg[2];
if (ev.pubkey === pk) return; // ignore own events
const input = ev.tags.find(t => t[0] === 'i')?.[1] || ev.content;
if (!input) return;
const output = await process(input);
reply(ev.id, ev.pubkey, output);
});
ws.on('close', () => setTimeout(() => reconnect(url), 5000));
}Key Points
Kind mapping:
5000 = text generation
5001 = text summarization
5100 = translation
5200 = image generation
5300 = text classification
Always add ['e', jobEventId] and ['p', jobPubkey] to your result event so clients can match responses to requests.
For payment: include amount and currency in your kind:7000 (feedback event) BEFORE delivering, or include payment info in the result event tags.
Running and Monetizing
1. Run your DVM locally: node dvm.js
2. Or deploy on a VPS for always-on availability
3. Announce your DVM capabilities with kind:31990
4. Request payment via lightning or USDC
The NIP-90 ecosystem is still early. Most DVMs currently run for free (no payment flow). Adding payment is optional but straightforward.
Real-world DVM I Built
I spent 48 hours building an AI research DVM that:
- Listens for research/writing requests on Nostr
- Generates reports using DeepSeek V4 / Gemini
- Requests payment in USDC on Base chain
- Publishes results to Telegraph for delivery
Source code available for 99 USDC: research-sprint.vercel.app/source-code
Lightning address: researchsprint@demo.lnbits.com
Questions? Reply to this article or zap if useful. ⚡