What you'll have at the end
You: what's the weather in Berlin?
Oracle: (calls list_capabilities → load_capability(weather) → get_current_weather)
Berlin is currently 14°C with light rain.A live oracle on localhost:3000 with:
- The 16 bundled plugins resolved at boot.
- A custom Weather plugin loading on demand.
- A
GET /weather/now?city=Xpublic HTTP route. - A streaming chat endpoint over SSE.
Step 0 — prerequisites
- Node.js 22+
- pnpm —
npm install -g pnpm - The IXO Mobile App or a 12/24-word mnemonic (for offline auth)
- An OpenRouter API key — get one at openrouter.ai
Step 1 — install the CLI
pnpm add -g qiforge-cli # npm package name
pnpm approve-builds -g # approve protobufjs when prompted
qiforge-cli --help # the installed binary is `qiforge-cli`If it prints the help text, the CLI is ready.
Step 2 — scaffold a project
qiforge-cli new my-oracleThe CLI walks through:
- Auth — pick SignX (QR code) or offline (mnemonic). Offline is faster for local dev.
- Network — pick
devnetfor development. - Oracle profile — name, description, price, model. Defaults are fine.
- Entity creation — the CLI writes a transaction to register your oracle's entity DID on chain. Confirm when prompted (SignX needs the mobile app; offline signs locally).
- Matrix bot — provisioned automatically.
Entity creation registers the oracle's API URL as http://localhost:4000 by default, but the runtime boots on PORT=3000. To make your oracle reachable at its registered URL, either set PORT=4000 in .env to match, or change the registered URL later with qiforge-cli update-oracle-api-url.
The result is a plugin-runtime-shaped project:
my-oracle/
├── src/
│ ├── main.ts # calls createOracleApp({ config, plugins })
│ ├── config.ts # OracleConfig
│ └── plugins/ # your plugins go here
├── .claude/
│ └── skills/qiforge-oracle/ # Claude Code skill (project-local)
├── .env # Matrix creds, ORACLE_*, mnemonic
├── oracle.config.json # name, model, prompt, DID
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── CLAUDE.md # bootstrapping for Claude CodeThe scaffold drops a Claude Code skill at .claude/skills/qiforge-oracle/. Open the project in Claude Code (or any agent that reads .claude/skills/) and the skill auto-loads — your AI helper now has dense, project-local references for adding plugins, adding tools, wiring env, and writing tests with createTestRuntime. Nothing to install separately.
Cloning instead of scaffolding — to study a finished plugin-based oracle, clone the boilerplate and run the example:
git clone https://github.com/ixoworld/qiforge
cd qiforge/apps/qiforge-example
cp .env.example .envThen follow Step 3 onward against apps/qiforge-example/.
Step 3 — fill in .env
Open your project's .env (my-oracle/.env from qiforge-cli new, or apps/qiforge-example/.env if you cloned the boilerplate). The CLI populated identity vars; you need to add the LLM key and any plugin vars:
- OPEN_ROUTER_API_KEY=
+ OPEN_ROUTER_API_KEY=sk-or-v1-...
# Optional — enables specific plugins:
+ MEMORY_MCP_URL=https://memory-mcp.your-deployment.example/sse
+ MEMORY_ENGINE_URL=https://memory-engine.your-deployment.example
+ FIRECRAWL_MCP_URL=https://firecrawl-mcp.your-deployment.example/sse
+ SANDBOX_MCP_URL=https://sandbox-mcp.your-deployment.example/sse
+ WEATHER_DEFAULT_UNITS=celsiusPlugins whose env vars you leave blank simply don't load — they're skipped with a [boot] excluded: <plugin> (<env-var>) line.
See Environment variables reference for the complete list.
Step 4 — install + boot
cd my-oracle # or qiforge/apps/qiforge-example
pnpm install
pnpm devYou should see something like:
[Nest] Starting Nest application...
[Nest] OraclePlugin {memory} loaded (always)
[Nest] OraclePlugin {domain-indexer} loaded (always)
[Nest] OraclePlugin {editor} loaded (on-demand)
[Nest] OraclePlugin {user-preferences} loaded (always)
[Nest] OraclePlugin {weather} loaded (on-demand)
[boot] excluded plugins: composio (COMPOSIO_API_KEY), slack (SLACK_BOT_OAUTH_TOKEN), tasks (REDIS_URL)
Oracle 'QiForge Example Oracle' (runtime v0.X.Y) listening on :3000
[plugin] matrix pending → loadedThe excluded plugins are normal — they need env vars you didn't set. To enable one, fill its env vars and restart.
Step 5 — hit a public plugin endpoint
The Weather plugin exposes GET /weather/now. It's marked auth-excluded via getAuthExcludedRoutes(), so no UCAN header needed:
curl 'http://localhost:3000/weather/now?city=Berlin'You should get back JSON:
{
"ok": true,
"city": "Berlin",
"temp_c": 14.2,
"units": "celsius",
"conditions": "Slight rain",
"latitude": 52.52,
"longitude": 13.405
}This proves: the plugin loaded, its Nest module mounted, the auth exclusion worked, and the upstream call to Open-Meteo succeeded.
Step 6 — send a chat message
For interactive chat, use the CLI's streaming TUI:
qiforge-cli --chatThen type:
what's the weather in Berlin?The agent walks this path:
list_capabilities— seesweatherison-demand, not loaded yet.load_capability({ names: ['weather'] })— gets the manifest back withwhenToUseand the tool list.loadedPluginsstate field now contains'weather'.get_current_weather({ city: 'Berlin' })— the actual tool fires.- Final response — natural-language summary of the result.
Watch the server logs at the same time — you'll see the weather middleware print model call started / model call complete (Xms) for each LLM step.
What you just verified
| You saw | What it proves |
|---|---|
| Boot log listing plugins | Loader + topo sort + manifest validation worked. |
excluded plugins: line with reasons | Env-driven opt-in (autoDetect) is wired correctly. |
GET /weather/now returning JSON | Plugin Nest modules mount + auth exclusions apply. |
Agent calling load_capability then get_current_weather | Dynamic discovery and loadedPlugins state work end-to-end. |
| Middleware logs around each model call | getMiddlewares hooks are firing. |
Where to go next
Task-by-task recipes — createOracleApp, plugin recipes, test, deploy.
Recreate the Weather plugin from scratch.
createOracleApp optionsEvery option you can pass — config, plugins, features, hooks.
Bundled pluginsThe 16 plugins shipped with the runtime.
For AI agentsDense one-pager with every signature inlined.
Understand the modelOptional. The mental model, runtime layers, plugins vs skills.
Troubleshooting
| Symptom | Likely fix |
|---|---|
[boot-error] Plugin '<name>' env validation failed for '<X>' | Either set the env var or disable the plugin with features: { name: false }. |
Boot hangs after Nest application started | Matrix init is still pending. Check MATRIX_BASE_URL and MATRIX_ORACLE_ADMIN_* env vars. |
| All authenticated requests return 401 | UCAN signing mnemonic not loaded — see Identity and auth. |
| The agent doesn't call my plugin's tool | Check it's loaded in app.plugins.status(). If it's on-demand, prompt the agent more clearly so it calls load_capability. |
Full coverage: Troubleshooting.