Two months ago I knew nothing about Chinese astrology. I didn't even know my own zodiac sign.
Now I've built an app that calculates BaZi (八字) charts and generates AI-powered readings. Here's what went right, what broke, and why DeepSeek is both amazing and frustrating.
What is BaZi?
BaZi — literally "Eight Characters" — is a Chinese fortune-telling system based on your birth date and time. It maps your life to five elements (Wood, Fire, Earth, Metal, Water) and their interactions.
A BaZi chart looks at four pillars: year, month, day, and hour of birth. Each pillar has a Heavenly Stem and an Earthly Branch, giving you eight characters total. From these, you derive the Day Master (your core self), identify element balances, and interpret life patterns.
Westerners might call it "Chinese astrology on steroids." There's no hand-waving about "Mercury in retrograde" — BaZi is algorithmic. Given the same inputs, you get the same chart every time. That predictability is why I thought: I can code this.
The Tech Stack
- Backend: FastAPI (Python), single file, no ORM
- Astrology engine: lunar-python — a Chinese library that handles the lunar calendar and sexagenary cycle calculations
-
AI: DeepSeek Chat API (
deepseek-chat, notdeepseek-reasoner) - Frontend: Next.js 16 with Tailwind CSS
- Payments: Gumroad (digital product) + NowPayments (crypto)
- Hosting: Single VPS, Nginx reverse proxy, HTTPS via Let's Encrypt
Total cost to build: ~$5/month VPS + API calls (~$0.01 per reading).
The Hard Part: DeepSeek Prompt Engineering
I tried DeepSeek's reasoning model first (deepseek-reasoner). Big mistake. The reasoning tokens consumed 16K tokens of context, then the final content field came back empty. Not an error — just empty. No explanation. The reasoning was "complete" in the model's mind, but the actual response never materialized.
Switching to deepseek-chat fixed the empty response problem immediately. But it introduced a new one: the model kept confusing the user's gender.
In BaZi, the Day Master's gender determines which elements are favorable. If you get the gender wrong, the entire reading is wrong. DeepSeek kept defaulting to male or ignoring the gender field entirely.
The fix that worked: I moved the gender instruction to the very top of the system prompt, before everything else. Not buried in the middle. First sentence: The person is [male/female]. And I reinforced it in every user message: Gender: [male/female], Birth: [date], Time: [time].
Only then did it stop hallucinating the gender.
# The working system prompt structure
system_prompt = f"""You are a professional BaZi fortune teller.
The person is {gender}. Use 乾造 for male, 坤造 for female.
BaZi Chart:
{chart_data}
Interpret this chart according to classical BaZi principles...
"""
What lunar-python Actually Does
lunar-python is the unsung hero here. It handles:
- Converting Gregorian dates to lunar calendar dates
- Calculating the sexagenary cycle (干支) for any timestamp
- Determining the exact hour pillar (時柱) based on birth time
- Identifying the Day Master (日主) element and strength
- Mapping all ten gods (十神) relationships between stems
Without this library, I'd be reimplementing a thousand years of Chinese calendar math. The author (6tail) has been maintaining it since 2015 and it's rock solid.
from lunar_python import Lunar, Solar
solar = Solar.fromYmdHms(1988, 12, 30, 14, 0, 0)
lunar = solar.getLunar()
# Get the eight characters
bazi = lunar.getEightChar()
print(bazi.getYear()) # 戊辰
print(bazi.getMonth()) # 甲子
print(bazi.getDay()) # 己未
print(bazi.getTime()) # 辛未
The Architecture Decisions I Don't Regret
No database for readings. Each reading is a stateless API call. The BaZi chart is calculated server-side, sent to DeepSeek, and returned. Nothing is stored. This simplified deployment dramatically — no migrations, no backups, no GDPR headaches.
Single VPS, no microservices. FastAPI on port 8000, Next.js on port 3001, Nginx in front. It's not trendy, but it works and the latency is under 2 seconds end-to-end.
Gumroad for payments. I thought about Stripe. Gumroad took 10 minutes to set up. They handle VAT, refunds, and file delivery. For a solo dev shipping a side project, the 10% fee is worth not building billing infrastructure.
What I'd Do Differently
Test with real users sooner. The first version had Chinese-only output because my test data was all Chinese prompts. Took a real user to point out that English-speaking visitors got Chinese responses.
Add analytics from day one. I shipped without any tracking and had no idea if anyone was visiting. Turned out my first real user found the site through Instagram, not any of my planned channels.
DeepSeek rate limits are brutal. At peak times, API latency jumps from 2s to 15s. Adding a queue or fallback model is on the roadmap.
The Product
If you're curious, the app is at soulchart.cc. There's a free preview that shows your BaZi chart with the Day Master and element distribution. Full readings with AI interpretation are paid — keeps the API costs sustainable.
I built this because I was fascinated by the intersection of ancient divination and modern AI. The math is deterministic, but the interpretation is where it gets interesting. DeepSeek doesn't "believe" in BaZi — it just understands the symbolic system surprisingly well.
That's either profound or mildly unsettling. I haven't decided which.
Built with FastAPI, DeepSeek, lunar-python, and Next.js. Open to questions about the BaZi calculation pipeline or prompt engineering approach.
Top comments (0)