Building a Nostr NIP-90 DVM in Node.js — Complete Working Example

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. ⚡

Report Page