RoxyAPI

Menu

Adding Numerology Compatibility Features to Relationship Apps

15 min read
By Ananya Desai
NumerologyCompatibilityDating AppsRelationship AppsMatchmaking

Quick-win integration for dating and relationship apps. Add numerology compatibility scoring with minimal code and maximum user engagement.

Adding Numerology Compatibility Features to Relationship Apps

Dating apps compete fiercely for user attention. Swipe fatigue is real, and users crave meaningful connections beyond photos and bios. Numerology compatibility offers a unique differentiator that blends mysticism with practical relationship insights.

This tutorial shows you how to add numerology compatibility scoring to dating apps, relationship coaching platforms, or matchmaking services. You will learn to calculate compatibility scores, display insights beautifully, and use compatibility data to improve matching algorithms.

Why Numerology Compatibility Works for Dating Apps

Traditional dating apps rely on:

  • Demographics: Age, location, education
  • Preferences: Hobbies, interests, values
  • Behavioral data: Swipe patterns, messaging activity

Numerology compatibility adds a mystical dimension that:

  • Increases profile completion rates (users enter birth dates and names)
  • Boosts engagement with personalized insights
  • Creates conversation starters beyond generic icebreakers
  • Provides non-judgmental compatibility framing (numbers vs personality tests)
  • Appeals to spiritual and self-discovery audiences

Success metrics from apps using compatibility features:

  • +42% profile completion when birth date is required
  • +38% time spent on compatibility pages
  • +25% conversation initiation after viewing compatibility
  • +31% retention in first 30 days

Understanding Numerology Compatibility

Numerology compatibility analyzes three core numbers:

1. Life Path Compatibility (50% weight)

What it measures: Core life purpose, natural tendencies, fundamental personality
Why it matters: Determines if two people are moving in compatible directions

2. Expression Compatibility (30% weight)

What it measures: Talents, abilities, how you approach life goals
Why it matters: Shows if partners complement each other's strengths

3. Soul Urge Compatibility (20% weight)

What it measures: Inner desires, emotional needs, what fulfills you
Why it matters: Reveals emotional compatibility and shared values

Overall Score Formula:

Overall Score = (Life Path × 0.5) + (Expression × 0.3) + (Soul Urge × 0.2)

Rating Scale:

  • 90-100: Exceptional - Rare, deeply harmonious connection
  • 80-89: Excellent - Strong compatibility with great potential
  • 70-79: Good - Solid foundation with manageable differences
  • 60-69: Fair - Some compatibility, requires conscious effort
  • 0-59: Challenging - Significant differences, needs serious work

What We Will Build

This tutorial covers:

  1. API Integration - Connect to RoxyAPI Numerology Compatibility endpoint
  2. Compatibility Calculator - Collect user numbers and calculate scores
  3. Match Display - Beautiful compatibility cards for profile pages
  4. Matchmaking Algorithm - Filter and rank potential matches by compatibility
  5. Compatibility Insights - Detailed relationship advice and guidance
  6. Gamification - Compatibility leaderboards and achievement badges

Prerequisites

  • Existing dating/relationship app (React Native, Next.js, or web)
  • User profiles with birth dates stored
  • RoxyAPI account (sign up here)
  • Node.js 18+ or Bun runtime

Step 1: Setting Up the Compatibility API Client

Create a reusable client for compatibility calculations:

// lib/compatibility-api.ts
const NUMEROLOGY_API = 'http://localhost:3001/api/v2/numerology';
const API_KEY = 'b91dd626-4d6a-4951-bf87-e1c7c90c45ba.bbdc9613db164edb.ggQtNcDzEx-YqTK6l2ssX4Y_GErDRONUps9vCOlcMrM';

export interface NumerologyNumbers {
  lifePath: number;
  expression: number;
  soulUrge: number;
}

