@ixo/decision-oracle 0.2.31 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ ---
2
+ name: operate-oracle
3
+ description: Use when a deployed oracle built on @ixo/decision-oracle needs looking after — "upgrade the package", "pause / resume a collection", "why is my collection paused", "renew the delegation", "the Matrix token expired", "rotate a key", "answer a review", "a claim never landed on chain", "erase a claim", "what is my oracle doing", "set up alerts", "top up the admin account", admin-unfunded, engine-credits, engine-no-grant.
4
+ ---
5
+
6
+ # Operating a deployed oracle
7
+
8
+ ## Overview
9
+
10
+ Day-2 work on an oracle that `create-oracle` already deployed and got hired: upgrades, pauses,
11
+ renewals, reviews, recovery, and seeing what is going on. Each recipe below is short and ordered.
12
+ `<env>` is the wrangler env (`devnet`, `testnet`, `production` in the template), `<host>` that env's
13
+ `ORACLE_HOSTNAME`, `<n>` its network (`devnet` / `testnet` / `mainnet`).
14
+
15
+ **Core rule: run it, don't recite it.** Anything that signs on chain, deploys, or changes an
16
+ identity is dry-run first (no `--yes`), shown to the owner, and run only on their explicit OK.
17
+ Commands that call the oracle as your login take `--kit https://<host> --network <n>`; without
18
+ `--kit` they call the `ORACLE_HOSTNAME` in `.dev.vars` (usually localhost).
19
+
20
+ ## When to use
21
+
22
+ - A new `@ixo/decision-oracle` release is out
23
+ - A collection is paused, or should be
24
+ - A delegation, grant or Matrix token ran out; a key must change
25
+ - A review is waiting, a decision never reached the chain, or a claim's data must be erased
26
+ - Setting up logs, status and alerts
27
+
28
+ Not for: first setup (`create-oracle`), price changes (`update-pricing`), the Decision API's
29
+ sources and keys (`decision-api-oracle`).
30
+
31
+ ## 1. Upgrade the package
32
+
33
+ ```bash
34
+ bun update @ixo/decision-oracle # within your range; add --latest to cross a 0.x minor
35
+ bunx decision-oracle skills sync # refresh .claude/skills from the installed package
36
+ bun run db:migrate # the new migrations, on the local D1
37
+ bun run check # typecheck + lint + your handler tests
38
+ bunx wrangler d1 migrations apply DB --remote --env testnet # BEFORE that env's deploy
39
+ bunx wrangler deploy --env testnet
40
+ curl -s https://<testnet host>/health/ready # {"ok":true}
41
+ ```
42
+
43
+ Then the same three remote lines for `production`. Migrations ship inside
44
+ `node_modules/@ixo/decision-oracle/migrations`; new code against an old schema fails its
45
+ queries, so migrate first, every env. Compare your `wrangler.jsonc` with the new
46
+ `node_modules/@ixo/decision-oracle/templates/project/wrangler.jsonc` for a new binding, var or cron
47
+ trigger, and run `bun run cf-typegen` after adding one. Deploy testnet first and watch it before
48
+ mainnet.
49
+
50
+ ## 2. Pause and resume a collection
51
+
52
+ **Why it paused.** `GET /v1/admin/status` (section 5) counts paused collections by reason, and the
53
+ owner got a notice (Matrix + email) naming the reason and the fix:
54
+
55
+ | Reason | What happened | Lifts itself? |
56
+ | --- | --- | --- |
57
+ | `engine-credits` | the oracle owner's engine credits ran out — every collection pauses | yes, once the engine says billing is `ok` (the owner buys credits in the Decision Console) |
58
+ | `admin-unfunded` | the collection's admin account cannot pay an evaluation | yes, once topped up (section 7) — parked decisions are re-delivered |
59
+ | `engine-no-grant` | the Evaluate grant is gone (expired, revoked, or its quota used up) | no — re-grant, then re-register (section 3) |
60
+ | `rubric-unusable` | the anchored rubric no longer binds to its claim form | no — fix the rubric or `#vct`, then resume |
61
+ | `payer-credits` | per-review billing: the paying organisation's credits ran short | yes, once they top up |
62
+ | `oracle-plan` | per-review billing: IXO's plan no longer allows it | yes, once IXO's plan allows it |
63
+ | `payer-delegation` | per-review billing: nobody can pay (no live delegation) | yes, when the organisation signs a new one in the Decision Console |
64
+
65
+ An operator's own pause has no reason and nothing lifts it but a resume.
66
+
67
+ **Resume** — as the collection's owner, or whoever registered it: register again. It re-runs the
68
+ four gates (who, hired, protocol, paid), stores a fresh delegation if you pass one, and flips
69
+ `paused` → `active`:
70
+
71
+ ```bash
72
+ bunx decision-oracle register --collection <id> [--delegation delegation.car] --kit https://<host> --network <n>
73
+ ```
74
+
75
+ **Pause** — there is no CLI command. It is `POST /v1/collections/<id>/pause` (and `…/resume`) with a
76
+ UCAN invocation, allowed for the collection's owner, the registrant (while it still holds an
77
+ Evaluate grant) or an `ORACLE_ADMIN_DIDS` operator. One self-signed invocation per call:
78
+ `can: "collections/pause"` (or `collections/resume`), `with: "ixo:eval-oracle"`, audience the
79
+ oracle's did:web, no proofs, a short expiry. With `@ixo/ucan` (`bun add @ixo/ucan` if your project
80
+ cannot import it):
81
+
82
+ ```ts
83
+ // call.ts — bun call.ts POST /v1/collections/<id>/pause collections/pause
84
+ import { createInvocation, serializeInvocation, signerFromMnemonic } from "@ixo/ucan";
85
+
86
+ const [method = "GET", path = "/v1/admin/status", can = "admin/status"] = process.argv.slice(2);
87
+ const url = process.env.ORACLE_URL as string; // https://<host>
88
+ // The caller's Ed25519 signing key — the one on its did:ixo document. Read from the environment,
89
+ // never pasted into a chat or committed.
90
+ const { signer } = await signerFromMnemonic(
91
+ process.env.CALLER_ED_MNEMONIC as string,
92
+ process.env.CALLER_DID as `did:ixo:${string}`,
93
+ );
94
+ const audience = ((await (await fetch(`${url}/.well-known/did.json`)).json()) as { id: string }).id;
95
+ const token = await serializeInvocation(
96
+ await createInvocation({
97
+ issuer: signer,
98
+ audience,
99
+ capability: { can: can as `${string}/${string}`, with: "ixo:eval-oracle" },
100
+ proofs: [],
101
+ expiration: Math.floor(Date.now() / 1000) + 120,
102
+ facts: [{ nonce: crypto.randomUUID() }],
103
+ }),
104
+ );
105
+ const res = await fetch(`${url}${path}`, {
106
+ method,
107
+ headers: { authorization: `Bearer ${token}`, "x-auth-type": "ucan", "content-type": "application/json" },
108
+ ...(method === "GET" ? {} : { body: "{}" }),
109
+ });
110
+ console.log(res.status, await res.text());
111
+ ```
112
+
113
+ To stop all chain writes at once without pausing anything, set `"AUTO_SIGN": "false"` in that env's
114
+ `vars` and redeploy: decisions are still recorded, nothing is signed until it is back on.
115
+
116
+ ## 3. Renewals
117
+
118
+ | What | Lifetime | Renew |
119
+ | --- | --- | --- |
120
+ | Owner → oracle claim-bot delegation (per collection) | 30 days by default | as the collection's owner: `bunx decision-oracle delegation --for bot --audience did:ixo:<ORACLE_ADDRESS> --out delegation.car --network <n>` (dry run, then `--yes`), then `register --collection <id> --delegation delegation.car`. Only Matrix-stored claim bodies need it |
121
+ | `OWNER_DELEGATION_CAR` (engine billing) | 100 years by default (`--days` shortens it) | as the oracle's owner: `delegation --for engine --audience did:web:<engine host> --oracle did:web:<host> --out owner-delegation.car --yes`, then `bunx wrangler secret put OWNER_DELEGATION_CAR --env <env> < owner-delegation.car` and `engine register --env <env> --env-file .dev.vars.<env>` (update that file too) |
122
+ | Evaluate grant | 365 days, and an `agentQuota` of 100 `MsgEvaluateClaim`s (a reviewed claim uses 2) | the collection's owner: `create-collection --entity <deed> --protocol <did> --phase grants --evaluate <ORACLE_ADDRESS>` (dry run, then `--yes`), then `register --collection <id>` to lift `engine-no-grant` |
123
+ | `ORACLE_MATRIX_ACCESS_TOKEN` | until the homeserver drops it | `bunx decision-oracle oracle init --protocol <did> --kit-url https://<host> --relogin --network <n>` (dry run, then `--yes`; it mints a new Matrix device and writes the token into `.dev.vars`), then `bunx wrangler secret put ORACLE_MATRIX_ACCESS_TOKEN --env <env>` |
124
+ | Oracle wallet gas | `oracle init` tops it to 1 IXO | `oracle init … --only fund` (dry run, then `--yes`) |
125
+
126
+ **Key rotation is an identity change — warn the owner before doing it.** A new
127
+ `ORACLE_SECP_MNEMONIC` is a new `ORACLE_ADDRESS` and `ORACLE_DID`: every collection must grant the
128
+ new address, re-mint its claim-bot delegation for the new DID and re-register, the engine must be
129
+ re-bound (`engine register --yes` prints old → new), and `oracle init` must run again for the new
130
+ address (wallet, IID, key, Matrix account, `#orz`). A new `SERVICE_ED25519_MNEMONIC` changes the
131
+ did:web key: re-run `oracle init` so the `key` step puts the new key on the IID. A new host is a new did:web: re-mint
132
+ `OWNER_DELEGATION_CAR` for it and re-run `oracle init` with the new `--kit-url`. Never rotate
133
+ `UNIQUE_HMAC_KEY` or `WEBHOOK_ENCRYPTION_KEY`. Update your private backup of the secrets after any
134
+ change.
135
+
136
+ ## 4. Day-to-day operations
137
+
138
+ **Reviews.** A `review` verdict waits for a person the collection's company named (a current
139
+ controller of the collection's entity grants a reviewer `claims/review` for at most 30 days). The
140
+ oracle posts a card in the collection's Matrix review room and emails the registered contact; the
141
+ reviewer answers from that card or the Portal's claim page, and on the Decision API lane from the
142
+ Decision Console. You — the oracle's owner and operators — cannot answer: the engine refuses your
143
+ identities. `GET /v1/reviews/<claimId>` shows what is pending.
144
+
145
+ **CLI operator commands** (signed as your login; `--kit https://<host> --network <n>`):
146
+
147
+ | Command | Who | What |
148
+ | --- | --- | --- |
149
+ | `redeliver <claimId> [--yes]` | `ORACLE_ADMIN_DIDS` | a decided claim parked `blocked` because its facts could not be rebuilt: rebuilds them and signs its evaluation on chain |
150
+ | `reviewer-note <claimId>` | `ORACLE_ADMIN_DIDS` | (re)makes a held claim's reviewer note — one more AI call |
151
+ | `relink [--collection <id>]…` | `ORACLE_ADMIN_DIDS` | links watched collections to their company's Decision API sources, as register does |
152
+ | `erase (<claimId>… \| --collection <id>) [--yes]` | the collection's current chain owner | erases claims' transcripts and registry text; the decision, receipt and digests stay. The oracle never erases on its own |
153
+ | `scan --collection <id>` | anyone who may read the collection (its owner, the registrant, an Evaluate-grant holder) | finds new claims now instead of at the next tick |
154
+
155
+ **A decision that never reached the chain.** Read `chainStatus` / `chainError` on
156
+ `GET /v1/claims/<claimId>` (a UCAN `claims/read`, same snippet). `blocked` with no grant ⇒ re-grant;
157
+ an unfunded admin ⇒ section 7; `held` ⇒ your own `policies.act` hook parked it; `flagged` ⇒ waiting
158
+ for a review; facts not rebuilt ⇒ `redeliver`.
159
+
160
+ **An errored ingest.** In the Cloudflare dashboard (Workers → Workflows → the claim-ingest
161
+ workflow) rerun the errored instance; the ledger converges.
162
+
163
+ ## 5. See what is happening
164
+
165
+ ```bash
166
+ bunx wrangler tail --env <env> # live logs; add --format pretty
167
+ curl -s https://<host>/health/ready # {"ok":true} or 503 {"ok":false}
168
+ ```
169
+
170
+ - **`GET /v1/admin/status`** — which readiness check failed (and the command to fix `identity` /
171
+ `owner-delegation`), pending reviews, paused collections by reason, and the engine's billing
172
+ status. Only DIDs in `ORACLE_ADMIN_DIDS` (operators, `did:ixo`) or `STATUS_READER_DIDS`
173
+ (read-only monitors, any DID method) may call it; with both unset it is `403` for everyone. Call
174
+ it with the snippet in section 2 (its defaults are this route).
175
+ - **Workers Logs** — every log line and metric, in the Cloudflare dashboard (Observability).
176
+ - **Sentry** — `bunx wrangler secret put SENTRY_DSN --env <env>`. Each condition that needs a
177
+ person (a pause, a dead webhook, a lost settlement, a failing cron job) becomes one issue tagged
178
+ `alert`; the cron triggers check in as monitors. Tune with the `SENTRY_TRACES_SAMPLE_RATE` and
179
+ `SENTRY_ENVIRONMENT` vars.
180
+ - **Slack alerts** — `bunx wrangler secret put OPS_ALERT_WEBHOOK_URL --env <env>` (a Slack-compatible
181
+ incoming webhook): one message per condition per 30 minutes.
182
+ - **Uptime** — point a monitor at `GET /health/ready` every minute, not `/health`.
183
+
184
+ `node_modules/@ixo/decision-oracle/docs/guide.md` §12 has the full alert list and a ten-minute
185
+ Sentry setup.
186
+
187
+ ## 6. Notices
188
+
189
+ Pause, resume and review notices go to Matrix (needs `ORACLE_MATRIX_ACCESS_TOKEN`) and email (needs
190
+ `EMAIL_FROM_ADDRESS` on a domain onboarded to Cloudflare Email Service). Either unset is a logged
191
+ skip, never a blocked claim. The email goes to the contact given at register
192
+ (`register --contact-email <addr>` changes it).
193
+
194
+ ## 7. Top up a collection's admin account
195
+
196
+ The collection's admin account (the deed's) pays every evaluation, rejects included, plus the
197
+ verdict's own payment. When it cannot, the decision is parked `blocked` and the collection pauses
198
+ `admin-unfunded`; the owner's notice names the account and the amount needed. Check and top up:
199
+
200
+ ```bash
201
+ bunx decision-oracle org balance --entity <deed did> --network <n>
202
+ bunx decision-oracle create-collection --entity <deed did> --protocol <protocol did> \
203
+ --phase fund --fund <amount in uixo base units> --network <n> # dry run, then --yes
204
+ ```
205
+
206
+ `--fund` sends `uixo` from your login's wallet. For USDC on mainnet:
207
+ `bunx decision-oracle org fund --entity <deed did> --amount <whole USDC> --coin usdc --network mainnet`.
208
+ Within a minute the oracle sees the balance, resumes the collection and re-delivers the parked
209
+ decisions — nothing to re-run.
210
+
211
+ ## Troubleshooting
212
+
213
+ | Symptom | Cause → fix |
214
+ | --- | --- |
215
+ | `no such column` / `no such table` errors after an upgrade | the remote D1 is missing the new migrations — `bunx wrangler d1 migrations apply DB --remote --env <env>` |
216
+ | `/health/ready` 503 | `GET /v1/admin/status` names the check; `identity` ⇒ `ORACLE_ENTITY_DID` missing from that env's `vars` |
217
+ | Collection paused `engine-credits` | the owner buys engine credits in the Decision Console (Billing › Credits); it resumes by itself |
218
+ | Collection paused `engine-no-grant`, engine says `NO_EVALUATE_GRANT` | the grant expired or its 100 slots are used — re-grant (`--phase grants`), then `register` |
219
+ | Collection paused `admin-unfunded`, `payment withdrawal failed` | section 7 |
220
+ | Collection paused `rubric-unusable` | `bunx decision-oracle protocol review <did>` prints the issues; fix the anchored rubric or form, then resume |
221
+ | Matrix-stored claims fail to pull after a month | the claim-bot delegation expired — re-mint `--for bot` and `register --delegation` |
222
+ | Review cards stopped appearing | `ORACLE_MATRIX_ACCESS_TOKEN` was dropped — `oracle init --relogin`, then `wrangler secret put` |
223
+ | `MsgEvaluateClaim` fails `insufficient funds` | the oracle's own wallet is out of gas — `oracle init … --only fund --yes` |
224
+ | `chainStatus: flagged` for days | a person has not answered the review — nudge the company's reviewers |
225
+ | `register` / `redeliver` 401 | `--kit` names a different host than the did:web, or the call went to localhost — pass `--kit https://<host>` |
226
+ | `redeliver` / `relink` / `reviewer-note` 403 | your login's DID is not in that env's `ORACLE_ADMIN_DIDS` |
227
+ | `erase` 403 | only the collection's current chain owner may erase |
228
+ | `.dev.vars` edited but nothing changed | a running `wrangler dev` reloads source, not secrets — restart it; deployed secrets change only with `wrangler secret put` |
229
+
230
+ ## Output contract
231
+
232
+ Say plainly: what was run per env (versions, migrations, deploys), the `/health/ready` answer, each
233
+ collection's status and reason before and after, what was renewed and when it next expires, any
234
+ transaction hashes, and every step still waiting on the owner.
@@ -38,8 +38,9 @@ There are two jobs here, and picking the wrong one fails on chain:
38
38
  `--yes` after they explicitly say to proceed. These commands spend real funds (gas).
