@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.
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: decision-api-oracle
3
- description: Use when setting up, deploying or testing a Decision API oracle (API_LANE on) — the shipment-delivery example on devnet (shipment-delivery-oracle.devnet.ixo.earth), or a provider turning the lane on for their own oracle — or when debugging one - a 401 on the console, PROTOCOL_NOT_ALLOWED, 503 payments-disabled, plan_inactive, an oracle the Decision Console does not list, member or review emails that never arrive.
3
+ description: Use when setting up, deploying or testing a Decision API oracle (API_LANE on) — a provider turning the lane on for their own oracle, from protocol to a verified paid claim — or when debugging one - a 401 on the console, PROTOCOL_NOT_ALLOWED, 503 payments-disabled, plan_inactive, an oracle the Decision Console does not list, member or review emails that never arrive.
4
4
  ---
5
5
 
6
- # A Decision API oracle, from nothing to a passing smoke
6
+ # A Decision API oracle, from nothing to a verified claim
7
7
 
8
8
  ## Overview
9
9
 
@@ -14,73 +14,77 @@ plan from the moment it registers, unless IXO sets another);
14
14
  the payments facilitator settles each claim the ways that plan allows — by card and/or USDC on Base
15
15
  for a third-party oracle, PAY only for IXO-run ones — into the oracle's own earnings addresses
16
16
  (`EARNINGS_ADDRESS` / `EARNINGS_BASE_WALLET`); the Decision Console
17
- (`decision-console.devnet.ixo.earth`) is the UI over all of it.
17
+ (`decision-console.devnet.ixo.earth` on devnet) is the UI over all of it.
18
18
 
19
- The worked run is IXO's own example deployment, **SwiftDrop Express** (`shipment-delivery`) — a
20
- fictional last-mile courier with cash on delivery, IXO's devnet deployment at
21
- `shipment-delivery-oracle.devnet.ixo.earth`. Its README has the outcome table, the demo orders API
22
- and the courier demo page (`/courier`). A provider's own project (from
23
- `decision-oracle init`) follows the same steps with their names. Where the example has a script the
24
- template does not (`deploy:devnet`, an env-scoped `db:migrate:remote`, `bun run smoke`), the step
25
- says **example** and gives the template's equivalent — use that in a provider's project.
19
+ The run below is a provider's own project (from `decision-oracle init`), deployed to one wrangler
20
+ env (`devnet`, `testnet` or `production` in the template). `<project-name>` is the name `init` was
21
+ given, `<env>` the wrangler env, `<host>` that env's `ORACLE_HOSTNAME`.
26
22
 
27
23
  **Core principle: every step is an action you perform, in this order.** Run the commands; don't
28
24
  recite them. Steps only a human can do are marked **HUMAN** — say exactly what to do and wait.
29
25
  **Anything that signs on chain or changes an identity (publish, `oracle init`, a key the oracle
30
26
  already uses) is dry-run first, shown, and run with `--yes` only on the owner's explicit OK.**
31
27
 
28
+ **IXO's public demo.** SwiftDrop Express, a fictional courier with cash on delivery, runs this lane
29
+ on devnet at `https://shipment-delivery-oracle.devnet.ixo.earth` (its courier page is `/courier`).
30
+ Use it to see the lane working from the customer's side (`decision-api-client`); build your own
31
+ from the steps below.
32
+
32
33
  ## When to use
33
34
 
34
- - "Set up / deploy / test the Decision API example", "stand up shipment-delivery-oracle on devnet"
35
- - A provider turning `API_LANE` on for their own oracle
35
+ - "Turn on / deploy / test the Decision API for my oracle"
36
+ - A provider adding `API_LANE` to an oracle that already serves the chain lane
36
37
  - Any symptom in the troubleshooting table below
37
38
 
38
39
  Not for: the chain lane's collections (`create-collection`), authoring a rubric (`create-rubric`),
39
- writing handlers (`write-facts`).
40
+ writing handlers (`write-facts`), changing the price after launch (`update-pricing`), day-2
41
+ operations (`operate-oracle`).
40
42
 
41
43
  ## The run — in order
42
44
 
43
- Work from the project directory. `bunx decision-oracle`
44
- is the package CLI.
45
+ Work from the project directory. `bunx decision-oracle` is the package CLI.
45
46
 
46
47
  ### 0. Prerequisites
47
48
 
48
49
  ```bash
49
- bunx wrangler whoami # the Cloudflare account the oracle deploys to
50
- bunx decision-oracle login --network devnet # the OWNER's AuthHub session — once
50
+ bunx wrangler whoami # the Cloudflare account the oracle deploys to
51
+ bunx decision-oracle login --network devnet # the OWNER's AuthHub session — once per network
51
52
  ```
52
53
 
53
54
  - `login` opens a browser: run it in the background and tell the human to finish there (**HUMAN**).
54
55
  The session is the owner: it publishes the protocol, owns the oracle entity, and is the operator.
55
56
  Put the session's OWN DID (`did:ixo:<address>` — `login` prints the address) in
