RoxyAPI

Menu

Building Reproducible Tarot Readings with Seeded Random API Calls

14 min read
By Michael Anderson
TarotAPI IntegrationDaily ReadingPersonalization

Learn how to implement consistent, shareable tarot readings using seed-based randomness. Build features like personalized daily cards, reading history, and shareable results with practical code examples for web and mobile apps.

Building Reproducible Tarot Readings with Seeded Random API Calls

Most tarot APIs generate random results every time you call them. This creates problems:

  • Daily cards change when users refresh the app
  • Readings cannot be shared (your "3-card spread" is different from my "3-card spread" seconds later)
  • History tracking fails (you cannot retrieve yesterday's reading with same API call)
  • Testing breaks (unpredictable outputs make unit tests impossible)

The RoxyAPI Tarot API solves this with seeded randomness: provide an optional seed parameter, and you get identical results every time. Same seed = same cards in same order, guaranteed. This is the foundation for daily card features, shareable readings, and reading journals.

This guide shows you how to use seeded API calls to build production-ready features that your users expect.

What You Will Build

By the end of this tutorial, you will have implemented:

  1. Personalized Daily Cards - Each user gets one card per day, consistent across devices
  2. Reading History - Retrieve past readings by date without storing card data
  3. Shareable Readings - Generate unique URLs that show identical results to everyone
  4. Reproducible Testing - Write deterministic unit tests for tarot features
  5. Seed Management Strategies - Best practices for seed generation and storage

Prerequisites

  • RoxyAPI Tarot API Key (get one here)
  • Basic REST API knowledge
  • Framework of choice: React, React Native, Swift, Kotlin, or plain JavaScript

Understanding Seeded Randomness

Traditional Random APIs (The Problem)

# Call 1
curl -H "X-API-Key: YOUR_KEY" \
  -X POST https://roxyapi.com/api/v2/tarot/draw \
  -d '{"count": 3}'

# Result: [The Fool, Ten of Cups, Queen of Swords]

# Call 2 (one second later, same parameters)
curl -H "X-API-Key: YOUR_KEY" \
  -X POST https://roxyapi.com/api/v2/tarot/draw \
  -d '{"count": 3}'

# Result: [The Tower, Ace of Wands, King of Pentacles]
# ❌ Different cards! Cannot recreate first reading.

Problem: Every call generates new random results. You cannot:

  • Show users "today's card" consistently
  • Let users share readings with friends
  • Implement reading history without storing full card data in your database

Seeded Random APIs (The Solution)

# Call 1 with seed
curl -H "X-API-Key: YOUR_KEY" \
  -X POST https://roxyapi.com/api/v2/tarot/draw \
  -d '{"count": 3, "seed": "user123-2025-01-27"}'

# Result: [The Fool, Ten of Cups, Queen of Swords]

# Call 2 (hours later, same seed)
curl -H "X-API-Key: YOUR_KEY" \
  -X POST https://roxyapi.com/api/v2/tarot/draw \
  -d '{"count": 3, "seed": "user123-2025-01-27"}'

# Result: [The Fool, Ten of Cups, Queen of Swords]
# ✅ Identical cards! Reproducible reading.

How It Works:

  1. API generates reproducible results from your seed string
  2. Same seed always produces the same card sequence
  3. Different seeds produce different but equally random results
  4. No seed? You get true random draws each time

Key Insight: Seeds are NOT stored by the API. Your app controls seed generation and storage. The API is purely deterministic: seed input → cards output.

API Endpoints That Support Seeds

All RoxyAPI Tarot endpoints accept optional seed parameter:

Endpoint Seed Parameter Use Case
POST /tarot/daily Not needed (auto-generated) Daily card feature
POST /tarot/draw seed (optional string) General card draws
POST /tarot/three-card seed (optional string) Past-present-future spreads
POST /tarot/celtic-cross seed (optional string) 10-card spreads
POST /tarot/love-spread seed (optional string) Relationship readings
POST /tarot/career-spread seed (optional string) Career guidance
POST /tarot/custom-spread seed (optional string) Custom spread layouts
POST /tarot/yes-no seed (optional string) Yes/no oracle

Note: /tarot/daily endpoint generates seeds automatically using userId + date, so you do not need to manage seeds for daily cards.

Feature 1: Personalized Daily Cards

Requirements

  • Each user gets one card per day
  • Same card shows across all devices (web + mobile)
  • Card changes at midnight UTC
  • Users can view past daily cards

Implementation Strategy

The /tarot/daily endpoint automatically generates seeds using this formula:

seed = userId ? `${userId}-${date}` : date

Examples:

  • Logged-in user on Jan 27: user123-2025-01-27 → Card A
  • Same user on Jan 28: user123-2025-01-28 → Card B
  • Same user on Jan 27 again: user123-2025-01-27 → Card A (same as before)
  • Anonymous user on Jan 27: 2025-01-27 → Card C (all anonymous users get same card)

Code: React Daily Card Component

import { useState, useEffect } from 'react';

export default function DailyCard({ userId }) {
  const [card, setCard] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchDailyCard();
  }, [userId]);
  
  async function fetchDailyCard(targetDate = null) {
    setLoading(true);
    
    try {
      const response = await fetch('https://roxyapi.com/api/v2/tarot/daily', {
        method: 'POST',
        headers: {
          'X-API-Key': process.env.REACT_APP_TAROT_API_KEY,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          userId: userId || undefined,
          date: targetDate || undefined, // Defaults to today
        }),
      });
      
      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }
      
      const data = await response.json();
      setCard(data);
    } catch (error) {
      console.error('Failed to fetch daily card:', error);
    } finally {
      setLoading(false);
    }
  }
  
  if (loading) return <div>Loading your daily card...</div>;
  if (!card) return <div>Failed to load card</div>;
  
  return (
    <div className="daily-card">
      <h2>Your Card for {card.date}</h2>
      <p className="seed-info">Seed: {card.seed}</p>
      
      <div className="card-display">
        <img
          src={card.card.imageUrl}
          alt={card.card.name}
          className={card.card.reversed ? 'reversed' : ''}
        />
        <h3>{card.card.name}</h3>
        {card.card.reversed && <span className="badge">Reversed</span>}
      </div>
      
      <div className="keywords">
        {card.card.keywords.map((kw, i) => (
          <span key={i} className="keyword">{kw}</span>
        ))}
      </div>
      
      <p className="daily-message">{card.dailyMessage}</p>
      
      <div className="history-nav">
        <button onClick={() => {
          const yesterday = new Date();
          yesterday.setDate(yesterday.getDate() - 1);
          fetchDailyCard(yesterday.toISOString().split('T')[0]);
        }}>
          ← Yesterday
        </button>
        
        <button onClick={() => fetchDailyCard()}>
          Today
        </button>
      </div>
    </div>
  );
}

