Building Reproducible Tarot Readings with Seeded Random API Calls
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:
- Personalized Daily Cards - Each user gets one card per day, consistent across devices
- Reading History - Retrieve past readings by date without storing card data
- Shareable Readings - Generate unique URLs that show identical results to everyone
- Reproducible Testing - Write deterministic unit tests for tarot features
- 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:
- API generates reproducible results from your seed string
- Same seed always produces the same card sequence
- Different seeds produce different but equally random results
- 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
- No database storage needed: Seeds are generated from userId + date, so you can recreate any past reading without storing card IDs
- Works offline: Cache today's response, serve from cache until midnight UTC
- Anonymous users: Omit
userIdfor guest users (everyone gets same card per day) - 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:
- Are you including date in seed by mistake? (
user123-2025-01-27vsuser123) - 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:
- Reading Journal - Store seeds + user notes, recreate readings on demand
- Weekly Insights - Generate 7 seeds (one per day), show week at a glance
- Reading Comparisons - Compare two dates side-by-side using seeds
- Social Features - Friend readings, comment threads (seed as post ID)
- Gamification - Achievement tracking (drew The Fool 10 times this year)
Resources
- API Documentation: roxyapi.com/docs
- Product Page: roxyapi.com/products/tarot-api
- Pricing: roxyapi.com/pricing
- Support: [email protected]
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.