This page is for AI tools (Cursor, Claude Code, Codex, …) scaffolding a QiForge oracle. Humans should read Quickstart and Build a plugin instead.
If you are an AI agent: read top-to-bottom once. Every signature you need is inlined. Source paths cite the canonical files on the QiForge runtime (packages/oracle-runtime) and the reference oracle (apps/qiforge-example).
If you are working inside a scaffolded oracle project (one created with qiforge-cli new), there is already a Claude Code skill at .claude/skills/qiforge-oracle/SKILL.md with project-local guidance: adding plugins, adding tools, wiring env, writing tests with createTestRuntime. Load it first — it carries denser, scenario-specific references than this page.
TL;DR — what you produce
A QiForge oracle is one main.ts that calls createOracleApp({ config, plugins, … }) plus one folder per plugin under src/plugins/<name>/. The runtime handles HTTP, auth, the agent graph, the checkpointer, Matrix, and bundles 15 plugins by default. You ship glue code, not infrastructure.
main.ts shape
import { createOracleApp } from '@ixo/oracle-runtime';
import { MyPlugin } from './plugins/my-plugin/my-plugin.plugin.js';
const app = await createOracleApp({
config: {
name: 'My Oracle',
org: 'Acme',
description: 'One-line pitch the system prompt will use.',
},
plugins: [new MyPlugin()],
features: {
composio: false, // opt out of bundled composio
firecrawl: 'auto', // (default) — autoDetect from env
},
});
await app.listen(Number(process.env.PORT ?? 3000));
Source: apps/qiforge-example/src/main.ts.
createOracleApp — full options
packages/oracle-runtime/src/bootstrap/create-oracle-app.ts
interface CreateOracleAppOptions {
config: OracleConfig; // required
features?: Partial<Record<BundledFeatureName | string, FeatureToggle>>;
plugins?: OraclePlugin[];
nestModules?: Array<Type | DynamicModule>;
authExcludedRoutes?: AuthExcludedRoute[];
bundledPlugins?: OraclePlugin[]; // test override
env?: NodeJS.ProcessEnv; // test override
skipMatrixInit?: boolean; // test override
skipGracefulShutdown?: boolean; // test override
logger?: PluginLogger;
hooks?: MainAgentHooks;
}
type FeatureToggle = boolean | 'auto';
interface OracleConfig {
name: string;
org?: string;
description?: string;
prompt?: {
opening?: string; // full replacement of the identity preamble
communicationStyle?: string; // appended to operating principles (tone/voice)
capabilities?: string; // author elevator pitch above the capability list
customInstructions?: string; // verbatim `## Custom Instructions` block
};
}
interface AuthExcludedRoute { path: string; method?: RequestMethod }
interface MainAgentHooks {
checkpointerForUser?: (userDid: string) => Promise<BaseCheckpointSaver>;
resolveModel?: (role: ModelRole, params?: ChatOpenAIFields) => BaseChatModel;
getRoomTitle?: (roomId: string) => Promise<string | undefined>;
safetyModel?: BaseChatModel;
validationSkipToolNames?: string[];
operationalMode?: string;
editorSection?: string;
composioContext?: string;
userSecretsContext?: string;
degradedServicesBlock?: string;
}
createOracleApp returns an OracleApp:
interface OracleApp {
getNestApp(): INestApplication;
ambient: AmbientServices;
plugins: { status(): { loaded: string[]; excluded: { plugin: string; reason: string }[]; softDepGaps: { plugin: string; missing: string }[] } };
beforeListen(fn: (nestApp: INestApplication) => Promise<void> | void): void;
onError(handler: (err: Error, source: string) => void): void;
onPluginStatusChange(handler: (event: { plugin: string; from: 'pending'|'loaded'|'failed'; to: 'pending'|'loaded'|'failed'; reason?: string }) => void): void;
listen(port?: number): Promise<void>;
}
OraclePlugin — all 9 hooks
packages/oracle-runtime/src/plugin-api/oracle-plugin.ts
abstract class OraclePlugin {
abstract readonly name: string; // kebab-case
abstract readonly version: string;
abstract readonly manifest: PluginManifest;
readonly dependsOn?: string[]; // hard deps — boot fails if missing
readonly softDependsOn?: string[]; // soft deps — branches on availability
readonly configSchema?: z.ZodObject<any>; // merged into runtime env schema
autoDetect?(env: NodeJS.ProcessEnv): boolean; // skip plugin when false
readonly autoDetectHint?: string; // surfaced in boot errors
getTools?(ctx: PluginContext): PluginTool[] | Promise<PluginTool[]>;
getSubAgents?(ctx: PluginContext): PluginSubAgent[];
getRequestTools?(rtCtx: RuntimeContext): PluginTool[] | Promise<PluginTool[]>;
getRequestSubAgents?(rtCtx: RuntimeContext): PluginSubAgent[] | Promise<PluginSubAgent[]>;
getMiddlewares?(ctx: PluginContext): AgentMiddleware[];
getSharedState?(): Record<string, (state: any, runCtx: RuntimeContext) => unknown>;
getNestModules?(ctx?: PluginContext): Array<Type | DynamicModule>;
getAuthExcludedRoutes?(): AuthExcludedRoute[];
}
PluginManifest
interface PluginManifest {
title: string; // human name
summary: string; // one-line, shown in Tier-1 prompt for `always`
whenToUse: string[]; // triggers
whenNotToUse?: string[]; // anti-patterns
examples?: { user: string; thought?: string; tool: string; args?: Record<string, unknown> }[];
tags?: string[];
category?: 'data' | 'communication' | 'automation' | 'memory' | 'integration' | 'ui' | 'auth' | 'observability' | 'core';
visibility?: 'always' | 'on-demand' | 'silent'; // default: 'on-demand'
stability?: 'stable' | 'beta' | 'experimental';
}
interface PluginTool {
name: string;
description: string;
schema: z.ZodType;
handler: (args: unknown, ctx: RuntimeContext) => Promise<unknown>;
visibility?: 'always' | 'on-demand' | 'silent'; // override plugin default per tool
}
interface PluginSubAgent {
name: string; // e.g. 'call_memory_agent'
description: string;
systemPrompt: string | ((ctx: PluginContext) => string);
tools: PluginTool[] | ((ctx: PluginContext) => PluginTool[]);
model?: ModelRole; // default 'subagent'
middlewares?: AgentMiddleware[];
forwardTools?: boolean | string[]; // forward tool calls to main message stream
onComplete?: (result: string, ctx: RuntimeContext) => Promise<void>;
}
Visibility rules
| Visibility | Bound at boot? | In Tier-1 prompt? | Discoverable via list_capabilities? |
|---|
always | yes | yes | yes |
on-demand (default) | no — until load_capability({ names }) | no | yes |
silent | yes | no | no |
silent means not advertised — the tools are still bound and the agent can call them; they’re just kept out of the Tier-1 prompt and list_capabilities. It is not a security boundary.
Bundled plugins (15)
From packages/oracle-runtime/src/plugins/index.ts:
| Name | Visibility | Default | Required env |
|---|
memory | always | on | MEMORY_MCP_URL, MEMORY_ENGINE_URL |
portal | on-demand | on | — |
firecrawl | on-demand | auto-detect | FIRECRAWL_MCP_URL |
domain-indexer | always | on | — |
composio | on-demand | auto-detect | COMPOSIO_API_KEY |
sandbox | always | auto-detect | SANDBOX_MCP_URL |
skills | always | on (needs sandbox) | — |
editor | on-demand | on (needs Matrix) | — |
agui | on-demand | on | — |
slack | silent | auto-detect | SLACK_BOT_OAUTH_TOKEN |
tasks | on-demand | auto-detect | REDIS_URL |
credits | silent | on unless DISABLE_CREDITS=true | — |
calls | silent | on (placeholder stub — no tools) | — |
user-preferences | always | on | — |
matrix-group-chats | on-demand | on (opt out via features) | — (optional CHANNEL_MEMORY_*) |
Toggle via features in createOracleApp: true forces on, false forces off, 'auto' runs autoDetect.
Per-plugin reference pages: /build-an-oracle/reference/bundled-plugins/overview.
Core (Tier-0) env vars
From packages/oracle-runtime/src/config/base-env-schema.ts:
These are the exact names the runtime validates — set them character-for-character or the boot-time Zod check fails.
| Var | Required | Description |
|---|
ORACLE_NAME | yes | Oracle display name. |
NETWORK | yes | mainnet | testnet | devnet. |
ORACLE_DID | yes | The oracle’s own DID (did:ixo:ixo1...); the signer identity for downstream invocations. |
ORACLE_ENTITY_DID | yes | The oracle’s on-chain entity DID (did:ixo:entity:...). |
SECP_MNEMONIC | yes | Wallet mnemonic used to sign UCAN invocations to downstream services. |
RPC_URL | yes | IXO chain RPC endpoint. |
BLOCKSYNC_GRAPHQL_URL | yes | Blocksync GraphQL endpoint (UCAN validation reads it). |
MATRIX_BASE_URL | yes | Matrix homeserver URL. |
MATRIX_RECOVERY_PHRASE | yes | Recovery phrase for the oracle’s Matrix encryption. |
MATRIX_ORACLE_ADMIN_USER_ID | yes | Matrix user ID the bot logs in as. |
MATRIX_ORACLE_ADMIN_PASSWORD | yes | Matrix bot password. |
MATRIX_ORACLE_ADMIN_ACCESS_TOKEN | yes | Matrix bot access token. |
MATRIX_ACCOUNT_ROOM_ID | yes | Oracle’s Matrix account room (holds signing key + secrets). |
MATRIX_VALUE_PIN | yes | PIN for the oracle’s Matrix value store / vault. |
SQLITE_DATABASE_PATH | yes | Path for the per-user SQLite checkpointer DB. |
LLM_PROVIDER | optional | openrouter (default) | nebius. Selects the per-role model map. |
OPEN_ROUTER_API_KEY | conditional | Required when LLM_PROVIDER=openrouter (the default). |
NEBIUS_API_KEY | conditional | Required when LLM_PROVIDER=nebius. |
OPENAI_API_KEY | optional | Only if a plugin needs OpenAI directly. Not required for the agent. |
MATRIX_STORE_PATH | optional | Matrix store dir. Defaults to ./matrix-storage. |
UCAN_AUTH_MAX_TTL_SECONDS | optional | Max accepted lifetime of a user auth invocation. Default 900. |
UCAN_REAUTH_PROMPT_THROTTLE_SECONDS | optional | Min gap between Matrix re-auth nudges. Default 21600. |
ORACLE_SECRETS | optional | Operator secrets as KEY=val,KEY2=val2, surfaced as x-os-* headers. |
CORS_ORIGIN | optional | Allowed origins. Wildcard * is the default. |
PORT | optional | HTTP port. Defaults to 3000. |
NODE_ENV | optional | development (default) | production | test. |
LANGSMITH_TRACING | optional | true to enable LangSmith tracing. |
LANGSMITH_API_KEY | optional | LangSmith API key. |
LANGSMITH_PROJECT | optional | LangSmith project name. |
LANGSMITH_ENDPOINT | optional | LangSmith endpoint override. |
There is no ANTHROPIC_API_KEY — the agent’s model id is fixed per role in the provider model map; switch the provider with LLM_PROVIDER + its key, or override the main model via the resolveModel hook. Plugin-specific env vars (MEMORY_MCP_URL, FIRECRAWL_MCP_URL, SANDBOX_MCP_URL, COMPOSIO_API_KEY, SLACK_BOT_OAUTH_TOKEN, REDIS_URL, …) merge in via each plugin’s configSchema.
Copy-pasteable plugin template
// src/plugins/example/example.plugin.ts
import { OraclePlugin, type PluginManifest, type PluginTool, type RuntimeContext } from '@ixo/oracle-runtime';
import { z } from 'zod';
const manifest: PluginManifest = {
title: 'Example',
summary: 'One-line description shown in the Tier-1 prompt block.',
whenToUse: ['User asks about example things'],
whenNotToUse: ['User asks about unrelated things'],
visibility: 'on-demand',
stability: 'experimental',
};
export class ExamplePlugin extends OraclePlugin {
readonly name = 'example';
readonly version = '0.1.0';
readonly manifest = manifest;
readonly configSchema = z.object({
EXAMPLE_API_KEY: z.string().min(1),
});
autoDetect(env: NodeJS.ProcessEnv): boolean {
return Boolean(env.EXAMPLE_API_KEY);
}
readonly autoDetectHint = 'EXAMPLE_API_KEY';
override getTools(): PluginTool[] {
return [
{
name: 'get_example',
description: 'Fetch an example.',
schema: z.object({ query: z.string() }),
handler: async (args, rtCtx: RuntimeContext) => {
const { query } = args as { query: string };
rtCtx.logger.log(`Example tool called with ${query}`);
return { result: `echo: ${query}` };
},
},
];
}
}
Then in main.ts:
import { ExamplePlugin } from './plugins/example/example.plugin.js';
const app = await createOracleApp({
config: { name: 'My Oracle' },
plugins: [new ExamplePlugin()],
});
await app.listen(Number(process.env.PORT ?? 3000));
Where to dig deeper