@ixo/decision-oracle 0.2.1 → 0.2.3

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.
Files changed (33) hide show
  1. package/README.md +8 -20
  2. package/dist/cli/commands/create-collection.d.ts.map +1 -1
  3. package/dist/cli/commands/create-collection.js +6 -3
  4. package/dist/cli/commands/create-collection.js.map +1 -1
  5. package/dist/lib/email/template.js +1 -1
  6. package/dist/lib/email/template.js.map +1 -1
  7. package/dist/lib/payments/gate.js +2 -2
  8. package/dist/lib/payments/gate.js.map +1 -1
  9. package/dist/lib/payments/review-billing.js +1 -1
  10. package/dist/lib/payments/review-billing.js.map +1 -1
  11. package/dist/lib/register.js +1 -1
  12. package/dist/lib/register.js.map +1 -1
  13. package/dist/routes/v1/api/openapi.gen.d.ts.map +1 -1
  14. package/dist/routes/v1/api/openapi.gen.js +1 -1
  15. package/dist/routes/v1/api/openapi.gen.js.map +1 -1
  16. package/docs/api.md +2 -2
  17. package/docs/guide.md +1 -1
  18. package/openapi/decision-api.yaml +8 -8
  19. package/package.json +1 -1
  20. package/skills/README.md +2 -0
  21. package/skills/add-openhands-support/SKILL.md +18 -17
  22. package/skills/create-collection/SKILL.md +17 -4
  23. package/skills/create-collection/reference/collection.md +2 -1
  24. package/skills/create-oracle/SKILL.md +61 -16
  25. package/skills/create-rubric/SKILL.md +3 -2
  26. package/skills/create-rubric/reference/format-cheatsheet.md +1 -1
  27. package/skills/decision-api-client/SKILL.md +42 -88
  28. package/skills/decision-api-oracle/SKILL.md +35 -33
  29. package/skills/publish-protocol/SKILL.md +18 -3
  30. package/skills/publish-protocol/reference/publishing.md +1 -1
  31. package/skills/reference/engine-contract.md +1 -1
  32. package/skills/reference/linked-docs.md +35 -0
  33. package/skills/write-facts/reference/facts-api.md +3 -3
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: decision-api-client
3
- description: Use when testing a Decision API oracle from the CUSTOMER side — minting a test source, API key and payment delegation on devnet, running the TypeScript SDK (@ixo/decision-oracle-client) or its CLI against a live oracle, connecting Claude Code to an oracle's hosted MCP server (/v1/mcp), running the docs agent test — or when debugging a client-side failure (402 not paid, SCOPE, DELEGATION_EXPIRED, terms_mismatch, a receipt that will not verify).
3
+ description: Use when testing a Decision API oracle from the CUSTOMER side — minting a test source, API key and payment delegation on devnet, running the TypeScript SDK (@ixo/decision-oracle-client) or its CLI against a live oracle, connecting Claude Code to an oracle's hosted MCP server (/v1/mcp), or when debugging a client-side failure (402 not paid, SCOPE, DELEGATION_EXPIRED, terms_mismatch, a receipt that will not verify).
4
4
  ---
5
5
 
6
6
  # The Decision API from the customer side, on devnet
@@ -10,17 +10,17 @@ description: Use when testing a Decision API oracle from the CUSTOMER side — m
10
10
  A Decision API oracle (`API_LANE` on; set one up with `decision-api-oracle`) is used by customers
11
11
  through three doors, all over the same `/v1/api` routes with an API key:
12
12
 
13
- - the SDK and CLI, `@ixo/decision-oracle-client` (`packages/client` in this repo);
13
+ - the SDK and CLI, `@ixo/decision-oracle-client` (on npm);
14
14
  - the hosted MCP server at `https://<oracle>/v1/mcp` (four tools, the same key, or a sign-in
15
15
  through AuthHub with an approval per submit);
16
- - plain HTTP, with the docs (`site`, and its `llms-full.txt` for agents).
16
+ - plain HTTP, with the Decision API docs (their `llms-full.txt` is written for agents).
17
17
 
18
18
  A source in billing mode `x402` answers a submit with `402`. The payer pays it with the
19
19
  customer's **payment delegation**, a UCAN to the facilitator carrying a `billing/pay` cap. The
20
20
  price is held when the claim is accepted and paid only on the engine's receipt.
21
21
 
22
22
  The worked target is IXO's devnet example: SwiftDrop, at `https://shipment-delivery-oracle.devnet.ixo.earth`,
23
- protocol `did:ixo:entity:c2f5a188d28f3d4bf6351873d3d268ea`, price 20¢. Its `/demo/orders?route=<id>`
23
+ protocol `did:ixo:entity:c2f5a188d28f3d4bf6351873d3d268ea`, price 60¢. Its `/demo/orders?route=<id>`
24
24
  issues a fresh courier route per id, with real order ids and delivery codes. Use a new route per
25
25
  test claim, so the duplicate lane never sees a repeat.
26
26
 