56
- `ORACLE_ADMIN_DIDS` in every `vars` block of `wrangler.jsonc`. The example ships IXO's DID there —
57
- a copy of it must replace it with the owner's.
58
- - A custom domain is optional. Without one, delete each env's `routes` entry and set that env's `ORACLE_HOSTNAME` to the Worker's free `<worker name>.<account subdomain>.workers.dev` host (the first deploy prints it). With one, the account must hold the zone of the host, or the Custom Domain cannot attach — if `whoami` shows another account, stop and say so (**HUMAN**). Either way the host IS the did:web: pick it before `oracle init`.
57
+ `ORACLE_ADMIN_DIDS` in every `vars` block of `wrangler.jsonc`.
58
+ - A custom domain is optional. Without one, delete each env's `routes` entry and set that env's
59
+ `ORACLE_HOSTNAME` to the Worker's free `<worker name>.<account subdomain>.workers.dev` host (the
60
+ first deploy prints it). With one, the account must hold the zone of the host, or the Custom
61
+ Domain cannot attach — if `whoami` shows another account, stop and say so (**HUMAN**). Either way
62
+ the host IS the did:web: pick it before `oracle init`.
63
+
64
+ ### 1. The protocol
59
65
 
60
- ### 1. The protocol (example: publish `protocol/`; a provider: their protocol's DID)
66
+ Use an existing protocol's DID, or publish your own form and rubric (the `create-rubric` and
67
+ `publish-protocol` skills):
61
68
 
62
69
  ```bash
63
70
  bunx decision-oracle rubric validate --rubric protocol/rubric.json --form protocol/form.json
64
71
  bunx decision-oracle publish-protocol --rubric protocol/rubric.json --form protocol/form.json --network devnet
65
72
  ```
66
73
 
67
- The second line is a dry run: show the plan, get the owner's OK, then re-run it with `--yes`. The
68
- `publish-protocol` skill has the phases and failure modes. Then put the printed protocol DID in
69
- `src/protocol.ts` (`SHIPMENT_PROTOCOL_DID = "did:ixo:entity:…"`) and keep whatever
70
- `publish-protocol` left in `protocol/` — those are the anchored bytes. The rubric's four `ext`
71
- sources share the endpoint `https://shipment-delivery-oracle.devnet.ixo.earth/demo/orders`: it
72
- answers once step 5 deploys. Each is called with a UCAN from this oracle's did:web to the
73
- endpoint's own did:web (`did:web:shipment-delivery-oracle.devnet.ixo.earth`), capability
74
- `ixo:external-source` — the oracle never signs a capability the rubric names, and never an IXO
75
- service's (`api.md`, "How an external source is called"). Check the work order:
74
+ The second line is a dry run: show the plan, get the owner's OK, then re-run it with `--yes`. Keep
75
+ whatever `publish-protocol` left in `protocol/` — those are the anchored bytes. Put the protocol DID
76
+ in `supportedProtocols` in `src/index.ts`. Then read the work order:
76
77
 
77
78
  ```bash
78
79
  bunx decision-oracle protocol review <protocol did> --network devnet
79
80
  ```
80
81
 
81
- It must list `ext.orderKnown`, `ext.deliveryCodeValid`, `ext.cashReconciled`,
82
- `ext.dropoffNearAddress` (custom — one group of ours), `ai.photoShowsDelivery`, `unique`,
83
- `frequency`.
82
+ It lists every `ext` source and `ai` check the oracle must answer, plus any `unique` / `frequency`
83
+ rule. Write a handler for each name that needs one (`write-facts`) and get `bun run check` green.
84
+ An `ext` source is called with a UCAN from this oracle's did:web to the endpoint's own did:web,
85
+ capability `ixo:external-source` — the oracle never signs a capability the rubric names, and never
86
+ an IXO service's (`api.md`, "How an external source is called"). An endpoint you host yourself must
87
+ be live before claims arrive.
84
88
 
85
89
  ### 2. Identity
86
90
 
