@ixo/decision-oracle 0.2.30 → 0.2.32
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.
- package/README.md +5 -4
- package/dist/contracts/api.d.ts +6 -0
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/contracts/api.js +1 -0
- package/dist/contracts/api.js.map +1 -1
- package/dist/contracts/webhooks.d.ts +7 -0
- package/dist/contracts/webhooks.d.ts.map +1 -1
- package/dist/db/schema.d.ts +2 -2
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +3 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/lib/ixo/chain-write.d.ts +5 -3
- package/dist/lib/ixo/chain-write.d.ts.map +1 -1
- package/dist/lib/ixo/chain-write.js +68 -2
- package/dist/lib/ixo/chain-write.js.map +1 -1
- package/dist/routes/v1/api/openapi.gen.d.ts.map +1 -1
- package/dist/routes/v1/api/openapi.gen.js +1 -1
- package/dist/routes/v1/api/openapi.gen.js.map +1 -1
- package/dist/routes/v1/shared.d.ts +1 -1
- package/dist/workflows/claim-ingest.d.ts.map +1 -1
- package/dist/workflows/claim-ingest.js +4 -3
- package/dist/workflows/claim-ingest.js.map +1 -1
- package/docs/api.md +10 -2
- package/docs/guide.md +7 -3
- package/openapi/decision-api.yaml +7 -1
- package/package.json +2 -2
- package/skills/README.md +5 -3
- package/skills/create-collection/SKILL.md +35 -11
- package/skills/create-collection/reference/collection.md +1 -1
- package/skills/create-oracle/SKILL.md +73 -25
- package/skills/decision-api-client/SKILL.md +19 -25
- package/skills/decision-api-oracle/SKILL.md +150 -145
- package/skills/operate-oracle/SKILL.md +234 -0
- package/skills/publish-protocol/SKILL.md +3 -2
- package/skills/reference/engine-contract.md +11 -8
- package/skills/update-pricing/SKILL.md +129 -0
- package/skills/write-facts/SKILL.md +23 -11
- package/skills/write-facts/reference/facts-api.md +2 -2
- package/templates/project/CLAUDE.md +4 -3
|
@@ -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.
|
|
42
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,129 @@
|
|
|
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.
|
|
53
|
+
- **Free:** `minAmount: "0"` — any evaluation leg, or none, is accepted. Bound your engine spend
|
|
54
|
+
with an `openhands.admit` hook (an allow-list, a daily budget) if anyone can hire you.
|
|
55
|
+
- Price it so your 80% share covers the engine's per-decision fee: the chain pays each evaluation
|
|
56
|
+
payment 80% to the oracle, 10% to the network and 10% to the relayer node (chain parameters).
|
|
57
|
+
|
|
58
|
+
### 3. Edit — both places
|
|
59
|
+
|
|
60
|
+
1. `src/index.ts`: `pricing.evaluation` (devnet/testnet use it as written), plus `byProtocol` /
|
|
61
|
+
`api` if those change.
|
|
62
|
+
2. `wrangler.jsonc`: in EVERY env that sets them, `EVALUATION_DENOM` and `EVALUATION_MIN_AMOUNT`
|
|
63
|
+
(both or neither). Mainnet MUST set both — the worker refuses to start without them — and they
|
|
64
|
+
win over `src/index.ts` there. Named envs don't inherit vars: edit each one.
|
|
65
|
+
3. Update anything in your README / protocol docs that quotes the old price.
|
|
66
|
+
|
|
67
|
+
Then `bun run check`.
|
|
68
|
+
|
|
69
|
+
### 4. Deploy, per env
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bunx wrangler deploy --env <env> # testnet first, then production
|
|
73
|
+
curl -s https://<host>/v1/info | jq .pricing
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`/v1/info` must show the new `pricing` before you go on. No migration is needed for a price change.
|
|
77
|
+
|
|
78
|
+
### 5. Refresh the Agent Card (what apps show)
|
|
79
|
+
|
|
80
|
+
The card's price is what portals and the Openhands picker display. It does NOT update by itself.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bunx decision-oracle oracle init --protocol <your protocol did> --kit-url https://<host> \
|
|
84
|
+
--logo <your CURRENT logo url> [--openhands] --network <net> # dry run: only `acard do`
|
|
85
|
+
# owner's OK, then the same line with --yes
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- Pass your **current** logo URL (read it from your entity's `#pro`), or the run may replace it.
|
|
89
|
+
- The Openhands card's price is `pricing.api` if set, else `--price-pay <n>`, else the check fee when
|
|
90
|
+
it is in `upay`. `--price-pay` must be positive; for a free check in PAY just drop the flag.
|
|
91
|
+
- The dry run should show only `acard do replace (drifted …)`; everything else `skip`.
|
|
92
|
+
|
|
93
|
+
Verify: read the card back (`#acard` on your oracle entity) and check `price`.
|
|
94
|
+
|
|
95
|
+
### 6. Tell the apps
|
|
96
|
+
|
|
97
|
+
Apps that escrow your fee read `/v1/info` `pricing.evaluation` and your card. If an app hard-codes a
|
|
98
|
+
price for your oracle, tell its team the new `{denom, minAmount}`: an ask escrowing less is refused
|
|
99
|
+
402 at hire and its claims are never judged.
|
|
100
|
+
|
|
101
|
+
## What changes for collections already hired
|
|
102
|
+
|
|
103
|
+
- The gate runs when a collection is hired/registered, not afterwards. A collection already watched
|
|
104
|
+
keeps working at whatever evaluation leg it carries, and the chain pays that leg.
|
|
105
|
+
- New hires must meet the new minimum.
|
|
106
|
+
- A collection whose admin account cannot cover the leg on an evaluation is parked
|
|
107
|
+
`admin-unfunded` until it is topped up (the owner is told the amount).
|
|
108
|
+
|
|
109
|
+
## Where the fee lands
|
|
110
|
+
|
|
111
|
+
On every final `MsgEvaluateClaim` (approve AND reject) the chain pays the collection's evaluation
|
|
112
|
+
leg to the evaluating agent — your oracle's `ORACLE_ADDRESS` — less the network and relayer shares.
|
|
113
|
+
A claim marked FLAGGED while a person reviews pays nothing until its final verdict.
|
|
114
|
+
|
|
115
|
+
## Troubleshooting
|
|
116
|
+
|
|
117
|
+
| Symptom | Cause → fix |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| Worker fails to start on mainnet | `EVALUATION_DENOM` / `EVALUATION_MIN_AMOUNT` missing or only one set → set both |
|
|
120
|
+
| `/v1/info` shows the old price after deploy | mainnet: the env vars win over `src/index.ts` → edit the env's vars |
|
|
121
|
+
| 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 |
|
|
122
|
+
| Register refuses `pay_requires_ixo_managed` | PAY on a third-party oracle → use `uixo` or USDC.inj |
|
|
123
|
+
| App shows the old price | card not refreshed (step 5) or the app hard-codes it (step 6) |
|
|
124
|
+
| `--price-pay must be a positive PAY amount` | free check: drop `--price-pay` |
|
|
125
|
+
|
|
126
|
+
## Output contract
|
|
127
|
+
|
|
128
|
+
Say plainly: the old and new `pricing` per env (from `/v1/info`), the deployed version per env, the
|
|
129
|
+
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
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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.**
|
|
179
|
-
entity anchors (and each resource's `proof`),
|
|
180
|
-
|
|
181
|
-
|
|
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`
|
|
4
|
-
|
|
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)
|
|
30
|
-
|
|
31
|
-
`
|
|
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
|
|