> ## Documentation Index
> Fetch the complete documentation index at: https://ixoworld-mintlify-0aee4309.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# createOracleApp

> The single entry point of every QiForge oracle. Full signature, every option, the returned OracleApp shape, and the boot sequence the function runs.

## Overview

`createOracleApp(opts: CreateOracleAppOptions): Promise<OracleApp>` resolves the plugin set, validates env, populates registries, builds the dynamic `RuntimeAppModule`, bootstraps NestJS, and returns an `OracleApp` whose `listen()` runs `beforeListen` callbacks then starts the HTTP server.

Matrix init runs in the background — the returned promise resolves as soon as Nest is built. Status flips via `onPluginStatusChange`.

```ts theme={null}
import { createOracleApp } from '@ixo/oracle-runtime';
```

## CreateOracleAppOptions

```ts theme={null}
export interface CreateOracleAppOptions {
  config: OracleConfig;
  features?: Partial<Record<BundledFeatureName | (string & {}), FeatureToggle>>;
  manifestOverrides?: Partial<Record<BundledFeatureName | (string & {}), PluginManifestOverride>>;
  plugins?: OraclePlugin[];
  nestModules?: Array<Type | DynamicModule>;
  authExcludedRoutes?: AuthExcludedRoute[];
  bundledPlugins?: OraclePlugin[];
  env?: NodeJS.ProcessEnv;
  skipMatrixInit?: boolean;
  skipGracefulShutdown?: boolean;
  logger?: Logger;
  hooks?: MainAgentHooks;
}
```