39
39
  - **Signing is an AuthHub session, not a mnemonic.** `bunx decision-oracle login --network <n>` once —
40
40
  it opens the login URL, waits for the callback, and saves a session under the user's home
41
- directory. Every command after that reads it automatically. The session is also the Matrix
42
- identity the `resources`/`room` phases use. Never ask anyone to paste a mnemonic or a session file
41
+ directory. Every command after that reads it automatically. Uploads (the form, the rubric, the
42
+ profile) go to the IXO Vault, else the IXO IPFS gateway; only the `room` phase uses Matrix, as the
43
+ session's own Matrix user. Never ask anyone to paste a mnemonic or a session file
43
44
  into a message.
44
45
 
45
46
  ## When to use
@@ -78,14 +78,14 @@ entries are exactly what a handler returns, after the runner normalized it.
78
78
  | 400 `INVALID_ADDRESS_PROOF` | register proof bad | non-retryable; `ORACLE_SECP_MNEMONIC` ≠ `ORACLE_ADDRESS` — a config bug |
79
79
  | 403 `NO_EVALUATE_GRANT` | not hired for this collection | retry once (a grant may have just landed), then pause the collection |
80
80
  | 402 `INSUFFICIENT_CREDITS` | the OWNER's credits are exhausted | pause every watched collection, log `details.manageSubscriptionUrl`; the cron resumes when `/me` says ok |