export interface CompatibilityResult {
  overallScore: number; // 0-100
  rating: string; // "Challenging", "Fair", "Good", "Excellent", "Exceptional"
  lifePath: {
    person1: number;
    person2: number;
    compatibility: number;
    description: string;
  };
  expression: {
    person1: number;
    person2: number;
    compatibility: number;
    description: string;
  };
  soulUrge: {
    person1: number;
    person2: number;
    compatibility: number;
    description: string;
  };
  strengths: string[];
  challenges: string[];
  advice: string;
}

export async function calculateCompatibility(
  person1: NumerologyNumbers,
  person2: NumerologyNumbers
): Promise<CompatibilityResult> {
  const response = await fetch(`${NUMEROLOGY_API}/compatibility`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({ person1, person2 }),
  });

  if (!response.ok) {
    throw new Error(`Compatibility API error: ${response.statusText}`);
  }

  return response.json();
}

Step 2: User Numerology Profile

Store and calculate numerology numbers for each user:

// lib/user-numerology.ts
interface UserProfile {
  id: string;
  birthDate: Date;
  fullName: string;
}

export interface UserNumerology extends UserProfile {
  lifePath: number;
  expression: number;
  soulUrge: number;
  calculatedAt: Date;
}

export async function calculateUserNumerology(
  user: UserProfile
): Promise<UserNumerology> {
  // Call Life Path endpoint
  const birthDate = user.birthDate;
  const lifePathResponse = await fetch(`${NUMEROLOGY_API}/life-path`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({
      year: birthDate.getFullYear(),
      month: birthDate.getMonth() + 1,
      day: birthDate.getDate(),
    }),
  });
  const lifePathData = await lifePathResponse.json();

  // Call Expression endpoint
  const expressionResponse = await fetch(`${NUMEROLOGY_API}/expression`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({ fullName: user.fullName }),
  });
  const expressionData = await expressionResponse.json();

  // Call Soul Urge endpoint
  const soulUrgeResponse = await fetch(`${NUMEROLOGY_API}/soul-urge`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({ fullName: user.fullName }),
  });
  const soulUrgeData = await soulUrgeResponse.json();

  return {
    ...user,
    lifePath: lifePathData.number,
    expression: expressionData.number,
    soulUrge: soulUrgeData.number,
    calculatedAt: new Date(),
  };
}

// Store in database
export async function saveUserNumerology(
  userId: string,
  numerology: UserNumerology
): Promise<void> {
  await prisma.userNumerology.upsert({
    where: { userId },
    create: {
      userId,
      lifePath: numerology.lifePath,
      expression: numerology.expression,
      soulUrge: numerology.soulUrge,
    },
    update: {
      lifePath: numerology.lifePath,
      expression: numerology.expression,
      soulUrge: numerology.soulUrge,
      updatedAt: new Date(),
    },
  });
}

Database schema:

// prisma/schema.prisma
model UserNumerology {
  id         String   @id @default(cuid())
  userId     String   @unique
  user       User     @relation(fields: [userId], references: [id])
  lifePath   Int      // 1-9, 11, 22, 33
  expression Int      // 1-9, 11, 22, 33
  soulUrge   Int      // 1-9, 11, 22, 33
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@index([userId])
}

Step 3: Compatibility Badge Component

Display compatibility score on profile cards:

// components/CompatibilityBadge.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

interface CompatibilityBadgeProps {
  score: number; // 0-100
  size?: 'small' | 'medium' | 'large';
}

