relay-kinetic
v1.3.0
Published
Headless SDK for tracking organic video attribution. Ask users which TikTok creator/video sent them, then attribute app installs to specific videos.
Maintainers
Readme
Relay Attribution SDK
Headless SDK for tracking organic video attribution. Ask users which TikTok creator/video sent them, then attribute app installs to specific videos.
Features:
- 📊 Complete Funnel Tracking - Track every step from data request → modal shown → video selected → submission
- 🔗 Automatic Event Linking - Device ID links all events across user journey
- 🎯 Dual Purpose - Direct attribution + training data for Spot Lift statistical model
- 📈 Rich Analytics - Answer questions like "What % of users who see options actually submit?"
Installation
npm install relay-kineticQuick Start
import { RelayAttribution } from 'relay-kinetic';
// 1. Initialize with your API key
// API key format: relay_prod_{yourBrandId}_{randomSuffix}
RelayAttribution.init({ apiKey: 'relay_prod_md770wapgetepmr3xsb5hys5hx7skmxs_abc123' });
// 2. Get top accounts (ranked by views)
// IMPORTANT: Include deviceId for automatic event tracking
const { creators } = await RelayAttribution.getTopAccounts(4, {
deviceId: 'device_abc', // Automatically tracks data_sent event
adjustDeviceId: '355d5e...' // Optional: Links with Adjust attribution
});
// Show picker with top 4 accounts
// 3. User selects account → Get their top videos
const { videos } = await RelayAttribution.getVideos('verily.payme', 4);
// Show grid with 4 videos (thumbnails + titles)
// 4. Track when modal is shown to user
await RelayAttribution.submit({
eventType: 'modal_shown',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
videosShown: videos.map(v => v.videoId), // Track which videos were shown
});
// 5. User selects video → Track selection + submit attribution
await RelayAttribution.submit({
eventType: 'video_selected',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
selectedVideoId: 'ks7a2jf...',
positionSelected: 0, // Which position in list (0-indexed)
timeToDecision: 5200, // How long user took to decide (ms)
});
// 6. Submit final attribution
await RelayAttribution.submit({
eventType: 'install',
creator: 'verily.payme',
videoId: 'ks7a2jf...',
userId: 'user_123',
deviceId: 'device_abc',
ipAddress: '192.168.1.1',
platform: 'ios',
timestamp: Date.now(),
metadata: {
appVersion: '1.2.3',
referralCode: 'ABC123',
}
});Event Tracking
The SDK tracks a complete funnel from data request to final attribution. All events are linked via deviceId to enable powerful analytics.
Event Types
1. data_sent (Automatic)
- When: Automatically tracked when you call GET endpoints with
deviceId - Purpose: Track what data was sent to users (which creators/videos shown)
- Required Fields: None - automatic when deviceId provided to GET methods
2. modal_shown (Manual)
- When: Track when attribution modal is displayed to user
- Purpose: Measure modal view rate, understand drop-off
- Required Fields:
videosShown(array of video IDs)
3. modal_dismissed (Manual)
- When: Track when user closes modal without selection
- Purpose: Understand abandonment rate
- Required Fields:
videosShown, optionaltimeOnModal
4. video_selected (Manual)
- When: Track when user selects a specific video
- Purpose: Understand selection patterns, position bias
- Required Fields:
selectedVideoId,positionSelected
5. other_selected (Manual)
- When: Track when user selects "Other" / "None of these"
- Purpose: Measure relevance of shown options
- Required Fields:
videosShown
6. install / purchase / signup (Manual)
- When: Track attribution events (installs, conversions)
- Purpose: Direct attribution to specific videos
- Required Fields:
creator,videoId,ipAddress,timestamp
7. submission_success / submission_failure (Automatic)
- When: Automatically tracked when attribution submission completes
- Purpose: Monitor API reliability, debug issues
- Required Fields: None - automatic
Funnel Analytics Examples
Track complete user journey:
// Step 1: User requests data (automatic tracking)
const { creators } = await RelayAttribution.getTopAccounts(4, {
deviceId: 'device_abc' // → Logs data_sent event
});
// Step 2: Modal shown to user
await RelayAttribution.submit({
eventType: 'modal_shown',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
videosShown: ['video1', 'video2', 'video3'],
});
// Step 3a: User selects video
await RelayAttribution.submit({
eventType: 'video_selected',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
selectedVideoId: 'video1',
positionSelected: 0,
timeToDecision: 5200,
});
// Step 3b: OR user selects "Other"
await RelayAttribution.submit({
eventType: 'other_selected',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
videosShown: ['video1', 'video2', 'video3'],
timeToDecision: 3100,
});
// Step 4: Final attribution (automatic success/failure tracking)
await RelayAttribution.submit({
eventType: 'install',
creator: 'kmanbiv',
videoId: 'video1',
deviceId: 'device_abc',
userId: 'user_123',
ipAddress: '192.168.1.1',
platform: 'ios',
timestamp: Date.now(),
});Questions you can answer with event data:
- What % of users who receive data actually submit attribution?
- What % of users who see modal make a selection?
- Which position in list gets selected most often?
- How long do users spend deciding?
- Are the shown videos relevant to users?
- What's the conversion rate from data sent → attribution?
Getting Your API Key
Contact Relay to get your API key. Format: relay_prod_{yourBrandId}_{suffix}
The brandId is extracted from your key to filter data for your brand.
API
init(config)
Initialize the SDK with your API key.
RelayAttribution.init({
apiKey: 'relay_prod_yourBrandId_xyz',
baseUrl: 'https://relay-api.railway.app' // Optional, defaults to production
});getCreators()
Get list of all creators for your brand.
const { creators } = await RelayAttribution.getCreators();
// Returns all creators (unranked)getTopAccounts(limit?, options)
Get top N accounts (creators) ranked by total views.
const { creators } = await RelayAttribution.getTopAccounts(4, {
deviceId: 'device_abc', // Required for event tracking
adjustDeviceId: '355d5e...' // Optional
});
// Returns:
// {
// creators: [
// {
// username: "kmanbiv",
// displayName: "Kevin",
// avatar: "https://...",
// videoCount: 45,
// totalViews: 1200000
// }
// ]
// }Parameters:
limit(optional): Number of accounts to return (default: 4, max: 20)options(required): Event tracking parametersdeviceId(required): Device identifier for automaticdata_sentevent trackingadjustDeviceId(optional): Adjust device ID for attribution linking
getTrendingVideos(limit?, options)
Get trending videos across all creators. Videos can be filtered by time window (e.g., last 24 hours) or show all-time trending.
// Get trending videos from last 24 hours
const { videos, hours } = await RelayAttribution.getTrendingVideos(6, {
deviceId: 'device_abc', // Required for event tracking
adjustDeviceId: '355d5e...', // Optional
hours: 24 // Optional: time window
});
// Get all-time trending videos
const { videos } = await RelayAttribution.getTrendingVideos(6, {
deviceId: 'device_abc'
});
// Returns:
// {
// hours: 24, // null for all-time
// videos: [
// {
// videoId: "ns72c1x...",
// platformVideoId: "7123...",
// url: "https://www.tiktok.com/@kmanbiv/video/123",
// thumbnail: "https://pub-...r2.dev/thumbnails/video_123.jpg",
// title: "#cabinfever #covid19",
// username: "kmanbiv",
// recentViews: 15000, // Views in time window (or totalViews for all-time)
// totalViews: 299800,
// publishedAt: "2020-03-30T22:58:05.000Z"
// }
// ]
// }Parameters:
limit(optional): Number of videos to return (default: 6, max: 20)options(required): Event tracking parametersdeviceId(required): Device identifier for automaticdata_sentevent trackingadjustDeviceId(optional): Adjust device ID for attribution linkinghours(optional): Time window for trending calculation (e.g., 24 for last 24 hours). Omit for all-time trending.
getVideos(creator, limit?)
Get top N performing videos for a specific creator.
const { videos } = await RelayAttribution.getVideos('kmanbiv', 6);
// Returns:
// {
// creator: "kmanbiv",
// videos: [
// {
// videoId: "ns72c1x...",
// url: "https://www.tiktok.com/@kmanbiv/video/123",
// thumbnail: "https://pub-...r2.dev/thumbnails/video_123.jpg",
// hook: "Make $200 from home",
// title: "#cabinfever #covid19",
// viewCount: 299800,
// publishedAt: "2020-03-30T22:58:05.000Z"
// }
// ]
// }Parameters:
creator(required): Creator usernamelimit(optional): Number of videos to return (default: 4, max: 20)
submit(data)
Submit attribution events or track user interactions. Supports 7 event types.
Attribution Events (install, purchase, signup)
For initial install events, include creator and videoId:
// Initial install event - include creator + videoId
await RelayAttribution.submit({
eventType: 'install', // 'install' | 'purchase' | 'signup'
creator: 'kmanbiv', // Required for install: Selected creator username
videoId: 'ns72c1x...', // Required for install: Selected video ID
userId: 'user_123', // Required: Your app's user ID
deviceId: 'device_abc', // Required: Device identifier (links events together)
ipAddress: '192.168.1.1', // Required: User's IP address
platform: 'ios', // Required: 'ios' | 'android'
timestamp: Date.now(), // Required: Event timestamp
adjustDeviceId: '355d5e...', // Optional: Adjust device ID (if available)
appVersion: '1.2.3', // Optional: App version
metadata: { // Optional: Custom app data (any JSON object)
referralCode: 'ABC123',
userTier: 'premium',
}
});For follow-up events (purchase, signup), you can omit creator and videoId - they'll be auto-linked:
// Follow-up purchase event - creator/videoId auto-linked via deviceId
await RelayAttribution.submit({
eventType: 'purchase',
userId: 'user_123',
deviceId: 'device_abc', // Must match original install deviceId
ipAddress: '192.168.1.1',
platform: 'ios',
timestamp: Date.now(),
revenue: 9.99, // Include revenue for purchase events
currency: 'USD',
});Interaction Events
Track user interactions with attribution modal:
// 1. Modal shown to user
await RelayAttribution.submit({
eventType: 'modal_shown',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
videosShown: ['video1', 'video2', 'video3', 'video4'],
});
// 2. User dismissed modal without selecting
await RelayAttribution.submit({
eventType: 'modal_dismissed',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
videosShown: ['video1', 'video2', 'video3', 'video4'],
timeOnModal: 3500, // Optional: Time user spent viewing modal (ms)
});
// 3. User selected a video
await RelayAttribution.submit({
eventType: 'video_selected',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
selectedVideoId: 'video1',
positionSelected: 0, // Which position in list (0-indexed)
timeToDecision: 5200, // Optional: Time from modal open to selection (ms)
videosShown: ['video1', 'video2', 'video3', 'video4'], // Optional
});
// 4. User selected "Other" / "None of these"
await RelayAttribution.submit({
eventType: 'other_selected',
deviceId: 'device_abc',
userId: 'user_123',
platform: 'ios',
videosShown: ['video1', 'video2', 'video3', 'video4'],
timeToDecision: 3100, // Optional: Time from modal open to selection (ms)
});TypeScript Support
Fully typed with TypeScript. Import types:
import type {
Creator,
Video,
AttributionSubmission,
AttributionEvent,
ModalShownEvent,
ModalDismissedEvent,
VideoSelectedEvent,
OtherSelectedEvent,
AttributionResponse
} from 'relay-kinetic';The AttributionSubmission type is a discriminated union of all event types, enabling full type safety based on eventType.
Error Handling
try {
await RelayAttribution.submit(data);
} catch (error) {
console.error('Attribution failed:', error.message);
}React Native Example
import { useState, useEffect, useRef } from 'react';
import { View, FlatList, TouchableOpacity, Image, Text, Platform } from 'react-native';
import { RelayAttribution } from 'relay-kinetic';
import DeviceInfo from 'react-native-device-info';
function AttributionSurvey({ userId, onComplete }) {
const [creators, setCreators] = useState([]);
const [selectedCreator, setSelectedCreator] = useState(null);
const [videos, setVideos] = useState([]);
const modalOpenTime = useRef(null);
const deviceId = DeviceInfo.getUniqueId();
useEffect(() => {
RelayAttribution.init({ apiKey: process.env.RELAY_API_KEY });
loadCreators();
}, []);
const loadCreators = async () => {
// Automatically tracks data_sent event
const { creators } = await RelayAttribution.getTopAccounts(4, {
deviceId,
adjustDeviceId: await getAdjustDeviceId(), // Optional
});
setCreators(creators);
};
const selectCreator = async (username) => {
setSelectedCreator(username);
const { videos } = await RelayAttribution.getVideos(username, 6);
setVideos(videos);
// Track modal shown
modalOpenTime.current = Date.now();
await RelayAttribution.submit({
eventType: 'modal_shown',
deviceId,
userId,
platform: Platform.OS,
videosShown: videos.map(v => v.videoId),
});
};
const selectVideo = async (video, index) => {
const timeToDecision = Date.now() - modalOpenTime.current;
// Track video selection
await RelayAttribution.submit({
eventType: 'video_selected',
deviceId,
userId,
platform: Platform.OS,
selectedVideoId: video.videoId,
positionSelected: index,
timeToDecision,
videosShown: videos.map(v => v.videoId),
});
// Submit final attribution
await RelayAttribution.submit({
eventType: 'install',
creator: selectedCreator,
videoId: video.videoId,
userId,
deviceId,
ipAddress: await getIpAddress(),
platform: Platform.OS,
timestamp: Date.now(),
});
onComplete();
};
const selectOther = async () => {
const timeToDecision = Date.now() - modalOpenTime.current;
// Track "Other" selection
await RelayAttribution.submit({
eventType: 'other_selected',
deviceId,
userId,
platform: Platform.OS,
videosShown: videos.map(v => v.videoId),
timeToDecision,
});
onComplete();
};
const dismissModal = async () => {
const timeOnModal = Date.now() - modalOpenTime.current;
// Track modal dismissal
await RelayAttribution.submit({
eventType: 'modal_dismissed',
deviceId,
userId,
platform: Platform.OS,
videosShown: videos.map(v => v.videoId),
timeOnModal,
});
onComplete();
};
if (!selectedCreator) {
return (
<FlatList
data={creators}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => selectCreator(item.username)}>
<Text>{item.displayName}</Text>
</TouchableOpacity>
)}
/>
);
}
return (
<View>
<FlatList
data={videos}
numColumns={2}
renderItem={({ item, index }) => (
<TouchableOpacity onPress={() => selectVideo(item, index)}>
<Image source={{ uri: item.thumbnail }} />
<Text>{item.hook}</Text>
</TouchableOpacity>
)}
/>
<TouchableOpacity onPress={selectOther}>
<Text>Other / None of these</Text>
</TouchableOpacity>
<TouchableOpacity onPress={dismissModal}>
<Text>Dismiss</Text>
</TouchableOpacity>
</View>
);
}Bundle Size
~2KB gzipped. Zero dependencies.
License
MIT
