DocsEpistemic Agent Guide

Build TIAN — Your Personal Epistemic Agent

This guide walks you through integrating the askTIAN API into app.asktian.com — a conversational AI agent that synthesizes 8,000 years of civilizational wisdom into practical decision-making guidance. You will need an Enterprise API key for the full fan-out endpoints.

Enterprise tier required 49 endpoints · 29 systems · 5 traditions TypeScript · React 19 · Express

The askTIAN Personal Epistemic Agent is built on three layers: a Dynamic Deck frontend (swipeable cards), an Orchestration Engine backend (intent → endpoints → LLM synthesis), and a Memory Layer database (Pattern Profile, Decisions, Conversations). The API integration lives entirely in the Orchestration Engine.

LayerTechnologyaskTIAN API Role
Dynamic Deck (Frontend)React 19 + Tailwind 4 + PWARenders SSE streams; displays tradition animations
Orchestration Engine (Backend)Express + TypeScript + Bull queuesCalls API endpoints; synthesizes via LLM
Memory Layer (Database)PostgreSQL + RedisCaches Pattern Profiles (30-day TTL); stores Decisions
Enterprise key required for tian.global, tian.eastern, tian.western, tian.african, tian.islamic, tian.indian, and predict.binaryBatch. Contact [email protected] or upgrade via Pricing.
app.asktian.com
│
├── Frontend (React PWA)
│   ├── DynamicDeck.tsx         ← swipeable card carousel
│   ├── ChatInterface.tsx       ← SSE consumer + animations
│   └── OnboardingFlow.tsx      ← birth data → tian.global
│
├── Backend (Express)
│   ├── POST /api/chat          ← main SSE endpoint
│   │   ├── intentRouter.ts     ← GPT-4.1-nano classification
│   │   ├── contextAssembler.ts ← fetch Pattern Profile + history
│   │   ├── endpointSelector.ts ← intent → API endpoint list
│   │   ├── apiFanout.ts        ← parallel calls to api.asktian.com
│   │   ├── llmSynthesis.ts     ← TIAN personality synthesis
│   │   └── sseStreamer.ts      ← tradition_start / synthesis_chunk
│   └── Bull Queues
│       ├── dailyAnchorCompute  ← 3 AM: astrology.transits + LLM
│       ├── dailyAnchorDeliver  ← 7 AM: Email / Push / Telegram
│       └── followUpChecker     ← Hourly: pending decisions
│
└── api.asktian.com             ← askTIAN API (Enterprise key)
    ├── GET  tian.global        ← onboarding (145 TIAN pts)
    ├── GET  tian.eastern       ← eastern fan-out (85 TIAN pts)
    ├── GET  astrology.transits ← daily anchor (5 TIAN pts)
    ├── POST predict.binary     ← decision guidance (100 TIAN pts)
    └── GET  twelvepalaces.*    ← palace lookup (5 TIAN pts)

During onboarding, call GET /tian.global with the user's birth data. This single call fans out across all 28 traditions in parallel and returns a comprehensive cross-civilizational analysis. Store the response as a JSONB blob in your pattern_profiles table with a 30-day TTL. The user never sees this data directly — TIAN references it naturally in conversation.

# Step 1: Call tian.global during onboarding (background, while user completes MBTI/IKIGAI steps)
curl "https://api.asktian.com/api/trpc/tian.global?input=%7B%22json%22%3A%7B%22year%22%3A2026%2C%22month%22%3A4%2C%22day%22%3A7%2C%22hour%22%3A9%2C%22birthDate%22%3A%221990-03-14%22%2C%22surname%22%3A%22%E6%9D%8E%22%2C%22givenName%22%3A%22%E5%BF%97%E6%98%8E%22%2C%22luckyNumber%22%3A%2288%22%2C%22fullName%22%3A%22John+Smith%22%7D%7D" \
  -H "X-API-Key: at_live_xxxx"
# Step 2: Store in pattern_profiles table
# INSERT INTO pattern_profiles (user_id, tian_global_response, ttl_expires_at)
# VALUES ($1, $2, NOW() + INTERVAL '30 days')
# ON CONFLICT (user_id) DO UPDATE SET tian_global_response = $2, ttl_expires_at = NOW() + INTERVAL '30 days'
Cost note: tian.global costs 145 TIAN Points. With a 30-day cache, this is ~4.8 TIAN Points/day per user — far cheaper than calling individual endpoints daily. The profileRefresh cron (daily at 2 AM UTC) checks ttl_expires_at and refreshes only expired profiles.
FieldSourceUsed In
birthDateOnboarding formtian.global, tian.eastern, astrology.transits, twelvepalaces.*
birthTimeOnboarding form (optional, defaults noon)astrology.transits, tian.global
birthLocationGoogle Places autocompleteastrology.transits
surname + givenNameOnboarding formtian.global (Chinese name analysis)
fullNameOnboarding formtian.global (Western numerology)
luckyNumberOnboarding formtian.global (auspiciousness)
mbtiType4-question quick testLLM synthesis context only

Every user message is classified into one of five intents before any API call is made. Use GPT-4.1-nano (the cheapest model) for this — the task is pure classification, not reasoning. Temperature 0, max_tokens 20. The classification determines the entire downstream processing pipeline.

