DEV Community

Rajesh Lohith
Rajesh Lohith

Posted on

My AI astrologer isn't allowed to do math

I run horoscopes.today, an AI Vedic astrology site. It has one hard rule: the language model never calculates anything. A deterministic engine computes the birth chart first, and the model only explains what was computed.

This post covers why we split it that way, what the model actually receives, and how we exposed the engine over a REST API and an MCP server so an agent can ask for exactly the piece of the chart a question needs.

Why the model can't be trusted with the chart

Vedic (sidereal) astrology is mostly arithmetic before it is interpretation. To answer "what is my Moon sign?" you need:

  • the planet's tropical longitude at the birth moment, from an ephemeris
  • an ayanamsa correction to move it into the sidereal zodiac (about 23.7° for a 1990 birth under Lahiri)
  • the correct timezone and coordinates for the birth place
  • then the sign, the nakshatra (one of 27 lunar mansions), and the pada (quarter) it falls in

Questions about timing, such as "when does this period of my life change?", depend on dashas: nested planetary periods computed from the Moon's exact position at birth. Being off by a fraction of a degree moves those dates.

A language model asked to do this will give a confident, well-written and often wrong answer. It may also give a different answer the next time you ask the same question. For a product where someone types their birth details and asks about their marriage or career, that is unacceptable.

The split: engine calculates, model interprets

The flow for every reading looks like this:

  1. The user enters date, time and place of birth.
  2. The place is resolved to latitude, longitude and timezone. The engine has a location search over 162,000+ cities.
  3. The Jagannatha Hora engine computes the chart: signs, ascendant, planetary positions, nakshatras, divisional charts, dashas and transits.
  4. The model (Claude) receives that calculated chart context.
  5. It answers in plain language, and follow-up questions stay in the same thread, about the same chart.

The model's job is explanation, context and follow-ups. The chart's facts are fixed before the question is even read. Ask the same thing twice and you are still asking about the same chart.

What the engine returns

Here is the public REST call for a full chart, using the sample birth details from our API docs:

curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/horoscope" \
  -H "content-type: application/json" \
  -d '{
    "date": "1990-01-01",
    "time": "06:30:00",
    "place": "Bengaluru",
    "latitude": 12.9716,
    "longitude": 77.5946,
    "timezone": 5.5
  }'
Enter fullscreen mode Exit fullscreen mode

An abridged excerpt of the response:

{
  "birth_details": {
    "timezone_used": 5.5,
    "timezone_source": "derived",
    "request_settings": { "ayanamsa_mode": "LAHIRI", "calculation_type": "drik" }
  },
  "horoscope": {
    "ayanamsa_value": 23.7174169601642,
    "nakshatra_pada": {
      "Ascendant": { "nakshatra": "Moola", "pada": 4, "nakshatra_lord": "Kethu" }
    },
    "divisional_charts": {
      "D-1_rasi": {
        "Ascendant": { "sign": "Sagittarius", "longitude": 13.0166 },
        "Moon": { "sign": "Aquarius", "longitude": 3.3941 }
      }
    }
  },
  "engine_revision": 6
}
Enter fullscreen mode Exit fullscreen mode

Two details turned out to matter more than I expected:

  • The response echoes every setting it used. That covers the ayanamsa (20 modes are supported, Lahiri is the default), the calculation type and where the timezone came from. When a user says "another site gave me a different nakshatra," you can see exactly why.
  • engine_revision makes results cacheable. The same inputs return the same chart until calculated values change, and then the revision goes up. You cache by input plus revision, and the model always reads the same facts.

Why an MCP server, not just one big JSON blob

A full chart is large: 23 divisional charts, dozens of dasha systems, house strengths, ashtakavarga, transits. Dumping all of it into the context for every question is wasteful, and it gives the model more to misread.

So the same engine is also exposed as an MCP server with 33 individually callable tools. A model answering a timing question calls the dasha tool. A model answering a compatibility question calls the marriage-match tool with two people. The transit and muhurta tools also take an optional event location, for the common case of someone born in one city who is asking about something happening in another.

generate_horoscope is the expensive call, so it runs once. Later calls for the same birth details reuse the computed chart and return quickly.

Every tool result comes back twice: once as a JSON-encoded text part, and once as structuredContent in a consistent {"section": ..., "data": ...} envelope. Clients that only read text still work, and clients that read structured output don't have to parse strings.

Docs written for machines, too

Because agents and LLM-based fetchers read docs now, every reference page has machine-readable twins:

If you point an agent at the Markdown version, it gets the same facts as the HTML page without the navigation and styling.

What I'd tell anyone building an "AI + domain math" product

  • Draw a hard line between computing and explaining. If a number can be calculated, don't let the model produce it.
  • Return structured data, and say which settings produced it. Debugging trust issues becomes possible.
  • Make results deterministic and versioned. Same input, same output, with a revision number for when that changes.
  • Give the model small tools, not the whole database. It asks better follow-up questions when it can fetch exactly what it needs.

Try it

Astrology is offered for guidance and entertainment. The engineering problem underneath it, which is keeping a language model honest about facts it didn't compute, applies to many other domains. I'd love feedback on the architecture, and to hear how others handle the same problem.

Top comments (0)