@@ -88,17 +92,20 @@ Do `create-oracle` step 1 (two `derive --new --write` runs, the first `--as SERV
88
92
  the second is `ORACLE_SECP_MNEMONIC`) and step 6 (the owner's engine delegation), with:
89
93
 
90
94
  - the three public lines (`ORACLE_ADDRESS`, `ORACLE_DID`, `ORACLE_MATRIX_USER_ID`) written into BOTH
91
- the top-level and `env.devnet` `vars` of `wrangler.jsonc`;
95
+ the top-level and `env.<env>` `vars` of `wrangler.jsonc`;
92
96
  - the delegation minted for THIS host:
93
- `bunx decision-oracle delegation --for engine --audience did:web:decision-engine.devnet.ixo.earth --oracle did:web:shipment-delivery-oracle.devnet.ixo.earth --out owner-delegation.car --yes`.
97
+ `bunx decision-oracle delegation --for engine --audience did:web:<engine host> --oracle did:web:<host> --out owner-delegation.car --yes`
98
+ (the engine host is `decision-engine.devnet.ixo.earth`, `decision-engine.testnet.ixo.earth` or
99
+ `decision-engine.ixo.earth`).
94
100
 
95
101
  An oracle that already has keys keeps them — replacing a live oracle's key is an identity change:
96
102
  ask first. Never print a mnemonic or the CAR.
97
103
 
104
+ Turn the lane on in that env's `vars`: `"API_LANE": "true"`.
105
+
98
106
  ### 3. Secrets
99
107
 
100
- Write `.dev.vars.<env>` (git-ignored, like `.dev.vars`; the example's is `.dev.vars.devnet`) —
101
- never echo a value to the terminal:
108
+ Write `.dev.vars.<env>` (git-ignored, like `.dev.vars`) — never echo a value to the terminal:
102
109
 
103
110
  ```bash
104
111
  {
@@ -106,31 +113,41 @@ never echo a value to the terminal:
106
113
  printf 'OWNER_DELEGATION_CAR="%s"\n' "$(cat owner-delegation.car)"
107
114
  printf 'WEBHOOK_ENCRYPTION_KEY="%s"\n' "$(openssl rand -base64 32)"
108
115
  printf 'UNIQUE_HMAC_KEY="%s"\n' "$(openssl rand -base64 32)"
109
- } > .dev.vars.<env>
116
+ } > .dev.vars.<env> && chmod 600 .dev.vars.<env>
110
117
  ```
111
118
 
112
- The example's AI photo check runs on the Cloudflare AI Gateway (`AI_GATEWAY_ID` in
113
- `wrangler.jsonc` + the `ai` binding — no secret; the account needs AI Gateway credits). Only with
114
- `AI_PROVIDER=openrouter`: `OPENROUTER_API_KEY` — ask the owner for the key, **HUMAN**, and append it
115
- the same way. Optional: `SENTRY_DSN`. `ORACLE_MATRIX_ACCESS_TOKEN` is required but
116
- comes later — `oracle init` writes it (step 7).
119
+ AI checks run on the Cloudflare AI Gateway by default (`AI_GATEWAY_ID` in `wrangler.jsonc` + the
120
+ `ai` binding — no secret; the account needs AI Gateway credits). Only with `AI_PROVIDER=openrouter`:
121
+ `OPENROUTER_API_KEY` — ask the owner for the key, **HUMAN**, and append it the same way. Optional:
122
+ `SENTRY_DSN`. `ORACLE_MATRIX_ACCESS_TOKEN` is required but comes later — `oracle init` writes it
123
+ (step 7).
117
124
  `WEBHOOK_ENCRYPTION_KEY` and `UNIQUE_HMAC_KEY` are for life — generate them ONCE per deployment;
118
125
  on an existing deployment, never regenerate (rotating strands every webhook secret / blinds the
119
- duplicate registry). `.dev.vars` needs its own `WEBHOOK_ENCRYPTION_KEY` for `bun run dev`.
126
+ duplicate registry). **Keep a private backup of this file outside the repo** (a password manager,
127
+ or a 0600 file only the owner can read): Cloudflare never shows a secret again. `.dev.vars` needs
128
+ its own `WEBHOOK_ENCRYPTION_KEY` for `bun run dev`.
120
129
 
121
130
  ### 4. Cloudflare resources
122
131
 
123
- The example's names (a provider: their own, one set per env; `<env>` is the wrangler env you
124
- deploy — `devnet` in the example, `testnet` / `production` in the template):
132
+ One set per env, named as `init` printed them (the env's block already names the database and the
133
+ bucket this way):
125
134
 
126
135
  ```bash
127
- bunx wrangler d1 create shipment-delivery-oracle-devnet # → env.<env> d1 database_id
128
- bunx wrangler kv namespace create <project>-cache-devnet # → env.<env> kv id
129
- bunx wrangler r2 bucket create shipment-delivery-oracle-uploads-devnet
136
+ bunx wrangler d1 create <project-name>-<env> # → env.<env> d1 database_id
137
+ bunx wrangler kv namespace create <project-name>-cache-<env> # → env.<env> kv id
138
+ bunx wrangler r2 bucket create <project-name>-uploads-<env> # optional: file uploads
130
139
  ```
131
140
 
132
141
  Paste each printed id over its placeholder in `env.<env>` (only there — the top level is local
133
- dev). Then `bun run cf-typegen` and `bun run check`.
142
+ dev). For uploads, uncomment that env's `API_UPLOADS` line in `wrangler.jsonc`:
143
+
144
+ ```jsonc
145
+ "r2_buckets": [{ "binding": "API_UPLOADS", "bucket_name": "<project-name>-uploads-<env>" }],
146
+ ```
147
+
148
+ Without it `PUT /v1/api/uploads` answers 501 and claims can still attach https URLs. If you also
149
+ bind `ANSWERS_ARCHIVE`, both go in the ONE `r2_buckets` array. Then `bun run cf-typegen` and
150
+ `bun run check`.
134
151
 
135
152
  ### 5. Migrate and deploy
136
153
 
@@ -140,11 +157,6 @@ bunx wrangler deploy --env <env> --secrets-file .dev.vars.<env> # code + secre
140
157
  curl -s https://<host>/.well-known/did.json
141
158
  ```
