This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 2 โ Side Hustle.
What I built
BirthStar โ a single-file web app that turns one birthday into a star chart and a detailed horoscope. Not one horoscope, though: four traditions at once.
Enter a birthday and BirthStar computes, entirely in your browser:
- Your Western sun sign, its element, and its ruling planet
- Your Chinese zodiac animal, element, and yin/yang from the birth year
- The moon phase on the day you were born, with how lit it was
- Your life path number from the digits of the date
It draws those onto a zodiac wheel, then hands the chart to an open-weight model, which writes an original reading that weaves the four sources together โ including a "cross-currents" section where the traditions agree with each other, and where they pull in opposite directions. Any astrologer will tell you the disagreements are the interesting part.
The demo
Pick a date โ say, a Wood Dragon born under a full moon with a Leo sun and life path 5 โ and in seconds you get the wheel, the four-source fact sheet, and a reading in six sections: the chart at a glance, the sun sign core, the cross-currents, the life path, this week's horoscope, and lucky details (a color, a number, one small ritual).
Everything astrological that can be computed is computed locally with plain date math โ sign boundaries, the Chinese year cycle, a moon-phase approximation from the synodic month, digit-sum numerology. The model only does the writing. That split matters: the chart is identical for everyone with your birthday, every time; only the storytelling is generative.
Why open-weight is the point
BirthStar calls Llama 3.3 70B โ Meta's open-weight model โ through Groq's free tier.
- A birthday is personal data. People pay horoscope apps with their birth date, their email, and a subscription. BirthStar has no account, no server, and no database: the chart math never leaves the browser, and the only thing sent anywhere is the chart itself, to an API you key yourself.
- The business model only works open. A paid horoscope is a classic side hustle, but a closed API takes a cut of every reading and can reprice you overnight. An open-weight model on a free tier makes the marginal cost of a reading zero โ which is the entire economics of selling $2 readings.
- No lock-in. The model is one string in one file. Swap Llama for Qwen, Mixtral, or DeepSeek in ten seconds.
- Auditable by design. Because the deterministic layer is separate, anyone can check the chart math. The magic is in the synthesis, not in a black box pretending to calculate.
A closed model behind a paywall would make this a subscription app with a signup form. Open weights make it a file you can text to a friend.
What I'd do next
Birth time and place for a real rising sign and house system, a printable "full chart report" as the paid tier, and a daily-horoscope mode that keeps your chart and just advances the moon. All of it still in one file.
Try it
BirthStar is one self-contained HTML file, so here it is โ the complete source, in the post. Copy it, save it as birthstar.html, and open it in any browser. No server, no install, no build step.
- Copy everything in the code block below.
- Paste it into a plain-text editor and save the file as
birthstar.html. - Open the file, enter a birthday, draw your chart, paste a free Groq key from console.groq.com, and read what four traditions say about you. For entertainment, obviously. โจ
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BirthStar โ Your Birthday, Your Star Chart</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; background: #12101f; color: #e8e4f5; }
h1 { font-size: 1.8rem; }
.card { background: #1e1a33; border-radius: 12px; padding: 1.2rem; margin: 1rem 0; box-shadow: 0 2px 8px rgba(0,0,0,.35); }
label { display: block; margin: .6rem 0 .2rem; font-weight: 600; }
input { width: 100%; padding: .6rem; border: 1px solid #4a4370; border-radius: 8px; font-size: 1rem; box-sizing: border-box; background: #171430; color: #e8e4f5; }
button { background: #6d28d9; color: #fff; border: 0; border-radius: 8px; padding: .8rem 1.4rem; font-size: 1.05rem; cursor: pointer; margin-top: 1rem; width: 100%; }
button:disabled { background: #55507a; }
#out { white-space: pre-wrap; }
.hint { font-size: .85rem; color: #a49cc9; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem .9rem; margin-top: .6rem; }
.grid b { color: #c4b5fd; }
#chart { display: block; margin: 0 auto; }
details { margin-top: 1rem; }
</style>
</head>
<body>
<h1>โจ BirthStar</h1>
<p>Type in a birthday. Get a star chart and a detailed horoscope woven from several traditions at once โ Western zodiac, Chinese zodiac, moon phase, and numerology โ by an open-weight model. For entertainment, and priced like it should be: free.</p>
<div class="card">
<label for="dob">Birthday</label>
<input id="dob" type="date">
<label for="name">Name (optional)</label>
<input id="name" placeholder="e.g. Alex">
<button onclick="chart()">๐ฎ Draw my star chart</button>
<p class="hint">The chart math runs entirely in your browser โ your birthday never leaves this page.</p>
</div>
<div class="card" id="chartcard" style="display:none">
<h2>Your star chart</h2>
<svg id="chart" width="300" height="300" viewBox="0 0 300 300"></svg>
<div class="grid" id="facts"></div>
<p class="hint" id="cny"></p>
<div id="quick"></div>
</div>
<div class="card" id="aicard" style="display:none">
<h2>Your detailed reading</h2>
<label for="key">Groq API key (free at console.groq.com โ stays in your browser)</label>
<input id="key" type="password" placeholder="gsk_..." autocomplete="off">
<button id="go" onclick="reading()">โ๏ธ Write my reading</button>
<div id="out" style="margin-top:1rem"></div>
</div>
<details>
<summary>Why open-weight?</summary>
<p>BirthStar calls <b>llama-3.3-70b-versatile</b> โ Meta's open-weight model โ through Groq's free tier.
A birth date is personal: with an open model there is no account, no server storing your chart, and no
subscription wall between you and a bit of fun. Swap in any open model by changing one string.
The whole app is one HTML file: no build step, no server.</p>
</details>
<p class="hint">Astrology is entertainment, not science โ enjoy it that way. ๐</p>
<script>
const SIGNS = [
{n:'Capricorn', g:'โ', el:'Earth', ruler:'Saturn', from:[1,1]},
{n:'Aquarius', g:'โ', el:'Air', ruler:'Uranus', from:[1,20]},
{n:'Pisces', g:'โ', el:'Water', ruler:'Neptune', from:[2,19]},
{n:'Aries', g:'โ', el:'Fire', ruler:'Mars', from:[3,21]},
{n:'Taurus', g:'โ', el:'Earth', ruler:'Venus', from:[4,20]},
{n:'Gemini', g:'โ', el:'Air', ruler:'Mercury', from:[5,21]},
{n:'Cancer', g:'โ', el:'Water', ruler:'the Moon', from:[6,21]},
{n:'Leo', g:'โ', el:'Fire', ruler:'the Sun', from:[7,23]},
{n:'Virgo', g:'โ', el:'Earth', ruler:'Mercury', from:[8,23]},
{n:'Libra', g:'โ', el:'Air', ruler:'Venus', from:[9,23]},
{n:'Scorpio', g:'โ', el:'Water', ruler:'Pluto', from:[10,23]},
{n:'Sagittarius', g:'โ', el:'Fire', ruler:'Jupiter', from:[11,22]},
{n:'Capricorn', g:'โ', el:'Earth', ruler:'Saturn', from:[12,22]},
];
const ANIMALS = ['Rat','Ox','Tiger','Rabbit','Dragon','Snake','Horse','Goat','Monkey','Rooster','Dog','Pig'];
const STEM_ELEMENTS = ['Wood','Wood','Fire','Fire','Earth','Earth','Metal','Metal','Water','Water'];
const PHASES = ['New Moon','Waxing Crescent','First Quarter','Waxing Gibbous','Full Moon','Waning Gibbous','Last Quarter','Waning Crescent'];
const SYNODIC = 29.53058867;
function sunSign(m, d) {
let s = SIGNS[0];
for (const x of SIGNS) if (m > x.from[0] || (m === x.from[0] && d >= x.from[1])) s = x;
return s;
}
function chinese(year) {
const i = ((year - 4) % 12 + 12) % 12;
const e = STEM_ELEMENTS[((year - 4) % 10 + 10) % 10];
return { animal: ANIMALS[i], element: e, yy: (year % 2 === 0) ? 'Yang' : 'Yin' };
}
function lifePath(y, m, d) {
let s = String(y) + String(m).padStart(2,'0') + String(d).padStart(2,'0');
let n = s.split('').reduce((a,c) => a + (+c), 0);
while (n > 9 && n !== 11 && n !== 22 && n !== 33) n = String(n).split('').reduce((a,c) => a + (+c), 0);
return n;
}
function moonPhase(dateUTC) {
const ref = Date.UTC(2000, 0, 6, 18, 14) / 86400000; // known new moon
let age = ((dateUTC / 86400000 - ref) % SYNODIC + SYNODIC) % SYNODIC;
const idx = Math.floor((age / SYNODIC) * 8 + 0.5) % 8;
const illum = Math.round((1 - Math.cos(2 * Math.PI * age / SYNODIC)) / 2 * 100);
return { name: PHASES[idx], illum, age: age.toFixed(1) };
}
let DATA = null;
function drawWheel(signName) {
const svg = document.getElementById('chart');
const cx = 150, cy = 150, R = 140, r = 92;
let h = `<circle cx="${cx}" cy="${cy}" r="${R}" fill="#171430" stroke="#4a4370"/>`;
const order = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces'];
order.forEach((name, i) => {
const a0 = (i * 30 - 90) * Math.PI / 180, a1 = ((i + 1) * 30 - 90) * Math.PI / 180;
const x0 = cx + R * Math.cos(a0), y0 = cy + R * Math.sin(a0);
const x1 = cx + R * Math.cos(a1), y1 = cy + R * Math.sin(a1);
const fill = name === signName ? '#6d28d9' : '#241e45';
h += `<path d="M${cx},${cy} L${x0},${y0} A${R},${R} 0 0 1 ${x1},${y1} Z" fill="${fill}" stroke="#4a4370"/>`;
const am = (i * 30 - 75) * Math.PI / 180;
const tx = cx + (r + 22) * Math.cos(am), ty = cy + (r + 22) * Math.sin(am) + 7;
const g = SIGNS.find(s => s.n === name).g;
h += `<text x="${tx}" y="${ty}" fill="#e8e4f5" font-size="20" text-anchor="middle">${g}</text>`;
});
h += `<circle cx="${cx}" cy="${cy}" r="${r - 28}" fill="#12101f"/>`;
h += `<text x="${cx}" y="${cy + 8}" fill="#c4b5fd" font-size="22" text-anchor="middle">โ ${signName}</text>`;
svg.innerHTML = h;
}
function chart() {
const v = document.getElementById('dob').value;
if (!v) { alert('Pick a birthday first.'); return; }
const [y, m, d] = v.split('-').map(Number);
const dt = new Date(Date.UTC(y, m - 1, d));
const sun = sunSign(m, d);
const cn = chinese(y);
const lp = lifePath(y, m, d);
const moon = moonPhase(dt);
const weekday = dt.toLocaleDateString('en-US', { weekday: 'long', timeZone: 'UTC' });
const name = document.getElementById('name').value.trim();
DATA = { y, m, d, weekday, sun, cn, lp, moon, name };
drawWheel(sun.n);
document.getElementById('facts').innerHTML =
`<span><b>Sun sign</b><br>${sun.g} ${sun.n} (${sun.el}, ruled by ${sun.ruler})</span>` +
`<span><b>Born on a</b><br>${weekday}</span>` +
`<span><b>Chinese zodiac</b><br>${cn.yy} ${cn.element} ${cn.animal}</span>` +
`<span><b>Moon at birth</b><br>${moon.name}, ${moon.illum}% lit</span>` +
`<span><b>Life path number</b><br>${lp}</span>` +
`<span><b>Sources combined</b><br>Western ยท Chinese ยท Lunar ยท Numerology</span>`;
document.getElementById('cny').textContent = (m <= 2)
? 'Note: for January/February birthdays, the Chinese animal is read from the calendar year โ if you were born before Chinese New Year that year, traditional charts use the previous animal.'
: '';
document.getElementById('quick').innerHTML =
`<p>โ A ${sun.n} sun with a ${moon.name.toLowerCase()} birth moon, a ${cn.element} ${cn.animal} year, and life path ${lp} โ four traditions, one birthday. The detailed reading below weaves them together and calls out where they agree and where they argue.</p>`;
document.getElementById('chartcard').style.display = 'block';
document.getElementById('aicard').style.display = 'block';
}
async function reading() {
if (!DATA) return;
const key = document.getElementById('key').value.trim();
if (!key) { alert('Paste your free Groq API key first (console.groq.com).'); return; }
const btn = document.getElementById('go');
btn.disabled = true; btn.textContent = 'โจ Consulting the stars...';
const out = document.getElementById('out');
out.textContent = 'Asking the model...';
const { y, m, d, weekday, sun, cn, lp, moon, name } = DATA;
const prompt = `You are BirthStar, a warm, witty horoscope writer. Write an original, detailed horoscope reading that synthesizes FOUR sources for this person. Do not copy any published horoscope; invent the reading from the chart data.
Name: ${name || 'the seeker'}
Birthday: ${y}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')} (a ${weekday})
Western sun sign: ${sun.n} (${sun.el} sign, ruled by ${sun.ruler})
Chinese zodiac: ${cn.yy} ${cn.element} ${cn.animal}
Moon phase at birth: ${moon.name}, ${moon.illum}% illuminated
Life path number: ${lp}
Reply in exactly these sections:
1. THE CHART AT A GLANCE โ one or two sentences combining all four sources
2. SUN SIGN CORE โ the ${sun.n} engine of the personality
3. CROSS-CURRENTS โ where the ${cn.animal} year and the ${moon.name} moon agree with the sun sign, and where they pull against it
4. LIFE PATH ${lp} โ what this number adds
5. THIS WEEK โ a short, specific-feeling horoscope for the week ahead
6. LUCKY DETAILS โ one lucky color, number, and small ritual
Under 450 words. Playful but grounded. End with one line noting this is for entertainment.`;
try {
const r = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + key },
body: JSON.stringify({
model: 'llama-3.3-70b-versatile',
messages: [{ role: 'user', content: prompt }],
max_tokens: 900,
temperature: 0.8
})
});
if (!r.ok) throw new Error('API error ' + r.status);
const j = await r.json();
out.textContent = j.choices[0].message.content;
} catch (e) {
out.textContent = 'Error: ' + e.message + '\nCheck your API key and connection.';
}
btn.disabled = false; btn.textContent = 'โ๏ธ Write my reading';
}
</script>
</body>
</html>
Top comments (0)