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
- URL:
wss://relay.divine.video - Video Event Kind:
34236(vertical video) - Supported Metrics:
loop_count,likes,views,comments,avg_completion
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
- Maximum limit per query: 200 events
- Query rate: Up to 50 REQ messages per minute per connection
- Publish rate: Up to 10 EVENT messages per minute per pubkey
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
int_filters: Metrics you can use withint#<metric>filters (e.g.,int#likes)sort_fields: Fields you can use in thesortparametercursor_format: How pagination cursors are generated (for security)videos_kind: The Nostr event kind for videos (34236)metrics_freshness_sec: How often metrics are updated (hourly = 3600 seconds)limit_max: Maximum events you can request in a single query (200)
Support
For questions or issues:
- GitHub: https://github.com/rabble/nosflare
- Relay Maintainer: relay@divine.video