@@ -46,45 +46,42 @@ bunx decision-oracle login --network devnet # once; a browser step for the h
46
46
  ```
47
47
 
48
48
  The session's DID must be in the oracle's `ORACLE_ADMIN_DIDS` to set a source's billing mode and
49
- protocols. For IXO's example it already is. Its deed needs devnet PAY; top up in the console
49
+ protocols — on someone else's oracle, ask its operator to do those two steps. Its deed needs devnet PAY; top up in the console
50
50
  (`decision-console.devnet.ixo.earth`, Credits).
51
51
 
52
52
  ### 1. Credentials: a source, two keys and a delegation
53
53
 
54
- **In this repo** (one command; secrets are written to the file with mode 600 and never printed):
55
-
56
- ```bash
57
- cd packages/client
58
- bun run devnet:credentials -- --oracle https://shipment-delivery-oracle.devnet.ixo.earth \
59
- --protocol did:ixo:entity:c2f5a188d28f3d4bf6351873d3d268ea --out ~/.ixo/decision-api-devnet.env
60
- # --source src_… reuse an existing source; --max-pay 2 (PAY); --days 7
61
- ```
62
-
63
- It writes `IXO_ORACLE_URL`, `IXO_PROTOCOL_DID`, `IXO_SOURCE_ID`, `IXO_API_KEY` (claims:write,
64
- claims:read, webhooks:manage), `IXO_API_KEY_READONLY` (claims:read) and `IXO_PAYMENT_DELEGATION`.
65
-
66
- **Anywhere else:**
67
-
68
54
  ```bash
69
55
  eo() { bunx decision-oracle "$@" --kit https://<oracle> --network devnet --json; }
70
- eo api source create --name <n> --contact <email> # sourceId + webhookSecret, shown once
56
+ eo api source create --name <n> --contact <email> --entity <org did> # sourceId + webhookSecret, shown once
71
57
  eo api admin billing-mode <sourceId> x402 --yes # operator only
72
58
  eo api admin protocols <sourceId> <protocolDid> --yes # operator only
73
59
  eo api key create --source <sourceId> --scopes claims:write,claims:read --name test
74
60
  ```
75
61
 
76
- For the delegation, use the console's Keys page ("Payment delegation"), which shows
77
- `IXO_PAYMENT_DELEGATION` once.
62
+ `--entity` is required: the organisation the source belongs to, which the login must control (make
63
+ one with `decision-oracle org create --name …`). The source stays `pending-link` until
64
+ `eo api source link <sourceId> --yes`. For the delegation, use the console's Keys page ("Payment
65
+ delegation"), which shows `IXO_PAYMENT_DELEGATION` once.
66
+
67
+ Keep the values in a file of your own (mode 600), never printed: the client reads `IXO_ORACLE_URL`,
68
+ `IXO_API_KEY` and `IXO_PAYMENT_DELEGATION` from the environment. Mint a second, read-only key
69
+ (`--scopes claims:read`) for the `SCOPE` check.
78
70
 
79
71
  Revoke keys you no longer need: `eo api key revoke --source <id> <8-char prefix> --yes`.
80
72
 
81
- ### 2. The SDK smoke (this repo)
73
+ ### 2. The CLI and the SDK
74
+
75
+ With the `IXO_*` variables from step 1 in the environment:
82
76
 
83
77
  ```bash
84
- cd packages/client && bun run build && node dev/devnet-smoke.ts # --env <file> --photo <jpg>
78
+ npx @ixo/decision-oracle-client submit claim.json # waits up to 60 s; pays the 402
79
+ npx @ixo/decision-oracle-client watch apc_… # the timeline to the decision
80
+ npx @ixo/decision-oracle-client keys # the source's keys, masked
85
81
  ```
86
82
 
87
- It runs under plain node, through the BUILT dist. That is what a customer installs. It checks:
83
+ The SDK is the same package (`npm i @ixo/decision-oracle-client`; pass `paymentDelegation` to pay
84
+ 402s). A passing run shows:
88
85
 
89
86
  - the work order and its price;
90
87
  - an upload;
@@ -92,25 +89,12 @@ It runs under plain node, through the BUILT dist. That is what a customer instal
92
89
  - a decision, and a payment on the record;
93
90
  - `verifyReceipt` against the engine's did.json;
94
91
  - events;
95
- - a replay with the same key naming the same claim;
96
- - `SCOPE` for the read-only key;
97
- - the CLI reading the claim.
98
-
99
- It spends one claim. Every line must read `PASS`. Any verdict counts (`reject` is normal for a
100
- parcel photo that doesn't match).
92
+ - a replay with the same idempotency key naming the same claim;
93
+ - `SCOPE` for the read-only key.
101
94
 
102
- ### 3. The CLI by hand
103
-
104
- ```bash
105
- set -a; . ~/.ixo/decision-api-devnet.env; set +a
106
- node packages/client/dist/cli/index.js submit claim.json # waits up to 60 s; pays the 402
107
- node packages/client/dist/cli/index.js watch apc_… # the timeline to the decision
108
- node packages/client/dist/cli/index.js keys # the source's keys, masked
109
- ```
95
+ Any verdict counts (`reject` is normal for a parcel photo that doesn't match).
110
96
 
111
- Outside this repo: `npx @ixo/decision-oracle-client …` (once published).
112
-
113
- ### 4. MCP from Claude Code
97
+ ### 3. MCP from Claude Code
114
98
 
115
99
  The route ships with the oracle package, so the oracle must be deployed from a version that has
116
100
  `src/routes/v1/mcp`. Check with `curl -s -o /dev/null -w "%{http_code}" -X POST https://<oracle>/v1/mcp`:
@@ -159,55 +143,25 @@ the console) must make the next claim ask again. "Don't ask again" works only on
159
143
  `policies.allowStandingApprovals` is `true`; otherwise setting it is refused (`403`, reason
160
144
  `standing_approvals_off`) and every claim asks.
