REST API backend for the Marquinhos Discord bot ecosystem. Serves gamification (XP, levels, achievements), Wordle, maze minigame, Last.fm scrobbling, and Discord OAuth for the companion bot and web app.
- Bun 1.3.x — runtime, package manager, and test runner
- Node.js 22.x (only needed for editor tooling / type-checking with
tsc) - SQLite (bundled via
bun:sqlite, no external install required) - Docker (optional, for containerized runs)
git clone git@github.com:Devaneios/marquinhos-web-api.git
cd marquinhos-web-api
bun install
cp .env.example .env # if present, otherwise create manually — see belowRequired environment variables (.env):
| Variable | Purpose |
|---|---|
HTTP_PORT |
Port the HTTP server listens on |
HTTPS_PORT |
Port for HTTPS (if configured) |
NODE_ENV |
development / production |
SQLITE_PATH |
Path to the SQLite database file |
MARQUINHOS_API_KEY |
Shared secret for bot-to-API requests (Authorization: Bearer) |
MARQUINHOS_SECRET_KEY |
Key used to encrypt/decrypt Discord session tokens |
DISCORD_BOT_TOKEN |
Bot token, used for Discord API calls |
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET / DISCORD_REDIRECT_URI |
Discord OAuth config |
LASTFM_API_KEY / LASTFM_SHARED_SECRET / LASTFM_REDIRECT_URI |
Last.fm scrobbling OAuth |
SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET |
Spotify metadata lookups |
CORS_ORIGINS |
Comma-separated allowlist (defaults to localhost:4200 + prod web app) |
KNOWLEDGE_BASE_URL |
Base URL of the local devaneios-chats RAG service (e.g. http://devaneios-rag:8420), used by the bot to look up server lore/history. Optional — omit to run without it. |
KNOWLEDGE_BASE_API_KEY |
Bearer token sent to KNOWLEDGE_BASE_URL. Must match that service's RAG_API_KEY. |
OPENAI_API_KEY |
OpenAI credentials for every AI feature |
OPENAI_MODEL |
Model id (defaults to gpt-5.4-mini) |
OPENAI_REASONING_EFFORT |
Reasoning effort for thread calls (defaults to medium); the research pipeline sets its own per phase |
SEARXNG_URL |
SearXNG instance used by search_web and deep research (defaults to https://searxng.frois.net.br) |
AI_THREAD_TOKEN_BUDGET |
Transcript size that triggers thread compaction (defaults to 120000) |
AI_THREAD_RETENTION_DAYS |
How long an idle thread transcript is kept (defaults to 30) |
AI_RESEARCH_MAX_ROUNDS |
Search rounds per research job (defaults to 5) |
AI_RESEARCH_MAX_SEARCHES |
Total searches per research job (defaults to 30) |
AI_RESEARCH_MAX_FETCHES |
Total pages read per research job (defaults to 40) |
AI_RESEARCH_DEADLINE_MS |
Wall-clock deadline per research job (defaults to 900000; the last 3 min are reserved for analysis and writing) |
AI_RESEARCH_RETENTION_DAYS |
How long research jobs and their events are kept (defaults to 14) |
Run the database migrations and start the dev server:
bun run devsrc/index.ts runs runMigrations() and initializes gamification defaults on boot — a failure here crashes the process intentionally rather than serving with a broken config.
docker compose up --buildThe Dockerfile is a two-stage build: dependencies and the Wordle valid-guesses word list are compiled in a builder stage, then only node_modules, src/, and the generated word lists are copied into the oven/bun:1-alpine runtime image.
Health check:
curl http://localhost:3000/api/health
# { "status": "ok" }Bot-authenticated request (server-to-server):
curl -X POST http://localhost:3000/api/gamification/xp \
-H "Authorization: Bearer $MARQUINHOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "123", "guildId": "456", "eventType": "command"}'Web-authenticated request (from the Angular frontend, Discord token instead of the API key):
curl http://localhost:3000/api/gamification/level/123/456 \
-H "Authorization: Bearer <discord-access-token>" \
-H "marquinhos-agent: web"| Mount | Router | Notes |
|---|---|---|
/api/auth |
auth.route.ts |
Discord OAuth login/callback |
/api/user |
user.route.ts |
User profile/settings |
/api/scrobble |
scrobble.route.ts |
Last.fm scrobble ingestion |
/api/privacy-policy |
privacyPolicy.route.ts |
Static policy content |
/api/gamification |
gamification.route.ts |
XP, levels, leaderboard, game results |
/api/evolutive-achievements |
evolutiveAchievements.route.ts |
Tiered achievement progression |
/api/games/maze |
maze.route.ts |
Maze minigame sessions |
/api/wordle |
wordle.route.ts |
Wordle guesses and word list review |
/api/ai-chat |
aiChat.route.ts |
Tag replies, /ia threads, deep research jobs, traces |
| Method | Path | Purpose |
|---|---|---|
POST |
/api/ai-chat/respond |
The @Marquinhos tag flow: classify, answer in persona, revise |
POST |
/api/ai-chat/thread/ask |
One turn of an /ia perguntar thread — skips classification, keeps the transcript |
POST |
/api/ai-chat/research |
Starts a deep research job; returns 202 with a jobId |
GET |
/api/ai-chat/research/:jobId |
Job status, progress events, and the finished report |
GET |
/api/ai-chat/traces |
Recent AI traces |
GET |
/api/ai-chat/traces/:traceId |
One trace with every LLM call, tool call and exec |
- Runtime: Express app on Bun, entry point
src/index.ts. - Auth: two paths through the same middleware chain (
middlewares/botAuth.ts):- Requests with header
marquinhos-agent: webare routed toverifyDiscordToken(middlewares/userAuth.ts), which decrypts the token, checks expiry, and fetches the user's Discord identity + guild role. - All other requests are checked against
MARQUINHOS_API_KEYusing a timing-safe buffer comparison.
- Requests with header
- Persistence:
bun:sqlite, single file DB atSQLITE_PATH. Schema is created idempotently indatabase/sqlite.ts(CREATE TABLE IF NOT EXISTS), with incremental changes applied via numbered SQL files indatabase/migrations/and run throughdatabase/migrate.tsat boot. - Gamification:
services/gamification.tshandles XP awards, level-up detection, and cooldowns;services/evolutiveAchievements.tstracks per-user stat counters and auto-evolves tiered achievements when thresholds are crossed. Both are wired into the sameaddXPcall path. - Wordle: valid-guess word list is pre-generated at Docker build time (
scripts/build-valid-guesses.ts) fromwordlist.txt+ an external word frequency list, then loaded into memory once on boot (getValidationSet()) to avoid disk I/O per request. - AI features: three separate paths share one set of tools and one trace recorder.
- Tag flow (
AiChatService): two-layer intent classification, persona response per category, then a revision pass. Runs on Chat Completions. Unchanged by the/iawork. - Threads (
thread/AiThreadService): one turn of an/ia perguntarconversation. No classification — it goes straight to an agentic loop with the full tool set. Runs on the Responses API so the model's ownreasoningitems (withencrypted_content) can be stored and replayed on later turns;reasoning.context: 'all_turns'is what carries the reasoning forward. Reasoning is never posted to Discord, only kept in context and in the trace. Transcripts live inai_thread_itemsas raw API items, and are compacted into a summary once they passAI_THREAD_TOKEN_BUDGET. - Deep research (
research/DeepResearchService): a frontier search with a reflection loop — plan facets and 8-16 sub-queries, search SearXNG in parallel, rank and dedupe hits, let an LLM triage step pick which results are worth reading, compress each page to the claims it serves plus the follow-up queries it opens, push those follow-ups back onto the frontier (up to 3 levels deep), and keep going until an evidence floor is met (2 rounds, 12 relevant sources, 2 sources per facet) — the reflection step can say what to chase next but cannot end the job early. Then a dedicated analysis pass cross-reads every source before the report is written. The per-source compression is what keeps the synthesis call inside the context window. Jobs run detached in-process (research/ResearchOrchestrator) because a pass takes minutes; the bot polls.POST /researchis idempotent onidempotencyKeysince the bot's HTTP client retries.
- Tag flow (
- Agent tools (
services/aiChat/tools/):list_directory,grep_search,read_fileandexecute_coderun inside a Docker sandbox onNetworkMode: none;search_webandfetch_urlrun in the API process instead, which is what keeps arbitrary model-written code off the network.web/fetchPage.tsholds the SSRF screening, body cap and HTML→markdown conversion shared byfetch_urland the research reader. - Rate limiting:
express-rate-limitis wired intoindex.tsbut currently commented out — re-enable before exposing sensitive endpoints publicly. AI usage has its own daily limits inai_chat_config:user_daily_limit,global_daily_limit,agent_daily_limit, andresearch_daily_limit(much lower — one research job spends dozens of LLM calls). - Error handling: a 404 catch-all and a 4-arg Express error handler sit at the bottom of the middleware stack; error details are only returned in the response body outside of
production.
bun run typecheck # tsc --noEmit
bun run lint # eslint src/**/*.ts
bun run format:check # prettier --check
bun test tests/**/*.spec.ts- Husky +
lint-stagedrunprettier --writeandeslint --fixon staged*.tsfiles pre-commit. - Commit messages are linted with
commitlintagainst the conventional-commits config — usetype(scope): message(feat:,fix:,chore:, etc.). - Match existing patterns: controllers stay thin and delegate to
services/, routes bind controller methods with.bind(controller), and Zod schemas inschemas/validate request bodies viavalidateRequest.ts. - Open a PR against
main; CI must pass typecheck, lint, and tests before merge.
ISC