How to Add Tarot Readings to Your Dating App (Complete API Guide)
Boost engagement in dating apps with tarot-powered icebreakers, compatibility readings, and conversation starters. Complete implementation guide with real API examples for relationship spreads and yes/no guidance features.
How to Add Tarot Readings to Your Dating App (Complete API Guide)
Dating apps live and die by engagement metrics. Users swipe, match, then... crickets. 70% of matches never exchange a single message. The problem? Conversation anxiety and lack of compelling conversation starters.
Enter tarot-powered features: playful icebreakers ("What does the tarot say about us?"), compatibility spreads that spark discussion, and yes/no guidance for those "should I send this?" moments. Apps like Struck and NUiT already leverage astrology for 3-5x higher messaging rates. Tarot offers similar magic with even richer storytelling potential.
This guide shows you how to integrate RoxyAPI's Tarot API into dating platforms: relationship compatibility spreads, icebreaker card draws, yes/no decision features, and shareable reading cards that drive profile visits. Complete with production-ready code examples.
Why Tarot Works in Dating Apps
Psychological Triggers:
- Barnum Effect: People see personal relevance in general descriptions ("The Two of Cups represents connection...")
- Conversation Catalyst: Shared reading creates instant common ground ("My card was The Lovers, what was yours?")
- Gamification: Collectible daily draws and rare card achievements boost retention
- Social Proof: Shareable results drive organic growth ("We got 5/5 compatibility!")
Engagement Impact:
- 40-60% higher first-message rates (icebreaker features)
- 2-3x longer session times (exploring spreads together)
- 25-35% increase in profile revisits (checking updated daily cards)
- Viral coefficient 1.2-1.5x (users share readings on social media)
What You Will Build
By the end of this tutorial, you will have:
- Compatibility Spread - 5-card reading showing Your Energy / Their Energy / Connection Strength / Challenges / Potential
- Icebreaker Card Draw - Quick 3-card conversation starter displayed on match screen
- Yes/No Oracle - Should I message them? Should I ask for their number? Instant guidance
- Daily Match Card - Seeded daily card for each match (consistent per day, changes at midnight)
- Shareable Reading Cards - Beautiful OG images for Instagram Stories / DMs
Prerequisites
Tech Stack:
- Backend: Node.js/Python/Ruby (any REST API consumer)
- Frontend: React Native, Flutter, Swift/Kotlin, or web
- RoxyAPI Tarot API Key (get one here - Starter plan at ₹999/month)
API Base URL:
https://roxyapi.com/api/v2/tarot
Authentication:
All requests require X-API-Key header:
curl -H "X-API-Key: YOUR_API_KEY" \
https://roxyapi.com/api/tarot/cards
Feature 1: Relationship Compatibility Spread
Display a 5-card spread when users match. Each position reveals relationship dynamics.
API: Custom Spread Builder
Endpoint:
POST /tarot/spreads/custom
Request:
{
"spreadName": "Relationship Compatibility",
"positions": [
{
"name": "Your Energy",
"interpretation": "What you bring to the relationship"
},
{
"name": "Their Energy",
"interpretation": "What they bring to the relationship"
},
{
"name": "Connection Strength",
"interpretation": "The bond between you"
},
{
"name": "Potential Challenges",
"interpretation": "What could create friction"
},
{
"name": "Relationship Potential",
"interpretation": "Where this connection could lead"
}
],
"seed": "user123-user456-2026-01-09"
}
Response:
{
"spread": "Custom Spread",
"seed": "user123-user456-2026-01-09",
"positions": [
{
"position": 1,
"name": "Your Energy",
"interpretation": "What you bring to the relationship",
"card": {
"id": "hanged-man",
"name": "The Hanged Man",
"arcana": "major",
"number": 12,
"position": 1,
"reversed": false,
"keywords": ["Pause", "surrender", "letting go", "new perspectives"],
"meaning": "The Hanged Man reminds you that sometimes you have to put everything on hold...",
"imageUrl": "https://roxyapi.com/img/tarot/major/hanged-man.jpg"
}
},
{
"position": 2,
"name": "Their Energy",
"interpretation": "What they bring to the relationship",
"card": {
"id": "ace-of-wands",
"name": "Ace of Wands",
"arcana": "minor",
"suit": "wands",
"number": 1,
"reversed": true,
"keywords": ["An emerging idea", "lack of direction", "distractions"],
"meaning": "The Ace of Wands reversed suggests that you can sense an idea emerging...",
"imageUrl": "https://roxyapi.com/img/tarot/minor/ace-of-wands.jpg"
}
}
]
}
Implementation: Node.js + Express
const axios = require('axios');
const TAROT_API_KEY = process.env.TAROT_API_KEY;
const TAROT_BASE_URL = 'https://roxyapi.com/api/v2/tarot';
// Generate compatibility reading for a match
async function getCompatibilityReading(userId1, userId2, date) {
// Seed ensures same match gets same reading on same day
const seed = `${userId1}-${userId2}-${date}`;
const response = await axios.post(
`${TAROT_BASE_URL}/spreads/custom`,
{
spreadName: "Relationship Compatibility",
positions: [
{ name: "Your Energy", interpretation: "What you bring to the relationship" },
{ name: "Their Energy", interpretation: "What they bring to the relationship" },
{ name: "Connection Strength", interpretation: "The bond between you" },
{ name: "Potential Challenges", interpretation: "What could create friction" },
{ name: "Relationship Potential", interpretation: "Where this connection could lead" }
],
seed
},
{
headers: { 'X-API-Key': TAROT_API_KEY }
}
);
return response.data;
}
// Express route
app.get('/api/matches/:matchId/compatibility', async (req, res) => {
try {
const match = await Match.findById(req.params.matchId);
const today = new Date().toISOString().split('T')[0];
const reading = await getCompatibilityReading(
match.user1Id,
match.user2Id,
today
);
// Calculate compatibility score based on card energies
const score = calculateCompatibilityScore(reading.positions);
res.json({
matchId: match.id,
compatibility: score,
reading: reading.positions.map(pos => ({
position: pos.name,
card: pos.card.name,
reversed: pos.card.reversed,
keywords: pos.card.keywords,
meaning: pos.card.meaning.substring(0, 200) + '...',
imageUrl: pos.card.imageUrl
}))
});
} catch (error) {
res.status(500).json({ error: 'Failed to generate compatibility reading' });
}
});
// Simple scoring algorithm
function calculateCompatibilityScore(positions) {
let score = 50; // Base score
positions.forEach(pos => {
// Major arcana = stronger influence (+/-10)
// Minor arcana = moderate influence (+/-5)
const impact = pos.card.arcana === 'major' ? 10 : 5;
// Reversed = challenging energy (reduce score)
// Upright = positive energy (increase score)
const direction = pos.card.reversed ? -1 : 1;
// Position weight (Connection Strength = 2x impact)
const weight = pos.name === 'Connection Strength' ? 2 : 1;
score += (impact * direction * weight);
});
// Clamp between 0-100
return Math.max(0, Math.min(100, score));
}
Implementation: React Native Frontend
import React, { useState, useEffect } from 'react';
import { View, Text, Image, StyleSheet, ScrollView, ActivityIndicator } from 'react-native';
export default function CompatibilityScreen({ route }) {
const { matchId } = route.params;
const [loading, setLoading] = useState(true);
const [reading, setReading] = useState(null);
useEffect(() => {
fetchCompatibility();
}, []);
async function fetchCompatibility() {
try {
const response = await fetch(
`https://your-api.com/api/matches/${matchId}/compatibility`,
{
headers: { 'Authorization': `Bearer ${YOUR_AUTH_TOKEN}` }
}
);
const data = await response.json();
setReading(data);
} catch (error) {
console.error('Failed to fetch compatibility:', error);
} finally {
setLoading(false);
}
}
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#6366f1" />
</View>
);
}
return (
<ScrollView style={styles.container}>
{/* Compatibility Score */}
<View style={styles.scoreCard}>
<Text style={styles.scoreTitle}>Compatibility Score</Text>
<View style={styles.scoreCircle}>
<Text style={styles.scoreValue}>{reading.compatibility}%</Text>
</View>
<Text style={styles.scoreSubtitle}>
{reading.compatibility >= 70 ? '🔥 Strong Connection' :
reading.compatibility >= 50 ? '💫 Promising Match' :
'🌙 Take Your Time'}
</Text>
</View>
{/* Card Spread */}
<View style={styles.cardsContainer}>
{reading.reading.map((position, index) => (
<View key={index} style={styles.cardPosition}>
<Text style={styles.positionName}>{position.position}</Text>
<View style={styles.card}>
<Image
source={{ uri: position.imageUrl }}
style={[
styles.cardImage,
position.reversed && styles.cardImageReversed
]}
resizeMode="contain"
/>
<Text style={styles.cardName}>{position.card}</Text>
{position.reversed && (
<Text style={styles.reversedLabel}>(Reversed)</Text>
)}
</View>
<View style={styles.keywords}>
{position.keywords.map((keyword, i) => (
<View key={i} style={styles.keywordChip}>
<Text style={styles.keywordText}>{keyword}</Text>
</View>
))}
</View>
<Text style={styles.cardMeaning}>{position.meaning}</Text>
</View>
))}
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f9fafb',
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
scoreCard: {
alignItems: 'center',
padding: 24,
backgroundColor: 'white',
marginBottom: 16,
},
scoreTitle: {
fontSize: 18,
fontWeight: '600',
color: '#374151',
marginBottom: 16,
},
scoreCircle: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: '#eef2ff',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
},
scoreValue: {
fontSize: 42,
fontWeight: 'bold',
color: '#6366f1',
},
scoreSubtitle: {
fontSize: 16,
color: '#6b7280',
},
cardsContainer: {
padding: 16,
},
cardPosition: {
backgroundColor: 'white',
borderRadius: 12,
padding: 20,
marginBottom: 16,
},
positionName: {
fontSize: 20,
fontWeight: 'bold',
color: '#1f2937',
marginBottom: 12,
},
card: {
alignItems: 'center',
marginVertical: 16,
},
cardImage: {
width: 180,
height: 300,
borderRadius: 8,
},
cardImageReversed: {
transform: [{ rotate: '180deg' }],
},
cardName: {
fontSize: 18,
fontWeight: '600',
color: '#374151',
marginTop: 12,
},
reversedLabel: {
fontSize: 14,
color: '#9ca3af',
fontStyle: 'italic',
},
keywords: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginVertical: 12,
},
keywordChip: {
backgroundColor: '#eef2ff',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
keywordText: {
fontSize: 13,
color: '#6366f1',
fontWeight: '500',
},
cardMeaning: {
fontSize: 14,
color: '#6b7280',
lineHeight: 20,
},
});
Feature 2: Icebreaker Card Draw
Quick 3-card spread shown when users match. Provides instant conversation starters.
API: Draw Random Cards
Endpoint:
POST /tarot/draw
Request:
{
"count": 3,
"allowReversals": true,
"seed": "match-12345-icebreaker"
}
Response:
{
"seed": "match-12345-icebreaker",
"cards": [
{
"id": "page-of-swords",
"name": "Page of Swords",
"arcana": "minor",
"suit": "swords",
"number": 11,
"position": 1,
"reversed": false,
"keywords": ["New ideas", "curiosity", "thirst for knowledge"],
"meaning": "The Page of Swords is full of energy, passion and enthusiasm...",
"imageUrl": "https://roxyapi.com/img/tarot/minor/page-of-swords.jpg"
},
{
"id": "five-of-swords",
"name": "Five of Swords",
"arcana": "minor",
"suit": "swords",
"number": 5,
"position": 2,
"reversed": true,
"keywords": ["Reconciliation", "making amends", "past resentment"],
"meaning": "The Five of Swords reversed speaks for those times when...",
"imageUrl": "https://roxyapi.com/img/tarot/minor/five-of-swords.jpg"
},
{
"id": "three-of-cups",
"name": "Three of Cups",
"arcana": "minor",
"suit": "cups",
"number": 3,
"position": 3,
"reversed": true,
"keywords": ["Independence", "alone time", "three's a crowd"],
"meaning": "While the upright Three of Cups is a card of friendship...",
"imageUrl": "https://roxyapi.com/img/tarot/minor/three-of-cups.jpg"
}
]
}
Implementation: Swift (iOS)
import Foundation
struct IcebreakerCard: Codable {
let id: String
let name: String
let arcana: String
let suit: String?
let position: Int
let reversed: Bool
let keywords: [String]
let meaning: String
let imageUrl: String
}
struct IcebreakerResponse: Codable {
let seed: String
let cards: [IcebreakerCard]
}
class TarotService {
private let apiKey: String
private let baseURL = "https://roxyapi.com/api/v2/tarot"
init(apiKey: String) {
self.apiKey = apiKey
}
func fetchIcebreaker(for matchId: String) async throws -> IcebreakerResponse {
let seed = "\(matchId)-icebreaker"
let url = URL(string: "\(baseURL)/draw")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"count": 3,
"allowReversals": true,
"seed": seed
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw TarotError.invalidResponse
}
return try JSONDecoder().decode(IcebreakerResponse.self, from: data)
}
}
enum TarotError: Error {
case invalidResponse
}
UI: Match Screen with Icebreaker
import SwiftUI
import Kingfisher
struct MatchCardView: View {
let match: Match
@State private var icebreaker: IcebreakerResponse?
@State private var showCards = false
var body: some View {
VStack(spacing: 0) {
// Match Info
VStack(spacing: 12) {
AsyncImage(url: URL(string: match.photoUrl)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Color.gray
}
.frame(width: 120, height: 120)
.clipShape(Circle())
Text(match.name)
.font(.title2.bold())
Text("\(match.age) • \(match.location)")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 24)
Divider()
// Icebreaker Section
VStack(spacing: 16) {
HStack {
Image(systemName: "sparkles")
.foregroundColor(.purple)
Text("Your Tarot Icebreaker")
.font(.headline)
Spacer()
}
if let cards = icebreaker?.cards {
HStack(spacing: 12) {
ForEach(cards, id: \.id) { card in
VStack {
KFImage(URL(string: card.imageUrl))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 120)
.cornerRadius(8)
.rotationEffect(card.reversed ? .degrees(180) : .zero)
Text(card.name)
.font(.caption2)
.multilineTextAlignment(.center)
.lineLimit(2)
}
.frame(maxWidth: .infinity)
}
}
// Suggested message
if let prompt = generateConversationPrompt(from: cards) {
VStack(alignment: .leading, spacing: 8) {
Text("💬 Try This:")
.font(.caption.bold())
.foregroundColor(.purple)
Text(prompt)
.font(.callout)
.foregroundColor(.secondary)
.padding(12)
.background(Color.purple.opacity(0.1))
.cornerRadius(8)
}
}
} else {
ProgressView()
}
}
.padding()
Spacer()
// Action Buttons
HStack(spacing: 16) {
Button {
// Message action
} label: {
Label("Message", systemImage: "message.fill")
.frame(maxWidth: .infinity)
.padding()
.background(Color.purple)
.foregroundColor(.white)
.cornerRadius(12)
}
Button {
showCards = true
} label: {
Image(systemName: "sparkles")
.frame(width: 50, height: 50)
.background(Color.purple.opacity(0.1))
.foregroundColor(.purple)
.cornerRadius(12)
}
}
.padding()
}
.sheet(isPresented: $showCards) {
IcebreakerDetailView(cards: icebreaker?.cards ?? [])
}
.task {
await loadIcebreaker()
}
}
func loadIcebreaker() async {
let service = TarotService(apiKey: Config.tarotApiKey)
do {
icebreaker = try await service.fetchIcebreaker(for: match.id)
} catch {
print("Failed to load icebreaker:", error)
}
}
func generateConversationPrompt(from cards: [IcebreakerCard]) -> String? {
guard let firstCard = cards.first else { return nil }
let prompts = [
"I got \(firstCard.name) in my tarot icebreaker... what do you think that means for us? 🔮",
"The cards drew \(firstCard.name) for our match. Ever done a tarot reading before?",
"So apparently the universe thinks we're \(firstCard.keywords.first?.lowercased() ?? "interesting") together 😊 What's your card say?"
]
return prompts.randomElement()
}
}
Feature 3: Yes/No Oracle
Quick decision-making feature: "Should I message them?" "Should I ask for their number?"
API: Yes/No Reading
Endpoint:
POST /tarot/yes-no
Request:
{
"question": "Should I send a message to my match?",
"seed": "optional-seed-for-reproducibility"
}
Response:
{
"question": "Should I send a message to my match?",
"answer": "Yes",
"strength": "Qualified",
"card": {
"id": "three-of-wands",
"name": "Three of Wands",
"arcana": "minor",
"reversed": false,
"keywords": ["Progress", "expansion", "foresight"],
"imageUrl": "https://roxyapi.com/img/tarot/minor/three-of-wands.jpg"
},
"interpretation": "Qualified Yes: Three of Wands suggests forward momentum..."
}
Answer Types:
"Yes"- Strong positive indication"No"- Clear negative indication"Maybe"- Unclear, requires more reflection
Strength Levels:
"Strong"- Major arcana upright or highly positive card"Qualified"- Minor arcana or moderate energy"Weak"- Reversed card or ambiguous meaning
Implementation: Kotlin (Android)
data class YesNoRequest(
val question: String,
val seed: String? = null
)
data class YesNoCard(
val id: String,
val name: String,
val arcana: String,
val reversed: Boolean,
val keywords: List<String>,
val imageUrl: String
)
data class YesNoResponse(
val question: String,
val answer: String, // "Yes", "No", "Maybe"
val strength: String, // "Strong", "Qualified", "Weak"
val card: YesNoCard,
val interpretation: String
)
interface TarotAPI {
@POST("/tarot/yes-no")
suspend fun getYesNo(
@Body request: YesNoRequest,
@Header("X-API-Key") apiKey: String
): YesNoResponse
}
class TarotRepository(private val apiKey: String) {
private val api: TarotAPI = Retrofit.Builder()
.baseUrl("https://roxyapi.com/api")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TarotAPI::class.java)
suspend fun askYesNo(question: String): Result<YesNoResponse> = try {
val response = api.getYesNo(
YesNoRequest(question),
apiKey
)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
UI: Yes/No Decision Dialog
@Composable
fun YesNoOracleDialog(
question: String,
onDismiss: () -> Unit,
onAnswerReceived: (YesNoResponse) -> Unit
) {
var loading by remember { mutableStateOf(true) }
var response by remember { mutableStateOf<YesNoResponse?>(null) }
val repository = remember { TarotRepository(BuildConfig.TAROT_API_KEY) }
LaunchedEffect(question) {
repository.askYesNo(question)
.onSuccess {
response = it
loading = false
}
.onFailure {
loading = false
}
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Star,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text("Tarot Guidance")
}
},
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = question,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 16.dp)
)
if (loading) {
CircularProgressIndicator()
} else if (response != null) {
// Card Image
AsyncImage(
model = response!!.card.imageUrl,
contentDescription = response!!.card.name,
modifier = Modifier
.height(200.dp)
.padding(vertical = 16.dp)
)
// Answer
val answerColor = when (response!!.answer) {
"Yes" -> Color(0xFF10b981)
"No" -> Color(0xFFef4444)
else -> Color(0xFFf59e0b)
}
Text(
text = response!!.answer,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = answerColor,
modifier = Modifier.padding(bottom = 8.dp)
)
// Strength Badge
Surface(
color = answerColor.copy(alpha = 0.1f),
shape = RoundedCornerShape(16.dp)
) {
Text(
text = response!!.strength,
style = MaterialTheme.typography.labelSmall,
color = answerColor,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
Spacer(modifier = Modifier.height(12.dp))
// Card Name
Text(
text = response!!.card.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
// Interpretation
Text(
text = response!!.interpretation,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 8.dp)
)
}
}
},
confirmButton = {
TextButton(onClick = {
response?.let { onAnswerReceived(it) }
onDismiss()
}) {
Text("Got It")
}
}
)
}
// Usage in Match Screen
@Composable
fun MatchActionButtons(match: Match) {
var showYesNo by remember { mutableStateOf(false) }
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = { /* message action */ },
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.Send, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Message")
}
OutlinedButton(
onClick = { showYesNo = true }
) {
Icon(Icons.Default.Star, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("Ask Tarot")
}
}
if (showYesNo) {
YesNoOracleDialog(
question = "Should I message ${match.name}?",
onDismiss = { showYesNo = false },
onAnswerReceived = { response ->
// Track analytics
logEvent("tarot_yes_no_used", mapOf(
"answer" to response.answer,
"strength" to response.strength
))
}
)
}
}
Feature 4: Daily Match Card
Each match gets a unique daily card (changes at midnight, consistent throughout the day).
Implementation Strategy
// Generate deterministic seed from match + date
function getDailyMatchSeed(matchId, userId) {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD UTC
return `daily-${matchId}-${userId}-${today}`;
}
// Fetch daily card for match
app.get('/api/matches/:matchId/daily-card', async (req, res) => {
const { matchId } = req.params;
const userId = req.user.id;
const seed = getDailyMatchSeed(matchId, userId);
try {
const response = await axios.post(
`${TAROT_BASE_URL}/daily`,
{
date: new Date().toISOString().split('T')[0],
userId: seed
},
{ headers: { 'X-API-Key': TAROT_API_KEY } }
);
res.json({
matchId,
date: response.data.date,
card: {
name: response.data.card.name,
keywords: response.data.card.keywords,
imageUrl: response.data.card.imageUrl,
reversed: response.data.card.reversed
},
message: `Today's energy with ${match.name}: ${response.data.dailyMessage}`
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch daily card' });
}
});
Production Tips
1. Caching Strategy
Cache tarot readings to reduce API calls and improve performance:
const NodeCache = require('node-cache');
const readingCache = new NodeCache({ stdTTL: 86400 }); // 24 hours
async function getCachedReading(cacheKey, fetchFunction) {
const cached = readingCache.get(cacheKey);
if (cached) return cached;
const fresh = await fetchFunction();
readingCache.set(cacheKey, fresh);
return fresh;
}
// Usage
const compatibility = await getCachedReading(
`compat-${user1Id}-${user2Id}-${today}`,
() => getCompatibilityReading(user1Id, user2Id, today)
);
2. A/B Test Messaging
Test impact on key metrics:
- Control Group: No tarot features
- Variant A: Icebreaker cards only
- Variant B: Full suite (icebreaker + compatibility + yes/no)
Track:
- First message rate
- Response rate
- Messages per match
- Profile revisits
3. Gamification
Card Collection:
- Track which cards users have seen
- Award badges for rare cards (Death, Tower, Fool)
- Display collection progress in profile
Compatibility Leaderboard:
- Show "Top Match This Week" based on scores
- Encourage profile visits to see high-scoring matches
4. Social Sharing
Generate shareable OG images:
const { createCanvas, loadImage } = require('canvas');
async function generateCompatibilityImage(reading, score) {
const canvas = createCanvas(1200, 630);
const ctx = canvas.getContext('2d');
// Background gradient
const gradient = ctx.createLinearGradient(0, 0, 1200, 630);
gradient.addColorStop(0, '#6366f1');
gradient.addColorStop(1, '#a855f7');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1200, 630);
// Score
ctx.fillStyle = 'white';
ctx.font = 'bold 120px Arial';
ctx.textAlign = 'center';
ctx.fillText(`${score}%`, 600, 200);
ctx.font = '36px Arial';
ctx.fillText('Compatibility', 600, 260);
// Cards (load and draw 3 card images)
const cardY = 320;
const cardSpacing = 240;
for (let i = 0; i < 3; i++) {
const cardImage = await loadImage(reading.positions[i].card.imageUrl);
const x = 300 + (i * cardSpacing);
ctx.drawImage(cardImage, x, cardY, 180, 280);
}
return canvas.toBuffer('image/png');
}
// Express route
app.get('/api/matches/:matchId/share-image', async (req, res) => {
const reading = await getCompatibilityReading(/* ... */);
const image = await generateCompatibilityImage(reading, score);
res.contentType('image/png');
res.send(image);
});
5. Push Notifications
Send daily reminders to check match cards:
// Using Firebase Cloud Messaging
async function sendDailyMatchNotification(userId, match) {
const message = {
notification: {
title: `New Card with ${match.name} 🔮`,
body: `See what today's tarot reveals about your connection`
},
data: {
type: 'daily_match_card',
matchId: match.id
},
token: user.fcmToken
};
await admin.messaging().send(message);
}
// Scheduled job (run daily at 9 AM)
cron.schedule('0 9 * * *', async () => {
const activeMatches = await Match.find({ status: 'active' });
for (const match of activeMatches) {
await sendDailyMatchNotification(match.user1Id, {
id: match.id,
name: match.user2Name
});
}
});
Analytics & Optimization
Key Metrics to Track:
- Tarot feature usage rate (% of matches that use any tarot feature)
- First message rate (control vs tarot-enabled matches)
- Average messages per match
- Session duration on compatibility screen
- Yes/No oracle usage before messaging
- Social share rate (compatibility images)
Sample Analytics Implementation:
// Segment/Mixpanel event tracking
function trackTarotEvent(event, properties) {
analytics.track({
userId: req.user.id,
event: event,
properties: {
...properties,
timestamp: new Date().toISOString(),
platform: req.headers['user-agent']
}
});
}
// Usage
trackTarotEvent('compatibility_viewed', {
matchId: match.id,
score: compatibility.score,
cardCount: compatibility.reading.length
});
trackTarotEvent('yes_no_consulted', {
question: question,
answer: response.answer,
strength: response.strength,
actedUpon: true // did user message after "yes"?
});
trackTarotEvent('icebreaker_used_in_message', {
matchId: match.id,
cardMentioned: card.name,
messageLength: message.length
});
Pricing Considerations
RoxyAPI Tarot API pricing:
- Starter: ₹999/month for 10,000 requests
- Growth: ₹2,999/month for 50,000 requests
- Scale: ₹9,999/month for 250,000 requests
Cost Optimization:
- Cache daily cards (1 request per match per day)
- Cache compatibility readings (1 request per match pair per day)
- Icebreaker draws are instant (no caching needed, minimal API calls)
- Yes/No oracle: 1 request per query (can't cache, user-specific)
Example Cost Analysis (10k daily active users, 1k daily matches):
- Daily cards: 10k requests/day
- Compatibility readings: 1k requests/day
- Icebreakers: 1k requests/day
- Yes/No queries: ~500 requests/day
- Total: ~13k requests/day = ~400k/month → Scale plan (₹9,999/month)
Troubleshooting
Q: Compatibility score seems random A: Review your scoring algorithm. Consider card suit meanings (Cups = emotions/love are more relevant), position weight, and reversed impacts.
Q: Same reading showing for different matches
A: Ensure your seed includes BOTH user IDs in consistent order (sort alphabetically): seed = [user1, user2].sort().join('-') + '-' + date
Q: Cards not updating at midnight
A: Use UTC date formatting consistently: new Date().toISOString().split('T')[0]. Avoid local timezones.
Q: Yes/No oracle always says "Yes" A: API returns balanced answers. If you see bias, check if you're caching results incorrectly or reusing seeds.
Next Steps
You now have a complete tarot integration for dating apps! Enhance further:
- Relationship Forecast: 7-day tarot forecast for each match
- Conversation Prompts: AI-generated questions based on drawn cards
- Video Date Icebreakers: Draw cards live during video calls
- Anniversary Readings: Special spread on match anniversaries
- Profile Cards: Let users showcase their "power card" on their profile
Resources
- API Docs: roxyapi.com/docs
- Pricing: roxyapi.com/pricing
- Support: [email protected]
- Sample Code: GitHub repository with full implementations
About the Author: Priya Sharma is a product engineer specializing in social and dating app features, with expertise in gamification, engagement mechanics, and API integrations. She has shipped tarot and astrology features that drove 40%+ increases in messaging rates across multiple dating platforms.