161
145
 
162
- ### 5. The docs agent test (this repo)
163
-
164
- ```bash
165
- (cd site && bun install && bun run build)
166
- set -a; . ~/.ixo/decision-api-devnet.env; set +a
167
- bun site/agent-test/run.ts # exit 0 pass · 1 fail · 2 skipped (no credentials)
168
- ```
169
-
170
- A Claude model gets only the site's `llms-full.txt`, the key, the delegation and one
171
- `http_request` tool locked to the oracle's origin. It must reach a decision it paid for itself.
172
- The script checks the result, not the model's word. It costs a few cents of model usage plus
173
- one claim.
174
-
175
- ### 6. Card scenarios (devnet, Stripe test mode)
146
+ ### 4. Card scenarios (devnet, Stripe test mode)
176
147
 
177
- Card needs an oracle whose protocol costs **≥ 50¢ per claim** (Stripe's minimum capture; SwiftDrop is
178
- 60¢ for this) and whose plan accepts `card`. Which scenario runs is set on the **facilitator side**:
179
- the payer's default saved card and default way to pay. Saving a test card and completing 3-D Secure
180
- need the facilitator's Stripe test key, so that half belongs to whoever owns the facilitator
181
- (ixo-pay-facilitator `.claude/skills/devnet-card-check`). This half pays:
148
+ Card needs an oracle whose protocol costs **≥ 50¢ per claim** (Stripe's minimum capture; the
149
+ example is 60¢ for this) and whose plan accepts `card`. Which scenario runs is set on the
150
+ **facilitator side**: the payer's default saved card and default way to pay. Saving a test card and
151
+ completing 3-D Secure need the facilitator's Stripe test key, so that half belongs to whoever runs
152
+ the facilitator. This half pays — an SDK submit with `prefer: "card"` (`"upay"` = PAY first; unset =
153
+ the payer's console default), and after a 3-D Secure confirm a resume with the SAME offer and key
154
+ (`{ idempotencyKey: err.idempotencyKey, resumePayment: err.paymentRequired }`):
182
155
 
183
- ```bash
184
- cd packages/client && bun run build
185
- node dev/devnet-card.ts submit card # prefer card; `upay` = PAY first; `none` = the console default
186
- node dev/devnet-card.ts resume # after a 3-D Secure confirm: the SAME offer, same key
187
- ```
188
-
189
- | Payer's default card (facilitator side) | Run | Expect |
156
+ | Payer's default card (facilitator side) | Submit with | Expect |
190
157
  | --- | --- | --- |
191
- | `pm_card_authenticationRequired` | `submit card` | `CARD_AUTHENTICATION_REQUIRED` + `confirmUrl`; once confirmed, `resume` → paid with the SAME PaymentIntent. Left unconfirmed, the facilitator cancels it ~17 min later, never charged |
192
- | `pm_card_chargeCustomerFail` | `submit card` | declined at authorize → paid with PAY for the same 402 |
193
- | `pm_card_createDispute` | `submit card`, then again once the dispute is open | paid by card; then `card_blocked` → PAY (no PaymentIntent created); after the dispute is won, card again |
194
- | `pm_card_visa`, preference `card` | `submit none` | the SDK reads `/v1/payer/preferences` with the delegation → card |
158
+ | `pm_card_authenticationRequired` | `prefer: "card"` | `CARD_AUTHENTICATION_REQUIRED` + `confirmUrl`; once confirmed, the resume → paid with the SAME PaymentIntent. Left unconfirmed, the facilitator cancels it ~17 min later, never charged |
159
+ | `pm_card_chargeCustomerFail` | `prefer: "card"` | declined at authorize → paid with PAY for the same 402 |
160
+ | `pm_card_createDispute` | `prefer: "card"`, then again once the dispute is open | paid by card; then `card_blocked` → PAY (no PaymentIntent created); after the dispute is won, card again |
161
+ | `pm_card_visa`, preference `card` | no `prefer` | the SDK reads `/v1/payer/preferences` with the delegation → card |
195
162
 
196
163
  Every claim's record names the rail: `stripe-manual-capture` / `usd` / cents, or `ixo-pay` / `upay`.
197
- A run that answers `SPEND_CAP` has used up the delegation's `maxAmount`: mint a new one. Proven end to
198
- end on 2026-10-01, pre-company-authority (2026-10-10) (SwiftDrop at 60¢; each card capture paid IXO
199
- its 20% fee).
200
-
201
- ## Working on the client in this repo (maintainers)
202
-
203
- - **`@ixo/x402-ixo-pay` comes from npm** (0.5.2, published from ixo-pay-facilitator
204
- `packages/x402-ixo-pay`) as an ordinary runtime dependency; the client's `dist/` imports it. A
205
- facilitator wire change means a new version published there, then a bump here.
206
- - **`packages/client/scripts/pack-test.sh`** (run first by `pack-smoke.sh`) proves the tarball
207
- stands alone: every dependency is a registry version (no git, `file:` or workspace spec), it
208
- installs with git turned off, and it pays and runs its bin under plain node.
209
- - **Types come from the OpenAPI.** Run `bun run generate` after the OpenAPI changes. It uses
210
- `npx` with TypeScript 5, because TS 7 has no JS compiler API and openapi-typescript needs one.
164
+ A run that answers `SPEND_CAP` has used up the delegation's `maxAmount`: mint a new one.
211
165
 
212
166
  ## When something fails
213
167
 
@@ -218,11 +172,11 @@ its 20% fee).
218
172
  | MCP `SUBMITTER_REQUIRED` | The protocol has a frequency rule (its work order says `facts.frequency: true`), so `submit_claim` needs `submitter`, your stable id for the person (e.g. the courier id). Without it the engine would reject the claim, so it is refused before any approval or charge. |
