main.ts that calls createOracleApp. The runtime boots a NestJS app, loads bundled + your plugins, validates env, builds the agent graph, and starts HTTP when you call listen().
Reference implementation: apps/qiforge-example/src/main.ts. Source: packages/oracle-runtime/src/bootstrap/create-oracle-app.ts.
Minimal main.ts
autoDetect(env) and the ones whose env is present load.
The recipe
Write your config
Put
OracleConfig in its own file so tests can import it without booting the runtime.entityDid is sourced from the ORACLE_ENTITY_DID env var — never put it in config. The prompt block is composed into the system prompt; absent fields fall back to runtime defaults. Source: plugin-api/types.ts (search OracleConfig).Add your plugins
Bundled plugins flow in automatically from The loader dedupes by
BUNDLED_PLUGINS — each runs its own autoDetect(env). Only list the ones you wrote yourself or bundled plugins that need constructor args:name, so an explicit instance overrides the bundled default of the same name. Source: bootstrap/plugin-loader.ts.Toggle bundled plugins with features
Mount your own Nest modules
Custom HTTP endpoints, event consumers, admin dashboards — anything that doesn’t fit the plugin model goes in
nestModules. Each gets full DI access to runtime services (Sessions, Messages, Secrets, UCAN, Auth, …).Exclude routes from auth
Every route defaults to going through Symmetric with each plugin’s
AuthHeaderMiddleware (requires x-ucan-delegation). Opt routes out via authExcludedRoutes:getAuthExcludedRoutes(). Both merge onto the runtime’s built-ins (/health, /docs). Recipe for the plugin side: Add HTTP endpoints.Customise the agent graph (optional)
hooks overrides the agent-build defaults. Every field is optional:Source:
graph/main-agent-types.ts (search MainAgentHooks).Change the AI model. The most common reason to set hooks is to swap the model. The runtime resolves the main agent’s model via resolveModel('main'); override it and spread params to keep the provider’s fallback models and latency sort:LLM_PROVIDER = openrouter default | nebius) and per-role default model ids are otherwise fixed in the runtime — there is no env var to change the main model id without this hook. You can also return any LangChain BaseChatModel (e.g. new ChatOpenAI({ model: 'gpt-4o' })), but doing so drops the OpenRouter fallback/latency wiring.Run a beforeListen hook
Run setup that must complete before HTTP starts accepting:Hooks run sequentially in registration order.
Observe plugin lifecycle
onPluginStatusChange fires when Matrix transitions pending → loaded (or failed). onError catches Matrix init + lifecycle errors. plugins.status() returns a snapshot of loaded, excluded, and soft-dep-gap plugins.Listen
listen() twice throws. Default port is 3000 — set the PORT env var or pass a number to listen() to override.All options at a glance
Full example
Where to read next
Write a plugin
End-to-end Weather plugin walkthrough.
Enable bundled plugins
The
features map in detail.createOracleApp reference
Flat reference table for every field.
Environment variables
Core + per-plugin env vars.