From fca4754c0dfc4f62c25ccae196e1951d4bd355fa Mon Sep 17 00:00:00 2001 From: youssefhany-ixo Date: Sat, 25 Jul 2026 15:11:57 +0300 Subject: [PATCH] docs(build-an-oracle): document the oracle-payments bundled plugin Adds the reference page for `oracle-payments`, the plugin that lets an oracle sell services from a Matrix chat: publish an Agent Card, let users contract on-chain, reserve payment when work starts, and submit a work claim on delivery. Also corrects the bundled-plugin set across the section. `BUNDLED_PLUGINS` is now a 14-entry tuple; `credits`, `slack`, `calls`, and `flows` are opt-in and must be passed as instances in `plugins`, so their pages and every "N bundled plugins" claim have been updated to match. `for-ai-agents` was also missing `vfs` from its table. --- .../develop/enable-bundled-plugins.mdx | 37 +-- .../plugin-recipes/add-config-and-env.mdx | 2 +- .../plugin-recipes/declare-dependencies.mdx | 2 +- build-an-oracle/for-ai-agents.mdx | 13 +- build-an-oracle/index.mdx | 4 +- build-an-oracle/quickstart.mdx | 4 +- .../reference/bundled-plugins/calls.mdx | 15 +- .../reference/bundled-plugins/credits.mdx | 13 +- .../bundled-plugins/oracle-payments.mdx | 231 ++++++++++++++++++ .../reference/bundled-plugins/overview.mdx | 39 +-- .../reference/bundled-plugins/slack.mdx | 12 +- build-an-oracle/reference/createoracleapp.mdx | 2 +- .../reference/environment-variables.mdx | 8 + build-an-oracle/reference/plugin-api.mdx | 2 +- build-an-oracle/understand/architecture.mdx | 4 +- .../understand/plugins-vs-skills.mdx | 2 +- .../understand/what-is-qiforge.mdx | 2 +- docs.json | 1 + 18 files changed, 316 insertions(+), 77 deletions(-) create mode 100644 build-an-oracle/reference/bundled-plugins/oracle-payments.mdx 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" ] }