Key Points

  1. No database storage needed: Seeds are generated from userId + date, so you can recreate any past reading without storing card IDs
  2. Works offline: Cache today's response, serve from cache until midnight UTC
  3. Anonymous users: Omit userId for guest users (everyone gets same card per day)
  4. Cross-device sync: Same userId + date = same card on web, iOS, Android

Advanced: Time Zone Handling

Users expect daily cards to change at their midnight, not UTC midnight. Strategy:

function getUserLocalDate() {
  // Get user's local date as YYYY-MM-DD
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const day = String(now.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

// Use local date for seed generation
const localDate = getUserLocalDate();
await fetchDailyCard(localDate);

Trade-off: User traveling across time zones may see card change mid-day. Solution: Store user's primary timezone in profile, always use that for seed generation.

Feature 2: Reading History Without Database Storage

Problem

Traditional approach requires storing every reading in your database:

CREATE TABLE readings (
  id UUID PRIMARY KEY,
  user_id UUID,
  created_at TIMESTAMP,
  card1_id TEXT,
  card1_reversed BOOLEAN,
  card2_id TEXT,
  card2_reversed BOOLEAN,
  -- etc...
);

This gets expensive at scale (1M users × 365 daily cards = 365M rows/year).

Solution: Seed-Based History

Store only the seed, not the cards:

CREATE TABLE readings (
  id UUID PRIMARY KEY,
  user_id UUID,
  created_at TIMESTAMP,
  seed TEXT,  -- Just the seed!
  spread_type TEXT  -- 'daily', 'three-card', 'celtic-cross'
);

Retrieve past reading by calling API with saved seed:

async function getReadingHistory(userId, limit = 10) {
  // Fetch seeds from your database
  const seeds = await db.query(
    'SELECT seed, created_at, spread_type FROM readings WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2',
    [userId, limit]
  );
  
  // Recreate readings from seeds
  const readings = await Promise.all(
    seeds.map(async ({ seed, created_at, spread_type }) => {
      const endpoint = getEndpointForSpreadType(spread_type);
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
          'X-API-Key': API_KEY,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ seed }),
      });
      
      const data = await response.json();
      
      return {
        date: created_at,
        ...data,
      };
    })
  );
  
  return readings;
}