81
- | 402 `NO_SUBSCRIPTION` (register) | the delegated owner has no subscription | non-retryable; the owner subscribes in the portal, then register again. A subscription made by the subscriptions workflow (not the portal) also needs a Stripe customer: the owner calls the subscriptions API's `POST /api/v1/payments/intent` once (a SetupIntent, no charge) |
81
+ | 402 `NO_SUBSCRIPTION` (register) | the delegated owner has no IXO credits account yet | non-retryable; the owner buys credits in the Decision Console (Billing › Credits), signed in as the account that minted `OWNER_DELEGATION_CAR` — the first purchase opens the account — then registers again |
82
82
  | 403 `DELEGATION_NOT_FOR_ORACLE` (register) | the CAR's `billing/pay` names a different oracle | non-retryable; re-mint with `--oracle did:web:<this host>` |
83
83
  | 400 `BAD_REQUEST` (register, naming `ownerDelegation`) | the CAR is malformed, mis-addressed or expired | non-retryable; re-mint, then register again |
84
84
  | 409 `RUBRIC_PIN_MISMATCH` | our pin ≠ the chain's `#rub` proof | invalidate the cached manifest, re-read, rebuild, retry once |
85
85
  | 409 `STALE_REVIEW` | the review answered against a stale digest | refetch the decision, ask the reviewer again |