// src/orchestration/intentRouter.ts
const INTENT_ROUTER_PROMPT = `You are the intent router for askTIAN. Classify the user's message into exactly one category:
1. DECISION_GUIDANCE — asking for advice on a specific choice or dilemma
2. TRANSIT_UPDATE — asking about current astrological weather or energy
3. OUTCOME_REPORT — reporting the result of a past decision
4. PREFERENCE_CHANGE — changing notification settings or delivery channels
5. GENERAL_CHAT — general conversation, questions about Pattern Science, greetings
Return ONLY the category name. No explanation.`;

export async function classifyIntent(message: string): Promise<Intent> {
  const res = await openai.chat.completions.create({
    model: 'gpt-4.1-nano',
    messages: [
      { role: 'system', content: INTENT_ROUTER_PROMPT },
      { role: 'user', content: message }
    ],
    temperature: 0,
    max_tokens: 20
  });
  return res.choices[0].message.content?.trim() as Intent;
}
IntentTrigger ExampleAPI CallsTIAN Pts
DECISION_GUIDANCEShould I take the job in Singapore?predict.binary + astrology.transits + tian.eastern~155
TRANSIT_UPDATEWhat does this week look like for me?astrology.transits~5
OUTCOME_REPORTI took the job. It's going great.None (DB update only)0
PREFERENCE_CHANGEStop sending me Telegram messages.None (DB update only)0
GENERAL_CHATTell me about BaZi.None (LLM only, uses cached profile)~0

The endpointSelector.ts maps each intent to a specific set of API endpoints. The selection is deterministic — no LLM involved. For DECISION_GUIDANCE, always call predict.binary first (it's the fastest signal), then fan out to the tradition endpoints in parallel.

IntentPrimary EndpointSecondary EndpointsNotes
DECISION_GUIDANCEpredict.binary (100 pts)astrology.transits (5 pts) · tian.eastern (85 pts)Fan out in parallel. tian.eastern covers BaZi, I Ching, Qimen, Mahabote, 十二宮, and 12 more.
TRANSIT_UPDATEastrology.transits (5 pts)almanac.today (5 pts)Cache per user per day (Redis, 24h TTL).
GENERAL_CHAT (birth-related)tian.eastern (85 pts)Only if the user asks about their own chart. Use cached profile otherwise.
Daily Anchor (cron)astrology.transits (5 pts)Pre-computed at 3 AM local. Cached in daily_anchors table.
Onboardingtian.global (145 pts)One-time. Cached 30 days. Refreshed by profileRefresh cron.
Cost optimisation: For DECISION_GUIDANCE, check the Redis cache for astrology.transits first (TTL 24h). If cached, skip the transit call and use the cached result — saving 5 TIAN Points per decision.
// src/orchestration/endpointSelector.ts
export function selectEndpoints(intent: Intent, hasTransitCache: boolean): string[] {
  switch (intent) {
    case 'DECISION_GUIDANCE':
      return [
        'predict.binary',
        ...(hasTransitCache ? [] : ['astrology.transits']),
        'tian.eastern',
      ];
    case 'TRANSIT_UPDATE':
      return hasTransitCache ? [] : ['astrology.transits', 'almanac.today'];
    case 'GENERAL_CHAT':
      return []; // Use cached Pattern Profile only
    default:
      return [];
  }
}

All API calls for a single user message are executed in parallel using Promise.allSettled. Each call is wrapped in a Redis cache check (24h TTL). As each tradition's result arrives, the SSE streamer emits a tradition_result event — this drives the "Theatrical Computation" animations on the frontend.

// src/orchestration/apiFanout.ts — parallel execution with Redis cache
export async function executeApiFanout(
  endpoints: string[],
  params: Record<string, any>
): Promise<ApiCallResult[]> {
  const calls = endpoints.map(async (endpoint): Promise<ApiCallResult> => {
    const cacheKey = `api:${endpoint}:${JSON.stringify(params)}`;
    const cached = await redis.get(cacheKey);
    if (cached) return { endpoint, success: true, data: JSON.parse(cached), cached: true };

    try {
      const url = `https://api.asktian.com/api/trpc/${endpoint}?input=${encodeURIComponent(JSON.stringify({ json: params }))}`;
      const res = await fetch(url, { headers: { 'X-API-Key': process.env.ASKTIAN_API_KEY! } });
      const body = await res.json();
      const data = body.result.data.json;
      await redis.setex(cacheKey, 86400, JSON.stringify(data)); // 24h TTL
      return { endpoint, success: true, data, cached: false };
    } catch (err: any) {
      return { endpoint, success: false, error: err.message, cached: false };
    }
  });

  return Promise.allSettled(calls).then(results =>
    results.map(r => r.status === 'fulfilled' ? r.value : { endpoint: 'unknown', success: false, error: 'rejected', cached: false })
  );
}

The SSE streamer emits five event types. The frontend listens for these and triggers the corresponding tradition animation (hexagram flip, planetary orbit, etc.).

SSE EventPayloadFrontend Action
tradition_start{"tradition":"i_ching","label":"Consulting the I Ching..."}Show tradition name + start animation
tradition_result{"tradition":"i_ching","summary":"Hexagram 42, Increase"}Display inline result
synthesis_start{"label":"Synthesizing across traditions..."}Trigger convergence animation
synthesis_chunk{"text":"The patterns strongly favor..."}Stream narrative token by token
synthesis_end{"decision_id":"uuid","follow_up_suggested":true}Show 'Set a reminder?' prompt

The Daily Anchor is a pre-computed reading delivered every morning. A Bull queue processes users in batches of 250 at 3 AM local time (to stay under the 300 req/15 min rate limit), then delivers via Email, PWA Push, and Telegram at 7 AM. Only astrology.transits is called — the Pattern Profile is fetched from the database.

// src/jobs/dailyAnchorCompute.ts — batch processing with rate limit awareness
const BATCH_SIZE = 250;          // Stay under 300 req / 15 min
const BATCH_DELAY_MS = 15 * 60 * 1000; // 15 min between batches

export async function computeDailyAnchors() {
  const users = await db.users.findActive();
  const batches = chunk(users, BATCH_SIZE);

  for (let i = 0; i < batches.length; i++) {
    await Promise.allSettled(
      batches[i].map(async (user) => {
        const profile = await db.patternProfiles.findByUserId(user.id);
        if (!profile) return;

        // Check Redis cache first — transit data is valid for 24h
        const cacheKey = `transits:${user.birth_date}:${today()}`;
        let transits = await redis.get(cacheKey);
        if (!transits) {
          const res = await askTianApi.get('astrology.transits', {
            birthDate: user.birth_date,
            birthTime: user.birth_time,
            birthLocation: user.birth_location
          });
          transits = JSON.stringify(res.data);
          await redis.setex(cacheKey, 86400, transits);
        }

        const reading = await llm.generate({
          prompt: DAILY_ANCHOR_PROMPT,
          context: { pattern_summary: extractSummary(profile), transit_data: JSON.parse(transits) }
        });

        await db.dailyAnchors.upsert({ userId: user.id, readingDate: today(), ...reading });
        await deliveryQueue.add({ userId: user.id }, { delay: hoursUntil7AM(user.timezone) });
      })
    );
    if (i < batches.length - 1) await delay(BATCH_DELAY_MS);
  }
}
Rate limit: The API allows 300 requests per 15 minutes. With batches of 250 and 15-minute delays, you can process 1,000 users/hour safely. For 10,000 users, the full pre-computation takes ~10 hours — start the cron at 3 AM and it finishes well before 7 AM delivery.
EndpointCache KeyTTLRationale
tian.globaltian_global:{userId}30 days (DB)Birth data doesn't change. Refresh only on profile TTL expiry.
astrology.transitstransits:{birthDate}:{date}24h (Redis)Transits change daily. One call per user per day.
predict.binaryNoneEach question is unique. Do not cache.
tian.easterneastern:{birthDate}:{date}24h (Redis)Eastern systems are date-sensitive. Cache per day.
almanac.todayalmanac:{date}24h (Redis)Same almanac for all users on the same date. Share the cache.
twelvepalaces.calculatepalace:{zodiac}:{month}:{day}:{hour}Permanent (Redis)Pure function of birth inputs. Cache forever.
// Shared Redis cache helper
export async function cachedApiCall<T>(
  key: string,
  ttlSeconds: number,
  fetcher: () => Promise<T>
): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached) as T;
  const result = await fetcher();
  await redis.setex(key, ttlSeconds, JSON.stringify(result));
  return result;
}