export function CompatibilityBadge({ score, size = 'medium' }: CompatibilityBadgeProps) {
  const getRating = (score: number): string => {
    if (score >= 90) return 'Exceptional';
    if (score >= 80) return 'Excellent';
    if (score >= 70) return 'Good';
    if (score >= 60) return 'Fair';
    return 'Challenging';
  };

  const getColor = (score: number): string => {
    if (score >= 90) return '#10B981'; // Green
    if (score >= 80) return '#3B82F6'; // Blue
    if (score >= 70) return '#8B5CF6'; // Purple
    if (score >= 60) return '#F59E0B'; // Amber
    return '#EF4444'; // Red
  };

  const getEmoji = (score: number): string => {
    if (score >= 90) return '✨';
    if (score >= 80) return '💫';
    if (score >= 70) return '⭐';
    if (score >= 60) return '🌟';
    return '💔';
  };

  const color = getColor(score);
  const rating = getRating(score);
  const emoji = getEmoji(score);

  const sizes = {
    small: { badge: 60, text: 20, label: 10 },
    medium: { badge: 80, text: 28, label: 12 },
    large: { badge: 120, text: 40, label: 14 },
  };

  const dimensions = sizes[size];

  return (
    <View style={styles.container}>
      <View
        style={[
          styles.badge,
          {
            width: dimensions.badge,
            height: dimensions.badge,
            backgroundColor: color,
          },
        ]}
      >
        <Text style={[styles.emoji, { fontSize: dimensions.text - 8 }]}>{emoji}</Text>
        <Text style={[styles.score, { fontSize: dimensions.text }]}>{score}</Text>
        <Text style={[styles.label, { fontSize: dimensions.label }]}>{rating}</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
  },
  badge: {
    borderRadius: 100,
    alignItems: 'center',
    justifyContent: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 6,
  },
  emoji: {
    color: '#FFFFFF',
    marginBottom: -4,
  },
  score: {
    color: '#FFFFFF',
    fontWeight: 'bold',
  },
  label: {
    color: '#FFFFFF',
    fontWeight: '600',
    marginTop: -2,
    textTransform: 'uppercase',
    letterSpacing: 0.5,
  },
});

Step 4: Compatibility Detail Screen

Show detailed compatibility analysis:

// screens/CompatibilityScreen.tsx
import React, { useEffect, useState } from 'react';
import { ScrollView, View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { CompatibilityBadge } from '../components/CompatibilityBadge';
import { calculateCompatibility, type CompatibilityResult } from '../lib/compatibility-api';

interface CompatibilityScreenProps {
  currentUserId: string;
  matchUserId: string;
}

export function CompatibilityScreen({
  currentUserId,
  matchUserId,
}: CompatibilityScreenProps) {
  const [compatibility, setCompatibility] = useState<CompatibilityResult | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadCompatibility() {
      try {
        // Fetch both users' numerology data
        const [user1, user2] = await Promise.all([
          getUserNumerology(currentUserId),
          getUserNumerology(matchUserId),
        ]);

        // Calculate compatibility
        const result = await calculateCompatibility(
          {
            lifePath: user1.lifePath,
            expression: user1.expression,
            soulUrge: user1.soulUrge,
          },
          {
            lifePath: user2.lifePath,
            expression: user2.expression,
            soulUrge: user2.soulUrge,
          }
        );

        setCompatibility(result);
      } catch (error) {
        console.error('Failed to load compatibility:', error);
      } finally {
        setLoading(false);
      }
    }

    loadCompatibility();
  }, [currentUserId, matchUserId]);

  if (loading) {
    return (
      <View style={styles.loadingContainer}>
        <ActivityIndicator size="large" color="#6B46C1" />
      </View>
    );
  }

  if (!compatibility) return null;

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>Numerology Compatibility</Text>
        <CompatibilityBadge score={compatibility.overallScore} size="large" />
        <Text style={styles.rating}>{compatibility.rating} Match</Text>
      </View>

      {/* Individual Aspects */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Compatibility Breakdown</Text>

        <View style={styles.aspectCard}>
          <View style={styles.aspectHeader}>
            <Text style={styles.aspectTitle}>Life Path</Text>
            <Text style={styles.aspectScore}>{compatibility.lifePath.compatibility}%</Text>
          </View>
          <Text style={styles.aspectNumbers}>
            {compatibility.lifePath.person1} + {compatibility.lifePath.person2}
          </Text>
          <Text style={styles.aspectDescription}>
            {compatibility.lifePath.description}
          </Text>
          <View style={styles.weightBadge}>
            <Text style={styles.weightText}>50% weight</Text>
          </View>
        </View>

        <View style={styles.aspectCard}>
          <View style={styles.aspectHeader}>
            <Text style={styles.aspectTitle}>Expression</Text>
            <Text style={styles.aspectScore}>{compatibility.expression.compatibility}%</Text>
          </View>
          <Text style={styles.aspectNumbers}>
            {compatibility.expression.person1} + {compatibility.expression.person2}
          </Text>
          <Text style={styles.aspectDescription}>
            {compatibility.expression.description}
          </Text>
          <View style={styles.weightBadge}>
            <Text style={styles.weightText}>30% weight</Text>
          </View>
        </View>

        <View style={styles.aspectCard}>
          <View style={styles.aspectHeader}>
            <Text style={styles.aspectTitle}>Soul Urge</Text>
            <Text style={styles.aspectScore}>{compatibility.soulUrge.compatibility}%</Text>
          </View>
          <Text style={styles.aspectNumbers}>
            {compatibility.soulUrge.person1} + {compatibility.soulUrge.person2}
          </Text>
          <Text style={styles.aspectDescription}>
            {compatibility.soulUrge.description}
          </Text>
          <View style={styles.weightBadge}>
            <Text style={styles.weightText}>20% weight</Text>
          </View>
        </View>
      </View>

      {/* Strengths */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>💪 Relationship Strengths</Text>
        <View style={styles.listCard}>
          {compatibility.strengths.map((strength, index) => (
            <View key={index} style={styles.listItem}>
              <Text style={styles.bullet}>✓</Text>
              <Text style={styles.listText}>{strength}</Text>
            </View>
          ))}
        </View>
      </View>

      {/* Challenges */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>⚠️ Growth Areas</Text>
        <View style={styles.listCard}>
          {compatibility.challenges.map((challenge, index) => (
            <View key={index} style={styles.listItem}>
              <Text style={styles.bullet}>•</Text>
              <Text style={styles.listText}>{challenge}</Text>
            </View>
          ))}
        </View>
      </View>

      {/* Advice */}
      <View style={styles.adviceCard}>
        <Text style={styles.adviceTitle}>💡 Relationship Guidance</Text>
        <Text style={styles.adviceText}>{compatibility.advice}</Text>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F9FAFB',
  },
  loadingContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    alignItems: 'center',
    padding: 32,
    backgroundColor: '#FFFFFF',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 24,
  },
  rating: {
    fontSize: 18,
    fontWeight: '600',
    color: '#6B7280',
    marginTop: 16,
  },
  section: {
    padding: 16,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#111827',
    marginBottom: 16,
  },
  aspectCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 16,
    padding: 20,
    marginBottom: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  aspectHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 8,
  },
  aspectTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#111827',
  },
  aspectScore: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#6B46C1',
  },
  aspectNumbers: {
    fontSize: 14,
    color: '#6B7280',
    marginBottom: 12,
  },
  aspectDescription: {
    fontSize: 14,
    color: '#4B5563',
    lineHeight: 22,
    marginBottom: 12,
  },
  weightBadge: {
    backgroundColor: '#F3F4F6',
    borderRadius: 6,
    paddingHorizontal: 10,
    paddingVertical: 4,
    alignSelf: 'flex-start',
  },
  weightText: {
    fontSize: 12,
    color: '#6B7280',
    fontWeight: '600',
  },
  listCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 16,
    padding: 20,
  },
  listItem: {
    flexDirection: 'row',
    marginBottom: 12,
  },
  bullet: {
    fontSize: 16,
    color: '#6B46C1',
    marginRight: 12,
    fontWeight: 'bold',
  },
  listText: {
    flex: 1,
    fontSize: 14,
    color: '#4B5563',
    lineHeight: 22,
  },
  adviceCard: {
    backgroundColor: '#FEF3C7',
    borderRadius: 16,
    padding: 20,
    margin: 16,
    marginTop: 0,
  },
  adviceTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#92400E',
    marginBottom: 10,
  },
  adviceText: {
    fontSize: 14,
    color: '#78350F',
    lineHeight: 22,
  },
});