function getEndpointForSpreadType(type) {
  const baseUrl = 'https://roxyapi.com/api/v2/tarot';
  switch (type) {
    case 'daily': return `${baseUrl}/daily`;
    case 'three-card': return `${baseUrl}/three-card`;
    case 'celtic-cross': return `${baseUrl}/celtic-cross`;
    default: return `${baseUrl}/draw`;
  }
}

Benefits:

  • 95% smaller database: Store seed strings (20-50 bytes) instead of full card objects (500+ bytes)
  • No data migration: Card meanings update automatically when API improves interpretations
  • Infinite history: Seeds never expire, can recreate readings from years ago

Trade-offs:

  • Requires API call per reading (batch requests to minimize latency)
  • Cannot query by card (e.g., "show all readings with The Fool")
  • Dependent on API availability (cache recent readings client-side as fallback)

Feature 3: Shareable Readings

Use Case

User gets 3-card spread, wants to share with friend: "Check out my reading!"

Implementation

Step 1: Generate Shareable Seed

Use unique reading ID as seed:

import { v4 as uuidv4 } from 'uuid';

async function createShareableReading(spreadType = 'three-card') {
  // Generate unique seed
  const readingId = uuidv4(); // e.g., '123e4567-e89b-12d3-a456-426614174000'
  
  // Create reading with this seed
  const response = await fetch(`https://roxyapi.com/api/v2/tarot/${spreadType}`, {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ seed: readingId }),
  });
  
  const reading = await response.json();
  
  // Generate shareable URL
  const shareUrl = `https://yourapp.com/readings/${readingId}`;
  
  return {
    reading,
    shareUrl,
    readingId,
  };
}

Step 2: Public Reading View

Create route that recreates reading from URL seed:

// pages/readings/[readingId].js (Next.js example)

export async function getServerSideProps({ params }) {
  const { readingId } = params;
  
  // Recreate reading using readingId as seed
  const response = await fetch('https://roxyapi.com/api/v2/tarot/three-card', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.TAROT_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ seed: readingId }),
  });
  
  const reading = await response.json();
  
  return {
    props: {
      reading,
      readingId,
    },
  };
}

export default function SharedReading({ reading, readingId }) {
  return (
    <div className="shared-reading">
      <h1>Tarot Reading</h1>
      <p>Reading ID: {readingId}</p>
      
      <div className="cards">
        {reading.cards.map((card, i) => (
          <div key={i} className="card">
            <img src={card.imageUrl} alt={card.name} />
            <h3>{card.name}</h3>
            <p><strong>{reading.positions[i].name}:</strong></p>
            <p>{reading.positions[i].interpretation}</p>
          </div>
        ))}
      </div>
      
      <button onClick={() => {
        navigator.clipboard.writeText(window.location.href);
        alert('Link copied! Share with friends.');
      }}>
        📋 Copy Link
      </button>
    </div>
  );
}

Step 3: Social Sharing

Add Open Graph meta tags for rich previews:

<Head>
  <title>My Tarot Reading | YourApp</title>
  <meta property="og:title" content="Check out my tarot reading!" />
  <meta property="og:description" content={`Past: ${reading.cards[0].name}, Present: ${reading.cards[1].name}, Future: ${reading.cards[2].name}`} />
  <meta property="og:image" content={reading.cards[1].imageUrl} />
  <meta property="og:url" content={`https://yourapp.com/readings/${readingId}`} />
</Head>

Mobile Share Sheet (React Native)

import { Share } from 'react-native';