<AccordionGroup>
  <Accordion title="config" icon="id-card">
    * **Type:** `OracleConfig`
    * **Required:** yes

    Inline oracle config — `name`, optional `org`, optional `description`, optional `prompt`. The runtime fills in `entityDid` from the `ORACLE_ENTITY_DID` env var, so don't put it here.

    ```ts theme={null}
    config: {
      name: 'My Oracle',
      org: 'My Org',
      description: 'What this oracle is for',
      prompt: {
        opening: '...',
        communicationStyle: '...',
        capabilities: '...',
        customInstructions: '...',
      },
    }
    ```

    **prompt sub-fields** — all four are optional. Omit any you don't need.

    **`opening`**

    Used **verbatim** as the identity preamble — the very first section of the assembled system prompt. Write it as a complete paragraph describing who the oracle is and what it does.

    If `opening` is absent, the runtime generates a fallback from the other top-level fields:

    * `name` + `org` + `description` present → `"You are {name}, an AI agent operated by {org}. {description}."`
    * `org` missing → `"You are {name}. {description}."`
    * Both `org` and `description` missing → `"You are {name}."`

    Providing `opening` gives you full control over tone, persona, and framing — use it whenever the generated fallback is too generic.

    **`communicationStyle`**

    Injected inside the hardcoded **"## Operating principles"** section of the prompt. If set, it appears as a paragraph within that section immediately after the 7 standard operating-principle bullets. If empty or absent, the field is omitted entirely — the operating-principles section still appears, just without the custom style block.

    Use this to steer the agent's tone: formal, concise, empathetic, domain-specific jargon preferences, and so on.

    **`capabilities`**

    Rendered **above** the Tier-1 plugin capability block (the auto-generated list of `visibility='always'` plugins). No header is added by the runtime around this text — write your own heading if you want one. If empty or absent, the field is omitted entirely.

    Use this to describe high-level skills the oracle has that aren't obvious from the plugin manifest alone — things like domain expertise, supported workflows, or what the agent should proactively offer.

    **`customInstructions`**

    Free-form standing guidance injected **verbatim** into a dedicated `## Custom Instructions` section of the system prompt. Use it for house style, domain rules, compliance guardrails, or any directive that doesn't fit `communicationStyle` (tone) or `capabilities` (the elevator pitch). The runtime renders the section only when there is content.

    Operating guides contributed by on-demand capabilities the agent loads mid-thread (e.g. the Flow Builder guide, which appears once `flows` is in `loadedPlugins`) are appended **after** your text in the same section — so they cost no tokens on turns where the capability is never loaded.

    ```ts theme={null}
    prompt: {
      customInstructions: `Always cite the registry document ID when quoting issuance data.
    Never promise a delivery date you can't verify against the indexer.
    Decline any request to move funds — you advise, you do not transact.`,
    }
    ```

    **`entityDid`**

    Do **not** set this in code. The runtime reads it from the `ORACLE_ENTITY_DID` environment variable and populates it automatically during boot. Setting it inline has no effect.

    **Full example**

    ```ts theme={null}
    config: {
      name: 'Aria',
      org: 'Acme Climate',
      description: 'A carbon-project advisory oracle for Acme Climate portfolio managers.',
      prompt: {
        opening: `You are Aria, the carbon-project advisory oracle operated by Acme Climate.
    You help portfolio managers track project status, assess methodology compliance,
    and draft stakeholder communications. You have deep knowledge of Verra VCS,
    Gold Standard, and REDD+ frameworks.`,
        communicationStyle: `Be precise and data-driven. Lead with numbers and deadlines.
    Use plain English — avoid jargon unless the user demonstrates familiarity.
    When something is uncertain, say so explicitly rather than hedging with filler phrases.`,
        capabilities: `## What Aria can do
    - Retrieve live project status and registry issuance data via the IXO domain indexer.
    - Draft verification reports, CORSIAs letters, and board summaries.
    - Search and cite methodology documents from the oracle knowledge base.
    - Schedule follow-up reminders and track open action items across conversations.`,
        customInstructions: `Always cite the registry document ID when quoting issuance data.
    Treat any methodology version older than 18 months as "needs review" and flag it.
    You advise on carbon projects — you never authorise transactions or move funds.`,
      },
    }
    ```
  </Accordion>

  <Accordion title="features" icon="toggle-on">
    * **Type:** `Partial<Record<BundledFeatureName | (string & {}), FeatureToggle>>` where `FeatureToggle = boolean | 'auto'`
    * **Required:** no

    Override the default opt-in state per plugin. Keys are plugin **names** — the kebab-case `name` field of each plugin (e.g. `'domain-indexer'`, `'user-preferences'`), not a camelCase alias. Values: `true` (force on), `false` (force off), `'auto'` (run `autoDetect(env)`). Omitted keys are treated as `'auto'`.

    ```ts theme={null}
    features: {
      slack: false,
      composio: true,
      'domain-indexer': 'auto',
    }
    ```
  </Accordion>

  <Accordion title="manifestOverrides" icon="sliders">
    * **Type:** `Partial<Record<BundledFeatureName | (string & {}), PluginManifestOverride>>` where `PluginManifestOverride = Partial<PluginManifest>`
    * **Required:** no
    * **Added in:** `@ixo/oracle-runtime` 1.2.0

    Per-plugin overrides for fields on a loaded plugin's [manifest](/build-an-oracle/reference/manifest-schema). Use this to retune a bundled plugin's discovery — most commonly its [`visibility`](/build-an-oracle/understand/visibility-tiers) — **without forking the plugin source**. Typical uses: flip a noisy `always` plugin to `on-demand`, hide one behind `silent`, or relabel its `summary` / `tags` / `whenToUse`.

    Keys are plugin **names** (the same kebab-case identifiers used by [`features`](#features), e.g. `'domain-indexer'`). Values are a `Partial<PluginManifest>` — set only the fields you want to change.

    ```ts theme={null}
    import { createOracleApp } from '@ixo/oracle-runtime';

    const app = await createOracleApp({
      config,
      manifestOverrides: {
        // Drop a noisy bundled plugin out of the Tier-1 prompt.
        'domain-indexer': { visibility: 'on-demand' },
        // Hide a transport plugin entirely; its tools still bind.
        portal: { visibility: 'silent' },
        // Sharpen the trigger phrases the agent sees for memory.
        memory: {
          summary: 'Long-term user memory store.',
          whenToUse: ['When the user refers to a past conversation or stored preference.'],
        },
      },
    });
    ```

    **Merge semantics**

    Each override is **shallow-merged** onto the plugin's own manifest before validation and registration:

    * Keys you set win; keys you omit keep the plugin default.
    * `undefined` values are ignored — a sparse override never blanks out a field.
    * Arrays and nested objects are replaced wholesale (not deep-merged). For example, supplying `whenToUse: [...]` **replaces** the plugin's full list rather than appending to it.

    The merged manifest is what the runtime validates and what every downstream reader sees — the Tier-1 prompt block, the `list_capabilities` / `load_capability` meta-tools, and the agent's visibility index — so an override is the single source of truth from boot onwards.

    **Validation**

    Because the merged manifest is run through the same `validateManifest` rules as an authored one (see [Manifest schema](/build-an-oracle/reference/manifest-schema)), an override is held to the same constraints. For example, an override that empties `whenToUse` on a non-silent plugin fails boot with a clear `Plugin manifest validation failed` error.

    **Unknown keys**

    Override keys that don't match a loaded plugin (e.g. the plugin is excluded by `features` or simply misspelt) are **logged and ignored** — they do not fail boot:

    ```text theme={null}
    [boot] manifestOverrides references 'composio', which is not a loaded plugin — ignored (event: boot.plugin.manifest_override_unknown)
    ```

    Watch for that event when a retune doesn't appear to take effect.

    <Note>
      The same option is available on [`createTestRuntime`](/build-an-oracle/develop/test-your-oracle) with identical shape and semantics, so a test can exercise a plugin under a retuned visibility (e.g. forcing `silent`) without a bespoke fixture.
    </Note>
  </Accordion>

  <Accordion title="plugins" icon="puzzle-piece">
    * **Type:** `OraclePlugin[]`
    * **Required:** no

    Your plugin instances. Plus any bundled plugins you want to instantiate with constructor args (`new EditorPlugin({ matrixClient })`, `new CreditsPlugin({ redis, network })`). The plugin loader dedupes by `name`, so an explicit instance overrides the bundled default of the same name.

    ```ts theme={null}
    import { EditorPlugin, FlowsPlugin } from '@ixo/oracle-runtime';
    import { WeatherPlugin } from './plugins/weather/index.js';

    plugins: [
      new EditorPlugin({ matrixClient }), // overrides the bundled editor
      new FlowsPlugin({ matrixClient }),  // opt-in, not bundled
      new WeatherPlugin(),                // your own plugin
    ]
    ```
  </Accordion>

  <Accordion title="nestModules" icon="server">
    * **Type:** `Array<Type | DynamicModule>`
    * **Required:** no

    Your own NestJS modules. Spread into `RuntimeAppModule.imports`. They get full DI access to the Tier-0 services (Sessions, Messages, Secrets, UCAN, …) and can declare controllers, providers, `OnModuleInit`, `OnModuleDestroy`.

    ```ts theme={null}
    @Module({ controllers: [VersionController] })
    class VersionModule {}

    nestModules: [VersionModule]
    ```

    Routes these modules expose still pass through `AuthHeaderMiddleware` unless you list them in `authExcludedRoutes`.
  </Accordion>

  <Accordion title="authExcludedRoutes" icon="globe">
    * **Type:** `AuthExcludedRoute[]`
    * **Required:** no

    Host-declared routes that must not pass through `AuthHeaderMiddleware`. Use for routes contributed by `nestModules` (webhooks, OAuth callbacks, public probes). Symmetric with each plugin's `getAuthExcludedRoutes()` hook — both lists merge onto the runtime's built-in exclusions (`/health`, `/docs`).

    ```ts theme={null}
    import { RequestMethod } from '@nestjs/common';

    authExcludedRoutes: [
      { path: 'version', method: RequestMethod.GET },
    ]
    ```
  </Accordion>

  <Accordion title="bundledPlugins" icon="boxes-stacked">
    * **Type:** `OraclePlugin[]`
    * **Required:** no

    Override the bundled plugin set. Provided primarily for tests so the harness can spin up `createOracleApp` without dragging in the full bundled catalog. Production callers should leave this unset.

    ```ts theme={null}
    // test only — boot with a single plugin instead of the full catalog
    bundledPlugins: [new MemoryPlugin()]
    ```
  </Accordion>

  <Accordion title="env" icon="file-lines">
    * **Type:** `NodeJS.ProcessEnv`
    * **Required:** no

    Override `process.env`. Tests use this to inject a clean, controlled env so the boot doesn't depend on the machine's environment. Production code should leave it unset.

    ```ts theme={null}
    // test only — inject a controlled env instead of process.env
    env: { ...baseTestEnv, LLM_PROVIDER: 'nebius' }
    ```
  </Accordion>

  <Accordion title="skipMatrixInit" icon="circle-pause">
    * **Type:** `boolean`
    * **Required:** no

    Skip starting the Matrix background init. Tests set this so the factory resolves without touching real Matrix. Do not use in production.

    ```ts theme={null}
    // test only
    skipMatrixInit: true
    ```
  </Accordion>

  <Accordion title="skipGracefulShutdown" icon="circle-pause">
    * **Type:** `boolean`
    * **Required:** no

    Skip registering the SIGTERM/SIGINT shutdown handler. Tests set this so the harness does not leak process-level listeners. Do not use in production.

    ```ts theme={null}
    // test only
    skipGracefulShutdown: true
    ```
  </Accordion>

  <Accordion title="logger" icon="scroll">
    * **Type:** `Logger`
    * **Required:** no

    Override the bootstrap logger — any object implementing the `Logger` interface (`log`, `error`, `warn`, optional `debug`/`verbose`/`child`). Falls back to NestJS `Logger`.

    ```ts theme={null}
    import { Logger } from '@nestjs/common';

    logger: new Logger('MyOracle')
    ```
  </Accordion>

  <Accordion title="hooks" icon="link">
    * **Type:** `MainAgentHooks`
    * **Required:** no

    Overrides for the agent build — the chat model (`resolveModel`), the per-user checkpointer (`checkpointerForUser`), and the prompt/middleware toggles. Merged on top of the runtime's defaults, and **host hooks win**.

    This is where you **change the AI model**. See [MainAgentHooks](#mainagenthooks) below for all ten fields, the `resolveModel` example, and the `'main'`-only nuance.

    ```ts theme={null}
    import { getProviderChatModel } from '@ixo/oracle-runtime';

    hooks: {
      resolveModel: (role, params) =>
        getProviderChatModel(role, {
          ...params,
          ...(role === 'main' && { model: 'google/gemini-3.1-flash-lite' }),
        }),
    }
    ```
  </Accordion>
</AccordionGroup>

## MainAgentHooks

`hooks` lets you override how the runtime builds the main agent on every request, without forking the runtime. The runtime merges your hooks over its own defaults — **your hooks win** (`{ ...defaultHooks, ...opts.hooks }`). The only default the runtime sets is `checkpointerForUser` (a per-user SQLite store); everything else is unset unless you provide it.

```ts theme={null}
import type { MainAgentHooks } from '@ixo/oracle-runtime';
```

### Change the AI model

This is the answer to "how do I change the model?". `resolveModel` is the model resolver. Return any LangChain `BaseChatModel` for the given role:

```ts theme={null}
import { createOracleApp, getProviderChatModel } from '@ixo/oracle-runtime';

const app = await createOracleApp({
  config,
  plugins,
  hooks: {
    // role is 'main' | 'subagent' | 'utility' | (string & {}).
    // Spreading `params` preserves the provider's fallback models + latency sort.
    resolveModel: (role, params) =>
      getProviderChatModel(role, {
        ...params,
        ...(role === 'main' && { model: 'google/gemini-3.1-flash-lite' }),
      }),
  },
});
```

<Warning>
  **The runtime only ever calls `resolveModel('main')`.** Sub-agent, utility, vision and guard models are resolved separately, straight through the provider config (`ambient.llm.get(role)` → `getProviderChatModel`). So a `resolveModel` hook changes **only the main agent's model** unless your own plugin code also routes its roles through the hook. Branch on `role === 'main'` (as above) so you don't accidentally rewrite a role you never see.
</Warning>

<Note>
  **Where model IDs come from.** Each role (`main`, `subagent`, `vision`, `guard`, …) maps to a hardcoded model ID per provider. The provider is chosen by env: `LLM_PROVIDER` = `openrouter` (default) or `nebius`, with the matching key (`OPEN_ROUTER_API_KEY` / `NEBIUS_API_KEY`). There is **no env var to change the main model ID** — overriding it is exactly what `resolveModel` is for. Building the model with `getProviderChatModel` (rather than `new ChatOpenAI(...)`) keeps the OpenRouter fallback-models list and latency sort the `'main'` role ships with.
</Note>

Prefer a different SDK model? Return your own LangChain model instead — but you lose the provider's fallback/latency wiring, so you own retries and outages:

```ts theme={null}
import { ChatOpenAI } from '@langchain/openai'; // add this dependency yourself

hooks: {
  resolveModel: () => new ChatOpenAI({ model: 'gpt-4o' }),
}
```

### All ten fields

| Field                     | Type                                                            | What it does                                                                                                                                                           |
| ------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolveModel`            | `(role: ModelRole, params?: ChatOpenAIFields) => BaseChatModel` | Resolves the chat model. Called once, with `'main'`. Default: `ambient.llm.get('main')`.                                                                               |
| `checkpointerForUser`     | `(userDid: string) => Promise<BaseCheckpointSaver>`             | Per-user conversation store. Default: per-user SQLite synced to Matrix — override to swap the backend.                                                                 |
| `getRoomTitle`            | `(roomId: string) => Promise<string \| undefined>`              | Page-title lookup. **Providing it adds the page-context middleware** to the stack; omitting it leaves that middleware out.                                             |
| `safetyModel`             | `BaseChatModel`                                                 | Cheap classifier model. **Providing it adds the safety-guardrail middleware**; omitting it leaves it out.                                                              |
| `validationSkipToolNames` | `string[]`                                                      | Tool names whose dangling `ToolMessage` outputs are stripped before the next model call — typically sub-agent tools whose output the caller summarises, not the model. |
| `operationalMode`         | `string`                                                        | Replaces the default operational-mode prompt block. (The editor plugin's per-page mode still overrides this when a page is open.)                                      |
| `editorSection`           | `string`                                                        | Editor prompt block — normally populated by the editor plugin; set it only if you compose the section yourself.                                                        |
| `composioContext`         | `string`                                                        | Composio guidance prompt block — normally populated by the composio plugin.                                                                                            |
| `userSecretsContext`      | `string`                                                        | Per-key secret bullet list rendered into the prompt (e.g. `- _USER_SECRET_FOO`).                                                                                       |
| `degradedServicesBlock`   | `string`                                                        | A "some services are degraded" notice appended to the system prompt body.                                                                                              |

`ModelRole` is `'main' | 'subagent' | 'utility' | (string & {})`. `ChatOpenAIFields` is `Record<string, unknown>` — the optional fields forwarded to the underlying chat model.

**Swap the checkpointer** — supply your own LangGraph saver per user:

```ts theme={null}
import type { BaseCheckpointSaver } from '@langchain/langgraph';

hooks: {
  checkpointerForUser: async (userDid: string): Promise<BaseCheckpointSaver> => {
    return myStore.saverFor(userDid);
  },
}
```

**Turn on the conditional middlewares** — both are added to the stack only when their hook is present:

```ts theme={null}
hooks: {
  // adds the page-context middleware
  getRoomTitle: async (roomId) => myRoomTitles.get(roomId),
  // adds the safety-guardrail middleware
  safetyModel: getProviderChatModel('guard'),
}
```

**Prompt blocks** — the string fields are appended to dedicated prompt sections. Most are populated by their owning plugin; set them only when you're driving the section yourself:

```ts theme={null}
hooks: {
  operationalMode: 'You are operating in read-only audit mode. Never call mutating tools.',
  validationSkipToolNames: ['call_research_agent'],
}
```

## OracleApp

The resolved `OracleApp` exposes:

```ts theme={null}
export interface OracleApp {
  getNestApp(): INestApplication;
  ambient: AmbientServices;
  plugins: { status(): PluginStatusReport };
  beforeListen(fn: (nestApp: INestApplication) => Promise<void> | void): void;
  onError(handler: (err: Error, source: string) => void): void;
  onPluginStatusChange(handler: (event: PluginStatusChangeEvent) => void): void;
  listen(port?: number): Promise<void>;
}
```

<AccordionGroup>
  <Accordion title="getNestApp()" icon="server">
    Returns the underlying `INestApplication`. Use it to add Express middleware, register global filters, or do anything else NestJS exposes. Mainly an escape hatch — `nestModules` and `beforeListen` cover most cases.
  </Accordion>

  <Accordion title="ambient" icon="layer-group">
    The production `AmbientServices` bag — used by `MessagesController` to build per-request `RuntimeContext`s before invoking `createMainAgent`. Forks normally don't touch this directly.
  </Accordion>

  <Accordion title="plugins.status()" icon="list-check">
    Returns a snapshot of loader/exclusion/soft-dep state.

    ```ts theme={null}
    {
      loaded: string[];
      excluded: Array<{ plugin: string; reason: string }>;
      softDepGaps: Array<{ plugin: string; missing: string }>;
    }
    ```
  </Accordion>

  <Accordion title="beforeListen(fn)" icon="circle-play">
    Register a callback that runs after Nest is built but before HTTP starts accepting. Multiple registrations run in order, awaiting each.
  </Accordion>

  <Accordion title="onError(handler)" icon="triangle-exclamation">
    Subscribe to background errors. The runtime currently surfaces Matrix init failures here (with `source: 'matrix-init'`). If no handler is registered, errors log to the bootstrap logger.
  </Accordion>

  <Accordion title="onPluginStatusChange(handler)" icon="bell">
    Subscribe to plugin lifecycle changes. The current emitter is the Matrix init flow:

    ```ts theme={null}
    { plugin: 'matrix', from: 'pending', to: 'loaded' | 'failed', reason?: string }
    ```

    Multiple handlers may register; each is invoked for every event.
  </Accordion>

  <Accordion title="listen(port?)" icon="play">
    Start the HTTP server. Honours `beforeListen` callbacks first. Port resolution: explicit `port` arg → `PORT` env (validated, defaults to **3000**). Throws if called twice.
  </Accordion>
</AccordionGroup>

## Behaviour

The function executes these steps in order:

1. Validates `config` (`name` required).
2. Resolves plugin set: bundled + your plugins, deduped by `name`, with `features` toggles and `autoDetect` predicates applied. Excluded plugins surface in `plugins.status().excluded` with their reason.
3. Topologically sorts by `dependsOn`. Cycles or missing hard deps fail boot.
4. Shallow-merges any `manifestOverrides` onto each loaded plugin's manifest, then validates the **merged** manifest. Hard violations fail boot. Override keys that don't match a loaded plugin are logged (`boot.plugin.manifest_override_unknown`) and ignored.
5. Composes env schema (base + all loaded plugins' `configSchema`) and validates `process.env`. Missing required vars fail boot with `[boot-error] Plugin '<name>' env validation failed for '<field>'`.
6. Cross-field check: the API key for the selected `LLM_PROVIDER` is present. (Schema-merge can't express this.)
7. Builds the internal `OracleIdentity` from `config` + validated env.
8. Populates the six registries (tools, sub-agents, middleware, manifests, configSchema, sharedState).
9. Collects `getNestModules` from every loaded plugin.
10. Builds the dynamic `RuntimeAppModule` and bootstraps NestJS (`NestFactory.create`). Installs a global `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })`, enables CORS with the framework's allowed headers (`authorization`, `x-auth-type`, `x-ucan-delegation`, …), and mounts Swagger UI at `/docs`.

<Note>
  The global `ValidationPipe` applies to **every** controller — including ones you add via `nestModules` or a plugin's `getNestModules`. Annotate request DTOs with `class-validator` decorators: unknown body fields are rejected (`forbidNonWhitelisted`), unannotated props are stripped (`whitelist`), and nested DTOs are instantiated (`transform`). A controller that silently 400s on an extra field is hitting this pipe.
</Note>

11. Builds `AmbientServices`.
12. Warms the boot caches (each registry runs its boot-time hooks once).
13. Wires the default checkpointer (`UserMatrixSqliteSyncService` + `SqliteSaver`), then merges host `hooks` over it.
14. Populates the `OracleRuntimeBundleHolder` so `MessagesController` can read it on each request.
15. Schedules background Matrix init (unless `skipMatrixInit: true`). Registers a graceful-shutdown handler (unless `skipGracefulShutdown: true`).
16. Returns the `OracleApp`.

Step 15 (Matrix init) runs asynchronously and does not block the returned promise. `listen()` blocks on `beforeListen` callbacks before starting HTTP.

## Throws

* `Error('createOracleApp: \'config\' is required.')` — `config` missing.
* `Error('createOracleApp: \'config.name\' is required.')` — `config.name` empty.
* Plugin resolution errors (cycle, missing hard dep, manifest invalid, env invalid). Each error is reported via `logger.error` with `[boot-error] …` prefix and a remediation hint.
* `Error('createOracleApp: ORACLE_ENTITY_DID env is required and was empty after validation.')` — when the validated env's `ORACLE_ENTITY_DID` is empty.
* `Error('OracleApp.listen called twice.')` — when `listen()` is called more than once.

## Related guides

* [Create your oracle](/build-an-oracle/develop/create-oracle-app) — hands-on walkthrough.
* [Using bundled plugins](/build-an-oracle/develop/enable-bundled-plugins) — the `features` field in detail.
* [Plugin API reference](/build-an-oracle/reference/plugin-api) — what plugin instances must implement.