219
173
  | The AI asks you to upload a photo | There is no upload over sign-in. Give it an https link; `submit_claim` attaches `{ question, url }` and the oracle fetches it. No `mediaType` is needed: the oracle reads it from the URL's extension, else a `HEAD`, when it accepts the claim. |
220
174
  | Claude Code refuses `submit_claim` ("real-world transaction") | Its safety check is holding back a tool that may spend. Allow only this tool: `/permissions` › Allow › `mcp__<server>__submit_claim`. Never turn the safety check off. Every charge still needs the user's AuthHub approval or their auto-approve limits. |
221
- | `PaymentError DELEGATION_WRONG_ORACLE` | The delegation is bound to other oracles. Re-sign it naming this oracle's `did:web` (console Keys › Payment delegation; `devnet:credentials` binds to `--oracle`). |
175
+ | `PaymentError DELEGATION_WRONG_ORACLE` | The delegation is bound to other oracles. Re-sign it naming this oracle's `did:web` (console Keys › Payment delegation). |
222
176
  | `PaymentError CARD_NOT_SAVED` | The payer has no saved card and the 402 took card only (or `prefer: "card"` with no PAY to fall back to). Save a card on the console's card page, or pay with PAY. |
223
177
  | `PaymentError CARD_AUTHENTICATION_REQUIRED` | The bank wants 3-D Secure. Open `err.confirmUrl` and confirm within 15 min, then resubmit with `{ idempotencyKey: err.idempotencyKey, resumePayment: err.paymentRequired }` — never a fresh offer. `OFFER_EXPIRED` on the resume: the gate no longer takes that offer; submit afresh. `INVALID_RESUME`: the offer passed is not this oracle's card 402. |
224
178
  | `402 INSUFFICIENT_CREDITS` (+ `fundingUrl`) | The session's deed has too little devnet PAY. Top up in the console. |
225
- | `402 SPEND_CAP` | The key's daily cap or the delegation's `maxAmount` is spent. Mint a delegation with a higher `--max-pay`. |
179
+ | `402 SPEND_CAP` | The key's daily cap or the delegation's `maxAmount` is spent. Mint a new delegation with a higher cap (console Keys › Payment delegation). |
226
180
  | `403 PAYER_NOT_MEMBER` | The delegation's issuer isn't the source's owner or a member. Mint it from the owner's session. |
227
181
  | `409 IDEMPOTENCY_MISMATCH` | Same `Idempotency-Key`, different body. Use a new key per new claim. |
228
182
  | `terms_mismatch` on the paid resend | `accepted` wasn't the offer entry verbatim. Hand-built headers must copy it byte for byte. |
@@ -11,7 +11,9 @@ The Decision API (`API_LANE`) is an oracle's second intake: paid, off-chain clai
11
11
  `/v1/api`, decided by the same engine, never written to chain. The oracle holds the sources, keys,
12
12
  uploads, reviews and webhooks; the engine decides and bills per the oracle's **plan** (the Standard
13
13
  plan from the moment it registers, unless IXO sets another);
14
- the payments facilitator settles each claim in PAY; the Decision Console
14
+ the payments facilitator settles each claim the ways that plan allows — by card and/or USDC on Base
15
+ for a third-party oracle, PAY only for IXO-run ones — into the oracle's own earnings addresses
16
+ (`EARNINGS_ADDRESS` / `EARNINGS_BASE_WALLET`); the Decision Console
15
17
  (`decision-console.devnet.ixo.earth`) is the UI over all of it.
16
18
 
17
19
  The worked run is **`examples/shipment-delivery`** in this package's repo — SwiftDrop Express, a
@@ -44,18 +46,16 @@ is the package CLI.
44
46
  ### 0. Prerequisites
45
47
 
46
48
  ```bash
47
- bunx wrangler whoami # the Cloudflare account that owns the ixo.earth zone
49
+ bunx wrangler whoami # the Cloudflare account that owns your host's zone
48
50
  bunx decision-oracle login --network devnet # the OWNER's AuthHub session — once
49
51
  ```