// Usage:
const transits = await cachedApiCall(
  `transits:${user.birth_date}:${today()}`,
  86400, // 24h
  () => askTianApi.get('astrology.transits', { birthDate: user.birth_date })
);

With aggressive caching, the API cost per active user is well within the margins of the Premium tier ($14.99/month). The key insight: tian.global is called once per month, not per conversation.

ComponentCalls/MonthTIAN Pts EachTotal PtsUSD (~$0.001/pt)
Daily Anchor (astrology.transits)305150$0.15
Daily Anchor LLM synthesis30~10300$0.30
Pattern Profile refresh (tian.global)1140140$0.14
Decision Guidance (predict.binary)3 (free tier)100300$0.30
Decision Guidance (tian.eastern)3 (free tier)85255$0.26
Decision Guidance (astrology.transits)3 (cached)00$0.00
Decision LLM synthesis3~50150$0.15
Free tier total1,295 pts$1.30
Premium (30 decisions)~9,500 pts$9.50
Gross margin: Free tier costs $1.30 in API, generates $0 revenue. Premium tier costs $9.50, generates $14.99 → 37% gross margin. The Daily Anchor is the largest fixed cost — consider offering a "Lite" tier without Daily Anchor to reduce free tier burn.

All requests to api.asktian.com require an X-API-Key header with your Enterprise API key. The key is scoped to your server — never expose it to the frontend. Use environment variables and rotate quarterly.

# Server-side only — never expose to browser
export const askTianApi = axios.create({
  baseURL: 'https://api.asktian.com/api/trpc',
  headers: {
    'X-API-Key': process.env.ASKTIAN_API_KEY,  // Enterprise key
    'Content-Type': 'application/json',
  },
  timeout: 30000, // 30s — tian.global can take 10-15s
});

// For GET endpoints, encode params as input query param:
const res = await askTianApi.get(`/astrology.transits?input=${encodeURIComponent(JSON.stringify({ json: params }))}`);

