- Docs
- What To Build
- Tarot Reading App
Build a tarot reading app
Ship a three-mode tarot app (daily card, three-card spread, yes-no oracle) with card imagery and reversed handling. Time to ship: 30 minutes.
Tarot is the highest-frequency divination surface Roxy ships. This tutorial wires the three most-popular features: daily card for daily active users, three-card for the entry reading, yes-no for impulse queries. One HTML file, card imagery included.
What you can build with this
- Daily tarot widgets for wellness and lifestyle apps
- Three-card past/present/future spreads
- Yes-no oracles for impulse decisions
- Celtic Cross readers (10-position premium spread)
- Love tarot spreads for dating apps
- Cached card browsers across the full 78-card deck
What you need, 30 seconds
- A Roxy API key. Get one on the pricing page.
- A text editor and a browser. That is it.
Step 1, call your first endpoint
Three separate tarot calls, same pattern. Pick a language.
# Daily card
curl -X POST https://roxyapi.com/api/v2/tarot/daily \
-H "X-API-Key: $ROXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"seed":"user-42"}'
# Three-card spread
curl -X POST https://roxyapi.com/api/v2/tarot/spreads/three-card \
-H "X-API-Key: $ROXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question":"what should I focus on this week"}'
# Yes or no
curl -X POST https://roxyapi.com/api/v2/tarot/yes-no \
-H "X-API-Key: $ROXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question":"should I take the job"}'
import { createRoxy } from '@roxyapi/sdk';
const roxy = createRoxy(process.env.ROXY_API_KEY!);
const { data: daily } = await roxy.tarot.getDailyCard({ body: { seed: 'user-42' } });
console.log(daily.card.name, daily.card.imageUrl);
const { data: spread } = await roxy.tarot.castThreeCard({
body: { question: 'what should I focus on this week' },
});
spread.positions.forEach(p => console.log(p.label, p.card.name));
const { data: yn } = await roxy.tarot.castYesNo({
body: { question: 'should I take the job' },
});
console.log(yn.answer); // "Yes" | "No" | "Maybe"
import os
from roxy_sdk import create_roxy
roxy = create_roxy(os.environ['ROXY_API_KEY'])
daily = roxy.tarot.get_daily_card(seed='user-42')
print(daily['card']['name'])
spread = roxy.tarot.cast_three_card(question='what should I focus on this week')
for p in spread['positions']:
print(p['label'], p['card']['name'])
yn = roxy.tarot.cast_yes_no(question='should I take the job')
print(yn['answer'])
claude mcp add-json --scope user roxy-tarot '{"type":"http","url":"https://roxyapi.com/mcp/tarot","headers":{"X-API-Key":"YOUR_KEY"}}'
Then ask Claude, "draw a daily tarot card for user 42" or "cast a three-card spread for me about my career." The agent picks the right tool. Full setup for Cursor, Claude Desktop, Antigravity, and other clients: MCP guide.
Step 2, build the app
Create tarot.html and paste this block.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Tarot Reading</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; max-width: 700px; margin: 0 auto; padding: 20px; background: #0f0f1a; color: #e0e0e0; }
h1 { text-align: center; margin-bottom: 24px; color: #c9a0dc; }
.tabs { display: flex; gap: 8px; margin-bottom: 24px; justify-content: center; }
.tabs button { padding: 10px 20px; border: 1px solid #333; border-radius: 8px; background: #1a1a2e; color: #aaa; cursor: pointer; }
.tabs button.active { background: #7c3aed; color: white; border-color: #7c3aed; }
.cards { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-bottom: 24px; }
.card { background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 16px; width: 200px; text-align: center; }
.card img { width: 100%; border-radius: 8px; margin-bottom: 12px; }
.card img.reversed { transform: rotate(180deg); }
.card .position { font-size: 12px; text-transform: uppercase; color: #7c3aed; margin-bottom: 8px; }
.card .name { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
.card .rev { font-size: 11px; color: #e57373; margin-bottom: 4px; }
.card .kw { font-size: 12px; color: #999; margin-bottom: 8px; }
.card .meaning { font-size: 13px; line-height: 1.5; color: #ccc; }
.answer { text-align: center; background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 24px; margin-bottom: 24px; font-size: 48px; font-weight: 700; color: #c9a0dc; }
.message { text-align: center; font-size: 15px; line-height: 1.6; color: #ccc; margin-bottom: 24px; }
.draw-btn { display: block; width: 100%; max-width: 300px; margin: 0 auto 24px; padding: 14px; background: #7c3aed; color: white; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; }
</style>
</head>
<body>
<h1>Tarot Reading</h1>
<div class="tabs">
<button class="active" data-mode="daily">Daily Card</button>
<button data-mode="three-card">Past / Present / Future</button>
<button data-mode="yes-no">Yes / No</button>
</div>
<button class="draw-btn" id="draw">Draw</button>
<div id="out"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
let mode = 'daily';
document.querySelectorAll('.tabs button').forEach(b => {
b.onclick = () => {
mode = b.dataset.mode;
document.querySelectorAll('.tabs button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
document.getElementById('out').innerHTML = '';
};
});
function card(c, position) {
return '<div class="card">' +
(position ? '<div class="position">' + position + '</div>' : '') +
(c.imageUrl ? '<img src="' + c.imageUrl + '" alt="' + c.name + '"' + (c.reversed ? ' class="reversed"' : '') + ' />' : '') +
'<div class="name">' + c.name + '</div>' +
(c.reversed ? '<div class="rev">Reversed</div>' : '') +
'<div class="kw">' + (c.keywords || []).join(' · ') + '</div>' +
'<div class="meaning">' + (c.meaning || '') + '</div>' +
'</div>';
}
document.getElementById('draw').onclick = async () => {
const out = document.getElementById('out');
out.innerHTML = '<div style="text-align:center;padding:40px;color:#888;">Shuffling...</div>';
const urls = {
'daily': 'https://roxyapi.com/api/v2/tarot/daily',
'three-card': 'https://roxyapi.com/api/v2/tarot/spreads/three-card',
'yes-no': 'https://roxyapi.com/api/v2/tarot/yes-no',
};
const res = await fetch(urls[mode], {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
body: JSON.stringify({}),
});
const data = await res.json();
if (mode === 'daily') {
out.innerHTML =
'<div class="cards">' + card(data.card) + '</div>' +
'<div class="message">' + data.dailyMessage + '</div>';
} else if (mode === 'three-card') {
out.innerHTML =
'<div class="cards">' + data.positions.map(p => card(p.card, p.label || p.name)).join('') + '</div>' +
(data.reading ? '<div class="message">' + data.reading + '</div>' : '');
} else {
out.innerHTML =
'<div class="answer">' + data.answer + '</div>' +
'<div class="cards">' + card(data.card) + '</div>' +
(data.interpretation ? '<div class="message">' + data.interpretation + '</div>' : '');
}
};
</script>
</body>
</html>
Replace YOUR_API_KEY. Save. Double-click.
Step 3, add Celtic Cross
One more endpoint, same pattern. POST /tarot/spreads/celtic-cross returns 10 positions. Add a fourth tab with mode: 'celtic-cross', add the URL to the map, the render logic already handles positions[].
Step 4, ship it
Three production tweaks.
- Seed the daily card per user. Pass
{"seed": userId}in the body. Same user gets the same card all day. Perfect for push notifications and daily email cadence. - Move the key server-side. Browser-embedded keys are for prototyping only.
- Cache the card catalog. Call
GET /tarot/cardsonce, store it. Users can browse the full 78-card deck without hitting the API per card view.
Deploy to Cloudflare Pages or Vercel. Free tier covers thousands of visitors a month.
Gotchas
- Daily card returns
dailyMessage, notinterpretation. That field is on yes-no. Three-card usesreadingat the top level plus per-positioninterpretation. - Reversed cards set
card.reversed: true. Flip the image with CSS (transform: rotate(180deg)) and show a "Reversed" badge. Themeaningtext is already context-aware. - Seeded is deterministic per day. Pass
seed: userIdand the same user gets the same daily card for the whole calendar date. Reset happens at UTC midnight. imageUrlpoints to our CDN. No image hosting needed. The path is stable enough to cache client-side.- Three-card
positions[]is always length 3, withlabelvaluesPast,Present,Future. Celtic Cross returns length 10 with numbered positions.
What to build next
- The AI chatbot tutorial wires tarot tools to an LLM so users can ask natural-language questions.
- The tarot guide lists every spread and field shape.
- The dating app tutorial shows the astrology surface your users may want after a tarot reading.