Developer Documentation

← Back to Home

Developer Documentation

Divine Video Relay - Video Discovery & Sorting

Overview

The Divine Video relay (relay.divine.video) is a specialized Nostr relay with custom vendor extensions for discovering and sorting short-form videos by engagement metrics.

Relay Information


Quick Start

JavaScript / Node.js

import WebSocket from 'ws';

const ws = new WebSocket('wss://relay.divine.video');

ws.on('open', () => {
  // Query trending videos
  ws.send(JSON.stringify([
    'REQ',
    'trending',
    {
      kinds: [34236],
      sort: { field: 'loop_count', dir: 'desc' },
      limit: 20
    }
  ]));
});

ws.on('message', (data) => {
  const [type, subId, event] = JSON.parse(data);
  if (type === 'EVENT') {
    console.log('Video:', event);
  }
});

Python

import websocket
import json

ws = websocket.create_connection('wss://relay.divine.video')

# Query trending videos
query = ['REQ', 'trending', {
    'kinds': [34236],
    'sort': {'field': 'loop_count', 'dir': 'desc'},
    'limit': 20
}]
ws.send(json.dumps(query))

while True:
    message = json.loads(ws.recv())
    if message[0] == 'EVENT':
        print(f'Video: {message[2]}')
    elif message[0] == 'EOSE':
        break

Using wscat (testing)

wscat -c wss://relay.divine.video

# Then send:
["REQ","test",{"kinds":[34236],"sort":{"field":"loop_count","dir":"desc"},"limit":5}]

Basic Query Structure

All queries follow standard Nostr REQ format with added vendor extensions:

["REQ", "subscription_id", {
  "kinds": [34236],
  "sort": {
    "field": "loop_count",  // or "likes", "views", "comments", "avg_completion", "created_at"
    "dir": "desc"           // or "asc"
  },
  "limit": 20
}]

Common Query Examples

1. Most Looped Videos (Trending)

Get videos with the most loops (plays):

["REQ", "trending", {
  "kinds": [34236],
  "sort": {
    "field": "loop_count",
    "dir": "desc"
  },
  "limit": 50
}]

2. Most Liked Videos

Get videos sorted by number of likes:

["REQ", "most-liked", {
  "kinds": [34236],
  "sort": {
    "field": "likes",
    "dir": "desc"
  },
  "limit": 50
}]

3. Most Viewed Videos

Get videos sorted by view count:

["REQ", "most-viewed", {
  "kinds": [34236],
  "sort": {
    "field": "views",
    "dir": "desc"
  },
  "limit": 50
}]

4. Newest Videos First

Get most recently published videos:

["REQ", "newest", {
  "kinds": [34236],
  "sort": {
    "field": "created_at",
    "dir": "desc"
  },
  "limit": 50
}]

Filtering by Engagement Metrics

Use int#<metric> filters to set thresholds:

5. Popular Videos (minimum threshold)

Get videos with at least 100 likes:

["REQ", "popular", {
  "kinds": [34236],
  "int#likes": {"gte": 100},
  "sort": {
    "field": "loop_count",
    "dir": "desc"
  },
  "limit": 20
}]

6. Range Queries

Get videos with 10-100 likes:

["REQ", "moderate-engagement", {
  "kinds": [34236],
  "int#likes": {
    "gte": 10,
    "lte": 100
  },
  "sort": {
    "field": "created_at",
    "dir": "desc"
  },
  "limit": 50
}]

7. Highly Engaged Videos

Combine multiple metric filters:

["REQ", "highly-engaged", {
  "kinds": [34236],
  "int#likes": {"gte": 50},
  "int#loop_count": {"gte": 1000},
  "sort": {
    "field": "likes",
    "dir": "desc"
  },
  "limit": 20
}]

Hashtag Filtering

8. Videos by Hashtag

Get videos tagged with specific hashtags:

["REQ", "music-videos", {
  "kinds": [34236],
  "#t": ["music"],
  "sort": {
    "field": "likes",
    "dir": "desc"
  },
  "limit": 20
}]

9. Multiple Hashtags (OR logic)

Videos with ANY of these tags:

["REQ", "entertainment", {
  "kinds": [34236],
  "#t": ["music", "dance", "comedy"],
  "sort": {
    "field": "loop_count",
    "dir": "desc"
  },
  "limit": 50
}]

Author Queries

10. Videos by Specific Author

["REQ", "author-videos", {
  "kinds": [34236],
  "authors": ["pubkey_hex_here"],
  "sort": {
    "field": "created_at",
    "dir": "desc"
  },
  "limit": 20
}]

11. Top Videos by Author

["REQ", "author-top-videos", {
  "kinds": [34236],
  "authors": ["pubkey_hex_here"],
  "sort": {
    "field": "loop_count",
    "dir": "desc"
  },
  "limit": 10
}]

Pagination

12. Using Cursors for Infinite Scroll

The relay returns a cursor in the EOSE message for pagination:

// Send initial query
ws.send(JSON.stringify(['REQ', 'feed', {
  kinds: [34236],
  sort: {field: 'loop_count', dir: 'desc'},
  limit: 20
}]));

// Listen for EOSE message with cursor
ws.on('message', (data) => {
  const message = JSON.parse(data);

  if (message[0] === 'EOSE') {
    const subscriptionId = message[1];
    const cursor = message[2]; // Cursor for next page

    if (cursor) {
      // Fetch next page
      ws.send(JSON.stringify(['REQ', 'feed-page-2', {
        kinds: [34236],
        sort: {field: 'loop_count', dir: 'desc'},
        limit: 20,
        cursor: cursor
      }]));
    }
  }
});