86
86
  | 409 `CONTENT_MISMATCH` | `claimContent` doesn't match the claim (`details.reason` names the check) | non-retryable. Decide: a `decisions` row `status: content_mismatch`, `chainStatus: skipped`, the ingest fails and alerts. Fold: `foldError` |
87
87
  | 413 `PAYLOAD_TOO_LARGE` | the body (6 MiB) or the decoded `document` (2 MiB) is over the cap | non-retryable. Decide: a `decisions` row `status: payload_too_large`, `chainStatus: skipped`, the ingest fails and alerts. Fold: `foldError` |
88
- | 400 `REVIEWER_GRANT_INVALID` | the fold's `reviewerGrant` is absent (the decision has a company), malformed, mis-signed, expired or mis-scoped | non-retryable; `foldError` — the reviewer answers again with a valid grant (the chain reviewer-grant routes land with CA-M5a) |
88
+ | 400 `REVIEWER_GRANT_INVALID` | the fold's `reviewerGrant` is absent (the decision has a company), malformed, mis-signed, expired or mis-scoped | non-retryable; `foldError` — the reviewer reads their grant from `GET /v1/collections/:id/reviewers` and answers again |
89
89
  | 403 `REVIEWER_NOT_AUTHORIZED` | the grant's root is not a current controller, or an oracle/evaluator is in the chain | non-retryable; `foldError` — ask a company reviewer |