50
52
 
51
53
  - `login` opens a browser: run it in the background and tell the human to finish there (**HUMAN**).
52
54
  The session is the owner: it publishes the protocol, owns the oracle entity, and is the operator.
53
- Put the session's OWN DID (`did:ixo:<address>` — `login` prints the address; the session file
54
- `~/.ixo/publish-session-<network>.json` has the `did`) in `ORACLE_ADMIN_DIDS` in every `vars` block
55
- of `wrangler.jsonc`. The example ships IXO's DID there — a copy of it must
56
- replace it with the owner's.
57
- - The account must hold the zone of the host (`*.ixo.earth` for IXO's deployments), or the Custom
58
- Domain cannot attach. If `whoami` shows another account, stop and say so (**HUMAN**).
55
+ 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
+ - 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**).
59
59
 
60
60
  ### 1. The protocol (example: publish `protocol/`; a provider: their protocol's DID)
61
61
 
@@ -160,15 +160,14 @@ If the engine already binds this did:web to a DIFFERENT address, it prints old
160
160
  that is an identity change — ask the owner before re-running with `--yes`.
161
161
 
162
162
  Registering is the whole step for a provider: an oracle with no plan of its own runs at the
163
- engine's **Standard** plan at once (payable in PAY, take 20%, paid to the owner's deed account,
164
- listed in the Decision Console). Nothing to request. Done when the oracle appears in the engine's
163
+ engine's **Standard** plan at once (payable by card and/or USDC on Base, into the oracle's earnings
164
+ 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
165
165
  `GET /v1/oracles` (the console's list).
166
166
 
167
- **Example only (IXO's own oracles):** IXO staff switch the oracle to the IXO-owned plan in the
168
- engine's admin plan editor (**HUMAN**, IXO staff) — the example's README names its terms. Done when
169
- IXO confirms the oracle's plan version is > 0 (Standard is version 0).
167
+ An oracle IXO runs for itself moves to an IXO plan only on IXO's side: ask IXO to move it
168
+ (**HUMAN**), and it is done when IXO confirms the plan.
170
169
 
171
- ### 7. On chain — the full identity (mandatory, owner 2026-10-01)
170
+ ### 7. On chain — the full identity (mandatory)
172
171
 
173
172
  Every oracle built with the kit has a full on-chain identity — the entity, its `#pro` profile, its
174
173
  `#dmn` domain card and its `#acard` Agent Card (which lists the `decision-api` service at the lane's published
@@ -191,9 +190,12 @@ The oracle is not done until readiness passes. A 503 there names nothing; the op
191
190
 
192
191
  ### 8. Operator: a source, its protocol, its billing, a key
193
192
 
194
- The smoke (step 9) does all of this itself; for a real partner:
193
+ The smoke (step 9) does all of this itself; for a real partner. A source belongs to an
194
+ organisation entity (`--entity`): the partner's, or one made with `org create` (`org balance` /
195
+ `org fund` check and top up its admin account):
195
196
 
196
197
  ```bash
198
+ bunx decision-oracle org create --name "<partner>" --network devnet --yes # → { entityDid } — once, idempotent by name
197
199
  K="--kit https://shipment-delivery-oracle.devnet.ixo.earth --network devnet"
198
200
  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
199
201
  bunx decision-oracle api admin protocols <sourceId> <protocol did> $K # dry run → --yes
@@ -205,7 +207,9 @@ bunx decision-oracle api review-billing disable --source <sourceId> $K
205
207
  ```
206
208
 
207
209
  `review-billing` exists only while IXO's plan bills the oracle's reviews per review (`/v1/info`
208
- `reviewBilling.mode: "x402"`; `enable` refuses otherwise). The login must be the organisation's
210
+ `reviewBilling.mode: "x402"`; `enable` refuses otherwise), and needs `API_LANE` AND `API_PAYMENTS`
211
+ on, a published `pricing.api` price, and the organisation's collections created with
212
+ `--eval-amount 0` (an empty evaluation leg — `create-collection`). The login must be the organisation's
209
213
  owner on chain — the reviews come out of ITS credits — and `--cap` (PAY) is a lifetime total,
210
214
  never refunded: raise it by running `enable` again with a higher `--cap`.
211
215
 
@@ -238,18 +242,16 @@ Three people, three keys — never the same account twice:
238
242
  `api admin billing-mode` and `api admin protocols`. It must NOT be the organisation's
239
243
  controller or the reviewer: the engine refuses any review grant chain through the oracle's
240
244
  identities, its owner included.
241
- - **the organisation's controller** (`--org-controller`, a test account's key file or its address in
242
- `~/.ixo/test-accounts-registry.json`; on devnet the default is SwiftDrop's admin Sara,
243
- `~/.config/ixo-payouts/test-accounts/swiftdrop-sara.json`, controller of `--entity`'s default,
244
- SwiftDrop's organisation `did:ixo:entity:2eeb3a4c03f3755872ada894734e47d2`): a source belongs to
245
- an organisation (D10), so the controller creates it (`POST /v1/api/sources { entityDid }` →
245
+ - **the organisation's controller** (`--org-controller`, a key file or an account address; pass
246
+ `--entity` with the organisation it controls — the smoke's defaults are the example's own
247
+ devnet accounts): a source belongs to an organisation, so the controller creates it (`POST /v1/api/sources { entityDid }` →
246
248
  `pending-link`), signs the `SourceCompanyLink` (kid = the did:key of the Ed25519 key on its IID,
247
249
  canonical payload) and PUTs it to the ENGINE (`PUT /v1/sources/:oracleDid/:sourceId/company`,
248
250
  UCAN `company/link`), syncs (`POST /v1/api/sources/:id/company/sync` → `active`), mints and
249
251
  revokes the keys, and invites the reviewer. Its key needs no chain write: it already controls the
250
252
  entity.
251
- - **the reviewer** (`--reviewer`, from `~/.ixo/test-accounts.json`; default the network's first
252
- account that is neither of the two — make one with the `ixo-test-account` skill): a MEMBER. The
253
+ - **the reviewer** (`--reviewer <address|did>`, a third account that is neither of the two): a
254
+ MEMBER. The
253
255
  controller invites it by DID with role `reviewer` and PUTs its role grant (≤ 30 days); the member
254
256
  registers a `did:key` reviewer key, grants it `claims/review` on the source and answers through
255
257
  the API with that two-link `reviewerGrant` (controller → member → key).
@@ -259,19 +261,18 @@ fixtures, decides one stop per row of the example's outcome table (approve prepa
259
261
  review cash short · review GPS far · partial refused · reject wrong code · reject unknown order ·
260
262
  review reused photo, held on `unique:SD-DUP-1` with its `duplicates[]` naming the earlier claim ·
261
263
  reject the same order claimed again by its courier (same `submitter`) · review dispatch down), answers the cash review (and the
262
- claim-level `SD-RT-CHECKS` asked beside it) as a member does in the Decision Console (devnet's
263
- reviewer is Dan — never an oracle operator's account, which the engine refuses in a reviewer grant), waits for the engine's fold (`decided partial`), walks
264
+ claim-level `SD-RT-CHECKS` asked beside it) as a member does in the Decision Console (never an
265
+ oracle operator's account, which the engine refuses in a reviewer grant), waits for the engine's fold (`decided partial`), walks
264
266
  the rest of the surface, then revokes its keys and DELETEs its sources as the controller (`--keep`
265
267
  keeps both). Each run spends ~13 engine decisions (and one review fold) of the owner's
266
268
  credits. Then, on the owner's OK:
267
269
 
268
270
  - `--frequency` — one courier over the daily limit (26 more decisions);
269
271
  - `--paid` — one x402 claim paid from the session's own devnet deed. Needs a **subscription deed**
270
- whose admin holds PAY and has granted the facilitator (`ixo173l540tvyu43ghmtstl2wu2lqztdt5e05e05c6`)
271
- a `SendAuthorization` — the subscriptions service's devnet purchase / top-up does both (**HUMAN**
272
- if absent). Check: `curl "https://devnet.ixo.earth/rest/cosmos/authz/v1beta1/grants?granter=<deed admin>&grantee=ixo173l540tvyu43ghmtstl2wu2lqztdt5e05e05c6&msg_type_url=/cosmos.bank.v1beta1.MsgSend"`.
273
- Spends one decision price of PAY (60¢ in the example). The next SwiftDrop smoke after the
274
- company-authority joint deploy runs with `--paid`.
272
+ whose admin holds PAY and has granted the payments facilitator's address a `SendAuthorization` —
273
+ the subscriptions service's devnet purchase / top-up does both (**HUMAN** if absent). Check:
274
+ `curl "https://devnet.ixo.earth/rest/cosmos/authz/v1beta1/grants?granter=<deed admin>&grantee=<facilitator address>&msg_type_url=/cosmos.bank.v1beta1.MsgSend"`.
275
+ Spends one decision price of PAY (60¢ in the example).
275
276
 
