# 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 a previous reading with the same API call)
- **Testing breaks** (unpredictable outputs make unit tests impossible)

The [RoxyAPI Tarot API](https://roxyapi.com/products/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](https://roxyapi.com/pricing))
- Basic REST API knowledge
- Framework of choice: React, React Native, Swift, Kotlin, or plain JavaScript

## Understanding Seeded Randomness

### Traditional Random APIs (The Problem)

```bash
# Call 1
curl -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -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" -H "Content-Type: application/json" \
  -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 the same daily card consistently
- Let users share readings with friends
- Implement reading history without storing full card data in your database

### Seeded Random APIs (The Solution)

```bash
# Call 1 with seed
curl -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -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" -H "Content-Type: application/json" \
  -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/spreads/three-card` | `seed` (optional string) | Past-present-future spreads |
| `POST /tarot/spreads/celtic-cross` | `seed` (optional string) | 10-card spreads |
| `POST /tarot/spreads/love` | `seed` (optional string) | Relationship readings |
| `POST /tarot/spreads/career` | `seed` (optional string) | Career guidance |
| `POST /tarot/spreads/custom` | `seed` (optional string) | Custom spread layouts |
| `POST /tarot/yes-no` | `seed` (optional string) | Yes/no oracle |

**Note**: `/tarot/daily` endpoint generates seeds automatically using `seed` + `date`, so you do not need to manage seeds for daily cards. Pass any unique identifier (userId, email hash, session token) as the `seed` field.

## 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 = inputSeed ? `${inputSeed}-${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

```jsx
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({
          seed: 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 seed + date, so you can recreate any past reading without storing card IDs
2. **Works offline**: Cache the response, serve from cache until midnight UTC
3. **Anonymous users**: Omit `seed` for guest users (everyone gets same card per day)
4. **Cross-device sync**: Same seed + date = same card on web, iOS, Android

### Advanced: Time Zone Handling

Users expect daily cards to change at **their midnight**, not UTC midnight. Strategy:

```javascript
function getUserLocalDate() {
  // Get 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**: A user traveling across time zones may see the card change mid-day. Solution: Store the primary timezone in the user profile, always use that for seed generation.

## Feature 2: Reading History Without Database Storage

### Problem
Traditional approach requires storing every reading in your database:

```sql
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:

```sql
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:

```javascript
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}/spreads/three-card`;
    case 'celtic-cross': return `${baseUrl}/spreads/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:

```javascript
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/spreads/${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:

```javascript
// 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/spreads/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.positions.map((pos, i) => (
          <div key={i} className="card">
            <img src={pos.card.imageUrl} alt={pos.card.name} />
            <h3>{pos.card.name}</h3>
            <p><strong>{pos.name}:</strong></p>
            <p>{pos.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:

```jsx
<Head>
  <title>My Tarot Reading | YourApp</title>
  <meta property="og:title" content="Check out my tarot reading!" />
  <meta property="og:description" content={`Past: ${reading.positions[0].card.name}, Present: ${reading.positions[1].card.name}, Future: ${reading.positions[2].card.name}`} />
  <meta property="og:image" content={reading.positions[1].card.imageUrl} />
  <meta property="og:url" content={`https://yourapp.com/readings/${readingId}`} />
</Head>
```

### Mobile Share Sheet (React Native)

```jsx
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:

```javascript
// ❌ 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

```javascript
// ✅ 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({
      seed: '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

```javascript
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/spreads/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

```javascript
test('celtic cross spread snapshot', async () => {
  const response = await fetch('https://roxyapi.com/api/v2/tarot/spreads/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: "user@email.com-password123" (never include passwords)
❌ Special chars: "user#123@$%" (URL-unsafe, breaks sharing)
```

### Collision Avoidance

Ensure seeds are unique to avoid unintended duplicates:

```javascript
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**

```javascript
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):

```javascript
// 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:

```javascript
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:

```javascript
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:

```javascript
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](https://roxyapi.com/pricing) 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

- **API Documentation**: [roxyapi.com/docs](https://roxyapi.com/docs)
- **Product Page**: [roxyapi.com/products/tarot-api](https://roxyapi.com/products/tarot-api)
- **Pricing**: [roxyapi.com/pricing](https://roxyapi.com/pricing)
- **Support**: support@roxyapi.com

---

**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.