// For POST endpoints (predict.binary, predict.binaryBatch):
const res = await askTianApi.post('/predict.binary', { json: params });
Security: Birth data is PII. Encrypt all birth columns at rest (AES-256). Never log birth data. Implement GDPR-compliant deletion: when a user deletes their account, delete all rows in users, pattern_profiles, conversations, and decisions.

The official asktian-sdk (v1.6.0) wraps all 49 endpoints with full TypeScript types. Install it on your backend server.

npm install [email protected]
import { AskTian } from 'asktian-sdk';

const client = new AskTian({ apiKey: process.env.ASKTIAN_API_KEY });

// Onboarding: generate Pattern Profile
const profile = await client.tian.global({
  year: 2026, month: 4, day: 7, hour: 9,
  birthDate: '1990-03-14',
  surname: '李', givenName: '志明',
  luckyNumber: '88', fullName: 'John Smith'
});

// Decision Guidance: binary prediction
const prediction = await client.predict.binary({
  question: 'Should I accept the VP role at the startup?',
  optionA: 'Accept', optionB: 'Decline',
  resolutionDate: '2026-12-31',
  privyToken: userPrivyToken // Required for non-Enterprise callers
});

// Daily Anchor: today's transits
const transits = await client.astrology.transits({
  birthDate: '1990-03-14',
  birthTime: '09:00',
  birthLocation: 'Singapore'
});

// Palace lookup: Twelve Palaces
const palace = await client.twelvepalaces.calculate({
  zodiacYear: 'Dragon', lunarMonth: 3, lunarDay: 15, birthHour: 'Si'
});

As of v3.9.0, four endpoints return rich profile and 4-dimension compatibility data sourced from the YouApp dataset. Agents can use these fields to build substantially richer personality context, relationship analysis, and wellness guidance without additional LLM calls.

10.1 Sign Profile — Mantra, Tarot & Medical Astrology

Both almanac.zodiacSign (field: westernProfile) and horoscope.calculate (field: signProfile) return a HoroscopeSignProfile object for the sun sign. Key agent-facing fields:

FieldExample (Capricorn)Agent use-case
mantra“I use.”Affirmation prompt prefix, daily intention card
majorArcanaThe Devil (XV)Tarot cross-reference, shadow work prompt
minorArcana2, 3, 4 of PentaclesWealth / career card spread context
medicalKnees, skin, connective tissueWellness prompt, body-awareness nudge
herbalAlliesHorsetail, comfrey, Solomon's sealHerbal recommendation, seasonal wellness card
symbolismThe Sea-GoatNarrative metaphor, archetype framing
// ── Option A: single call via tian.global (v1.8.0+) ──────────────────────────
// signProfile and compatProfile are returned directly — no extra almanac call needed.
const global = await client.tian.global({ birthdate: "1990-01-15", ... });
const sp = global.signProfile;   // TianGlobalSignProfile
const cp = global.compatProfile; // TianGlobalCompatProfile

// sp?.westernSign  → "capricorn"
// sp?.mantra       → "I Use"
// sp?.majorArcana  → "The Devil"
// cp?.loveDimensionLabel → "Positive"

// ── Option B: dedicated call via almanac.zodiacSign or horoscope.calculate ────
// Use when you need the full HoroscopeSignProfile (bodyParts, herbalAllies, etc.)
const zodiac = await client.almanac.zodiacSign({ birthDate: "1990-01-15" });
const profile = zodiac.westernProfile; // HoroscopeSignProfile (full)

// Build a daily intention card
const intentionCard = {
  mantra: profile?.mantra,         // e.g. "I Use"
  tarot: profile?.majorArcana,     // e.g. "The Devil (XV)"
  bodyFocus: profile?.bodyParts,   // e.g. "Knees, skin, connective tissue"
  herb: profile?.herbalAllies,     // e.g. "Horsetail, comfrey"
  archetype: profile?.symbolism,   // e.g. "The Sea-Goat"
};

// Inject into LLM system prompt (works with either option):
const mantra = sp?.mantra ?? profile?.mantra;
const arcana = sp?.majorArcana ?? profile?.majorArcana;
const systemPrompt = [
  'You are an epistemic agent for a Capricorn (' + mantra + ').',
  'Their Tarot archetype is ' + arcana + '.',
  'Their body focus this season: ' + (profile?.bodyParts ?? 'unknown') + '.',
].join(' ');

10.2 4-Dimension Compatibility — Love, Career, Wealth, Health

compatibility.zodiac returns dimensions (Chinese zodiac 4D).compatibility.birthday returns both dimensions (Chinese 4D) and horoscopeDimensions (Western 4D). Each dimension has a label (“Positive” / “Neutral” / “Negative”) and a prose text field.

// compatibility.zodiac — Chinese 4D
// ── compatibility.zodiac — Chinese 4D only ────────────────────────────────────
const { dimensions } = await client.compatibility.zodiac({ animal1: "rat", animal2: "dragon" });
// dimensions.love.label   → "Positive"
// dimensions.career.label → "Positive"
// dimensions.wealth.label → "Positive"
// dimensions.health.label → "Positive"
// dimensions.marriage.label → "Positive"

// ── compatibility.birthday — Chinese 4D + Western 4D in one call ──────────────
const result = await client.compatibility.birthday({ date1: "1990-01-15", date2: "1992-06-20" });
const { dimensions: cDims, horoscopeDimensions: wDims } = result;