276
277
  ### 10. Verify — the six member / review emails
277
278
 
@@ -292,7 +293,8 @@ With the owner's email as the test inbox, in the console (**HUMAN** clicks; you
292
293
  ### 11. Payments on (a provider turning it on)
293
294
 
294
295
  `API_PAYMENTS: "true"`, `X402_FACILITATOR_URL`, `X402_NETWORK` (`cosmos:devnet-1` / `cosmos:pandora-8`
295
- / `cosmos:ixo-5`) and `EARNINGS_ADDRESS` (the operator's own `ixo1…`, each offer's `payTo`; unset ⇒ no PAY)
296
+ / `cosmos:ixo-5`) and the payees — `EARNINGS_ADDRESS` (your own `ixo1…`, each offer's `payTo` for
297
+ PAY and USDC on IXO; unset ⇒ no PAY) and `EARNINGS_BASE_WALLET` (your `0x…`, for USDC on Base) —
296
298
  in the env's vars, a price (`pricing.api` in `defineOracle`, whole cents), and the
297
299
  oracle registered with the engine (step 6) — the facilitator reads the oracle's terms from its plan
298
300
  (Standard unless IXO set another). `API_PAYMENTS:
@@ -316,7 +318,7 @@ Decision Console shows the price and card eligibility read-only.
316
318
  | `PROTOCOL_NOT_ALLOWED` on submit | the source's allow-list lacks it — empty means NONE; `api admin protocols <src> <did> --yes` |
317
319
  | 503 `SOURCE_PAUSED` `payments-disabled` | `API_PAYMENTS` is off and the source bills `x402` — turn payments on, or `api admin billing-mode <src> none` |
318
320
  | 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 |
319
- | offers refused `plan_inactive`, `accepts: []` | the engine has nobody to pay: the oracle is not registered (step 6), or its owner has no oracle claims collection / deed account to receive the Standard payout (the owner's subscription sets that up — **HUMAN**); the refusal's reason says which |
321
+ | 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 |
320
322
  | Not listed in the console | not registered with the engine (step 6), or not payable (the row above); or the host is not a `*.ixo.earth` Custom Domain; or `CORS_ORIGINS` lacks `https://decision-console.devnet.ixo.earth` |
