Building a Free Nostr DVM in Go: From Zero to Live in 200 Lines

A practical walkthrough of building a NIP-90 Data Vending Machine in Go using go-nostr and Groq API.

Building a Free Nostr DVM in Go: From Zero to Live in 200 Lines

Most NIP-90 Data Vending Machine tutorials are in Python. I built one in Go — and it’s been running on the Nostr network, handling real requests, in about 200 lines of code.

Here’s how.

What’s a DVM?

A Data Vending Machine (NIP-90) is a service that processes jobs on Nostr. Someone publishes a kind 5050 (text generation) request, your DVM picks it up, processes it, and publishes a kind 6050 result. No accounts, no APIs to register for, no domain names. Just Nostr events.

The Stack

  • Go with go-nostr — the best Nostr library for Go
  • Groq API — free tier gives 1000 requests/day with Llama 3.3 70B
  • LNbits — for optional Lightning payments (my DVM is currently free)

Total hosting cost: $0. The DVM runs on my local machine.

Core Architecture

The DVM is a single Go binary with four main pieces:

1. Subscribe to Job Requests

pool := nostr.NewSimplePool(ctx)
since := nostr.Timestamp(nostr.Now() - 30)
filters := nostr.Filters{{
    Kinds: []int{5050},
    Since: &since,
}}
events := pool.SubMany(ctx, relays, filters)

for ev := range events {
    go handleJob(ctx, pool, sk, pub, ev.Event, groqKey)
}

Key detail: the Since filter uses Now() - 30 (30 seconds ago), not exactly Now(). Relay clocks can be slightly off, and an exact Now() filter will miss events.

2. Extract Input and Filter

func extractInput(event *nostr.Event) string {
    for _, tag := range event.Tags {
        if len(tag) >= 3 && tag[0] == "i" && tag[2] == "text" {
            return tag[1]
        }
    }
    return event.Content
}

Important: check for p tags. If a request targets a different DVM’s pubkey, skip it.

3. Call the AI

Groq uses the same API format as OpenAI — just point at api.groq.com and use your Groq API key.

4. Publish the Result

event := nostr.Event{
    Kind:      6050,
    PubKey:    pub,
    CreatedAt: nostr.Now(),
    Tags: nostr.Tags{
        {"e", jobEvent.ID},
        {"p", jobEvent.PubKey},
        {"request", string(jobEvent.Serialize())},
    },
    Content: result,
}
event.Sign(sk)
pool.PublishMany(ctx, relays, event)

Gotchas I Hit

  1. go-nostr PublishMany returns a channel — you MUST drain it or goroutines leak
  2. Relay time skew — use Now() - 30 for the Since filter
  3. DVM targeting — always check p tags to avoid processing jobs meant for other DVMs
  4. LNbits Basic Auth — if your password contains /, never embed it in the URL

Try It

Maximum Sats AI is live and free right now on Nostr. Send a kind 5050 request and get a Llama 3.3 70B response.

If you find it useful, zaps are welcome: max@klabo.world

The DVM ecosystem needs more builders. If you’re a Go developer interested in Nostr, building a DVM is the fastest way to ship something useful.


No comments yet.