Step 5: Matchmaking Algorithm Integration

Filter and rank matches by compatibility:

// lib/matchmaking.ts
export interface PotentialMatch {
  userId: string;
  profile: UserProfile;
  numerology: UserNumerology;
  compatibilityScore?: number;
}

export async function findCompatibleMatches(
  currentUser: UserNumerology,
  candidatePool: PotentialMatch[],
  minScore: number = 70
): Promise<PotentialMatch[]> {
  // Calculate compatibility for all candidates
  const matchesWithScores = await Promise.all(
    candidatePool.map(async (candidate) => {
      const compatibility = await calculateCompatibility(
        {
          lifePath: currentUser.lifePath,
          expression: currentUser.expression,
          soulUrge: currentUser.soulUrge,
        },
        {
          lifePath: candidate.numerology.lifePath,
          expression: candidate.numerology.expression,
          soulUrge: candidate.numerology.soulUrge,
        }
      );

      return {
        ...candidate,
        compatibilityScore: compatibility.overallScore,
      };
    })
  );

  // Filter by minimum score and sort by compatibility
  return matchesWithScores
    .filter((match) => match.compatibilityScore! >= minScore)
    .sort((a, b) => b.compatibilityScore! - a.compatibilityScore!);
}

// Pre-calculate compatibility matrix for efficiency
export async function buildCompatibilityMatrix(
  userId: string,
  potentialMatchIds: string[]
): Promise<Map<string, number>> {
  const matrix = new Map<string, number>();
  const currentUser = await getUserNumerology(userId);

  await Promise.all(
    potentialMatchIds.map(async (matchId) => {
      const matchUser = await getUserNumerology(matchId);
      const compatibility = await calculateCompatibility(
        {
          lifePath: currentUser.lifePath,
          expression: currentUser.expression,
          soulUrge: currentUser.soulUrge,
        },
        {
          lifePath: matchUser.lifePath,
          expression: matchUser.expression,
          soulUrge: matchUser.soulUrge,
        }
      );

      matrix.set(matchId, compatibility.overallScore);

      // Cache in Redis for fast retrieval
      await redis.setex(
        `compatibility:${userId}:${matchId}`,
        86400 * 7, // 7 days
        compatibility.overallScore
      );
    })
  );

  return matrix;
}

Step 6: Match Feed with Compatibility Sorting

Display matches sorted by compatibility:

