DEV Community

Robert Gilbert
Robert Gilbert

Posted on

BirthStar: One Birthday, Four Traditions, One Star Chart

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission ๐Ÿ’ธ

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.

  1. Copy everything in the code block below.
  2. Paste it into a plain-text editor and save the file as birthstar.html.
  3. 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>

Enter fullscreen mode Exit fullscreen mode

Top comments (0)