Inspiration
Daily horoscope and saju readings are a habit for many people: you read one in the morning and forget it by lunch. They tell you who you are, but they never ask you anything back.
Saju (Four Pillars) is the East Asian birth-chart tradition. It describes each day as a relationship between that day's "pillar" and your own. That relationship is a good way into a question about your day, but only if the question is honest about where it came from and remembers what you said yesterday.
What it does
Today's Reflection is a card inside Saju Today (saju.hun-is.com), a live saju app on the web and Android. The card is live on the web now and ships to Android in the next Play release.
- One observation, one question. Nemotron reads today's chart facts (the day pillar and how it relates to you, your day master and the solar term) together with your last few answers and moods. It names one echo or tension between them and asks you one question.
- "Based on" chips = provenance. Under the observation the card shows the exact facts the model was given, so you can see why you got this question. The observation cites those facts in parentheses, and in English a server-side check rejects any citation word that isn't in the list.
- You answer in your own words, and you can add an optional mood (Low / Okay / Good).
- A one-line mirror, not advice. The closing reflects back what you said, saves the pair and starts a streak. The next day's question cites your recent answers.
- Crisis wording is checked before the model. Before any model call, a deterministic keyword check runs on the answer. If it matches, the app shows a helpline line (findahelpline.com, Korea 109) and the model never answers. The same line is always visible under the answer box.
- Honest by design. Every card discloses the model: "NVIDIA Nemotron 3 Super on Nebius Token Factory". It is framed as reflection and entertainment, "not prophecy or medical, psychological, legal or financial advice". Guest mode needs no sign-up, and deleting the account deletes every answer.
A second surface uses the same contract. Daily Reflection is an open-source MCP server (MIT) that exposes the lens and the journal as tools (get_daily_lens, get_reflection_prompt, save_journal_entry, get_reflection_history). Its web simulation lets Nemotron drive them with native tool_calls through Token Factory.
How we built it
- Model:
nvidia/nemotron-3-super-120b-a12bon Nebius Token Factory, through the OpenAI-compatible/v1/chat/completions. Thinking is switched off withchat_template_kwargs: { enable_thinking: false },max_tokens300, temperature 0.7. - Facts before the model: the chart engine computes today's pillar, its relation to the user, the day master and the solar term. The journal supplies the last answers with an explicit day count ("last 3 answers"), which stops the model from making up a time span. These facts are handed to the model as plain data. The provenance chips are that same list, so the UI shows exactly what the model saw.
Post-check gates (turn 1): exactly one question and it comes last; no advice wording ("you should", "try to"…); citations limited to words in the given facts (inflections allowed, invented words rejected); Chinese characters only where they appear in the data; past answers cited whenever they exist; in Korean, no technical saju terms outside the citation. Closing: no advice, and it must not hand heavy words back to the user ("useless", "pointless"…).
Retry that knows why it failed: if a reply misses a gate, the second call gets the first reply plus the one rule it broke. If that also misses, a hand-written line for that day type is used (10 types × EN/KO), and those lines pass the same gates.
Stack: React + Vite front end (web + Capacitor Android), Node.js API on Oracle Cloud behind Cloudflare, Firebase Auth + Firestore, Node MCP server (Streamable HTTP) on Google Cloud Run for the open-source module.
Tests: the reflection route has a mocked-model contract suite in the app repo (gates, retry, fallback, crisis, streak, cache). The public repo has 21 passing tests (
npm test).
Challenges we ran into
- An empty answer with thinking on. Our first Token Factory calls returned empty
content. Nemotron was spending the wholemax_tokenson reasoning. The OpenRouter-stylereasoning: { enabled: false }field is ignored on Token Factory;chat_template_kwargs: { enable_thinking: false }works. - "100% valid" was not "good". The JSON/format contract parsed every time while the text still had real defects: a second question tucked into the observation, re-translated Chinese terms, advice wording. We stopped treating the parse rate as a quality metric and wrote a gate for each defect we measured.
- Over-strict gates. After launch, 2 of the first 9 production turns fell back to the deterministic line. A census of 48 samples, tallied by the gate that rejected them, showed the main cause was ours: the model wrote "rivalry" where the data said "rival peer". We now accept inflections, and the retry is told which rule it broke. On a 96-sample re-run, first-attempt misses fell from 12.5% to 5.2%, first-time English users went from 3/12 misses to 0/24, and 2 of 96 turns (2.1%) ended in the fallback line.
Accomplishments that we're proud of
- It is live in a real product, not a demo page: judges can use the same card our users see.
- Provenance you can see: every observation cites only facts shown right under it.
- A crisis check that doesn't depend on the model: a deterministic keyword list runs before any model call, and the same list is shared by the app and the open-source server. It is a keyword screen, not a clinical assessment, and we keep adding the misses we find.
What we learned
- On Token Factory, turn thinking off with the switch the model's chat template understands, and check for empty
contentexplicitly. - Tally each failure by its reason before you loosen or tighten a gate. Our guess about the cause was wrong; the census was not.
- A retry needs to be told why the first reply failed. In our census, a retry that was told the rule fixed every English miss.
What's next for Saju Today — Today's Reflection
- Ship the card to Android users in the next Play release after the current bundle.
- Weekly look-back: a short reflection on the week's answers, with the same provenance rule.
- Watch the per-reason miss log in production and keep the fallback rate under a few percent.
Built With
- capacitor
- cloudflare
- firebase
- firestore
- google-cloud-run
- model-context-protocol
- nebius-token-factory
- node.js
- nvidia-nemotron
- oracle-cloud
- react
- vite
Log in or sign up for Devpost to join the conversation.