![](https://i.nostr.build/AZ0L.jpg)
@ hodlbod
2025-02-13 02:03:33
Everyone knows that relays are central to how nostr works - they're even in the name: Notes and Other Stuff Transmitted by *Relays*. As time goes on though, there are three other letters which are becoming conspicuously absent from our beloved and ambiguously pronounceable acronym - "D", "V", and "M".
The hype cycle for DVMs seems to have reached escape velocity in a way few other things have - zaps being the possible exception. But *what* exactly DVMs are remains something of a mystery to many nostr developers - and how to build one may as well be written on clay tablets.
This blog post is designed to address that - below is a soup to nuts (no nutzaps though) guide to building a DVM flow, both from the client and the server side.
Here's what we'll be covering:
- Discovering DVM metadata
- Basic request/response flow
- Implementing a minimal example
Let's get started!
# DVM Metadata
First of all, it's helpful to know how DVMs are reified on the nostr network. While not strictly necessary, this can be useful for discovering DVMs and presenting them to users, and for targeting specific DVMs we want a response from.
[NIP 89](https://github.com/nostr-protocol/nips/blob/master/89.md) goes into this in more detail, but the basic idea is that anyone can create a `kind 31990` "application handler" event and publish it to the network with their own (or a dedicated) public key. This handler was originally intended to advertise clients, but has been re-purposed for DVM listings as well.
Here's what the "Fluffy Frens" handler looks like:
```json
{
"content": "{\"name\": \"Fluffy Frens\", \"picture\": \"https://image.nostr.build/f609311532c470f663e129510a76c9a1912ae9bc4aaaf058e5ba21cfb512c88e.jpg\", \"about\": \"I show recent notes about animals\", \"lud16\": \"discovery_content_fluffy@nostrdvm.com\", \"supportsEncryption\": true, \"acceptsNutZaps\": false, \"personalized\": false, \"amount\": \"free\", \"nip90Params\": {\"max_results\": {\"required\": false, \"values\": [], \"description\": \"The number of maximum results to return (default currently 100)\"}}}",
"created_at": 1738874694,
"id": "0aa8d1f19cfe17e00ce55ca86fea487c83be39a1813601f56f869abdfa776b3c",
"kind": 31990,
"pubkey": "7b7373dd58554ff4c0d28b401b9eae114bd92e30d872ae843b9a217375d66f9d",
"sig": "22403a7996147da607cf215994ab3b893176e5302a44a245e9c0d91214e4c56fae40d2239dce58ea724114591e8f95caed2ba1a231d09a6cd06c9f0980e1abd5",
"tags": [
["k", "5300"],
["d", "198650843898570c"]
]
}
```
This event is rendered in various clients using the kind-0-style metadata contained in the `content` field, allowing users to browse DVMs and pick one for their use case. If a user likes using a particular DVM, they might publish a `kind 31989` "application recommendation", which other users can use to find DVMs that are in use within their network.
Note the `k` tag in the handler event - this allows DVMs to advertise support only for specific job types. It's also important to note that even though the spec doesn't cover relay selection, most clients use the publisher's `kind 10002` event to find out where the DVM listens for events.
If this looks messy to you, you're right. See [this PR](https://github.com/nostr-protocol/nips/pull/1728) for a proposal to split DVMs out into their own handler kind, give them a dedicated pubkey along with dedicated metadata and relay selections, and clean up the data model a bit.
# DVM Flow
Now that we know what a DVM looks like, we can start to address how they work. My explanation below will elide some of the detail involved in [NIP 90](https://github.com/nostr-protocol/nips/blob/master/90.md) for simplicity, so I encourage you to read the complete spec.
The basic DVM flow can be a little (very) confusing to work with, because in essence it's a request/response paradigm, but it has some additional wrinkles.
First of all, the broker for the request isn't abstracted away as is usually the case with request/response flows. Regular HTTP requests involve all kinds of work in the background - from resolving domain names to traversing routers, VPNs, and ISP infrastructure. But developers don't generally have to care about all these intermediaries.
With DVMs, on the other hand, the essential complexity of relay selection can't simply be ignored. DVMs often advertise their own relay selections, which should be used rather than a hard-coded or randomly chosen relay to ensure messages are delivered. The benefit of this is that DVMs can avoid censorship, just as users can, by choosing relays that are willing to broker their activity. DVMs can even select multiple relays to broker requests, which means that clients might receive multiple copies of the same response.
Secondly, the DVM request/response model is far more fluid than is usually the case with request/response flows. There are a set of standard practices, but the flow is flexible enough to admit exceptions to these conventions for special use cases. Here are some examples:
- Normally, clients p-tag the DVM they wish to address. But if a client isn't picky about where a response comes from, they may choose to send an open request to the network and collect responses from multiple DVMs simultaneously.
- Normally, a client creates a request before collecting responses using a subscription with an e-tag filter matching the request event. But clients may choose to skip the request step entirely and collect responses from the network that have already been created. This can be useful for computationally intensive tasks or common queries, where a single result can be re-used multiple times.
- Sometimes, a DVM may respond with a `kind 7000` job status event to let clients know they're working on the request. This is particularly useful for longer-running tasks, where feedback is useful for building a responsive UX.
- There are also some details in the spec regarding monetization, parameterization, error codes, encryption, etc.
# Example DVM implementation
For the purposes of this blog post, I'll keep things simple by illustrating the most common kind of DVM flow: a `kind 5300` [content discovery](https://www.data-vending-machines.org/kinds/5300/) request, addressed to a particular DVM. If you're interested in other use cases, please visit [data-vending-machines.org](https://data-vending-machines.org) for additional documented kinds.
The basic flow looks like this:
- The DVM starts by listening for `kind 5300` job requests on some relays it has selected and advertised via NIP 89 (more on that later)
- A client creates a request event of `kind 5300`, p-tagged with the DVM's pubkey and sends it to the DVM's relay selections.
- The DVM receives the event and processes it, issuing optional `kind 7000` job status events, and eventually issuing a `kind 6300` job result event (job result event kinds are always 1000 greater than the request's kind).
- The client listens to the same relays for a response, and when it comes through does whatever it wants to with it.
Here's a swimlane diagram of that flow:
![DVM Flow](https://coracle-media.us-southeast-1.linodeobjects.com/dvmflow.png)
To avoid massive code samples, I'm going to implement our DVM entirely using nak (backed by the power of the human mind).
The first step is to start our DVM listening for requests that it wants to respond to. Nak's default pubkey is `79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798`, so we'll only listen for requests sent to nak.
```bash
nak req -k 5300 -t p=79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
```
This gives us the following filter:
```json
["REQ","nak",{"kinds":[5300],"#p":["79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"]}]
```
To open a subscription to `nos.lol` and stream job requests, add `--stream wss://nos.lol` to the previous request and leave it running.
Next, open a new terminal window for our "client" and create a job request. In this case, there's nothing we need to provide as `input`, but we'll include it just for illustration. It's also good practice to include an `expiration` tag so we're not asking relays to keep our ephemeral requests forever.
```bash
nak event -k 5300 -t p=79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -t expiration=$(( $(date +%s) + 30 )) -t input=hello
```
Here's what comes out:
```json
{
"kind": 5300,
"id": "0e419d0b3c5d29f86d2132a38ca29cdfb81a246e1a649cb2fe1b9ed6144ebe30",
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"created_at": 1739407684,
"tags": [
["p", "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"],
["expiration", "1739407683"],
["input", "hello"]
],
"content": "",
"sig": "560807548a75779a7a68c0ea73c6f097583e2807f4bb286c39931e99a4e377c0a64af664fa90f43e01ddd1de2e9405acd4e268f1bf3bc66f0ed5a866ea093966"
}
```
Now go ahead and publish this event by adding `nos.lol` to the end of your `nak` command. If all goes well, you should see your event pop up in your "dvm" subscription. If so, great! That's half of the flow.
Next, we'll want our client to start listening for `kind 6300` responses to the request. In your "client" terminal window, run:
```bash
nak req -k 6300 -t e=<your-eventid-here> --stream nos.lol
```
Note that if you only want to accept responses from the specified DVM (a good policy in general to avoid spam) you would include a `p` tag here. I've omitted it for brevity. Also notice the `k` tag specifies the request kind plus `1000` - this is just a convention for what kinds requests and responses use.
Now, according to [data-vending-machines.org](https://www.data-vending-machines.org/kinds/5300/), `kind 5300` responses are supposed to put a JSON-encoded list of e-tags in the `content` field of the response. Weird, but ok. Stop the subscription in your "dvm" terminal and respond to your "client" with a recommendation to read my first note:
```bash
nak event -k 6300 -t e=a65665a3a4ca2c0d7b7582f4f0d073cd1c83741c25a07e98d49a43e46d258caf -c '[["e","214f5898a7b75b7f95d9e990b706758ea525fe86db54c1a28a0f418c357f9b08","wss://nos.lol/"]]' nos.lol
```
Here's the response event we're sending:
```json
{
"kind": 6300,
"id": "bb5f38920cbca15d3c79021f7d0051e82337254a84c56e0f4182578e4025232e",
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"created_at": 1739408411,
"tags": [
["e", "a65665a3a4ca2c0d7b7582f4f0d073cd1c83741c25a07e98d49a43e46d258caf"]
],
"content": "[[\"e\",\"214f5898a7b75b7f95d9e990b706758ea525fe86db54c1a28a0f418c357f9b08\",\"wss://nos.lol/\"]]",
"sig": "a0fe2c3419c5c54cf2a6d9a2a5726b2a5b766d3c9e55d55568140979354003aacb038e90bdead43becf5956faa54e3b60ff18c0ea4d8e7dfdf0c8dd97fb24ff9"
}
```
Notice the `e` tag targets our original request.
This should result in the job result event showing up in our "client" terminal. Success!
If something isn't working, I've also create a video of the full process with some commentary which you can find [here](https://coracle-media.us-southeast-1.linodeobjects.com/nakflow.mov).
Note that in practice, DVMs can be much more picky about the requests they will respond to, due to implementations failing to follow [Postel's law](https://en.wikipedia.org/wiki/Robustness_principle). Hopefully that will improve over time. For now, here are a few resources that are useful when working with or developing DVMs:
- [dvmdash](https://dvmdash.live)
- [data-vending-machines.org](https://data-vending-machines.org)
- [noogle](https://noogle.lol/)
- [nostrdvm](https://github.com/believethehype/nostrdvm)
# Conclusion
I started this post by hinting that DVMs might be as fundamental as relays are to making nostr work. But (apart from the fact that we'd end up with an acronym like DVMNOSTRZ+*, which would only exascerbate the pronounciation wars (if such a thing were possible)), that's not exactly true.
DVMs have emerged as a central paradigm in the nostr world because they're a generalization of a design pattern unique to nostr's architecture - but which exists in many other places, including NIP 46 signer flows and NIP 47 wallet connect. Each of these sub-protocols works by using relays as neutral brokers for requests in order to avoid coupling services to web addresses.
This approach has all kinds of neat benefits, not least of which is allowing service providers to host their software without having to accept incoming TCP connections. But it's really an emergent property of relays, which not only are useful for brokering communication between users (aka storing events), but also brokering communication between machines.
The possibilities of this architecture have only started to emerge, so be on the lookout for new applications, and don't be afraid to experiment - just please, don't serialize json inside json 🤦♂️