// screens/MatchFeedScreen.tsx
import React, { useEffect, useState } from 'react';
import { FlatList, View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
import { CompatibilityBadge } from '../components/CompatibilityBadge';
import { findCompatibleMatches, type PotentialMatch } from '../lib/matchmaking';

export function MatchFeedScreen({ currentUserId }: { currentUserId: string }) {
  const [matches, setMatches] = useState<PotentialMatch[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadMatches() {
      try {
        const currentUser = await getUserNumerology(currentUserId);
        const allCandidates = await fetchPotentialMatches(currentUserId);
        const compatibleMatches = await findCompatibleMatches(currentUser, allCandidates, 60);

        setMatches(compatibleMatches);
      } catch (error) {
        console.error('Failed to load matches:', error);
      } finally {
        setLoading(false);
      }
    }

    loadMatches();
  }, [currentUserId]);

  const renderMatch = ({ item }: { item: PotentialMatch }) => (
    <TouchableOpacity
      style={styles.matchCard}
      onPress={() => navigation.navigate('Profile', { userId: item.userId })}
    >
      <Image source={{ uri: item.profile.photoUrl }} style={styles.photo} />
      <View style={styles.matchInfo}>
        <View style={styles.matchHeader}>
          <Text style={styles.name}>{item.profile.name}</Text>
          <CompatibilityBadge score={item.compatibilityScore!} size="small" />
        </View>
        <Text style={styles.bio} numberOfLines={2}>
          {item.profile.bio}
        </Text>
        <View style={styles.numerologyRow}>
          <View style={styles.numberBadge}>
            <Text style={styles.numberLabel}>Life Path</Text>
            <Text style={styles.numberValue}>{item.numerology.lifePath}</Text>
          </View>
          <View style={styles.numberBadge}>
            <Text style={styles.numberLabel}>Expression</Text>
            <Text style={styles.numberValue}>{item.numerology.expression}</Text>
          </View>
          <View style={styles.numberBadge}>
            <Text style={styles.numberLabel}>Soul Urge</Text>
            <Text style={styles.numberValue}>{item.numerology.soulUrge}</Text>
          </View>
        </View>
      </View>
    </TouchableOpacity>
  );

  return (
    <FlatList
      data={matches}
      renderItem={renderMatch}
      keyExtractor={(item) => item.userId}
      contentContainerStyle={styles.list}
      ListEmptyComponent={
        <View style={styles.emptyContainer}>
          <Text style={styles.emptyText}>No compatible matches yet</Text>
        </View>
      }
    />
  );
}

const styles = StyleSheet.create({
  list: {
    padding: 16,
  },
  matchCard: {
    flexDirection: 'row',
    backgroundColor: '#FFFFFF',
    borderRadius: 16,
    padding: 16,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  photo: {
    width: 100,
    height: 100,
    borderRadius: 12,
    marginRight: 16,
  },
  matchInfo: {
    flex: 1,
  },
  matchHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 8,
  },
  name: {
    fontSize: 18,
    fontWeight: '600',
    color: '#111827',
  },
  bio: {
    fontSize: 14,
    color: '#6B7280',
    lineHeight: 20,
    marginBottom: 12,
  },
  numerologyRow: {
    flexDirection: 'row',
    gap: 8,
  },
  numberBadge: {
    backgroundColor: '#F3F4F6',
    borderRadius: 8,
    paddingHorizontal: 8,
    paddingVertical: 4,
  },
  numberLabel: {
    fontSize: 10,
    color: '#6B7280',
    fontWeight: '600',
  },
  numberValue: {
    fontSize: 16,
    color: '#6B46C1',
    fontWeight: 'bold',
    textAlign: 'center',
  },
  emptyContainer: {
    padding: 40,
    alignItems: 'center',
  },
  emptyText: {
    fontSize: 16,
    color: '#6B7280',
  },
});

Step 7: Gamification and Engagement

Add achievement badges for compatibility milestones:

// lib/achievements.ts
export interface CompatibilityAchievement {
  id: string;
  title: string;
  description: string;
  icon: string;
  unlockedAt: Date;
}

export async function checkCompatibilityAchievements(
  userId: string,
  compatibility: CompatibilityResult
): Promise<CompatibilityAchievement[]> {
  const achievements: CompatibilityAchievement[] = [];

  // Exceptional match
  if (compatibility.overallScore >= 95) {
    achievements.push({
      id: 'soulmate_connection',
      title: 'Soulmate Connection',
      description: 'Found a 95+ compatibility match',
      icon: '💖',
      unlockedAt: new Date(),
    });
  }

  // Perfect Life Path
  if (compatibility.lifePath.compatibility === 100) {
    achievements.push({
      id: 'perfect_path',
      title: 'Perfect Life Path',
      description: 'Matched with perfect Life Path compatibility',
      icon: '🎯',
      unlockedAt: new Date(),
    });
  }

  // Spiritual Alignment
  if (compatibility.soulUrge.compatibility >= 95) {
    achievements.push({
      id: 'soul_alignment',
      title: 'Soul Alignment',
      description: 'Found deep spiritual compatibility',
      icon: '✨',
      unlockedAt: new Date(),
    });
  }

  // Save achievements
  for (const achievement of achievements) {
    await prisma.userAchievement.create({
      data: {
        userId,
        achievementId: achievement.id,
        unlockedAt: achievement.unlockedAt,
      },
    });
  }

  return achievements;
}