// ── Combined report builder → single LLM-ready string ─────────────────────────
function dimLine(label: string, d?: { label: string; text: string }) {
  if (!d) return '';
  return label + ' [' + d.label + ']: ' + d.text;
}

const relationshipSummary = [
  '=== Chinese Zodiac Compatibility ===',
  dimLine('Love',     cDims?.love),
  dimLine('Career',   cDims?.career),
  dimLine('Wealth',   cDims?.wealth),
  dimLine('Health',   cDims?.health),
  dimLine('Marriage', cDims?.marriage),
  '',
  '=== Western Horoscope Compatibility ===',
  dimLine('Love',   wDims?.love),
  dimLine('Career', wDims?.career),
  dimLine('Wealth', wDims?.wealth),
  dimLine('Health', wDims?.health),
].filter(Boolean).join('
');

// Inject into LLM system prompt:
const systemPrompt =
  'You are a relationship counsellor. Use the following compatibility data to answer the user.

' +
  relationshipSummary;

10.3 Chinese Zodiac Profile — 60-Element Enrichmentv3.9.0

almanac.zodiacSign also returns chineseProfile (ChineseZodiacProfile), covering all 60 combinations of 12 animals × 5 elements. Use it to build identity cards, onboarding flows, or compatibility pre-screens.

const { chineseProfile } = await client.almanac.zodiacSign({ birthDate: "1990-05-15" });
// chineseProfile.zodiac     → "Metal Horse"
// chineseProfile.personality → prose paragraph
// chineseProfile.career      → career guidance prose
// chineseProfile.love        → relationship style prose
// chineseProfile.compatibility → compatible animals prose

// Onboarding identity card:
const identityCard = {
  title: chineseProfile.zodiac,
  summary: chineseProfile.personality.slice(0, 200) + "...",
  careerHint: chineseProfile.career,
  loveStyle: chineseProfile.love,
};

client.tian.global.stream() returns an AsyncGenerator<TianGlobalStreamChunk> that yields four event types progressively as the server fans out to 7 tradition engines and streams the LLM synthesis. Use TianGlobalStreamChunk (v1.9.0+) instead of the generic TianStreamEvent to get typed access to signProfile and compatProfile on the done event.

Event types

type
key fields
when
system
name, score, result
After each tradition engine completes
synthesis_chunk
chunk (string)
During LLM synthesis (streaming tokens)
done
blendedScore, signProfile, compatProfile
After full synthesis completes
error
message, code
On any server-side failure

Option A — Node.js / server-side (recommended)

import { AskTianClient, TianGlobalStreamChunk } from "asktian-sdk";

const client = new AskTianClient({ apiKey: process.env.ASKTIAN_API_KEY! });

let synthesis = "";

for await (const event of client.tian.global.stream({
  birthdate:  "1990-01-15",
  birthTime:  "06:30",
  birthPlace: "Singapore",
  question:   "Should I launch my business this quarter?",
}) as AsyncGenerator<TianGlobalStreamChunk>) {

  if (event.type === "system") {
    // Each tradition result arrives here progressively
    console.log(`[${event.name}] score: ${event.score}`);
  }

  if (event.type === "synthesis_chunk") {
    // Stream tokens to your UI via SSE / WebSocket
    process.stdout.write(event.chunk);
    synthesis += event.chunk;
  }

  if (event.type === "done") {
    // Blended score + rich profile anchors
    console.log("\nBlended score:",  event.blendedScore);
    console.log("Mantra:",           event.signProfile?.mantra);
    console.log("Tarot archetype:",  event.signProfile?.majorArcana);
    console.log("Love label:",       event.compatProfile?.loveDimensionLabel);
  }
}

Option B — Proxy pattern (frontend → your server → askTIAN SSE)

Never expose your API key to the browser. Instead, open an SSE route on your server that proxies the stream:

// server/routes/tianStream.ts (Express)
import express from "express";
import { AskTianClient, TianGlobalStreamChunk } from "asktian-sdk";

const router = express.Router();
const client = new AskTianClient({ apiKey: process.env.ASKTIAN_API_KEY! });

router.get("/stream/tian/global", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const { birthdate, birthTime, question } = req.query as Record<string, string>;

  try {
    for await (const event of client.tian.global.stream({ birthdate, birthTime, question }) as AsyncGenerator<TianGlobalStreamChunk>) {
      res.write(`data: ${JSON.stringify(event)}\n\n`);
      if (event.type === "done" || event.type === "error") break;
    }
  } catch (err) {
    res.write(`data: ${JSON.stringify({ type: "error", message: String(err) })}\n\n`);
  } finally {
    res.end();
  }
});

export default router;
SDK type cast required: client.tian.global.stream() returns AsyncGenerator<TianStreamEvent> at the base type level. Cast to AsyncGenerator<TianGlobalStreamChunk> to access the typed signProfile and compatProfile fields on the done event. This is safe — the server always emits these fields when available.