90
90
  | 400 `VALIDATION` | the facts' shape is wrong | non-retryable; a producer bug — fails loudly |
91
91
  | 429 `RATE_LIMITED` | the engine's per-IP limiter | retryable; the step backs off (`Retry-After` honoured) |
@@ -120,8 +120,15 @@ pay the evaluation leg) is package-side only — the engine never sees it.
120
120
  `decision.status` is one of `decided` / `pending_review` / `unusable`. `receipt` is null ONLY for
121
121
  `pending_review` (it is minted after the fold); `decided` and `unusable` both carry one — an
122
122
  `unusable` receipt proves the engine refused and why, in codes. The package stores both `cid` and
123
- `jws` on the decision row, signs `MsgEvaluateClaim` only for `decided`, and puts the receipt CID in
124
- `verification_proof`. Receipts resolve publicly at the engine's `GET /v1/receipts/:cid`.
123
+ `jws` on the decision row and, for a `decided` verdict, signs `MsgEvaluateClaim` with the receipt
124
+ CID as `verification_proof`. Receipts resolve publicly at the engine's `GET /v1/receipts/:cid`.
125
+
126
+ Since 0.2.31 a `pending_review` claim is signed too (when signing is on), so the chain shows that a
127
+ person is deciding: `MsgEvaluateClaim` with status FLAGGED (EvaluationStatus `5`), no amount, and
128
+ `verification_proof` = the decision's `resultDigest` (there is no receipt before the fold). The
129
+ decision row's `chainStatus` becomes `flagged`. FLAGGED pays nothing and is not final: the fold's
130
+ verdict is signed over it later, with the fold's receipt CID. Each `MsgEvaluateClaim` uses one slot
131
+ of the evaluate grant's `agentQuota`, so a reviewed claim uses TWO.
125
132
 
126
133
  ## Human review
127
134
 
@@ -152,10 +159,6 @@ names no URL and any 408/429; retryable). Replacing a stored answer therefore ta
152
159
  signature. A fold's receipt names the attestation in `evidence` by CID, and the fold's Matrix card
