Two tabs, one tiny server:
- XP Calculator — a Pathfinder 2e encounter XP calculator (no server required; works as a static page too).
- Shadowdark Oracle — a Gemini-powered oracle that reads your character sheet and, in the voice of a disappointed elder god, roasts your dump stat in iambic pentameter, foretells your timestamped demise, carves you an epitaph, and whispers cryptic warnings about doorways you haven't seen yet. It can also read the prophecy aloud. Feed it a character as pasted text, an image/PDF of a sheet, or a Shadowdarklings JSON export (parsed in-browser into an editable stat block).
The Oracle never persists your character anywhere — sheets and prophecies live only in your browser's localStorage, and the server is stateless. It relays a single call to Gemini and forgets you instantly, as the gods do.
Browser (tabs UI, localStorage history)
│ POST /api/oracle { text } OR { image: { mimeType, data(base64) } } → JSON prophecy
│ POST /api/oracle/speak { text } → audio/wav
▼
Express server (server.js) ──@google/genai──▶ Gemini API
• serves the static site (index.html, css, js)
• holds GEMINI_API_KEY server-side (never shipped to the browser)
- Text model:
gemini-2.5-flash(configurable) - TTS:
gemini-2.5-flash-preview-tts→ raw PCM, wrapped into a WAV on the server so the browser can play it
A Gemini API key from Google AI Studio. (The consumer "Gemini" subscription and the API key are separate — you need the API key.)
npm install
cp .env.example .env # then put your key in GEMINI_API_KEY
npm start # serves on http://localhost:8080Open http://localhost:8080:
- XP Calculator works with or without the key.
- Shadowdark Oracle needs the key (the
/api/*endpoints return 503 without it).
Optional .env overrides: GEMINI_TEXT_MODEL, GEMINI_TTS_MODEL, GEMINI_TTS_VOICE (e.g. Charon, Kore, Puck), ORACLE_MAX_CONCURRENCY.
- Create a service from this repo — Railway auto-detects Node from
package.jsonand runsnpm start. - Add a service variable
GEMINI_API_KEYwith your key. - Generate a domain. The app listens on the injected
PORTand binds0.0.0.0.
The free tier (0.5 CPU / 0.5 GB) is enough — the server is a thin stateless proxy. Uploads are capped at ~10 MB and concurrent Gemini calls are throttled to protect memory and stay under the preview TTS rate limits.
- Party level input — adversary/hazard rows update to the computed creature level
- Accomplishment / Adversary / Hazard XP — check off or count encounters
- Calculate / Clear, with party level persisted to
localStorage