142
159
 
143
- **Example:** `bun run db:migrate:remote` and `bun run deploy:devnet -- --secrets-file
144
- .dev.vars.devnet` are those two lines for `devnet`; also check the demo orders API and the
145
- courier page answer (`curl -s https://shipment-delivery-oracle.devnet.ixo.earth/demo/orders/ORD-1-C`,
146
- `/courier`). The template has no env-scoped migrate script — use the `wrangler` line.
147
-
148
160
  Later deploys: `bunx wrangler deploy --env <env>` (secrets persist); one secret: `bunx wrangler
149
161
  secret put <NAME> --env <env>`. The did.json `id` must be `did:web:<host>`.
150
162
 
@@ -163,29 +175,26 @@ bunx decision-oracle engine register --env <env> --env-file .dev.vars.<env>
163
175
  ```
164
176
 
165
177
  It proves the oracle's address to the engine (ADR-36) with the owner's delegation and prints the
166
- billing status. A 402 is the owner's credits: send them to the printed URL (**HUMAN**), then re-run.
178
+ billing status. A 402 is the owner's engine credits: the owner buys them in the Decision Console
179
+ (Billing › Credits), signed in as the account that minted the delegation (**HUMAN**), then re-run.
167
180
  If the engine already binds this did:web to a DIFFERENT address, it prints old → new and refuses:
168
181
  that is an identity change — ask the owner before re-running with `--yes`.
169
182
 
170
183
  Registering is the whole step for a provider: an oracle with no plan of its own runs at the
171
184
  engine's **Standard** plan at once (payable by card and/or USDC on Base, into the oracle's earnings
172
- addresses; listed in the Decision Console). PAY is for IXO-run oracles only. Nothing to request. Done when the oracle appears in the engine's
173
- `GET /v1/oracles` (the console's list).
174
-
175
- An oracle IXO runs for itself moves to an IXO plan only on IXO's side: ask IXO to move it
176
- (**HUMAN**), and it is done when IXO confirms the plan.
185
+ addresses; listed in the Decision Console). PAY is for IXO-run oracles only. Nothing to request.
186
+ Done when the oracle appears in the engine's `GET /v1/oracles` (the console's list).
177
187
 
178
188
  ### 7. On chain — the full identity (mandatory)
179
189
 
180
190
  Every oracle built with the kit has a full on-chain identity — the entity, its `#pro` profile, its
181
- `#dmn` domain card and its `#acard` Agent Card (which lists the `decision-api` service at the lane's published
182
- price) — even when the API lane is all it serves. **Required, not optional:** a deployed oracle
183
- without `ORACLE_ENTITY_DID` fails `/health/ready` (its `identity` check). In order:
191
+ `#dmn` domain card and its `#acard` Agent Card (which lists the `decision-api` service at the lane's
192
+ published price) — even when the API lane is all it serves. **Required, not optional:** a deployed
193
+ oracle without `ORACLE_ENTITY_DID` fails `/health/ready` (its `identity` check). In order:
184
194
 
185
- 1. Run `create-oracle` step 8 — `bunx decision-oracle oracle init --protocol <did> --kit-url https://shipment-delivery-oracle.devnet.ixo.earth --network devnet`
195
+ 1. Run `create-oracle` step 8 — `bunx decision-oracle oracle init --protocol <did> --kit-url https://<host> --logo <url|file> --network devnet`
186
196
  as a dry run, show the plan, and re-run the identical line with `--yes` only on the owner's OK.
187
- `--logo <url>` is optional but recommended (without it the portal and the console show a
188
- placeholder) — ask the owner for one.
197
+ Ask the owner for a logo (without it the portal and the console show a placeholder).
189
198
  2. Pin the entity DID it prints as `ORACLE_ENTITY_DID` in `wrangler.jsonc` — the top-level `vars`
190
199
  AND every `env.<name>.vars` (named envs don't inherit).
191
200
  3. `bunx wrangler secret put ORACLE_MATRIX_ACCESS_TOKEN --env <env>` with the value it wrote into
@@ -198,16 +207,16 @@ The oracle is not done until readiness passes. A 503 there names nothing; the op
198
207
 
199
208
  ### 8. Operator: a source, its protocol, its billing, a key
200
209
 
201
- The smoke (step 9) does all of this itself; for a real partner. A source belongs to an
202
- organisation entity (`--entity`): the partner's, or one made with `org create` (`org balance` /
203
- `org fund` check and top up its admin account):
210
+ A source belongs to an organisation entity (`--entity`): the partner's, or one made with
211
+ `org create` (`org balance` / `org fund` check and top up its admin account):
204
212
 