321
323
  | Every API claim rejects with `frequency` | the caller sends no `submitter` — an uncountable window fails closed; send the courier id |
322
324
  | 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 |
@@ -332,5 +334,5 @@ Decision Console shows the price and card eligibility read-only.
332
334
  Say plainly what is done and what is owed: the protocol DID and where it was written, the
333
335
  worker's host and did:web, the oracle entity DID (`/v1/info` `entityDid`) and the `/health/ready`
334
336
  answer, which ids and secrets were set (names only), the engine registration and
335
- billing status, whether the oracle is listed/payable (for IXO's own: the plan version IXO confirmed), the smoke's last line and
337
+ billing status, whether the oracle is listed/payable, the smoke's last line and
336
338
  any FAIL lines verbatim, the email rows seen, and every HUMAN step still open.
@@ -14,8 +14,9 @@ submissions against it. This skill composes two things:
14
14
 
15
15
  - **`create-rubric`** authors and validates the form + rubric pair. The decision engine owns the
16
16
  rubric format; `bunx decision-oracle rubric validate` is that engine over HTTP.
17
- - **`bunx decision-oracle publish-protocol`** does the chain authoring: `MsgCreateEntity` → Matrix
18
- upload → `MsgAddLinkedResource` (`#vct` + `#rub`) + `MsgAddService` → source the Matrix room.
17
+ - **`bunx decision-oracle publish-protocol`** does the chain authoring: `MsgCreateEntity` → upload
18
+ to the IXO Vault (else the IXO IPFS gateway; never Matrix media) → `MsgAddLinkedResource` (`#vct`
19
+ + `#rub`) + `MsgAddService` → source the Matrix room.
19
20
  Phases `entity` / `resources` / `room` (default `all`), resumable with `--entity <did> --phase <p>`.
20
21
  Plus `rub`, which is a different job — see below.
21
22
 
@@ -89,6 +90,17 @@ There are two jobs here, and picking the wrong one fails on chain:
89
90
  protocols are linked to this entity". `create-collection`'s `reference/claims-stack.md` has the
90
91
  model.
91
92
 
93
+ **Proofs.** Both proofs this command writes are the sha256 HEX of the exact uploaded bytes — the
94
+ engine string-compares them, and the `#rub` proof IS the rubric's id. A protocol published through
95
+ the Portal carries a CID as its `#vct` proof instead. Never recompute someone else's proof: read the
96
+ anchored value off chain and use it verbatim (`--phase rub` does exactly that).
97
+ `../reference/linked-docs.md` has the rules for every linked document in one place.
98
+
99
+ **No profile yet — the protocol shows as "Unnamed".** `publish-protocol` writes no `#pro` profile,
100
+ so Portals and Consoles list the protocol as "Unnamed" until one is anchored on it. The profile is a
101
+ `#pro` linked resource: type `Settings`, `application/ld+json`, its proof the CIDv1 of the uploaded
102
+ bytes. This CLI has no flag for it — tell the user it is still owed.
103
+
92
104
  ## Adopting a protocol that already has its form — `--phase rub`
93
105
 
94
106
  Use this when the protocol EXISTS and carries a `#vct` already: IXO's own oracle-service-claim
@@ -116,7 +128,7 @@ Three things to expect:
116
128
  prints `already anchored` and does nothing — no upload, no transaction, no fee.
117
129
  - **Only the protocol's owner can anchor.** If the session doesn't own it, the rubric is still
118
130
  uploaded and the exact messages are printed, composed for the owner to broadcast. Send them those
119
- lines; they sign them. (Same posture as `oracle init --phase backlink`.)
131
+ lines; they sign them. (Same posture as `oracle init --only backlink`.)
120
132
  - **The room is not this command's business.** A protocol that already has claims already has its
121
133
  room; `--phase rub` never touches it.
122
134
 
@@ -149,3 +161,6 @@ owed: a phase that failed, an un-invited bot, or a rubric change that would need
149
161
 
150
162
  `reference/publishing.md` — phase-by-phase detail (what each signs and uploads), resuming a partial
151
163
  run, the Matrix identity rule, and the network endpoints.
164
+
165
+ `../reference/linked-docs.md` — the linked-document rules: where uploads go, each proof's format,
166
+ and how to read one.
@@ -64,7 +64,7 @@ bunx decision-oracle publish-protocol --phase rub --protocol <did> \
64
64
  **Ownership.** The chain checks the signer against the DID's authentication relationship, so only
65
65
  the protocol's owner can anchor. When the session is not the owner the messages are composed for the
66
66
  owner, printed, and not broadcast — the upload has already happened, so all they have to do is sign
67
- the printed lines. Same behaviour as `oracle init --phase backlink`.
67
+ the printed lines. Same behaviour as `oracle init --only backlink`.
68
68
 
69
69
  `--form` stays required: it is what the engine's binder validates the rubric against before anything
70
70
  is written. Get it with `rubric fetch-form --protocol <did> --out form.json`, which downloads the
@@ -78,7 +78,7 @@ 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 |
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) |
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 |
@@ -0,0 +1,35 @@
1
+ # Linked documents (what an entity's LinkedResources point at)
2
+
3
+ Every public document an entity carries — a protocol's form (`#vct`) and rubric (`#rub`), an
4
+ oracle's `#orz`, `#pro` profile, `#dmn` domain card and `#acard` Agent Card, an ask's documents —
5
+ is a LinkedResource: an https URL (`serviceEndpoint`) plus a hash of the exact bytes (`proof`).
6
+ The CLI writes and reads them for you; these are the rules it follows, for anything you write or
7
+ read yourself.
8
+
9
+ ## Where the bytes go
10
+
11
+ 1. **The IXO Vault first** — `https://{devnet.|testnet.|}vfs.ixo.earth/public/cid/<cid>`, uploaded
12
+ under a UCAN signed by the document's OWNER (the AuthHub session). Public at once, no auth.
13
+ 2. **The IXO IPFS gateway as the fallback** — `https://ipfs.gateway.ixo.world/ipfs/<cid>`. Never a
14
+ public gateway (`ipfs.io` and friends rate-limit).
15
+ 3. **Never Matrix media for a new document.** Old documents keep their Matrix download URLs and stay
16
+ readable; nothing new is written there.
17
+
18
+ `serviceEndpoint` is always that full https URL — never a bare `ipfs:` or `mxc://`.
19
+
20
+ ## The proof, by resource
21
+
22
+ | Resource | `proof` |
23
+ | --- | --- |
24
+ | `#vct`, `#rub`, `#orz` (`application/json`) | **sha256 hex** of the bytes. The engine string-compares it: a rubric's `claimSchema.proof` must equal the `#vct` proof verbatim, and a `#rub` proof is the rubric's id |
25
+ | `#pro` (type `Settings`), `#dmn`, `#acard` (`application/ld+json`) | **CIDv1, raw, sha256** (`bafkrei…`) of the bytes |
26
+
27
+ A protocol published through the Portal has a CID as its `#vct` proof. **Never recompute someone
28
+ else's proof** — read the anchored value off chain and use it verbatim.
29
+
30
+ ## Reading one
31
+
32
+ `readLinkedDoc(resource)` from `@ixo/decision-oracle/facts` (node-safe): a plain GET with no auth.
33
+ It accepts Vault and gateway URLs, bare `ipfs:` / `ipfs://` CIDs (resolved through the IXO
34
+ gateway) and old Matrix download URLs, hashes the bytes the way the proof was made (hex or CID), and
35
+ fails closed on a mismatch. `linkedDocUrl(endpoint)` gives just the fetchable URL.
@@ -88,8 +88,8 @@ over whatever a handler put there. **A handler cannot turn its own failure into
88
88
  **A deterministic failure is a fact; a transient one is a retry.** Don't `catch` a transient