Available Metrics

Metric Description Tag Name
loop_count Number of times video was looped/replayed loops
likes Number of likes likes
views Number of views views
comments Number of comments comments
avg_completion Average completion rate (0-100) Not in tags yet
created_at Unix timestamp of publication Event's created_at

Reading Metrics from Events

When you receive an EVENT, metrics are in the tags array:

ws.on('message', (data) => {
  const [type, subId, event] = JSON.parse(data);

  if (type === 'EVENT') {
    // Extract metrics from tags
    const loops = getTagValue(event.tags, 'loops');
    const likes = getTagValue(event.tags, 'likes');
    const views = getTagValue(event.tags, 'views');
    const comments = getTagValue(event.tags, 'comments');
    const vineId = getTagValue(event.tags, 'd'); // Original Vine ID

    console.log(`Video ${vineId}: ${loops} loops, ${likes} likes`);
  }
});

function getTagValue(tags, tagName) {
  const tag = tags.find(t => t[0] === tagName);
  return tag ? parseInt(tag[1]) || 0 : 0;
}
# Python example
def handle_event(event):
    tags = event['tags']

    # Extract metrics
    loops = get_tag_value(tags, 'loops')
    likes = get_tag_value(tags, 'likes')
    views = get_tag_value(tags, 'views')
    vine_id = get_tag_value(tags, 'd')

    print(f'Video {vine_id}: {loops} loops, {likes} likes')

def get_tag_value(tags, tag_name):
    for tag in tags:
        if tag[0] == tag_name:
            return int(tag[1]) if len(tag) > 1 else 0
    return 0

Feed Recommendations

For You Feed

Trending content from last 24 hours:

["REQ", "for-you", {
  "kinds": [34236],
  "since": 1704067200,
  "sort": {"field": "loop_count", "dir": "desc"},
  "limit": 50
}]

Discover Feed

High engagement, diverse content:

["REQ", "discover", {
  "kinds": [34236],
  "int#likes": {"gte": 20},
  "int#loop_count": {"gte": 500},
  "sort": {"field": "created_at", "dir": "desc"},
  "limit": 100
}]

Trending Feed

Pure virality - most loops:

["REQ", "trending", {
  "kinds": [34236],
  "sort": {"field": "loop_count", "dir": "desc"},
  "limit": 50
}]

Rate Limits


Error Handling

The relay will send a CLOSED message if a query is invalid:

ws.on('message', (data) => {
  const message = JSON.parse(data);

  if (message[0] === 'CLOSED') {
    const subscriptionId = message[1];
    const reason = message[2];
    console.log(`Subscription ${subscriptionId} closed: ${reason}`);

    // Common reasons:
    // - 'invalid: unsupported sort field'
    // - 'invalid: limit exceeds maximum (200)'
    // - 'blocked: kinds [...] not allowed'
  }
});

Testing

You can test queries using wscat:

# Connect to relay
wscat -c wss://relay.divine.video

# Send query (paste this after connecting)
["REQ", "test", {"kinds": [34236], "sort": {"field": "loop_count", "dir": "desc"}, "limit": 5}]

NIP-11 Relay Information (Discovery)

Checking Relay Capabilities

Before using vendor extensions, check the relay's NIP-11 document to verify support:

curl -H "Accept: application/nostr+json" https://relay.divine.video

JavaScript Example

async function getRelayCapabilities(relayUrl) {
  // Convert wss:// to https://
  const httpUrl = relayUrl.replace('wss://', 'https://').replace('ws://', 'http://');

  const response = await fetch(httpUrl, {
    headers: {'Accept': 'application/nostr+json'}
  });

  const relayInfo = await response.json();

  if (relayInfo.divine_extensions) {
    console.log('Supported sort fields:', relayInfo.divine_extensions.sort_fields);
    console.log('Supported filters:', relayInfo.divine_extensions.int_filters);
    console.log('Max limit:', relayInfo.divine_extensions.limit_max);
  }

  return relayInfo;
}

// Usage
const info = await getRelayCapabilities('wss://relay.divine.video');

Python Example

import requests

def get_relay_capabilities(relay_url):
    http_url = relay_url.replace('wss://', 'https://').replace('ws://', 'http://')

    response = requests.get(http_url, headers={
        'Accept': 'application/nostr+json'
    })

    relay_info = response.json()

    if 'divine_extensions' in relay_info:
        print(f"Supported sort fields: {relay_info['divine_extensions']['sort_fields']}")
        print(f"Supported filters: {relay_info['divine_extensions']['int_filters']}")

    return relay_info

# Usage
info = get_relay_capabilities('wss://relay.divine.video')

Example NIP-11 Response

{
  "name": "Divine Video Relay",
  "description": "A specialized Nostr relay for Divine Video's 6-second short-form videos",
  "supported_nips": [1, 2, 4, 5, 9, 11, 12, 15, 16, 17, 20, 22, 33, 40],
  "divine_extensions": {
    "int_filters": ["loop_count", "likes", "views", "comments", "avg_completion"],
    "sort_fields": ["loop_count", "likes", "views", "comments", "avg_completion", "created_at"],
    "cursor_format": "base64url-encoded HMAC-SHA256 with query hash binding",
    "videos_kind": 34236,
    "metrics_freshness_sec": 3600,
    "limit_max": 200
  }
}

What Each Field Means


Support

For questions or issues: