packages/oracle-runtime/src/plugins/oracle-payments/
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.
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:
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 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-chainSubmitClaimAuthorization 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_NSin@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
1
They ask what the oracle does
The classifier routes to support. The model calls
list_services, which posts a service catalog card into the room.2
They pick a service
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.3
They approve the grant in the IXO Portal
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.4
The oracle notices immediately
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.5
The next work request starts a job
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.Environment variables
Everything is optional — but the paid lane needsEVAL_ENGINE_URL.
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.
What it contributes
-
Tools: six, built per request. Which ones the model sees depends on the thread’s mode.
- 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.contractedcache-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()andoraclePayments.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) postixo.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.
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.1
Publish an Agent Card
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.2
Point EVAL_ENGINE_URL at an evaluation engine
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.
3
Make sure that engine accepts intent-backed agent-work claims
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.4
Keep credits enabled
See below — “free support” means “no work claim”, not “free LLM”.
How this relates to credits
Two different lanes that do not overlap:
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
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. - 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
credits
The other billing lane — LLM usage metering.
sandbox
Where a file deliverable is produced and read from.
CLI reference
Every
qiforge-cli command, including the entity the card is anchored on.Environment variables
Every var in one table.