Skip to main content
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';
}

PluginTool, PluginSubAgent

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

VisibilityBound at boot?In Tier-1 prompt?Discoverable via list_capabilities?
alwaysyesyesyes
on-demand (default)no — until load_capability({ names })noyes
silentyesnono
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:
NameVisibilityDefaultRequired env
memoryalwaysonMEMORY_MCP_URL, MEMORY_ENGINE_URL
portalon-demandon
firecrawlon-demandauto-detectFIRECRAWL_MCP_URL
domain-indexeralwayson
composioon-demandauto-detectCOMPOSIO_API_KEY
sandboxalwaysauto-detectSANDBOX_MCP_URL
skillsalwayson (needs sandbox)
editoron-demandon (needs Matrix)
aguion-demandon
slacksilentauto-detectSLACK_BOT_OAUTH_TOKEN
taskson-demandauto-detectREDIS_URL
creditssilenton unless DISABLE_CREDITS=true
callssilenton (placeholder stub — no tools)
user-preferencesalwayson
matrix-group-chatson-demandon (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.
VarRequiredDescription
ORACLE_NAMEyesOracle display name.
NETWORKyesmainnet | testnet | devnet.
ORACLE_DIDyesThe oracle’s own DID (did:ixo:ixo1...); the signer identity for downstream invocations.
ORACLE_ENTITY_DIDyesThe oracle’s on-chain entity DID (did:ixo:entity:...).
SECP_MNEMONICyesWallet mnemonic used to sign UCAN invocations to downstream services.
RPC_URLyesIXO chain RPC endpoint.
BLOCKSYNC_GRAPHQL_URLyesBlocksync GraphQL endpoint (UCAN validation reads it).
MATRIX_BASE_URLyesMatrix homeserver URL.
MATRIX_RECOVERY_PHRASEyesRecovery phrase for the oracle’s Matrix encryption.
MATRIX_ORACLE_ADMIN_USER_IDyesMatrix user ID the bot logs in as.
MATRIX_ORACLE_ADMIN_PASSWORDyesMatrix bot password.
MATRIX_ORACLE_ADMIN_ACCESS_TOKENyesMatrix bot access token.
MATRIX_ACCOUNT_ROOM_IDyesOracle’s Matrix account room (holds signing key + secrets).
MATRIX_VALUE_PINyesPIN for the oracle’s Matrix value store / vault.
SQLITE_DATABASE_PATHyesPath for the per-user SQLite checkpointer DB.
LLM_PROVIDERoptionalopenrouter (default) | nebius. Selects the per-role model map.
OPEN_ROUTER_API_KEYconditionalRequired when LLM_PROVIDER=openrouter (the default).
NEBIUS_API_KEYconditionalRequired when LLM_PROVIDER=nebius.
OPENAI_API_KEYoptionalOnly if a plugin needs OpenAI directly. Not required for the agent.
MATRIX_STORE_PATHoptionalMatrix store dir. Defaults to ./matrix-storage.
UCAN_AUTH_MAX_TTL_SECONDSoptionalMax accepted lifetime of a user auth invocation. Default 900.
UCAN_REAUTH_PROMPT_THROTTLE_SECONDSoptionalMin gap between Matrix re-auth nudges. Default 21600.
ORACLE_SECRETSoptionalOperator secrets as KEY=val,KEY2=val2, surfaced as x-os-* headers.
CORS_ORIGINoptionalAllowed origins. Wildcard * is the default.
PORToptionalHTTP port. Defaults to 3000.
NODE_ENVoptionaldevelopment (default) | production | test.
LANGSMITH_TRACINGoptionaltrue to enable LangSmith tracing.
LANGSMITH_API_KEYoptionalLangSmith API key.
LANGSMITH_PROJECToptionalLangSmith project name.
LANGSMITH_ENDPOINToptionalLangSmith 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