horoscope.calculate returns a full signProfile object (identical shape to almanac.zodiacSign's westernProfile). Unlike the abbreviated TianGlobalSignProfile returned by tian.global, this full profile includes all nine Medical Astrology fields — making it the right choice when you need to build wellness cards, body-awareness prompts, or Tarot spread contexts.

When to use each source

SourceFields returnedBest for
tian.globalwesternSign, mantra, majorArcanaQuick prompt anchor — no extra call needed
horoscope.calculateAll 9 fields incl. medical, bodyParts, herbalAllies, famousPeopleWellness cards, Tarot spreads, full natal chart
almanac.zodiacSignwesternProfile (same 9 fields) + chineseProfileWhen you also need the Chinese zodiac profile in one call

Building a wellness prompt from signProfile

import { AskTianClient } from "asktian-sdk";

const client = new AskTianClient({ apiKey: process.env.ASKTIAN_API_KEY! });

// Full natal chart + signProfile in one call (1 $TIAN)
const chart = await client.horoscope.calculate({
  birthdate: "1990-01-15",
  birthHour: 6,
  question: "What should I focus on for my health this season?",
});

const sp = chart.signProfile;

// ── Daily wellness card ───────────────────────────────────────────────────────
const wellnessCard = {
  sign:       sp?.sign ?? chart.sunSign,
  bodyFocus:  sp?.bodyParts,          // e.g. "Knees, skin, connective tissue"
  herbs:      sp?.herbalAllies,       // e.g. "Horsetail, comfrey, Solomon's seal"
  tarot:      sp?.majorArcana,        // e.g. "The Devil (XV)"
  mantra:     sp?.mantra,             // e.g. "I Use"
  medical:    sp?.medical,            // e.g. "Skeletal system, joints, skin"
};

// ── Inject into LLM system prompt ────────────────────────────────────────────
const systemPrompt = [
  'You are a wellness guide for a ' + (sp?.sign ?? chart.sunSign) + ' sun sign.',
  sp?.mantra     ? 'Their affirmation mantra is: "' + sp.mantra + '".'                    : '',
  sp?.bodyParts  ? 'Body areas to focus on: ' + sp.bodyParts + '.'                        : '',
  sp?.medical    ? 'Medical astrology note: ' + sp.medical + '.'                           : '',
  sp?.herbalAllies ? 'Supportive herbs: ' + sp.herbalAllies + '.'                          : '',
  sp?.majorArcana  ? 'Their Tarot archetype is ' + sp.majorArcana + '.'                   : '',
  sp?.symbolism    ? 'Symbolic anchor: ' + sp.symbolism + '.'                              : '',
].filter(Boolean).join(' ');

console.log(systemPrompt);
// → "You are a wellness guide for a capricorn sun sign. Their affirmation mantra is: 'I Use'. Body areas to focus on: Knees, skin, connective tissue. Medical astrology note: Skeletal system, joints, skin. Supportive herbs: Horsetail, comfrey, Solomon's seal. Their Tarot archetype is The Devil (XV). Symbolic anchor: The Sea-Goat."

Tarot spread context builder

// Build a Tarot reading context block from signProfile
function buildTarotContext(sp: { sign?: string; majorArcana?: string; minorArcana?: string; symbolism?: string } | undefined) {
  if (!sp) return '';
  return [
    '=== Tarot Context for ' + (sp.sign ?? 'Unknown') + ' ===',
    sp.majorArcana ? 'Major Arcana: ' + sp.majorArcana : '',
    sp.minorArcana ? 'Minor Arcana: ' + sp.minorArcana : '',
    sp.symbolism   ? 'Symbolic archetype: ' + sp.symbolism : '',
  ].filter(Boolean).join('\n');
}

const tarotContext = buildTarotContext(chart.signProfile);
// → "=== Tarot Context for capricorn ===\nMajor Arcana: The Devil (XV)\nMinor Arcana: 2, 3, 4 of Pentacles\nSymbolic archetype: The Sea-Goat"

compatibility.zodiac returns a dimensions block with five YouApp-sourced dimensions for any Chinese zodiac animal pair. Each dimension carries a label (Positive / Neutral / Negative) and a prose text field. This is the lightest-weight compatibility call — no birth dates required, just two animal names.

Dimension overview

DimensionField pathAgent use-case
Lovedimensions.loveRomantic compatibility card, dating app feature
Careerdimensions.careerBusiness partner assessment, co-founder fit
Wealthdimensions.wealthFinancial partnership guidance
Healthdimensions.healthWellness synergy, lifestyle compatibility
Marriagedimensions.marriageLong-term relationship assessment

Quick compatibility card builder

import { AskTianClient } from "asktian-sdk";

const client = new AskTianClient({ apiKey: process.env.ASKTIAN_API_KEY! });

// Lightweight call — no birth dates needed (0 $TIAN for zodiac pair lookup)
const compat = await client.compatibility.zodiac({ animal1: "rat", animal2: "dragon" });
const { score, level, description, dimensions: d } = compat;

// ── Compatibility card ────────────────────────────────────────────────────────
const compatCard = {
  headline:  'Rat + Dragon: ' + level + ' (' + score + '/100)',
  summary:   description,
  love:      { label: d?.love?.label,     detail: d?.love?.text },
  career:    { label: d?.career?.label,   detail: d?.career?.text },
  wealth:    { label: d?.wealth?.label,   detail: d?.wealth?.text },
  health:    { label: d?.health?.label,   detail: d?.health?.text },
  marriage:  { label: d?.marriage?.label, detail: d?.marriage?.text },
};

// ── LLM prompt injection ──────────────────────────────────────────────────────
function dimLine(name: string, dim?: { label: string | null; text: string | null }) {
  if (!dim?.label) return '';
  return name + ' [' + dim.label + ']: ' + (dim.text ?? '');
}

const compatContext = [
  'Overall compatibility score: ' + score + '/100 (' + level + ')',
  dimLine('Love',     d?.love),
  dimLine('Career',   d?.career),
  dimLine('Wealth',   d?.wealth),
  dimLine('Health',   d?.health),
  dimLine('Marriage', d?.marriage),
].filter(Boolean).join('\n');

const systemPrompt =
  'You are a relationship counsellor specialising in Chinese astrology.\n' +
  'Use the following Rat + Dragon compatibility data to answer the user.\n\n' +
  compatContext;

Upgrade path: add Western 4D via compatibility.birthday

When you have both users' birth dates, upgrade to compatibility.birthday to get both the Chinese 4D dimensions and the Western 4D horoscopeDimensions in a single call. See Section 10.2 for the combined report builder pattern.

The v3.24.0 release adds four new fields and one new event to the SSE streaming contract for alltian.* blended endpoints. This section documents the complete event schema so the app.asktian.com frontend can map each event to the correct animation and UI transition without a separate catalogue lookup.

New in v3.24.0 — event additions

Event / FieldTypeDescriptionFrontend use
synthesis_start eventeventEmitted once, immediately before the first synthesis_chunk tokenTransition from tradition animation phase to synthesis reading phase
system.traditionstring"eastern" | "western" | "african" | "islamic" | "indian"Map to tradition-specific animation: Eastern → hexagram spin, Western → planetary orbit, African → shimmer pulse, Islamic → geometric pattern, Indian → mandala
system.iconUrlstringCDN URL for the tradition icon (original variant)Show tradition icon during theatrical animation without a separate catalogue lookup
system.systemSlugstringe.g. "qimen", "tarot", "ifa", "jyotish"Match against icon pack and system catalogue for rich card rendering

Complete SSE event sequence

The table below shows every event emitted by a tian.global stream call in order, with the v3.24.0 additions highlighted.

Event typeEmittedKey fieldsApp action
systemOnce per tradition (up to 29×)name, result, score, error?, tradition ✦, iconUrl ✦, systemSlug ✦Show tradition card with icon; start animation
synthesis_startOnce, before first token ✦(no payload)Transition to synthesis reading phase; hide tradition grid
synthesis_chunkOne per LLM tokentext (string)Append token to synthesis display
doneOnce, after last tokenblendedScore, synthesis, traditionScores, creditsUsed, responseTimeMs, signProfile?, compatProfile?Show final score; store reading in DB; enable share
errorOn failurecode, messageShow error toast; allow retry

✦ New in v3.24.0

Minimal TypeScript consumer (v3.24.0 contract)

import { AskTianClient } from "asktian-sdk";

const client = new AskTianClient({ apiKey: process.env.ASKTIAN_API_KEY! });

for await (const chunk of client.tian.global.stream({
  birthdate: "1983-08-03",
  birthTime: "22:05",
  question: "Should I change careers?",
  fullName: "Douglas Gan",
})) {
  if (chunk.type === "system") {
    // v3.24.0: tradition, iconUrl, systemSlug are now available
    console.log(`[${chunk.tradition}] ${chunk.name} — score ${chunk.score}`);
    console.log(`  icon: ${chunk.iconUrl}  slug: ${chunk.systemSlug}`);
  }

  if (chunk.type === "synthesis_start") {
    // v3.24.0: transition from tradition animation to synthesis reading
    console.log("Synthesis starting — switch UI phase");
  }

  if (chunk.type === "synthesis_chunk") {
    process.stdout.write(chunk.text);
  }

  if (chunk.type === "done") {
    console.log(`Blended score: ${chunk.blendedScore}`);
    console.log(`Credits used: ${chunk.creditsUsed}`);
    // signProfile and compatProfile available here for Pattern Card
  }

  if (chunk.type === "error") {
    console.error(`Stream error ${chunk.code}: ${chunk.message}`);
  }
}

Tradition → animation mapping

tradition valueSystems includedSuggested animation
easternQimen, BaZi, Zi Wei, I Ching, Feng Shui, Mahabote, 十二宮 …Hexagram spin / trigram reveal
westernTarot, Numerology, Runes, Astrology, Horoscope …Planetary orbit / card flip
africanIfá, Vodun, HakataShimmer pulse / cowrie scatter
islamicRammal, Khatt al-RamlGeometric pattern unfold
indianJyotish, Anka ShastraMandala bloom

App-side event type alignment

The app.asktian.com frontend uses internal event names that differ from the API SSE event types. The table below maps the app's internal events to the API contract so the SSE consumer can translate without an adapter layer.

App eventAPI eventNotes
tradition_start(none)Use synthesis_start as the phase-transition signal instead
tradition_resultsystemsystem.tradition + system.iconUrl + system.systemSlug added in v3.24.0
tradition_complete(none)Use done event as the all-traditions-complete signal
synthesis_startsynthesis_startDirect 1:1 mapping — new in v3.24.0
synthesis_chunksynthesis_chunkDirect 1:1 mapping
synthesis_completedoneFull synthesis text available in done.synthesis
errorerrorDirect 1:1 mapping

The Daily Anchor card is the primary daily engagement hook in the app.asktian.com Dynamic Deck. It shows a personalised daily reading that combines the Chinese almanac energy for today with the user's natal chart context. A dedicated daily.anchor endpoint is under active development for v3.24.0 API. Until it lands, the same result can be composed from two existing calls.

Interim approach — two-call composition (available now)

Combine almanac.daily (today's almanac energy) withhoroscope.calculate (user's natal chart + sign profile) to build the Daily Anchor card. Cache the horoscope result for 90 days — the natal chart never changes.

import { AskTianClient, HoroscopeResponse, AlmanacZodiacResponse } from "asktian-sdk";

const client = new AskTianClient({ apiKey: process.env.ASKTIAN_API_KEY! });

// Call 1: Today's almanac (no birth data required — cache for 24 hours per date)
const almanac = await client.almanac.daily({
  date: new Date().toISOString().split("T")[0], // "2026-04-09"
});

// Call 2: User's natal chart (cache for 90 days — natal chart never changes)
const horoscope: HoroscopeResponse = await client.horoscope.calculate({
  birthdate: user.birthdate,   // "1983-08-03"
  birthHour: user.birthHour,   // 22
});

// Compose the Daily Anchor card
const dailyAnchor = {
  date:          almanac.date,
  twelveValue:   almanac.twelveValue,          // e.g. "成" (Achievement)
  deity:         almanac.deity,                // e.g. "青龍" (Azure Dragon)
  fortune:       almanac.fortune,              // "good_fortune" | "neutral" | "caution"
  auspicious:    almanac.auspiciousActivities, // array of activity strings
  inauspicious:  almanac.inauspiciousActivities,
  sunSign:       horoscope.sunSign,            // e.g. "Leo"
  mantra:        horoscope.signProfile?.mantra,        // e.g. "I Will"
  majorArcana:   horoscope.signProfile?.majorArcana,   // e.g. "Strength"
  synthesis:     horoscope.synthesis,          // LLM natal chart interpretation
};

Response field reference — two-call composition

FieldSourceDescriptionCache TTL
twelveValuealmanac.dailyChinese twelve-value (成, 破, 危 …)24 hours
deityalmanac.dailyDay deity (青龍, 朱雀, 白虎 …)24 hours
fortunealmanac.dailyOverall fortune: good_fortune | neutral | caution24 hours
auspiciousalmanac.dailyActivities favoured today (Chinese)24 hours
inauspiciousalmanac.dailyActivities to avoid today (Chinese)24 hours
sunSignhoroscope.calculateUser's Western sun sign90 days
mantrahoroscope.calculateSign mantra for daily focus (via signProfile)90 days
majorArcanahoroscope.calculateTarot archetype for the sign (via signProfile)90 days
synthesishoroscope.calculateLLM natal chart interpretation90 days

Future approach — daily.anchor single call (v3.24.0 API)

When daily.anchor lands, a single call replaces both interim calls and adds two new fields: luckyHours (derived from the user's BaZi day master element) and dominantTheme(a plain-language summary of the most significant current transit). Cost: 3 TIAN Points.

// Future — single call once daily.anchor is available (v3.24.0 API)
const anchor = await client.daily.anchor({
  birthdate: user.birthdate,   // "1983-08-03"
  birthHour: user.birthHour,   // 22
  question:  "What should I focus on today?",  // optional
});

// anchor.date              — "2026-04-09"
// anchor.dominantTheme     — "Mercury retrograde squares natal Saturn — review before acting"
// anchor.almanacHighlight  — { twelveValue, deity, fortune }
// anchor.dailyMessage      — LLM-synthesised 2-3 sentence daily reading
// anchor.luckyHours        — ["10:00-12:00", "15:00-17:00"]
// anchor.avoidHours        — ["13:00-14:00"]
// anchor.creditsUsed       — 3

Migration path

Design the Daily Anchor card component to accept a single DailyAnchorData prop. Populate it from the two-call composition today; swap the data source todaily.anchor when the endpoint is available. The card component itself requires no changes.

FieldInterim sourceFuture sourceNotes
twelveValue / deity / fortunealmanac.dailydaily.anchor.almanacHighlightSame data, restructured
auspicious / inauspiciousalmanac.dailydaily.anchor.almanacHighlightEnglish via language param in v3.24.0 API
sunSign / mantra / majorArcanahoroscope.calculatedaily.anchor (inline)No separate call needed
dailyMessage(compose manually)daily.anchor.dailyMessageLLM synthesis included
luckyHours / avoidHours(not available)daily.anchorBaZi day master derived
dominantTheme(not available)daily.anchorCurrent transit summary

More integration guides

The /guides section has step-by-step engineering guides for the app.asktian.com integration, the Telegram bot, and the Chat API — each with working code, error handling, and a sprint roadmap.

View all guides

Sprint 1 — Foundation

Sprint 2 — Orchestration

Sprint 3 — Daily Anchor & Delivery

Sprint 4 — Telegram Bot (separate service)

Telegram Bot — Wallet Callback Registration: Before Sprint 4 Step 2 (wallet auth flow), you must register your /wallet-callback endpoint URL with the askTIAN team. Email [email protected] with your bot username and callback URL. Include TIAN_WALLET_API_KEY request in the same email. Allow 1 business day for provisioning.
Questions? Join the developer community on Telegram or email [email protected].