89
89
  engine/chain error yourself and turn it into `fact.error(...)` — that freezes a verdict over
90
90
  trouble that was never the claim's fault, and a claim is decided once, so it can never be re-run
91
- (task-14, 2026-09-14: a devnet claim was held for review on every fact because the engine's own 5xx
92
- got frozen this way). `tools.agreement()` and `tools.chain.*` already let a retryable failure
91
+ (a claim whose every fact froze over the engine's own 5xx this way is held for review on all of
92
+ them). `tools.agreement()` and `tools.chain.*` already let a retryable failure
93
93
  propagate instead of catching it — see below.
94
94
 
95
95
  ## One call, many facts
@@ -149,7 +149,7 @@ does: `readAnswer(answers, "$q.row.col")` and `rootQuestionName("$deliveryPhoto"
149
149
  answers `[]`. Blocksync being *unreachable* is a different thing and throws, which the runner passes
150
150
  through as the step's retry signal — so a chain outage retries instead of freezing a verdict. Catch
151
151
  it only if "could not read the chain" is genuinely the fact you mean, and then say so with
152
- `fact.error`. `tools.agreement()` follows the same rule against the eval engine (task-14): a real
152
+ `fact.error`. `tools.agreement()` follows the same rule against the eval engine: a real
153
153
  "no agreement on file" is `null`, a transient engine outage throws.
154
154
 
155
155
  **There is no GENERAL chain message-history read, and you must not build one.** The one thing an