diff --git a/build-an-oracle/develop/enable-bundled-plugins.mdx b/build-an-oracle/develop/enable-bundled-plugins.mdx
index 1f20db7..11699dc 100644
--- a/build-an-oracle/develop/enable-bundled-plugins.mdx
+++ b/build-an-oracle/develop/enable-bundled-plugins.mdx
@@ -1,6 +1,6 @@
---
title: "Enable bundled plugins"
-description: "Toggle the 16 bundled QiForge plugins via the features map — opt out, force on, or let auto-detect handle it — and retune their manifests."
+description: "Toggle the 14 bundled QiForge plugins via the features map — opt out, force on, or let auto-detect handle it — and retune their manifests."
icon: "boxes-stacked"
---
@@ -16,7 +16,7 @@ const app = await createOracleApp({
config,
features: {
composio: true, // force on (set COMPOSIO_API_KEY)
- slack: false, // force off
+ 'matrix-group-chats': false, // force off
firecrawl: 'auto', // same as omitting — runs autoDetect
},
plugins: [], // your own plugins go here
@@ -31,9 +31,9 @@ That's the whole surface. The runtime pre-loads every bundled plugin instance fr
- [`BUNDLED_PLUGINS`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/plugins/index.ts) is a fixed 16-plugin tuple — memory, portal, firecrawl, domain-indexer, composio, sandbox, skills, editor, agui, slack, tasks, credits, calls, user-preferences, matrix-group-chats, vfs.
+ [`BUNDLED_PLUGINS`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/plugins/index.ts) is a fixed 14-plugin tuple — memory, portal, firecrawl, domain-indexer, composio, sandbox, skills, editor, agui, tasks, user-preferences, matrix-group-chats, vfs, oracle-payments.
- You do not import or instantiate the plugins you want at defaults — they are already there.
+ You do not import or instantiate the plugins you want at defaults — they are already there. `credits`, `slack`, `calls`, and `flows` are **not** in the tuple: `features` never reaches them, so you construct each one and pass it in [`plugins`](#plugins-that-are-not-bundled).
@@ -70,19 +70,17 @@ Each plugin's `autoDetect` predicate decides whether to opt in when you leave it
| `memory` | `MEMORY_MCP_URL` set | Visibility `always` |
| `portal` | always on | Visibility `on-demand` |
| `firecrawl` | `FIRECRAWL_MCP_URL` set | Visibility `on-demand` |
-| `domain-indexer` | always on | Visibility `always` |
+| `domain-indexer` | always on | Visibility `on-demand` |
| `composio` | `COMPOSIO_API_KEY` set | Visibility `on-demand` |
| `sandbox` | `SANDBOX_MCP_URL` set | Visibility `always` |
| `skills` | always on | Visibility `always`; depends on `sandbox` |
-| `editor` | always on | Needs `matrixClient` — instantiate explicitly |
+| `editor` | always on | Visibility `on-demand` — auto-loaded whenever the request carries an active editor session (`editorRoomId`/`spaceId`), no `load_capability` step needed. Needs `matrixClient` — instantiate explicitly |
| `agui` | always on | Visibility `on-demand` |
-| `slack` | `SLACK_BOT_OAUTH_TOKEN` set | Visibility `silent` (transport) |
| `tasks` | `REDIS_URL` set | Visibility `on-demand`; BullMQ-backed async tasks (needs `REDIS_URL`) |
-| `credits` | always on | Visibility `silent`; pass `redis` for production |
-| `calls` | always on | Visibility `silent`; placeholder stub (no tools yet) |
| `user-preferences` | always on | Visibility `always` |
| `matrix-group-chats` | always on | Visibility `on-demand`; gating middleware + tools fire only in Matrix group rooms (`memberCount > 2`) |
-| `vfs` | always on | Visibility `always`; worker URLs derived from `NETWORK`. Contributes tools only when the oracle has a UCAN signing key and the user granted filesystem access |
+| `vfs` | always on | Visibility `on-demand`, except `vfs_search` + `vfs_read` which are per-tool `always` so file recall needs no load step. Worker URLs derived from `NETWORK`. Contributes tools only when the oracle has a UCAN signing key and the user granted filesystem access |
+| `oracle-payments` | unless `ORACLE_PAYMENTS_DISABLED=true` | Visibility `always`; Matrix-only commerce lane. A no-op until the oracle publishes an Agent Card, and paid work needs `EVAL_ENGINE_URL` |
Full per-plugin env vars: [plugin catalog](/build-an-oracle/reference/bundled-plugins/overview) and [environment variables reference](/build-an-oracle/reference/environment-variables).
@@ -136,9 +134,11 @@ Full per-plugin env vars: [plugin catalog](/build-an-oracle/reference/bundled-pl
-## Plugins that need constructor args
+## Plugins that are not bundled
-Two bundled plugins take a live runtime object you provide — instantiate explicitly and pass them via `plugins`:
+`credits`, `slack`, `calls`, and `flows` ship inside `@ixo/oracle-runtime` but are absent from `BUNDLED_PLUGINS`, so `features` never reaches them. Load one by constructing it and passing it in `plugins` — anything in that array loads unconditionally.
+
+The same array is how you replace a bundled plugin with a custom-constructed instance (the loader dedupes by `name`, and yours wins). `editor` is the usual case: the bundled instance builds its own Matrix client from the `MATRIX_*` admin env vars, so pass `matrixClient` only to reuse a client your app already keeps synced.
```ts
import { createOracleApp, EditorPlugin, CreditsPlugin } from '@ixo/oracle-runtime';
@@ -156,8 +156,8 @@ const redis = process.env.REDIS_URL ? new Redis(process.env.REDIS_URL) : null;
const app = await createOracleApp({
config,
plugins: [
- new EditorPlugin({ matrixClient }),
- ...(redis ? [new CreditsPlugin({ redis, network: 'devnet' })] : []),
+ new EditorPlugin({ matrixClient }), // overrides the bundled instance
+ ...(redis ? [new CreditsPlugin({ redis, network: 'devnet' })] : []), // not bundled — this is the only way it loads
],
});
```
@@ -165,7 +165,7 @@ const app = await createOracleApp({
Live example: [apps/qiforge-example/src/main.ts](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/apps/qiforge-example/src/main.ts).
-The bundled `editorPlugin` and `creditsPlugin` instances boot in stub form (for testing). For production behaviour, instantiate them yourself and pass the live objects in.
+`credits` without a Redis client loads in pass-through mode and skips the settlement cron. Since it is also where LLM token metering lives, an oracle that never wires it has no metering at all.
## Retune a bundled plugin's manifest
@@ -176,8 +176,9 @@ The bundled `editorPlugin` and `creditsPlugin` instances boot in stub form (for
const app = await createOracleApp({
config,
manifestOverrides: {
- // Take a noisy `always` bundled plugin out of the Tier-1 prompt.
- 'domain-indexer': { visibility: 'on-demand' },
+ // Promote an `on-demand` bundled plugin into the Tier-1 prompt so the
+ // agent sees it without a `load_capability` step.
+ 'domain-indexer': { visibility: 'always' },
// Hide a transport plugin entirely; its tools still bind.
portal: { visibility: 'silent' },
},
@@ -196,7 +197,7 @@ Override keys that don't match a loaded plugin are logged (`boot.plugin.manifest
// loaded: ['memory', 'domain-indexer', 'editor', 'user-preferences', 'weather'],
// excluded: [
// { plugin: 'composio', reason: 'auto-detect precondition not met (COMPOSIO_API_KEY)' },
- // { plugin: 'slack', reason: 'feature flag set to false' },
+ // { plugin: 'tasks', reason: 'feature flag set to false' },
// ],
// softDepGaps: [],
// }
diff --git a/build-an-oracle/develop/plugin-recipes/add-config-and-env.mdx b/build-an-oracle/develop/plugin-recipes/add-config-and-env.mdx
index 61eded3..b5c43b5 100644
--- a/build-an-oracle/develop/plugin-recipes/add-config-and-env.mdx
+++ b/build-an-oracle/develop/plugin-recipes/add-config-and-env.mdx
@@ -70,7 +70,7 @@ The runtime merges every loaded plugin's `configSchema` onto a base schema, vali
config,
features: {
weather: true, // force on, skip autoDetect
- slack: false, // force off
+ tasks: false, // force off
composio: 'auto', // explicit auto (the default)
},
});
diff --git a/build-an-oracle/develop/plugin-recipes/declare-dependencies.mdx b/build-an-oracle/develop/plugin-recipes/declare-dependencies.mdx
index b95815a..ddc14d8 100644
--- a/build-an-oracle/develop/plugin-recipes/declare-dependencies.mdx
+++ b/build-an-oracle/develop/plugin-recipes/declare-dependencies.mdx
@@ -70,7 +70,7 @@ A plugin references other plugins by `name`. `dependsOn` is hard (boot fails on
features: {
sandbox: true, // force on
skills: 'auto', // load when its autoDetect (and deps) pass
- slack: false, // force off
+ firecrawl: false, // force off
},
});
```
diff --git a/build-an-oracle/for-ai-agents.mdx b/build-an-oracle/for-ai-agents.mdx
index fba9837..45e8f20 100644
--- a/build-an-oracle/for-ai-agents.mdx
+++ b/build-an-oracle/for-ai-agents.mdx
@@ -14,7 +14,7 @@ If you are an AI agent: read top-to-bottom once. Every signature you need is inl
## 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//`. The runtime handles HTTP, auth, the agent graph, the checkpointer, Matrix, and bundles 16 plugins by default. You ship glue code, not infrastructure.
+A QiForge oracle is **one `main.ts`** that calls `createOracleApp({ config, plugins, … })` plus **one folder per plugin** under `src/plugins//`. The runtime handles HTTP, auth, the agent graph, the checkpointer, Matrix, and bundles 14 plugins by default. You ship glue code, not infrastructure.
## main.ts shape
@@ -180,7 +180,7 @@ interface PluginSubAgent {
`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)
+## Bundled plugins (14)
From [`packages/oracle-runtime/src/plugins/index.ts`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/plugins/index.ts):
@@ -195,15 +195,16 @@ From [`packages/oracle-runtime/src/plugins/index.ts`](https://github.com/ixoworl
| `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_*`) |
+| `vfs` | always | on | — (worker URLs from `NETWORK`) |
+| `oracle-payments` | always | on unless `ORACLE_PAYMENTS_DISABLED=true` | — (`EVAL_ENGINE_URL` for the paid lane) |
Toggle via `features` in `createOracleApp`: `true` forces on, `false` forces off, `'auto'` runs `autoDetect`.
+**Not in `BUNDLED_PLUGINS`** — these ship in the package but `features` never reaches them; construct each one and pass it in `plugins`: `credits` (`new CreditsPlugin({ redis, network })` — also where LLM token metering lives), `slack` (`new SlackPlugin()`), `calls` (stub), `flows` (`new FlowsPlugin({ matrixClient })`).
+
Per-plugin reference pages: [`/build-an-oracle/reference/bundled-plugins/overview`](/build-an-oracle/reference/bundled-plugins/overview).
## Core (Tier-0) env vars
@@ -245,7 +246,7 @@ These are the exact names the runtime validates — set them character-for-chara
| `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`.
+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`, `REDIS_URL`, `EVAL_ENGINE_URL`, `AGENT_CARD_PATH`, …) merge in via each plugin's `configSchema`.
## Copy-pasteable plugin template
diff --git a/build-an-oracle/index.mdx b/build-an-oracle/index.mdx
index 22a5a48..5340fc0 100644
--- a/build-an-oracle/index.mdx
+++ b/build-an-oracle/index.mdx
@@ -38,8 +38,8 @@ That's a working oracle. One call to [`createOracleApp`](/build-an-oracle/develo
Per-request agent build, dynamic tool loading via meta-tools, four always-on middleware (capability gating, tool validation, repetition guard, retry).
-
- `memory`, `skills`, `sandbox`, `portal`, `firecrawl`, `composio`, `editor`, `agui`, `slack`, `credits`, `user-preferences`, and more — toggle them via the `features` map.
+
+ `memory`, `skills`, `sandbox`, `portal`, `firecrawl`, `composio`, `editor`, `agui`, `vfs`, `oracle-payments`, `user-preferences`, and more — toggle them via the `features` map.
9 typed hooks let your plugin contribute tools, sub-agents, middleware, HTTP routes, shared state, and env vars. Boot-time and per-request flavours.
diff --git a/build-an-oracle/quickstart.mdx b/build-an-oracle/quickstart.mdx
index d943a88..36e3d6c 100644
--- a/build-an-oracle/quickstart.mdx
+++ b/build-an-oracle/quickstart.mdx
@@ -14,7 +14,7 @@ Oracle: (calls list_capabilities → load_capability(weather) → get_current_we
A live oracle on `localhost:3000` with:
-- The 16 bundled plugins resolved at boot.
+- The 14 bundled plugins resolved at boot.
- A custom Weather plugin loading on demand.
- A `GET /weather/now?city=X` public HTTP route.
- A streaming chat endpoint over SSE.
@@ -202,7 +202,7 @@ Watch the server logs at the same time — you'll see the weather middleware pri
Every option you can pass — config, plugins, features, hooks.
- The 16 plugins shipped with the runtime.
+ The 14 plugins that load by default, plus the four you wire in yourself.
Dense one-pager with every signature inlined.
diff --git a/build-an-oracle/reference/bundled-plugins/calls.mdx b/build-an-oracle/reference/bundled-plugins/calls.mdx
index 7428ed4..92bc6b0 100644
--- a/build-an-oracle/reference/bundled-plugins/calls.mdx
+++ b/build-an-oracle/reference/bundled-plugins/calls.mdx
@@ -12,24 +12,15 @@ icon: "phone"
| Version | `0.0.0` |
| Visibility | `silent` |
| Stability | `experimental` |
-| Default state | Loaded by default (stub — contributes nothing) |
+| Default state | Opt-in — not in `BUNDLED_PLUGINS` (stub — contributes nothing) |
-`calls` is a **placeholder stub**, not a shipped feature. It is the only bundled entry created via `stub('calls', 'Calls')` in `plugins/index.ts` — version `0.0.0`, no tools, no sub-agents, no middleware, no Nest modules. Don't rely on it.
+`calls` is a **placeholder stub**, not a shipped feature. It is created via `stub('calls', 'Calls')` in `plugins/index.ts` — version `0.0.0`, no tools, no sub-agents, no middleware, no Nest modules. Don't rely on it.
## Status: deferred
-`calls` exists in `BUNDLED_PLUGINS` only so the `features` toggle key resolves. Because it declares no `autoDetect`, the loader loads it by default — but it contributes nothing, so loading it has no effect. The legacy `apps/app` codebase had a `@Controller('calls')` for LiveKit integration; the `getNestModules` API hook would technically unblock a real implementation. Deferred for now.
-
-It contributes nothing, so there is nothing to enable. You can keep it from loading at all with:
-
-```ts
-const app = await createOracleApp({
- config,
- features: { calls: false }, // skip the stub entirely
-});
-```
+`calls` is exported from the package but is not in `BUNDLED_PLUGINS`, so nothing loads it. It contributes nothing, so there is nothing to enable either. The legacy `apps/app` codebase had a `@Controller('calls')` for LiveKit integration; the `getNestModules` API hook would technically unblock a real implementation. Deferred for now.
See the framework's [follow-ups](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/docs/spec-and-roadmap/follow-ups.md) for the rebuild plan.
diff --git a/build-an-oracle/reference/bundled-plugins/credits.mdx b/build-an-oracle/reference/bundled-plugins/credits.mdx
index 7375830..d58a1aa 100644
--- a/build-an-oracle/reference/bundled-plugins/credits.mdx
+++ b/build-an-oracle/reference/bundled-plugins/credits.mdx
@@ -12,7 +12,7 @@ icon: "coins"
| Visibility | `silent` |
| Stability | `stable` |
| Category | `core` |
-| Default state | On unless `DISABLE_CREDITS=true` |
+| Default state | Opt-in — not in `BUNDLED_PLUGINS`. Construct it and pass it in `plugins`. |
| Depends on | — |
## Summary
@@ -65,14 +65,11 @@ Without a Redis client the middleware loads in pass-through mode and the cron mo
## Opt out / Opt in
-```ts
-const app = await createOracleApp({
- config,
- features: { credits: false }, // never load
-});
+`credits` is not in `BUNDLED_PLUGINS`, so nothing loads it unless you pass an instance in `plugins` (see [Production constructor](#production-constructor) above). Once wired, `DISABLE_CREDITS=true` still turns it off via `autoDetect` without touching the code.
-// Or via env: DISABLE_CREDITS=true
-```
+
+LLM token metering **is** this plugin. An oracle that never wires it has no metering at all — worth knowing before you leave it out, especially alongside [`oracle-payments`](/build-an-oracle/reference/bundled-plugins/oracle-payments), where "free support" means "no work claim", not "free LLM".
+
## Where to read next
diff --git a/build-an-oracle/reference/bundled-plugins/oracle-payments.mdx b/build-an-oracle/reference/bundled-plugins/oracle-payments.mdx
new file mode 100644
index 0000000..a2a7c81
--- /dev/null
+++ b/build-an-oracle/reference/bundled-plugins/oracle-payments.mdx
@@ -0,0 +1,231 @@
+---
+title: "oracle-payments"
+description: "Sell services from the chat — publish an Agent Card, let users contract the oracle on-chain, and get paid for delivered work."
+icon: "handshake"
+---
+
+**Source:** [`packages/oracle-runtime/src/plugins/oracle-payments/`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/plugins/oracle-payments/)
+
+| Attribute | Value |
+| --- | --- |
+| Feature key | `oracle-payments` |
+| Visibility | `always` |
+| Stability | `beta` |
+| Category | `ui` |
+| Default state | On unless `ORACLE_PAYMENTS_DISABLED=true` |
+| Depends on | — (reads the [`sandbox`](/build-an-oracle/reference/bundled-plugins/sandbox) bridge when delivering a file) |
+| Surface | Matrix rooms. HTTP turns get the support tools only — routing, engagements, and cards are Matrix-side. |
+
+## Summary
+
+Turns the user's Matrix DM room with the oracle into a place they can **discover, contract, and pay for work**. The oracle plays two roles in the same room:
+
+- **Support** (free) — explains what the oracle sells, what it costs, and whether the user is contracted.
+- **Work** (paid) — performs one contracted service and hands over the result.
+
+A cheap classifier routes every incoming Matrix message to one of the two. Once a paid job starts in a thread, routing is **sticky** for that thread until the job ends. Work only starts if the user has contracted the oracle on-chain; otherwise the oracle posts an interactive contract card into the chat and stays in support mode.
+
+Nothing here is a subscription. The user authorizes a **quota of jobs** at a **per-job price ceiling**, and each job is settled individually against a claim that an independent evaluator judges.
+
+## The moving parts
+
+### Agent Card
+
+The list of services the oracle sells. It is a signed document anchored on the oracle's entity as the `#acard` LinkedResource, and each service declares:
+
+| Field | Meaning |
+| --- | --- |
+| `id` | Stable service id — what `show_contract` takes and what the claim records. |
+| `name`, `description` | What the user sees on the cards. |
+| `price` | `{ amount, currency }`, priced in USDC. |
+| `deliverables` | What the user actually receives. |
+| `doneMeans` | 1–10 plain sentences defining "done right". These become the evaluation criteria the delivered work is judged against — write them as if a stranger will grade the work using only that list. |
+| `tags`, `examples` | Optional routing hints for the classifier. |
+
+Publish one with `qiforge-cli agent-card`. That command anchors the card on the entity, writes a local copy into the project, and sets [`AGENT_CARD_PATH`](#environment-variables) in `.env` — worth keeping, because the local file self-describes the plugin manifest so the model knows its own services and prices **without a tool call**.
+
+
+No card anywhere means no paid lane: the classifier stays off and the oracle behaves exactly as it did before. Support chat still works.
+
+
+### Contract
+
+The user grants an on-chain `SubmitClaimAuthorization` over their claim collection, scoped to the services they picked, carrying:
+
+- a **quota** — how many jobs the oracle may bill for;
+- a **max amount** — the ceiling on a single job;
+- an **intent duration** — how long a reservation may sit before the chain releases it on its own. It defaults to **1 hour** (`DEFAULT_INTENT_DURATION_NS` in `@ixo/oracles-chain-client`) and is set per grant at contracting time. The oracle reads this value back — it cannot set or shorten it.
+
+### Engagement
+
+One paid job, keyed to a Matrix thread: thread = session = engagement = claim. **Only one engagement can be active per user at a time** — the chain permits one active intent per (agent, claim collection), so a second concurrent job could never reserve its payment. A work request while another job is running is refused at the gate and the oracle says which job is running and offers to wait or cancel.
+
+### Reservation (escrow)
+
+When a job starts, the oracle reserves the service price on-chain (`MsgClaimIntent`). This is unconditional — there is no unreserved path and no flag. The reservation is released when the job is delivered, cancelled, or the intent duration lapses.
+
+### Delivery
+
+`deliver_work` does two things at once: it puts the file in the room for the user, and it submits a **work claim** recording the service, the request, the work summary, the honest result status, the deliverable, and any proofs. An independent evaluation engine judges the claim against the service's `doneMeans`. Approval releases the escrow to the oracle; rejection returns it to the user.
+
+Honesty is structural, not a matter of prompt discipline: the claim's `request` and `workSummary` are extracted from the thread by a separate model the work agent does not control, and the evaluator inspects the real deliverable.
+
+### Cancellation
+
+`cancel_work` submits a release claim — `resultStatus: 'unable'`, no deliverables, the user's reason in `proofs`. Submitting it frees the reservation immediately, so the user can start a new job right away; the evaluator then rejects it (deterministically, because there are no deliverables), which returns the escrow. **A cancellation still costs one quota slot** — the chain decrements quota on submit regardless of outcome.
+
+## How a user contracts
+
+
+
+ The classifier routes to support. The model calls `list_services`, which posts a service catalog card into the room.
+
+
+ The model calls `show_contract` with that `serviceId`. The card carries the price, the deliverables, and the `doneMeans` bullets, plus a **Contract this agent** action.
+
+
+ The card opens the Portal's agent-contract flow with the service preselected. Approving mints the `SubmitClaimAuthorization` grant and registers the contract with the evaluation engine.
+
+
+ The Portal posts an `ixo.oracle.contracted` event into the room. The oracle treats it purely as a cache-buster — it re-queries the engine rather than trusting the event — so "contract in the modal, then say go" works without waiting out a cache.
+
+
+ The gate passes, the oracle reserves the price on-chain, the thread flips to work mode, and it stays there until `deliver_work` or `cancel_work`.
+
+
+
+When the gate fails, the turn never errors — it proceeds in support mode with the reason attached, so the model can explain and offer the right next step:
+
+| Reason | What the model should do |
+| --- | --- |
+| `not_contracted` | Explain, then `show_contract`. |
+| `quota_exhausted` | Explain that the runs are used up, then `show_contract` to top up. |
+| `max_amount_too_low` | The per-job ceiling does not cover this service — `show_contract`. |
+| `service_not_contracted` | This specific service is not in the grant — `show_contract`. |
+| `engagement_in_progress` | Not a contracting problem. Name the running job and offer wait-or-`cancel_work`. Never show a contract card. |
+| `intent_failed` | The contract is fine; the on-chain reservation failed. Explain, do not re-contract. |
+
+## Environment variables
+
+Everything is optional — but the paid lane needs `EVAL_ENGINE_URL`.
+
+| Var | Required | Description |
+| --- | --- | --- |
+| `ORACLE_PAYMENTS_DISABLED` | no | Enum — exactly `'true'` or `'false'`. `'true'` excludes the plugin at boot (kill switch). |
+| `EVAL_ENGINE_URL` | no | Base URL of the evaluation engine. **Contract lookups are disabled without it** — every work request gates as `not_contracted`, so no paid work can start. Support chat is unaffected. |
+| `AGENT_CARD_PATH` | no | Path to the oracle's local agent-card JSON. Seeds the card cache (so discovery works before the card is anchored on-chain) and derives the plugin manifest from the card. An explicitly set path that cannot be read, is not JSON, is not a valid card, or describes a different entity than `ORACLE_ENTITY_DID` **fails boot** — a misconfigured card is a config error, not a silent fallback. |
+| `ORACLE_PAYMENTS_ROUTER_MODEL` | no | Model id for the support/work classifier. Defaults to the provider's `routing` role model. |
+| `ORACLE_PAYMENTS_EXTRACTOR_MODEL` | no | Model id for the trusted `request`/`workSummary` extractor. Defaults to the provider's `custom_medium` role model. |
+| `ORACLE_PAYMENTS_MAX_DELIVERABLE_MB` | no | Coerced positive number; default `25`. Size ceiling on a single delivered file. |
+| `PORTAL_URL` | no | Portal base URL. When set, the delivery receipt and payment cards carry a deep link to the claim; when unset the link is omitted. |
+
+Read but not owned: `ORACLE_ENTITY_DID` (which entity's card to resolve), `ORACLE_DID` + `SECP_MNEMONIC` + `MATRIX_VALUE_PIN` (claim signing), `MATRIX_ACCOUNT_ROOM_ID` (where the pending-claims index lives), `NETWORK` (price → denom), `BLOCKSYNC_GRAPHQL_URL` (card + evaluation reads) — all from the [core base schema](/build-an-oracle/reference/environment-variables#tier-0-core).
+
+## What it contributes
+
+- **Tools:** six, built per request. Which ones the model sees depends on the thread's mode.
+
+ | Tool | Mode | What it does |
+ | --- | --- | --- |
+ | `list_services` | support | Posts the service catalog card and returns the same list so the reply is grounded. |
+ | `show_contract` | support | Posts the contract card for one `serviceId`. Errors with the valid ids if the id is unknown. |
+ | `get_contract_status` | support | Read-only: is this user contracted, which services, how much quota is left, what the per-job ceiling is. |
+ | `deliver_work` | work | Hands the finished work to the user **and** submits the work claim. Called exactly once per job. |
+ | `cancel_work` | work | Closes the job early and releases the reservation with a release claim. |
+ | `get_thread_attachment` | both | Lists the files the user shared in **this thread**, each mapped to the sandbox path it was archived to under `/workspace/output/`. Downloads nothing. |
+
+- **Sub-agents:** none.
+- **Middleware:** none. (Routing happens on the Matrix ingress lane, before the graph.)
+- **Nest modules:** one — the contract-record lookup and its `ixo.oracle.contracted` cache-bust listener, the thread-engagement store, the contract gate, the escrow-first engagement start, the delivery lane, the registrar that plugs this plugin's knowledge into the core message router, and a cron that reports each submitted claim's evaluation outcome back into its thread.
+- **HTTP routes:** none.
+- **Shared state:** `oraclePayments.services()` and `oraclePayments.engagement(roomId, threadId)`.
+
+### Delivering a file
+
+`deliver_work` takes either written content (`kind: 'text'`, materialized as markdown) or a file the oracle produced in the sandbox (`kind: 'file'`, read from a path under `/workspace/data/`). Either way real bytes are attached to the claim — a claim whose deliverables do not resolve to bytes is rejected by design.
+
+## Matrix events
+
+The plugin (and the core router) post `ixo.oracle.component` timeline events. The IXO Portal renders each `component` as an interactive card; clients without a renderer (Element, for one) show the plain-text `body` fallback.
+
+| `component` | Posted by | Purpose |
+| --- | --- | --- |
+| `list_services` | `list_services` | Service catalog — one row per contractable service. |
+| `show_contract` | `show_contract` | Contract proposal — opens the Portal flow with the service preselected. |
+| `work_status` | the router + the tool wrapper | Per-turn liveness card (`routing` → `working` → `delivering` → `done`, or `superseded`). Updates in place via `m.replace`. |
+| `work_delivered` | `deliver_work` | Receipt: the file, the summary, the cost, the claim id, and the claim deep link. |
+| `payment_update` | the claim-status cron | Evaluation outcome — `approved`, `rejected`, `under_review`, or `disputed` — worded differently for a delivery than for a cancellation. |
+
+The Portal posts one event **into** the room: `ixo.oracle.contracted`, after a successful contract registration. The oracle treats it as an untrusted cache-buster only.
+
+## Deployment prerequisites
+
+Chat and support work with none of this. Only the paid lane is gated.
+
+
+
+ `qiforge-cli agent-card` anchors the `#acard` LinkedResource on the oracle entity and writes the local copy `AGENT_CARD_PATH` points at. Without a card there are no services to sell.
+
+
+ It is the source of truth for "is this user contracted, for what, with how much quota left". Unset means no user can ever pass the gate.
+
+
+ Every job reserves its payment before work starts and settles with `useIntent: true`. An oracle pointed at an engine that rejects those claims will lock escrow it can never settle.
+
+
+ See below — "free support" means "no work claim", not "free LLM".
+
+
+
+## How this relates to `credits`
+
+Two different lanes that do not overlap:
+
+| | [`credits`](/build-an-oracle/reference/bundled-plugins/credits) | `oracle-payments` |
+| --- | --- | --- |
+| Bills for | LLM token usage | delivered work |
+| Unit | per-user credit budget | one job, at the card's service price |
+| Settles via | the usage-claim cron | one agent-work claim per job, escrow-backed |
+| Who judges | nobody — metering is mechanical | an independent evaluator, against `doneMeans` |
+
+**Token metering _is_ the `credits` plugin.** With `credits` off there is no metering at all, so a commerce oracle should run with it on: the free support persona is then metered as ordinary LLM usage. "Free" describes the absence of a work claim, not the absence of cost.
+
+## Opt out / Opt in
+
+```ts
+const app = await createOracleApp({
+ config,
+ features: { 'oracle-payments': false }, // never load
+});
+
+// Or via env: ORACLE_PAYMENTS_DISABLED=true
+```
+
+## When to use it
+
+- The oracle sells something concrete a user would pay for, delivered as a file or a document.
+- Users talk to the oracle over Matrix (the IXO Portal chat surface or a Matrix client).
+- You want the payment to depend on the work being judged good, not on the oracle asserting it was.
+
+## When NOT to use it
+
+- Metering LLM usage or enforcing a subscription — that is [`credits`](/build-an-oracle/reference/bundled-plugins/credits).
+- Oracles that only serve HTTP. Routing, engagements, and the cards are Matrix-side; over HTTP only the support tools bind, and the cards have no room to post into.
+- Free oracles with nothing to sell. Leave the plugin loaded and publish no card — it stays a no-op.
+
+## Where to read next
+
+
+
+ The other billing lane — LLM usage metering.
+
+
+ Where a file deliverable is produced and read from.
+
+
+ Every `qiforge-cli` command, including the entity the card is anchored on.
+
+
+ Every var in one table.
+
+
diff --git a/build-an-oracle/reference/bundled-plugins/overview.mdx b/build-an-oracle/reference/bundled-plugins/overview.mdx
index 33f1a3d..18bd79b 100644
--- a/build-an-oracle/reference/bundled-plugins/overview.mdx
+++ b/build-an-oracle/reference/bundled-plugins/overview.mdx
@@ -4,7 +4,7 @@ description: "Reference for every plugin the oracle runtime ships with — toggl
icon: "boxes-stacked"
---
-The runtime bundles 16 plugins that load by default; opt out per plugin via the `features` map on `createOracleApp`. One additional plugin — [`flows`](/build-an-oracle/reference/bundled-plugins/flows) — ships in the package but is **opt-in only**: wire it in explicitly via the `plugins` array.
+The runtime bundles 14 plugins that load by default; opt out per plugin via the `features` map on `createOracleApp`. Four more ship in the package but are **not** in the default set — [`credits`](/build-an-oracle/reference/bundled-plugins/credits), [`slack`](/build-an-oracle/reference/bundled-plugins/slack), [`calls`](/build-an-oracle/reference/bundled-plugins/calls), and [`flows`](/build-an-oracle/reference/bundled-plugins/flows) — so you wire each one in explicitly via the `plugins` array. The authoritative list is [`BUNDLED_PLUGINS`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/plugins/index.ts).
## At a glance
@@ -19,20 +19,22 @@ The runtime bundles 16 plugins that load by default; opt out per plugin via the
| [`skills`](/build-an-oracle/reference/bundled-plugins/skills) | `always` | On | — | `sandbox` |
| [`editor`](/build-an-oracle/reference/bundled-plugins/editor) | `always` | On | — | — |
| [`agui`](/build-an-oracle/reference/bundled-plugins/agui) | `on-demand` | On | — | — |
-| [`slack`](/build-an-oracle/reference/bundled-plugins/slack) | `silent` | Auto-detect | `SLACK_BOT_OAUTH_TOKEN` | — |
| [`tasks`](/build-an-oracle/reference/bundled-plugins/tasks) | `on-demand` | Auto-detect | `REDIS_URL` | — |
-| [`credits`](/build-an-oracle/reference/bundled-plugins/credits) | `silent` | On unless `DISABLE_CREDITS=true` | — | — |
-| [`calls`](/build-an-oracle/reference/bundled-plugins/calls) | `silent` | On (stub) | — | — |
| [`user-preferences`](/build-an-oracle/reference/bundled-plugins/user-preferences) | `always` | On | — | — |
| [`matrix-group-chats`](/build-an-oracle/reference/bundled-plugins/matrix-group-chats) | `on-demand` | On | — | — |
| [`vfs`](/build-an-oracle/reference/bundled-plugins/vfs) | `always` | On | — (URLs from `NETWORK`) | — |
+| [`oracle-payments`](/build-an-oracle/reference/bundled-plugins/oracle-payments) | `always` | On unless `ORACLE_PAYMENTS_DISABLED=true` | — (`EVAL_ENGINE_URL` for the paid lane) | — |
+| [`credits`](/build-an-oracle/reference/bundled-plugins/credits) | `silent` | Opt-in (not bundled) | — | — |
+| [`slack`](/build-an-oracle/reference/bundled-plugins/slack) | `silent` | Opt-in (not bundled) | `SLACK_BOT_OAUTH_TOKEN` | — |
+| [`calls`](/build-an-oracle/reference/bundled-plugins/calls) | `silent` | Opt-in (not bundled, stub) | — | — |
| [`flows`](/build-an-oracle/reference/bundled-plugins/flows) | `on-demand` | Opt-in (not bundled) | — | `editor` Qi Flow engine |
`Default state` legend:
- **On** — loaded by default; opt out with `features: { name: false }`.
- **Auto-detect** — loaded when its env var is set; opt in by setting it, force on with `features: { name: true }`, force off with `false`.
-- **(stub)** — placeholder entry in `BUNDLED_PLUGINS` so feature toggles work; full implementation deferred.
+- **Opt-in (not bundled)** — absent from `BUNDLED_PLUGINS`, so `features` never reaches it. Construct it and pass it in [`plugins`](#wiring-custom-constructed-plugins).
+- **(stub)** — placeholder entry; full implementation deferred.
## How to use `features`
@@ -41,7 +43,7 @@ const app = await createOracleApp({
config,
features: {
composio: false, // never load even if COMPOSIO_API_KEY is set
- slack: true, // force load even if SLACK_BOT_OAUTH_TOKEN is missing (will fail env validation)
+ sandbox: true, // force load even if SANDBOX_MCP_URL is missing (will fail env validation)
'domain-indexer': 'auto', // explicit auto (same as omitting)
},
});
@@ -79,18 +81,9 @@ const app = await createOracleApp({
Render interactive UI components in the browser.
-
- Slack bot transport.
-
Schedule the agent to run on time-based triggers, in the background.
-
- Per-user credit enforcement and claim settlement.
-
-
- LiveKit call integration (stub — deferred).
-
Tone / format / language preferences.
@@ -100,6 +93,18 @@ const app = await createOracleApp({
Read, write, search, and share the user's real files on their Virtual Filesystem.
+
+ Sell services from the chat — contract on-chain, deliver work, get paid.
+
+
+ Per-user credit enforcement and claim settlement (opt-in).
+
+
+ Slack bot transport (opt-in).
+
+
+ LiveKit call integration (opt-in stub — deferred).
+
Author and inspect multi-step Qi Flow templates (opt-in).
@@ -107,9 +112,9 @@ const app = await createOracleApp({
## Wiring custom-constructed plugins
-Some plugins accept constructor args. Pass a custom instance via the `plugins` array — the loader dedupes by name, so your instance overrides the bundled default.
+Anything in the `plugins` array is always loaded — `features` never gates it, and the loader dedupes by name, so your instance overrides a bundled default of the same name. This is how you load the four opt-in plugins, and how you replace a bundled one with a custom-constructed instance.
-**`credits` genuinely needs a Redis client.** Without one the enforcement middleware loads in pass-through mode and the settlement cron is skipped, so production must construct it explicitly:
+**`credits` genuinely needs a Redis client.** It is not in the bundled set, so nothing loads it unless you do. Without a Redis client the enforcement middleware loads in pass-through mode and the settlement cron is skipped, so production must construct it explicitly:
```ts
import { createOracleApp, CreditsPlugin } from '@ixo/oracle-runtime';
diff --git a/build-an-oracle/reference/bundled-plugins/slack.mdx b/build-an-oracle/reference/bundled-plugins/slack.mdx
index 0c63dbe..d72e1ad 100644
--- a/build-an-oracle/reference/bundled-plugins/slack.mdx
+++ b/build-an-oracle/reference/bundled-plugins/slack.mdx
@@ -11,7 +11,7 @@ icon: "slack"
| Visibility | `silent` |
| Stability | `stable` |
| Category | `core` |
-| Default state | Auto-detect (env: `SLACK_BOT_OAUTH_TOKEN`) |
+| Default state | Opt-in — not in `BUNDLED_PLUGINS`. Construct it and pass it in `plugins`. |
| Depends on | — |
## Summary
@@ -39,15 +39,19 @@ Connects a Slack bot to the oracle. The plugin contributes **no agent-visible to
## Opt out / Opt in
+`slack` is not in `BUNDLED_PLUGINS`, so `features` never reaches it. Load it by passing an instance:
+
```ts
+import { createOracleApp, SlackPlugin } from '@ixo/oracle-runtime';
+
const app = await createOracleApp({
config,
- features: { slack: false }, // never load
- // features: { slack: true }, // force load (will fail env validation if SLACK_BOT_OAUTH_TOKEN missing)
- // features: { slack: 'auto' }, // run autoDetect (default)
+ plugins: [new SlackPlugin()],
});
```
+Its `autoDetect` still applies once wired: without `SLACK_BOT_OAUTH_TOKEN` it is skipped at boot and no module is registered.
+
## When to use it
- You want the same oracle reachable from Slack as well as the web client.
diff --git a/build-an-oracle/reference/createoracleapp.mdx b/build-an-oracle/reference/createoracleapp.mdx
index 9c8f9a8..2b6dc71 100644
--- a/build-an-oracle/reference/createoracleapp.mdx
+++ b/build-an-oracle/reference/createoracleapp.mdx
@@ -133,7 +133,7 @@ export interface CreateOracleAppOptions {
```ts
features: {
- slack: false,
+ tasks: false,
composio: true,
'domain-indexer': 'auto',
}
diff --git a/build-an-oracle/reference/environment-variables.mdx b/build-an-oracle/reference/environment-variables.mdx
index 1ba9aa2..adb8bc9 100644
--- a/build-an-oracle/reference/environment-variables.mdx
+++ b/build-an-oracle/reference/environment-variables.mdx
@@ -123,6 +123,13 @@ Only required when the named plugin is loaded.
| `matrix-group-chats` | `GROUP_CHAT_ROOM_INFO_TTL_MS` | No | Coerced int, min `60000`; default `1800000`. How long roomInfo (membership, DM flag) stays cached. |
| `vfs` | `VFS_MAX_READ_LINES` | No | Coerced positive int; default `2000`. Max lines a single `vfs_read` window returns. |
| `vfs` | `VFS_REQUEST_TIMEOUT_MS` | No | Coerced positive int; default `20000`. Per-request timeout to the VFS worker. |
+| `oracle-payments` | `ORACLE_PAYMENTS_DISABLED` | No | Enum — exactly `'true'` or `'false'`. `'true'` excludes the whole plugin at boot. |
+| `oracle-payments` | `EVAL_ENGINE_URL` | No | URL. Evaluation engine base URL. Without it contract lookups are disabled, so no user can start paid work; support chat is unaffected. |
+| `oracle-payments` | `AGENT_CARD_PATH` | No | Path to the oracle's local agent-card JSON (written by `qiforge-cli agent-card`). Seeds the card cache and derives the plugin manifest from the card. A path that is set but unreadable, malformed, or describing a different entity than `ORACLE_ENTITY_DID` fails boot. |
+| `oracle-payments` | `ORACLE_PAYMENTS_ROUTER_MODEL` | No | Model id for the support/work classifier. Defaults to the provider's `routing` role model. |
+| `oracle-payments` | `ORACLE_PAYMENTS_EXTRACTOR_MODEL` | No | Model id for the trusted request/work-summary extractor. Defaults to the provider's `custom_medium` role model. |
+| `oracle-payments` | `ORACLE_PAYMENTS_MAX_DELIVERABLE_MB` | No | Coerced positive number; default `25`. Size ceiling on a single delivered file. |
+| `oracle-payments` | `PORTAL_URL` | No | URL. Portal base URL — when set, the delivery receipt and payment cards carry a deep link to the claim. |
## Variables read but not owned
@@ -132,6 +139,7 @@ Some plugins read variables that live in another schema:
- **`skills`** reads `NETWORK` and forwards as `X-IXO-Network`.
- **`sandbox`** reads `ORACLE_SECRETS` (core) and `SKILLS_CAPSULES_BASE_URL` (owned by `skills`) and forwards them as headers.
- **`vfs`** reads the core `NETWORK` (selects the bundled VFS + UCAN Store worker URLs — nothing to configure) and `SANDBOX_MCP_URL` (owned by `sandbox`; when set, adds the two sandbox↔files bridge tools).
+- **`oracle-payments`** reads the core `ORACLE_ENTITY_DID` (whose Agent Card to resolve), `ORACLE_DID` + `SECP_MNEMONIC` + `MATRIX_VALUE_PIN` (claim signing), `MATRIX_ACCOUNT_ROOM_ID` (where the pending-claims index is stored), `NETWORK` (price → denom), and `BLOCKSYNC_GRAPHQL_URL` (card and evaluation reads).
These are declared in the plugin's sibling schemas (typed `optional()`); a missing value just skips the matching header instead of failing the plugin build.
diff --git a/build-an-oracle/reference/plugin-api.mdx b/build-an-oracle/reference/plugin-api.mdx
index 7d6f352..c6b4dd8 100644
--- a/build-an-oracle/reference/plugin-api.mdx
+++ b/build-an-oracle/reference/plugin-api.mdx
@@ -210,4 +210,4 @@ Boot-time errors in `autoDetect`, `configSchema` validation, or `manifest` valid
- [PluginContext](/build-an-oracle/reference/plugin-context)
- [RuntimeContext](/build-an-oracle/reference/runtime-context)
- [Manifest schema](/build-an-oracle/reference/manifest-schema)
-- [Plugin catalog](/build-an-oracle/reference/bundled-plugins/overview) — the 16 bundled plugins.
+- [Plugin catalog](/build-an-oracle/reference/bundled-plugins/overview) — the 14 bundled plugins.
diff --git a/build-an-oracle/understand/architecture.mdx b/build-an-oracle/understand/architecture.mdx
index f4b7a36..b551d42 100644
--- a/build-an-oracle/understand/architecture.mdx
+++ b/build-an-oracle/understand/architecture.mdx
@@ -18,7 +18,7 @@ graph TB
subgraph Runtime["@ixo/oracle-runtime"]
Core["Always-on Nest modules
Sessions · Messages · WS · Secrets · UCAN · Auth · Subscription · Throttler · Checkpointer"]
- Bundled["16 bundled plugins
memory · skills · sandbox · editor · agui · portal · firecrawl · domain-indexer · composio · slack · credits · user-preferences · matrix-group-chats · tasks · calls · vfs"]
+ Bundled["14 bundled plugins
memory · skills · sandbox · editor · agui · portal · firecrawl · domain-indexer · composio · user-preferences · matrix-group-chats · tasks · vfs · oracle-payments"]
Agent["LangChain agent
built per request"]
end
@@ -51,7 +51,7 @@ You never edit this layer. Updates come via `pnpm update @ixo/oracle-runtime`.
### Layer 3 — Bundled plugins
-16 plugins shipped inside the runtime package, each independently toggleable via `features`. See the [Plugin catalog](/build-an-oracle/reference/bundled-plugins/overview) for what each one does.
+14 plugins shipped inside the runtime package and loaded by default, each independently toggleable via `features`. Four more (`credits`, `slack`, `calls`, `flows`) ship in the package but are opt-in — you construct them and pass them in `plugins`. See the [Plugin catalog](/build-an-oracle/reference/bundled-plugins/overview) for what each one does.
## Bootstrap, in one picture
diff --git a/build-an-oracle/understand/plugins-vs-skills.mdx b/build-an-oracle/understand/plugins-vs-skills.mdx
index d6bc4c0..deb17ba 100644
--- a/build-an-oracle/understand/plugins-vs-skills.mdx
+++ b/build-an-oracle/understand/plugins-vs-skills.mdx
@@ -74,7 +74,7 @@ Not directly. Skills run in a sandbox; they don't see plugins or Nest DI. They g
The recipe.
- The 16 bundled plugins — including `skills` and `sandbox`.
+ The 14 bundled plugins — including `skills` and `sandbox`.
diff --git a/build-an-oracle/understand/what-is-qiforge.mdx b/build-an-oracle/understand/what-is-qiforge.mdx
index 81413ec..f30f23b 100644
--- a/build-an-oracle/understand/what-is-qiforge.mdx
+++ b/build-an-oracle/understand/what-is-qiforge.mdx
@@ -8,7 +8,7 @@ icon: "compass"
An **Agentic Oracle** is an AI agent that owns a verifiable identity on IXO, talks to users through encrypted per-user Matrix rooms, authenticates every request via UCAN delegation, and composes its capabilities from plugins (compiled in) and skills (discovered at runtime).
-**QiForge is the framework that lets you ship one.** The runtime (`@ixo/oracle-runtime`) handles bootstrap, auth, the agent loop, the checkpointer, Matrix wiring, and 16 bundled plugins. Your oracle is a thin `main.ts` plus whatever custom plugins you write.
+**QiForge is the framework that lets you ship one.** The runtime (`@ixo/oracle-runtime`) handles bootstrap, auth, the agent loop, the checkpointer, Matrix wiring, and 14 bundled plugins. Your oracle is a thin `main.ts` plus whatever custom plugins you write.
## Three layers, one diagram
diff --git a/docs.json b/docs.json
index 80d9c2d..f801680 100644
--- a/docs.json
+++ b/docs.json
@@ -226,6 +226,7 @@
"build-an-oracle/reference/bundled-plugins/user-preferences",
"build-an-oracle/reference/bundled-plugins/matrix-group-chats",
"build-an-oracle/reference/bundled-plugins/vfs",
+ "build-an-oracle/reference/bundled-plugins/oracle-payments",
"build-an-oracle/reference/bundled-plugins/flows"
]
}