x402-vap
v0.1.0
Published
Verifiable Attention Protocol SDK - Earn rewards for real, verified attention
Maintainers
Readme
@vap/sdk
Verifiable Attention Protocol SDK - Earn rewards for real, verified attention on any platform
Overview
VAP (Verifiable Attention Protocol) enables platforms to reward users for genuine attention through cryptographic proof-of-attention. Users earn micro-rewards by proving they're actively engaged through WebSocket heartbeat challenges.
How it works:
- User connects to VAP session via WebSocket
- Server sends cryptographic challenges every 5 seconds
- User's wallet signs challenges proving presence
- Server validates signatures and accumulates attention credits
- Rewards paid out instantly or accumulated for batch settlement
Features
- ✅ Cryptographic Proof: Ed25519 signature-based attention verification
- ✅ Real-Time Rewards: Instant micro-payments for verified attention
- ✅ State Channels: Off-chain accumulation with on-chain settlement
- ✅ Wallet Agnostic: Works with any Solana wallet or embedded wallet
- ✅ Framework Agnostic: Vanilla JS, React, Vue, Svelte
- ✅ Auto-Reconnect: Handles disconnections gracefully
- ✅ TypeScript: Full type safety
Installation
npm install x402-vap
# or
pnpm add @vap/sdk
# or
yarn add @vap/sdkQuick Start
Vanilla JavaScript
import { VapClient } from '@vap/sdk';
const client = new VapClient({
wsUrl: 'wss://vap.rendernet.work/session?uid=user123&cid=channel456'
});
// Set up wallet signer
client.setSigner(async (message) => {
// Sign with your wallet
const signature = await wallet.signMessage(message);
return signature;
});
// Handle rewards
client.onReward((amount, unit) => {
console.log(`Earned ${amount} ${unit}!`);
// Update UI, show notification, etc.
});
// Start session
client.startVap();
// Stop when user leaves
client.stop();React Integration
import { VapClient } from '@vap/sdk';
import { useWallet } from '@solana/wallet-adapter-react';
import { useEffect, useState } from 'react';
export function useVapSession(userId: string, channelId: string) {
const { signMessage, publicKey } = useWallet();
const [totalRewards, setTotalRewards] = useState(0);
const [client, setClient] = useState<VapClient | null>(null);
useEffect(() => {
if (!publicKey || !signMessage) return;
const vapClient = new VapClient({
wsUrl: `wss://vap.rendernet.work/session?uid=${userId}&cid=${channelId}`
});
// Configure signer
vapClient.setSigner(async (msg) => {
const sig = await signMessage(msg);
return sig;
});
// Handle rewards
vapClient.onReward((amount, unit) => {
setTotalRewards(prev => prev + amount);
console.log(`+${amount} ${unit}`);
});
// Start session
vapClient.startVap();
setClient(vapClient);
// Cleanup on unmount
return () => vapClient.stop();
}, [publicKey, signMessage, userId, channelId]);
return { totalRewards, isActive: !!client };
}
// Usage in component:
function VideoPlayer() {
const { totalRewards } = useVapSession('user-123', 'video-456');
return (
<div>
<video src="..." />
<div className="rewards-badge">
Earned: ${totalRewards.toFixed(4)} SOL
</div>
</div>
);
}API Reference
Class: VapClient
Constructor
new VapClient(options: VapOptions)Options:
interface VapOptions {
wsUrl: string; // WebSocket URL with user/channel params
}Example:
const client = new VapClient({
wsUrl: 'wss://vap.rendernet.work/session?uid=alice&cid=stream-789'
});Methods
setSigner(fn: SignerFunction): void
Configures the message signing function.
Parameters:
type SignerFunction = (message: Uint8Array) => Promise<Uint8Array>;Example with Phantom:
client.setSigner(async (msg) => {
const signature = await window.solana.signMessage(msg, 'utf8');
return signature.signature;
});Example with Privy:
import { useSolanaWallets } from '@privy-io/react-auth/solana';
const { wallets } = useSolanaWallets();
const wallet = wallets[0];
client.setSigner(async (msg) => {
const sig = await wallet.signMessage(msg);
return new Uint8Array(sig);
});onReward(handler: RewardHandler): void
Sets the reward callback handler.
Parameters:
type RewardHandler = (amount: number, unit: string) => void;Example:
client.onReward((amount, unit) => {
// Update state
setEarnings(prev => prev + amount);
// Show toast notification
toast.success(`+${amount} ${unit}`);
// Track analytics
analytics.track('reward_earned', { amount, unit });
});startVap(): void
Initiates the VAP session and WebSocket connection.
Example:
client.startVap();stop(): void
Stops the session and closes WebSocket connection.
Example:
// When user navigates away
window.addEventListener('beforeunload', () => {
client.stop();
});connect(): void
Manually connect/reconnect to WebSocket (called automatically by startVap()).
Advanced Usage
Auto-Reconnect with Exponential Backoff
import { VapClient } from '@vap/sdk';
class ResilientVapClient extends VapClient {
private reconnectAttempts = 0;
private maxReconnectDelay = 30000;
connect() {
super.connect();
if (this.ws) {
this.ws.onclose = () => {
const delay = Math.min(
1000 * Math.pow(2, this.reconnectAttempts),
this.maxReconnectDelay
);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
};
this.ws.onopen = () => {
this.reconnectAttempts = 0;
console.log('VAP connected');
};
}
}
}Session Analytics
class AnalyticsVapClient extends VapClient {
private sessionStart = Date.now();
private heartbeatsCount = 0;
private rewardsCount = 0;
constructor(opts: VapOptions) {
super(opts);
// Track heartbeats
this.onMessage((data) => {
if (data.nonce) {
this.heartbeatsCount++;
}
});
// Track rewards
this.onReward((amount) => {
this.rewardsCount++;
});
}
getSessionStats() {
return {
duration: Date.now() - this.sessionStart,
heartbeats: this.heartbeatsCount,
rewards: this.rewardsCount,
avgRewardInterval: this.heartbeatsCount / (this.rewardsCount || 1)
};
}
}Multiple Concurrent Sessions
// User watching multiple streams simultaneously
const sessions = [
new VapClient({ wsUrl: 'wss://vap.../session?cid=stream1' }),
new VapClient({ wsUrl: 'wss://vap.../session?cid=stream2' }),
new VapClient({ wsUrl: 'wss://vap.../session?cid=stream3' })
];
// Configure all with same signer
sessions.forEach(client => {
client.setSigner(walletSigner);
client.onReward((amt, unit) => {
console.log(`Earned ${amt} ${unit} from ${client.opts.wsUrl}`);
});
client.startVap();
});
// Stop all on page unload
window.addEventListener('beforeunload', () => {
sessions.forEach(c => c.stop());
});WebSocket Protocol
Connection
wss://vap.rendernet.work/session?uid=<userId>&cid=<channelId>Query Parameters:
uid- Unique user identifiercid- Channel/content identifier
Message Flow
Server → Client (Challenge):
{
"nonce": "abc123...",
"seq": 1,
"ts": 1699564800000
}Client → Server (Response):
{
"userId": "alice",
"channelId": "stream-789",
"seq": 1,
"sig": "base64_signature",
"pubkey": "base64_public_key",
"cts": 1699564801000
}Server → Client (Reward):
{
"reward": {
"amount": 0.0001,
"unit": "SOL"
}
}Examples
Next.js App with Privy
'use client';
import { VapClient } from '@vap/sdk';
import { usePrivy, useSolanaWallets } from '@privy-io/react-auth';
import { useEffect, useState } from 'react';
export default function VideoWithRewards() {
const { authenticated, login } = usePrivy();
const { wallets } = useSolanaWallets();
const [earnings, setEarnings] = useState(0);
useEffect(() => {
if (!authenticated || wallets.length === 0) return;
const wallet = wallets[0];
const client = new VapClient({
wsUrl: `wss://vap.rendernet.work/session?uid=${wallet.address}&cid=video-123`
});
client.setSigner(async (msg) => {
const sig = await wallet.signMessage(msg);
return new Uint8Array(sig);
});
client.onReward((amount) => {
setEarnings(prev => prev + amount);
});
client.startVap();
return () => client.stop();
}, [authenticated, wallets]);
if (!authenticated) {
return (
<button onClick={login} className="...">
Connect to Earn Rewards
</button>
);
}
return (
<div>
<video src="content.mp4" controls />
<div className="earnings">
You've earned: ${earnings.toFixed(4)}
</div>
</div>
);
}Vanilla JS (No Framework)
<!DOCTYPE html>
<html>
<head>
<script type="module">
import { VapClient } from 'https://esm.sh/@vap/sdk';
const client = new VapClient({
wsUrl: 'wss://vap.rendernet.work/session?uid=user&cid=page'
});
// Setup signer (Phantom example)
client.setSigner(async (msg) => {
const resp = await window.solana.signMessage(msg, 'utf8');
return resp.signature;
});
// Handle rewards
client.onReward((amount, unit) => {
document.getElementById('earnings').textContent =
`Earned: ${amount} ${unit}`;
});
// Start on page load
client.startVap();
// Stop on page unload
window.addEventListener('beforeunload', () => client.stop());
</script>
</head>
<body>
<div id="earnings">Earned: 0 SOL</div>
<div>Watch content to earn...</div>
</body>
</html>Configuration
Environment Variables
# WebSocket endpoint (optional, defaults to production)
NEXT_PUBLIC_VAP_WS_URL=wss://vap.rendernet.work/session
# User identifier
NEXT_PUBLIC_USER_ID=user-abc-123
# Channel identifier
NEXT_PUBLIC_CHANNEL_ID=content-xyz-789Troubleshooting
WebSocket Connection Fails
Symptom: onclose fires immediately after connect()
Solutions:
- Check network connectivity
- Verify WebSocket URL format
- Check CORS headers (should allow your origin)
- Ensure uid and cid params are present
Signature Verification Fails
Symptom: No rewards despite heartbeats
Solutions:
- Verify
setSigneris called beforestartVap() - Check wallet is connected and can sign messages
- Ensure signature format is raw bytes (Uint8Array)
- Test with
console.login signer function
Rewards Not Triggering
Symptom: Connected but onReward never fires
Solutions:
- Verify server-side VAP session is active
- Check minimum attention threshold (usually 30 seconds)
- Ensure channel has rewards enabled
- Check server logs for signature validation errors
Security
Message Signing
- All heartbeat responses must be signed with user's private key
- Server validates Ed25519 signatures on-chain public key
- Replay attacks prevented via sequence numbers and timestamps
- No private keys transmitted over WebSocket
Privacy
- No PII transmitted (only wallet addresses)
- Session data encrypted in transit (WSS)
- Minimal fingerprinting (only UA for fraud detection)
Performance
Typical metrics:
- WebSocket latency: <100ms
- Heartbeat interval: 5 seconds
- Signature verification: <10ms
- Reward accumulation: off-chain (instant)
- Settlement: batched every 5 minutes or on-demand
Changelog
v0.1.0 (2025-11-11)
- Initial release
- WebSocket client with heartbeat challenges
- Reward handler
- TypeScript support
Contributing
See CONTRIBUTING.md
License
MIT © 555x402
Support
- Docs: https://docs.rendernet.work/vap
- Issues: https://github.com/Render-Network-OS/555x402-vap-sdk/issues
- Discord: https://discord.gg/555x402
Related Packages
- @555x402/agg - Payment aggregation
- @555x402/hyperlink - Payment links
Earn while you engage 🎯