153
160
  edit says "Signed by \<did\>".
154
161
 
155
- **Deploy order:** portal first (an older package's review body schema is not strict, so the extra
156
- field is harmless), then engine and package TOGETHER — both require it, and the engine's fold body
157
- schema is strict, so forwarding `attestation` to an older engine 400s every fold into `foldError`.
158
-
159
162
  ## Dev lane
160
163
 
161
164
  `POST /v1/decisions/preview` — the same request, with `rubric` allowed as `{ doc }` and
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: update-pricing
3
+ description: Use when a running oracle built on @ixo/decision-oracle should charge a different price, become free, or charge per protocol — "make my checker cost $1", "make checks free", "change the evaluation fee", "change the Decision API price", "the app still shows the old price", a hire or register refused 402.
4
+ ---
5
+
6
+ # Changing an oracle's price
7
+
8
+ ## Overview
9
+
10
+ An oracle has up to three prices. Each lives in ONE place, and each needs a redeploy (and, for
11
+ what a card shows, a card refresh) to change:
12
+
13
+ | Price | What it is | Where it is set | Who pays |
14
+ | --- | --- | --- | --- |
15
+ | **Check fee** (`pricing.evaluation`) | the minimum on-chain evaluation payment a collection must carry; checked when a collection is hired or registered | `src/index.ts` `defineOracle({ pricing: { evaluation } })`, replaced per env by the `EVALUATION_DENOM` + `EVALUATION_MIN_AMOUNT` vars (REQUIRED on mainnet) | the collection (the ask's escrow), paid by the chain on every evaluation |
16
+ | **Per-protocol fee** (`pricing.byProtocol`) | a different check fee for named protocols (incl. `"0"`) | `defineOracle({ pricing: { byProtocol: { "<protocol did>": { denom, minAmount } } } })` | same |
17
+ | **Decision API price** (`pricing.api`) | the price per decision on the HTTP lane (`/v1/api`), micro-USD | `defineOracle({ pricing: { api: { amountMicroUsd, byProtocol? } } })`, or the `API_PRICE_USD_CENTS` var when `pricing.api` is absent | the API caller (x402), held on accept, paid on the receipt |
18
+
19
+ Separate from all three: the **engine's per-decision fee** is billed to the account that registered
20
+ the oracle (`decision-oracle engine register`). You don't set it here; it's your plan with the
21
+ decision engine.
22
+
23
+ **Core rule: every step is an action you perform, in order. The price change is a deploy, and the
24
+ card refresh signs on chain — dry run first, show it, and run with `--yes` only on the owner's OK.**
25
+
26
+ ## When to use
27
+
28
+ - The owner wants a new check fee, a free oracle, or a different fee for one protocol.
29
+ - The Decision API price changes.
30
+ - An app (e.g. the Openhands checker picker) still shows the old price.
31
+ - Hires or registers are refused `402 evaluation_underpaid`.
32
+
33
+ ## The run — in order
34
+
35
+ Work from the oracle project's directory.
36
+
37
+ ### 1. Read what is live now
38
+
39
+ ```bash
40
+ curl -s https://<host>/v1/info | jq '{pricing, supportedProtocols, openhands}'
41
+ ```
42
+
43
+ `pricing.evaluation` is what the hire/register gate demands today; `pricing.api` is the API price.
44
+
45
+ ### 2. Pick the coin and the amount
46
+
47
+ Amounts are **integer base units** (6 decimals: `1000000` = 1 coin). The denom is matched EXACTLY.
48
+
49
+ - **Third-party oracle:** IXO (`uixo`) anywhere, or on mainnet USDC — USDC.inj,
50
+ `ibc/77C12480BD8C29BF0ACEC2143736C15BAF8CD232CA27BAE09336677B828955E6`. **Never PAY (`upay`)**:
51
+ register refuses it on an oracle IXO does not run (`pay_requires_ixo_managed`).
52
+ - **IXO-managed oracle:** PAY (`upay`, 1 PAY = $1) is allowed — but only once the decision engine
53
+ marks the oracle `ixoManaged` on its plan (`GET /v1/oracles/me`). IXO sets that plan, and only
54
+ after the oracle is registered (`engine register`). Until then every hire/register in `upay` is
55
+ refused `409 pay_requires_ixo_managed`. Order: register → IXO sets the plan → first hire.
56
+ - **Free:** `minAmount: "0"` — any evaluation leg, or none, is accepted. Bound your engine spend
57
+ with an `openhands.admit` hook (an allow-list, a daily budget) if anyone can hire you.
58
+ - Price it so your 80% share covers the engine's per-decision fee: the chain pays each evaluation
59
+ payment 80% to the oracle, 10% to the network and 10% to the relayer node (chain parameters).
60
+
61
+ ### 3. Edit — both places
62
+
63
+ 1. `src/index.ts`: `pricing.evaluation` (devnet/testnet use it as written), plus `byProtocol` /
64
+ `api` if those change.
65
+ 2. `wrangler.jsonc`: in EVERY env that sets them, `EVALUATION_DENOM` and `EVALUATION_MIN_AMOUNT`
66
+ (both or neither). Mainnet MUST set both — the worker refuses to start without them — and they
67
+ win over `src/index.ts` there. Named envs don't inherit vars: edit each one.
68
+ 3. Update anything in your README / protocol docs that quotes the old price.
69
+
70
+ Then `bun run check`.
71
+
72
+ ### 4. Deploy, per env
73
+
74
+ ```bash
75
+ bunx wrangler deploy --env <env> # testnet first, then production
76
+ curl -s https://<host>/v1/info | jq .pricing
77
+ ```
78
+
79
+ `/v1/info` must show the new `pricing` before you go on. No migration is needed for a price change.
80
+
81
+ ### 5. Refresh the Agent Card (what apps show)
82
+
83
+ The card's price is what portals and the Openhands picker display. It does NOT update by itself.
84
+
85
+ ```bash
86
+ bunx decision-oracle oracle init --protocol <your protocol did> --kit-url https://<host> \
87
+ --logo <your CURRENT logo url> [--openhands] --network <net> # dry run: only `acard do`
88
+ # owner's OK, then the same line with --yes
89
+ ```
90
+
91
+ - Pass your **current** logo URL (read it from your entity's `#pro`), or the run may replace it.
92
+ - The Openhands card's price is `pricing.api` if set, else `--price-pay <n>`, else the check fee when
93
+ it is in `upay`. `--price-pay` must be positive; for a free check in PAY just drop the flag.
94
+ - The dry run should show only `acard do replace (drifted …)`; everything else `skip`.
95
+
96
+ Verify: read the card back (`#acard` on your oracle entity) and check `price`.
97
+
98
+ ### 6. Tell the apps
99
+
100
+ Apps that escrow your fee read `/v1/info` `pricing.evaluation` and your card. If an app hard-codes a
101
+ price for your oracle, tell its team the new `{denom, minAmount}`: an ask escrowing less is refused
102
+ 402 at hire and its claims are never judged.
103
+
104
+ ## What changes for collections already hired
105
+
106
+ - The gate runs when a collection is hired/registered, not afterwards. A collection already watched
107
+ keeps working at whatever evaluation leg it carries, and the chain pays that leg.
108
+ - New hires must meet the new minimum.
109
+ - A collection whose admin account cannot cover the leg on an evaluation is parked
110
+ `admin-unfunded` until it is topped up (the owner is told the amount).
111
+
112
+ ## Where the fee lands
113
+
114
+ On every final `MsgEvaluateClaim` (approve AND reject) the chain pays the collection's evaluation
115
+ leg to the evaluating agent — your oracle's `ORACLE_ADDRESS` — less the network and relayer shares.
116
+ A claim marked FLAGGED while a person reviews pays nothing until its final verdict.
117
+
118
+ ## Troubleshooting
119
+
120
+ | Symptom | Cause → fix |
121
+ | --- | --- |
122
+ | Worker fails to start on mainnet | `EVALUATION_DENOM` / `EVALUATION_MIN_AMOUNT` missing or only one set → set both |
123
+ | `/v1/info` shows the old price after deploy | mainnet: the env vars win over `src/index.ts` → edit the env's vars |
124
+ | Hire/register `402 evaluation_underpaid` | the collection's evaluation leg is below the minimum or in another denom → the app must escrow your coin and amount |
125
+ | Register refuses `pay_requires_ixo_managed` | PAY on an oracle the engine does not mark `ixoManaged` → use `uixo` or USDC.inj, or (IXO-run) have IXO set the plan after `engine register` |
126
+ | App shows the old price | card not refreshed (step 5) or the app hard-codes it (step 6) |
127
+ | `--price-pay must be a positive PAY amount` | free check: drop `--price-pay` |
128
+
129
+ ## Output contract
130
+
131
+ Say plainly: the old and new `pricing` per env (from `/v1/info`), the deployed version per env, the
132
+ card refresh tx (or why it was skipped), and which apps were told.
@@ -148,7 +148,7 @@ failure it is most likely to meet (an unmatched URL in `fakeTools` is a network
148
148
  ### 5. Run them against a real claim
149
149
 
150
150
  ```bash
151
- bunx decision-oracle facts run --protocol <protocol did> --claim protocol/sample-claim.json \
151
+ bunx decision-oracle facts run --protocol <protocol did> --claim claim.json \
152
152
  [--collection <id>] [--preview] [--facts src/facts/index.ts] [--network devnet] [--json]
153
153
  ```
154
154
 
@@ -156,12 +156,22 @@ Pulls the live manifest, runs your table over that claim, and prints the facts p
156
156
  issue (exit 1 if there are issues). `--preview` sends them to the engine's preview lane and prints
157
157
  the decision they would produce — nothing persisted, nothing billed, no grant needed.
158
158
 
159
- `--claim` takes either a flat answers object (the same file `submit-claim --answers` posts — the
160
- passport example's `protocol/sample-claim.json` is one) or the `{ answers, attachments?: [...] }`
161
- envelope. A flat file has no attachments, so a handler that reads one fails closed: run the passport
162
- example against its sample claim and the AI fact comes back `error` (the guard's path), not a stock
163
- verdict. To exercise a lane that needs files, hand `--claim` an envelope whose `attachments` entry
164
- points at a local photo.
159
+ `--claim` takes either a flat answers object (the claim form's answers, keyed by question name —
160
+ the same file `submit-claim --answers` posts) or an envelope when a handler needs files:
161
+
162
+ ```json
163
+ {
164
+ "answers": { "orderId": "ORD-1", "deliveredAt": "2026-11-01T10:00:00Z" },
165
+ "attachments": [
166
+ { "question": "deliveryPhoto", "name": "door.jpg", "mediaType": "image/jpeg", "path": "./door.jpg" }
167
+ ]
168
+ }
169
+ ```
170
+
171
+ `question` is the form question the file answers (what `tools.attachments.on()` filters by) and
172
+ `path` a local file, relative to the claim file. A flat file has no attachments, so a handler that reads one fails closed: an AI
173
+ check over a photo comes back `error` (its guard's path), not a stock verdict. To exercise a lane
174
+ that needs files, use the envelope.
165
175
 
166
176
  ## The rules a handler must never break
167
177
 
@@ -175,10 +185,12 @@ points at a local photo.
175
185
  manifest is the only work order, and the engine is the only judge.
176
186
  - **Read the agreement last.** `tools.agreement()` right before returning the fact it feeds — the
177
187
  engine records the snapshot current at decision time on its receipt.
178
- - **Read the chain through `tools.chain`, never by hand.** `linkedResources(entityDid)` for what an
179
- entity anchors (and each resource's `proof`), `messages(filter)` for what an account did on chain
180
- and when, `query()` for chain RPC. An empty answer is a FACT ("nothing anchored", "no such
181
- message"); an unreachable Blocksync throws, and the step retries.
188
+ - **Read the chain through `tools.chain`, never by hand.** It has three reads, all read-only:
189
+ `linkedResources(entityDid)` for what an entity anchors (and each resource's `proof`),
190
+ `claimIntent({ agentAddress, collectionId, before, after? })` for the newest `MsgClaimIntent` that
191
+ agent filed on the collection (or `null`), and `query()` for the chain RPC query client (balances,
192
+ grants, module state). There is no general message-history read. An empty answer is a FACT
193
+ ("nothing anchored", "no intent filed"); an unreachable Blocksync throws, and the step retries.
182
194
  - **One clock, one fan-out.** Use `ctx.now`, never `Date.now()`. Don't spawn your own concurrency:
183
195
  the runner already caps ext+ai at 4 in flight and caches every answer so a step retry never
184
196
  re-buys it.
@@ -1,7 +1,7 @@
1
1
  # The facts API — cheat sheet
2
2
 
3
- Full reference: `node_modules/@ixo/decision-oracle/docs/api.md` (or `packages/eval-oracle/docs/api.md`
4
- in this repo) — every export, every default, every cap. This page is the working subset.
3
+ Full reference: `node_modules/@ixo/decision-oracle/docs/api.md` — every export, every default,
4
+ every cap. This page is the working subset.
5
5
 
6
6
  Everything a handler is handed, everything it may hand back, and the rules the runner enforces
7
7
  around it. Import surface: `@ixo/decision-oracle/facts` (node-safe) and `@ixo/decision-oracle/testing`
@@ -26,9 +26,10 @@ asks for, sends them to the IXO decision engine, and signs the verdict on chain.
26
26
 
27
27
  `.claude/skills/` holds the skills shipped with the package — `create-oracle` (the build runbook),
28
28
  `write-facts` (one fact handler at a time), `create-rubric`, `publish-protocol`,
29
- `create-collection`, `decision-api-oracle` (the Decision API lane: deploy, register, verify). Use
30
- them for those workflows rather than improvising, and refresh them after upgrading:
31
- `bunx decision-oracle skills sync`.
29
+ `create-collection`, `decision-api-oracle` (the Decision API lane: deploy, register, verify),
30
+ `decision-api-client` (calling it as a customer), `add-openhands-support`, `update-pricing` (changing
31
+ the price) and `operate-oracle` (upgrades, pauses, renewals, alerts). Use them for those workflows
32
+ rather than improvising, and refresh them after upgrading: `bunx decision-oracle skills sync`.
32
33
 
33
34
  ## The two rules
34
35