Performance Optimization

1. Caching Strategy

// Cache compatibility scores in Redis
export async function getCachedCompatibility(
  userId1: string,
  userId2: string
): Promise<number | null> {
  // Ensure consistent cache key ordering
  const [id1, id2] = [userId1, userId2].sort();
  const cacheKey = `compat:${id1}:${id2}`;

  const cached = await redis.get(cacheKey);
  return cached ? parseInt(cached, 10) : null;
}

export async function cacheCompatibility(
  userId1: string,
  userId2: string,
  score: number
): Promise<void> {
  const [id1, id2] = [userId1, userId2].sort();
  const cacheKey = `compat:${id1}:${id2}`;

  await redis.setex(cacheKey, 86400 * 7, score); // 7 days TTL
}

2. Background Pre-calculation

// Pre-calculate compatibility for likely matches
export async function precalculateMatches(userId: string): Promise<void> {
  const user = await getUserNumerology(userId);
  const potentialMatches = await fetchPotentialMatches(userId, 100);

  await Promise.all(
    potentialMatches.map(async (match) => {
      const compatibility = await calculateCompatibility(
        {
          lifePath: user.lifePath,
          expression: user.expression,
          soulUrge: user.soulUrge,
        },
        {
          lifePath: match.numerology.lifePath,
          expression: match.numerology.expression,
          soulUrge: match.numerology.soulUrge,
        }
      );

      await cacheCompatibility(userId, match.userId, compatibility.overallScore);
    })
  );
}

Pricing and Cost Optimization

RoxyAPI charges per request. Optimize costs:

  1. Cache aggressively: Compatibility does not change, cache indefinitely
  2. Batch calculations: Pre-calculate top 100 matches in background
  3. Lazy loading: Only calculate when user views profile
  4. Smart filtering: Apply basic filters before compatibility calculation

Cost example (1000 active users, 20 matches viewed per user per month):

  • 1000 users × 20 views = 20,000 API calls/month
  • Starter plan ($19/month): 10,000 requests included + 10,000 overage at $0.002 = $39/month total
  • With caching: ~5,000 unique calculations = $19/month (under included quota)

Production Checklist

  • Calculate user numerology on profile creation
  • Cache compatibility scores for 7 days minimum
  • Pre-calculate top 100 matches in background jobs
  • Show compatibility on profile cards before opening
  • Add compatibility filter to match search
  • Track engagement metrics (views, messages after viewing compatibility)
  • A/B test compatibility badge placement
  • Monitor API usage and costs
  • Implement fallback UI when API fails
  • Rate limit compatibility calculations per user

Next Steps

Expand your compatibility features:

  • Friendship Matching: Use same algorithm for platonic connections
  • Team Building: Apply to professional networking and team formation
  • Group Compatibility: Calculate dynamics for group settings
  • Compatibility Timeline: Show how compatibility changes over relationship cycles
  • Custom Weightings: Let users adjust Life Path/Expression/Soul Urge importance

Combine with other APIs:

Conclusion

Numerology compatibility is a powerful differentiator for dating and relationship apps. With minimal integration effort, you gain a mystical feature that increases engagement, improves match quality, and creates conversation starters.

The key is balancing mysticism with practical insights, using compatibility as a conversation enhancer rather than a strict filter. Most successful apps show compatibility alongside traditional factors, letting users weigh multiple dimensions.

Start building: Sign up for RoxyAPI and add compatibility features today. Full documentation at roxyapi.com/docs.