async function shareReading(readingId) {
  try {
    await Share.share({
      message: `Check out my tarot reading: https://yourapp.com/readings/${readingId}`,
      title: 'My Tarot Reading',
      url: `https://yourapp.com/readings/${readingId}`, // iOS only
    });
  } catch (error) {
    console.error('Share failed:', error);
  }
}

Feature 4: Reproducible Testing

Problem

Traditional random APIs make testing impossible:

// ❌ Test fails randomly
test('daily card returns The Fool', async () => {
  const card = await fetchDailyCard();
  expect(card.name).toBe('The Fool'); // Fails 77/78 times
});

Solution: Fixed Seeds for Tests

// ✅ Test passes every time
test('daily card with seed returns consistent result', async () => {
  const response = await fetch('https://roxyapi.com/api/v2/tarot/daily', {
    method: 'POST',
    headers: { 'X-API-Key': TEST_API_KEY },
    body: JSON.stringify({
      userId: 'test-user',
      date: '2025-01-01', // Fixed date
    }),
  });
  
  const data = await response.json();
  
  // This seed always produces same card
  expect(data.seed).toBe('test-user-2025-01-01');
  expect(data.card.name).toBe('The Emperor'); // Verified once, stable forever
  expect(data.card.reversed).toBe(false);
  expect(data.card.position).toBe(1);
});

Test Multiple Scenarios

const testCases = [
  {
    seed: 'test-scenario-1',
    expectedCards: ['The Fool', 'The Magician', 'The High Priestess'],
  },
  {
    seed: 'test-scenario-2',
    expectedCards: ['Ten of Cups', 'Ace of Swords', 'Queen of Pentacles'],
  },
];

testCases.forEach(({ seed, expectedCards }) => {
  test(`three-card spread with seed "${seed}" returns expected cards`, async () => {
    const response = await fetch('https://roxyapi.com/api/v2/tarot/three-card', {
      method: 'POST',
      headers: { 'X-API-Key': TEST_API_KEY },
      body: JSON.stringify({ seed }),
    });
    
    const data = await response.json();
    const cardNames = data.cards.map(c => c.name);
    
    expect(cardNames).toEqual(expectedCards);
  });
});

Snapshot Testing

test('celtic cross spread snapshot', async () => {
  const response = await fetch('https://roxyapi.com/api/v2/tarot/celtic-cross', {
    method: 'POST',
    headers: { 'X-API-Key': TEST_API_KEY },
    body: JSON.stringify({ seed: 'snapshot-test-seed' }),
  });
  
  const data = await response.json();
  
  // Jest snapshot - fails if API response structure changes
  expect(data).toMatchSnapshot();
});

Best Practices: Seed Generation

Seed Format Guidelines

Good Seeds:

✅ userId-date: "user123-2025-01-27"
✅ readingId: "reading_abc123xyz"
✅ compound: "user123-session456-timestamp1706284800"
✅ random: "a4f2e8d9c1b5a3f7" (generated securely)

Bad Seeds:

❌ Sequential: "1", "2", "3" (predictable, users can guess others' readings)
❌ Too short: "x" (poor entropy, collisions likely)
❌ Sensitive data: "[email protected]" (never include passwords)
❌ Special chars: "user#123@$%" (URL-unsafe, breaks sharing)

Collision Avoidance

Ensure seeds are unique to avoid unintended duplicates:

function generateDailyCardSeed(userId) {
  const date = new Date().toISOString().split('T')[0];
  return `daily-${userId}-${date}`;
}

function generateReadingSeed(userId, readingType) {
  const timestamp = Date.now();
  const random = Math.random().toString(36).substring(7);
  return `${readingType}-${userId}-${timestamp}-${random}`;
}

function generateShareableSeed() {
  // UUIDs guarantee uniqueness
  return crypto.randomUUID(); // Browser API
  // Or: import { v4 } from 'uuid'; return v4();
}

Security Considerations

DO:

  • ✅ Use unique random IDs for shareable readings (prevents guessing)
  • ✅ Generate seeds server-side for sensitive features
  • ✅ Rate-limit reading creation to prevent spam

DON'T:

  • ❌ Expose raw user database IDs in seeds
  • ❌ Use predictable patterns (attackers can enumerate all readings)
  • ❌ Include authentication tokens in seeds

Example: Secure Shareable Seeds

import { randomUUID } from 'crypto';

function generateSecureSeed(readingType) {
  // Generate cryptographically secure random ID
  const uniqueId = randomUUID(); // e.g., "a4f2e8d9-c1b5-a3f7-8e2d-9c4a1b3f5e7d"
  return `${readingType}-${uniqueId.split('-')[0]}`;
}

// Result: "three-card-a4f2e8d9"
// Users cannot predict or enumerate other readings

Advanced: Seed Rotation for Privacy

If users want to "reset" their daily cards (e.g., new year, new readings):

// User table
users {
  id
  seed_version INT DEFAULT 1
}

// Generate seed with version
function generateDailyCardSeed(userId, seedVersion, date) {
  return `daily-v${seedVersion}-${userId}-${date}`;
}

// Reset button
async function resetDailyCards(userId) {
  await db.query(
    'UPDATE users SET seed_version = seed_version + 1 WHERE id = $1',
    [userId]
  );
  
  // All future daily cards now use different seeds
}

Performance Optimization

Cache Responses Client-Side

Since seeds are deterministic, cache aggressively:

const readingCache = new Map(); // In-memory cache

async function fetchWithCache(endpoint, seed) {
  const cacheKey = `${endpoint}-${seed}`;
  
  if (readingCache.has(cacheKey)) {
    return readingCache.get(cacheKey);
  }
  
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ seed }),
  });
  
  const data = await response.json();
  readingCache.set(cacheKey, data);
  
  return data;
}

Batch History Requests

Fetch multiple readings in parallel:

async function getReadingHistory(userSeeds) {
  const readings = await Promise.all(
    userSeeds.map(async ({ seed, spreadType }) => {
      const endpoint = getEndpointForSpreadType(spreadType);
      return fetchWithCache(endpoint, seed);
    })
  );
  
  return readings;
}

Debugging: Verify Seed Consistency

Test that your seed generation produces consistent results:

async function testSeedConsistency(seed, iterations = 5) {
  const results = [];
  
  for (let i = 0; i < iterations; i++) {
    const response = await fetch('https://roxyapi.com/api/v2/tarot/draw', {
      method: 'POST',
      headers: { 'X-API-Key': API_KEY },
      body: JSON.stringify({ count: 3, seed }),
    });
    
    const data = await response.json();
    results.push(data.cards.map(c => c.name).join(', '));
  }
  
  const allSame = results.every(r => r === results[0]);
  console.log(`Seed consistency test: ${allSame ? 'PASSED' : 'FAILED'}`);
  console.log('Results:', results);
}

// Run test
testSeedConsistency('test-seed-123', 5);
// Expected output: All 5 results identical

Pricing and API Usage

Seeded calls count against your monthly request limit just like random calls. Check pricing plans for details.

Cost Optimization:

  • Cache daily cards aggressively (1 API call per user per day)
  • Store seeds in database (free), only call API when displaying reading
  • Use client-side caching for shareable readings (first user pays API call, subsequent viewers use cache)

Troubleshooting

Q: Same seed returns different cards on different days A: Seeds are deterministic forever. If results change, check:

  1. Are you including date in seed by mistake? (user123-2025-01-27 vs user123)
  2. Is seed generation logic consistent? (timezone issues, timestamp precision)

Q: Shareable readings show different cards for different users A: Verify seed is in URL and extracted correctly. Common issue: URL encoding (spaces become %20)

Q: Daily cards do not reset at midnight A: Check server timezone. Use UTC dates: new Date().toISOString().split('T')[0] not toLocaleDateString()

Q: Test seeds break after API update A: API card order never changes (78 cards in fixed sequence). If test breaks, you changed the seed or endpoint.

Next Steps

You now understand how to build reproducible tarot features! Explore further:

  1. Reading Journal - Store seeds + user notes, recreate readings on demand
  2. Weekly Insights - Generate 7 seeds (one per day), show week at a glance
  3. Reading Comparisons - Compare two dates side-by-side using seeds
  4. Social Features - Friend readings, comment threads (seed as post ID)
  5. Gamification - Achievement tracking (drew The Fool 10 times this year)

Resources


About the Author: Michael Anderson is a full-stack developer specializing in spiritual and wellness applications, with experience building daily card features, reading history systems, and shareable tarot experiences for mobile and web platforms.