205
213
  ```bash
206
214
  bunx decision-oracle org create --name "<partner>" --network devnet --yes # → { entityDid } — once, idempotent by name
207
- K="--kit https://shipment-delivery-oracle.devnet.ixo.earth --network devnet"
215
+ K="--kit https://<host> --network devnet"
208
216
  bunx decision-oracle api source create --name "<partner>" --contact <email> --entity <org did> $K # as a controller of the org; pending-link; secret shown ONCE
209
- bunx decision-oracle api admin protocols <sourceId> <protocol did> $K # dry run → --yes
210
- bunx decision-oracle api admin billing-mode <sourceId> none $K # only for a partner who must not pay; dry run → --yes
217
+ bunx decision-oracle api source link <sourceId> $K # dry run → --yes; pending-link → active
218
+ bunx decision-oracle api admin protocols <sourceId> <protocol did> $K # dry run → --yes
219
+ bunx decision-oracle api admin billing-mode <sourceId> none $K # only for a partner who must not pay; dry run → --yes
211
220
  bunx decision-oracle api key create --source <sourceId> --scopes claims:write,claims:read,webhooks:manage $K
212
221
  bunx decision-oracle api review-billing enable --source <sourceId> $K # --cap <PAY> (default 1000); "Pay for reviews": as the org's CHAIN OWNER; dry run → --yes
213
222
  bunx decision-oracle api review-billing status --source <sourceId> $K
@@ -217,32 +226,36 @@ bunx decision-oracle api review-billing disable --source <sourceId> $K
217
226
  `review-billing` exists only while IXO's plan bills the oracle's reviews per review (`/v1/info`
218
227
  `reviewBilling.mode: "x402"`; `enable` refuses otherwise), and needs `API_LANE` AND `API_PAYMENTS`
219
228
  on, a published `pricing.api` price, and the organisation's collections created with
220
- `--eval-amount 0` (an empty evaluation leg — `create-collection`). The login must be the organisation's
221
- owner on chain — the reviews come out of ITS credits — and `--cap` (PAY) is a lifetime total,
222
- never refunded: raise it by running `enable` again with a higher `--cap`.
229
+ `--eval-amount 0` (an empty evaluation leg — `create-collection`). The login must be the
230
+ organisation's owner on chain — the reviews come out of ITS credits — and `--cap` (PAY) is a
231
+ lifetime total, never refunded: raise it by running `enable` again with a higher `--cap`.
223
232
 
224
233
  The source is `pending-link` (no keys, no claims) until a controller of the organisation signs its
225
234
  `SourceCompanyLink`, PUTs it to the engine and syncs — the Decision Console's "add oracle" does all
226
- three, and so does `bunx decision-oracle api source link <sourceId> --yes`. The session runs `source create` / `key create` only as a controller; the operator's own
227
- session should not be one where its oracle's reviews matter (the engine refuses a review chain
228
- through the oracle's owner).
235
+ three, and so does `api source link <sourceId> --yes`. The session runs `source create` /
236
+ `key create` only as a controller; the operator's own session should not be one where its oracle's
237
+ reviews matter (the engine refuses a review chain through the oracle's owner).
229
238
 
230
239
  A new source allows exactly the oracle's `supportedProtocols` (empty until step 1's DID is in
231
- `src/protocol.ts` and deployed) — `admin protocols` REPLACES the list.
232
-
233
- ### 9. Verify — the smoke (example)
234
-
235
- The smoke is the example's (`scripts/smoke.ts`, over its protocol and fixtures); a provider's
236
- project has none — verify theirs with step 8's key: `PUT /v1/api/uploads`, `POST /v1/api/claims`
237
- with `Prefer: wait=60`, one claim per outcome their rubric can reach, and the review through the
238
- console. An upload attachment carries the `cid` its upload returned —
239
- `attachments[].{ question, uploadId, cid }`; accept refuses one without it or with another upload's.
240
- For the example:
241
-
242
- ```bash
243
- bun run smoke -- --url https://shipment-delivery-oracle.devnet.ixo.earth \
244
- [--entity <org did>] [--org-controller <key file|address>] [--reviewer <address>]
245
- ```
240
+ `src/index.ts` and deployed) — `admin protocols` REPLACES the list.
241
+
242
+ ### 9. Verify — one claim per outcome
243
+
244
+ With step 8's key (`Authorization: Bearer <key>` on `https://<host>/v1/api`), or the
245
+ `@ixo/decision-oracle-client` SDK/CLI (`decision-api-client`):
246
+
247
+ 1. `GET /v1/api/protocols/<protocol did>` — the work order and the price.
248
+ 2. `PUT /v1/api/uploads` — a file, if the protocol takes one (needs step 4's bucket). An upload
249
+ attachment carries the `cid` its upload returned — `attachments[].{ question, uploadId, cid }`;
250
+ accept refuses one without it or with another upload's. Without uploads, attach
251
+ `{ question, url }` with an https link.
252
+ 3. `POST /v1/api/claims` with `Prefer: wait=60` — one claim per outcome the rubric can reach
253
+ (approve, reject, review), each with its own answers so the duplicate lane never sees a repeat.
254
+ Send `submitter` (your stable id for the person) when the rubric has a `frequency` rule.
255
+ 4. Answer a `review` claim in the Decision Console as a reviewer (below), and watch it fold to a
256
+ final decision.
257
+ 5. With `API_PAYMENTS` on and a source in billing mode `x402`, one paid claim — the
258
+ `decision-api-client` skill has the payer side.
246
259
 
247
260
  Three people, three keys — never the same account twice:
248
261
 
@@ -250,37 +263,21 @@ Three people, three keys — never the same account twice:
250
263
  `api admin billing-mode` and `api admin protocols`. It must NOT be the organisation's
251
264
  controller or the reviewer: the engine refuses any review grant chain through the oracle's
252
265
  identities, its owner included.
253
- - **the organisation's controller** (`--org-controller`, a key file or an account address; pass
254
- `--entity` with the organisation it controls — the smoke's defaults are the example's own
255
- devnet accounts): a source belongs to an organisation, so the controller creates it (`POST /v1/api/sources { entityDid }` →
256
- `pending-link`), signs the `SourceCompanyLink` (kid = the did:key of the Ed25519 key on its IID,
257
- canonical payload) and PUTs it to the ENGINE (`PUT /v1/sources/:oracleDid/:sourceId/company`,
258
- UCAN `company/link`), syncs (`POST /v1/api/sources/:id/company/sync` → `active`), mints and
259
- revokes the keys, and invites the reviewer. Its key needs no chain write: it already controls the
260
- entity.
261
- - **the reviewer** (`--reviewer <address|did>`, a third account that is neither of the two): a
262
- MEMBER. The
263
- controller invites it by DID with role `reviewer` and PUTs its role grant (≤ 30 days); the member
264
- registers a `did:key` reviewer key, grants it `claims/review` on the source and answers through
265
- the API with that two-link `reviewerGrant` (controller → member → key).
266
-
267
- Every line `PASS`, then `ALL PASS`. It creates a free source (billing `none`), uploads the
268
- fixtures, decides one stop per row of the example's outcome table (approve prepaid · approve COD ·
269
- review cash short · review GPS far · partial refused · reject wrong code · reject unknown order ·
270
- review reused photo, held on `unique:SD-DUP-1` with its `duplicates[]` naming the earlier claim ·
271
- reject the same order claimed again by its courier (same `submitter`) · review dispatch down), answers the cash review (and the
272
- claim-level `SD-RT-CHECKS` asked beside it) as a member does in the Decision Console (never an
273
- oracle operator's account, which the engine refuses in a reviewer grant), waits for the engine's fold (`decided partial`), walks
274
- the rest of the surface, then revokes its keys and DELETEs its sources as the controller (`--keep`
275
- keeps both). Each run spends ~13 engine decisions (and one review fold) of the owner's
276
- credits. Then, on the owner's OK:
277
-
278
- - `--frequency` — one courier over the daily limit (26 more decisions);
279
- - `--paid` — one x402 claim paid from the session's own devnet deed. Needs a **subscription deed**
280
- whose admin holds PAY and has granted the payments facilitator's address a `SendAuthorization` —
281
- the subscriptions service's devnet purchase / top-up does both (**HUMAN** if absent). Check:
282
- `curl "https://devnet.ixo.earth/rest/cosmos/authz/v1beta1/grants?granter=<deed admin>&grantee=<facilitator address>&msg_type_url=/cosmos.bank.v1beta1.MsgSend"`.
283
- Spends one decision price of PAY (60¢ in the example).
266
+ - **the organisation's controller**: a source belongs to an organisation, so the controller
267
+ creates it (`POST /v1/api/sources { entityDid }` → `pending-link`), signs the `SourceCompanyLink`
268
+ (kid = the did:key of the Ed25519 key on its IID, canonical payload) and PUTs it to the ENGINE
269
+ (`PUT /v1/sources/:oracleDid/:sourceId/company`, UCAN `company/link`), syncs
270
+ (`POST /v1/api/sources/:id/company/sync` → `active`), mints and revokes the keys, and invites the
271
+ reviewer. `api source link` does the link and the sync. Its key needs no chain write: it already
272
+ controls the entity.
273
+ - **the reviewer** (a third account that is neither of the two): a MEMBER. The controller invites
274
+ it by DID with role `reviewer` and PUTs its role grant (≤ 30 days); the member registers a
275
+ `did:key` reviewer key, grants it `claims/review` on the source and answers through the API with
276
+ that two-link `reviewerGrant` (controller → member → key). The Decision Console does all of this
277
+ for the member.
278
+
279
+ Each decision spends one of the owner's engine credits; a review fold is free. Revoke test keys
280
+ afterwards (`api key revoke`).
284
281
 
285
282
  ### 10. Verify — the six member / review emails
286
283
 
@@ -294,27 +291,34 @@ With the owner's email as the test inbox, in the console (**HUMAN** clicks; you
294
291
  | 1 | "<inviter> invited you to <source>" | the owner invites a reviewer by email (Members → Invite) |
295
292
  | 2 | "<member> accepted your invitation to <source>" | the invitee accepts — to the inviter (a source owner without a member email gets it at the source's contact) |
296
293
  | 3 | "Your access to <source> is ready" | the acceptance grants the delegation — to the member |
297
- | 4 | "Review needed: <protocol> claim for <source>" | a claim goes `pending_review` (example: a COD stop with the cash short, from the courier page's stop 4) — to each member with `claims/review`, an email and `notifyReviews` on |
294
+ | 4 | "Review needed: <protocol> claim for <source>" | a claim goes `pending_review` — to each member with `claims/review`, an email and `notifyReviews` on |
298
295
  | 5 | "<n> claims need your review for <source>" | a second review claim inside 15 min is held, then sent as one batch by the minute tick |
299
296
  | 6 | "Still waiting" / "Two weeks waiting: …" | reminders at 3 and 14 days for an unanswered claim — confirm it is scheduled, don't wait |
300
297
 
301
- ### 11. Payments on (a provider turning it on)
298
+ ### 11. Payments on
299
+
300
+ In the env's vars:
301
+
302
+ - `API_PAYMENTS: "true"`;
303
+ - `X402_FACILITATOR_URL` — IXO's facilitator for the env's network: devnet
304
+ `https://facilitator.devnet.ixo.earth`, testnet `https://facilitator.testnet.ixo.earth`, mainnet
305
+ `https://facilitator.ixo.earth`;
306
+ - `X402_NETWORK` — `cosmos:devnet-1` / `cosmos:pandora-8` / `cosmos:ixo-5`;
307
+ - the payees: `EARNINGS_ADDRESS` (your own `ixo1…`, each offer's `payTo` for PAY and USDC on IXO;
308
+ unset ⇒ no PAY) and `EARNINGS_BASE_WALLET` (your `0x…`, for USDC on Base).
302
309
 
303
- `API_PAYMENTS: "true"`, `X402_FACILITATOR_URL`, `X402_NETWORK` (`cosmos:devnet-1` / `cosmos:pandora-8`
304
- / `cosmos:ixo-5`) and the payees — `EARNINGS_ADDRESS` (your own `ixo1…`, each offer's `payTo` for
305
- PAY and USDC on IXO; unset ⇒ no PAY) and `EARNINGS_BASE_WALLET` (your `0x…`, for USDC on Base) —
306
- in the env's vars, a price (`pricing.api` in `defineOracle`, whole cents), and the
307
- oracle registered with the engine (step 6) — the facilitator reads the oracle's terms from its plan
308
- (Standard unless IXO set another). `API_PAYMENTS:
309
- "false"` is the kill switch: new x402 submits answer 503 `payments-disabled`, `none` sources keep
310
- deciding. The example's devnet env ships with payments on.
310
+ Plus a price (below) and the oracle registered with the engine (step 6) — the facilitator reads the
311
+ oracle's terms from its plan (Standard unless IXO set another). `API_PAYMENTS: "false"` is the kill
312
+ switch: new x402 submits answer 503 `payments-disabled`, `none` sources keep deciding.
311
313
 
312
314
  ### Pricing and card
313
315
 
314
- The price per decision is `pricing.api` in `src/oracle.ts` (micro-USD, whole cents; `byProtocol` per
315
- protocol), or the `API_PRICE_USD_CENTS` var when `pricing.api` is absent. Change it and redeploy;
316
- the facilitator picks it up within ~5 min. **Card needs ≥ 50¢ per decision** (Stripe's minimum,
317
- checked per claim), so a cheaper price offers PAY only. The example is 60¢ for that reason. The
316
+ The price per decision is `pricing.api` in `defineOracle` in `src/index.ts`:
317
+ `pricing: { api: { amountMicroUsd: 600_000, byProtocol?: { "<protocol did>": … } } }` — micro-USD
318
+ (1 USD = 1 000 000), a whole number of cents (a multiple of 10 000). With no `pricing.api`, the
319
+ `API_PRICE_USD_CENTS` var is the price. Change it and redeploy; the facilitator picks it up within
320
+ ~5 min — the `update-pricing` skill walks the change, the card refresh included. **Card needs ≥ 50¢
321
+ per decision** (Stripe's minimum, checked per claim), so a cheaper price offers PAY only. The
318
322
  Decision Console shows the price and card eligibility read-only.
319
323
 
320
324
  ## Troubleshooting
@@ -325,17 +329,18 @@ Decision Console shows the price and card eligibility read-only.
325
329
  | Worker 500s on every request after deploy; logs say `WEBHOOK_ENCRYPTION_KEY: required when API_LANE is on` | the secret is missing in that env — `bunx wrangler secret put WEBHOOK_ENCRYPTION_KEY --env <env>` (for a new deployment only; never replace an existing one) |
326
330
  | Worker 500s on every request; logs say `CHAIN_LANE is off and API_LANE is not on` | an oracle with no lane — set `"API_LANE": "true"` in that env's `vars` (with its secrets), or remove `CHAIN_LANE` |
327
331
  | `register` / `scan` / an OpenHands webhook answers 409 `chain_lane_off` | the oracle is API-only (`CHAIN_LANE` `"false"`) — by design; set it back to `"true"` and redeploy to take chain hires again |
332
+ | `PUT /v1/api/uploads` answers 501 | no `API_UPLOADS` bucket bound in that env — step 4 |
328
333
  | `PROTOCOL_NOT_ALLOWED` on submit | the source's allow-list lacks it — empty means NONE; `api admin protocols <src> <did> --yes` |
329
334
  | 503 `SOURCE_PAUSED` `payments-disabled` | `API_PAYMENTS` is off and the source bills `x402` — turn payments on, or `api admin billing-mode <src> none` |
330
335
  | 402 again after a `PAYMENT-SIGNATURE` | the facilitator refused the payer: no deed / no `SendAuthorization` / not enough PAY — the fresh 402's error says which |
331
336
  | offers refused `plan_inactive`, `accepts: []` | the facilitator finds no active plan for the oracle: it is not registered with the engine (step 6 — registered, it runs on Standard at once), or, for an IXO-run oracle, IXO has not activated its plan (**HUMAN**: ask IXO). A way to pay whose payee is unset (`EARNINGS_ADDRESS` for PAY / USDC on IXO, `EARNINGS_BASE_WALLET` for USDC on Base) is simply not offered |
332
- | Not listed in the console | not registered with the engine (step 6), or not payable (the row above); or `https://<ORACLE_HOSTNAME>/.well-known/did.json` does not answer; or `CORS_ORIGINS` lacks `https://decision-console.devnet.ixo.earth` |
333
- | Every API claim rejects with `frequency` | the caller sends no `submitter` — an uncountable window fails closed; send the courier id |
337
+ | Not listed in the console | not registered with the engine (step 6), or not payable (the row above); or `https://<ORACLE_HOSTNAME>/.well-known/did.json` does not answer; or `CORS_ORIGINS` lacks the console's origin |
338
+ | Every API claim rejects with `frequency` | the caller sends no `submitter` — an uncountable window fails closed; send a stable id for the person |
334
339
  | 409 `REVIEW_EXPIRED` on a review answer | the paid claim's `answerBy` (its x402 reservation's expiry: extended up to 30 days for the review, else 24 h after verify) has passed — it can no longer be paid, and nothing was written. Answer paid reviews before the date the emails state |
335
- | Claims go `pending_review` on `SD-RT-CHECKS` (example) | an errored fact (onError `review`) held them for a person: the AI provider unavailable (the photo check — no gateway credits, or `OPENROUTER_API_KEY` missing on `openrouter`), or the orders endpoint unreachable (`DEMO_ORDERS` off, or an `ORD-…-E` order) — read the claim's report; fix it, and new claims decide again (the held ones are answered in the console). A rubric without a claim-level ReviewTask parks such claims `review` with NO task — give it one |
340
+ | Claims go `pending_review` on a claim-level review task you did not expect | an errored fact (onError `review`) held them for a person: the AI provider unavailable (no gateway credits, or `OPENROUTER_API_KEY` missing on `openrouter`), or an `ext` endpoint unreachable — read the claim's report; fix it, and new claims decide again (the held ones are answered in the console). A rubric without a claim-level ReviewTask parks such claims `review` with NO task — give it one |
336
341
  | `engine register` refuses: "registered … as <old> — this would re-bind it to <new>" | the did:web is bound to another key on the engine — the configured `ORACLE_SECP_MNEMONIC` is not the one it registered with. Restore the old key, or (owner's OK) `--yes` |
337
342
  | No emails at all | the `EMAIL_FROM_ADDRESS` domain is not onboarded, or `CONSOLE_URL` is unset — the lane logs and skips |
338
- | `engine register` 402 | the owner's credits — top up at the printed URL, re-run |
343
+ | `engine register` 402 | the owner's engine credits — buy them in the Decision Console (Billing › Credits), re-run |
339
344
  | `/health/ready` 503 while the worker otherwise serves | `GET /v1/admin/status` names the failed check; `identity` ⇒ no `ORACLE_ENTITY_DID` on a deployed host — finish step 7 (`oracle init`, pin it in every `vars` block, redeploy) |
340
345
  | `engine register` fails resolving the did:web | the env is not deployed yet, or `--env <env>` was left off (it registered `localhost`) |
341
346
 
@@ -344,5 +349,5 @@ Decision Console shows the price and card eligibility read-only.
344
349
  Say plainly what is done and what is owed: the protocol DID and where it was written, the
345
350
  worker's host and did:web, the oracle entity DID (`/v1/info` `entityDid`) and the `/health/ready`
346
351
  answer, which ids and secrets were set (names only), the engine registration and
347
- billing status, whether the oracle is listed/payable, the smoke's last line and
348
- any FAIL lines verbatim, the email rows seen, and every HUMAN step still open.
352
+ billing status, whether the oracle is listed/payable, each verify claim's outcome, the email rows
353
+ seen, and every HUMAN step still open.