@ixo/decision-oracle 0.2.24 → 0.2.26
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 -3
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/cron/payer-billing.d.ts.map +1 -1
- package/dist/cron/payer-billing.js +4 -0
- package/dist/cron/payer-billing.js.map +1 -1
- package/dist/cron/scan.d.ts +3 -1
- package/dist/cron/scan.d.ts.map +1 -1
- package/dist/cron/scan.js +36 -11
- package/dist/cron/scan.js.map +1 -1
- package/dist/define-oracle.d.ts.map +1 -1
- package/dist/define-oracle.js +4 -2
- package/dist/define-oracle.js.map +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/lib/chain-lane.d.ts +17 -0
- package/dist/lib/chain-lane.d.ts.map +1 -0
- package/dist/lib/chain-lane.js +21 -0
- package/dist/lib/chain-lane.js.map +1 -0
- package/dist/lib/ixo/chain-write.d.ts.map +1 -1
- package/dist/lib/ixo/chain-write.js +6 -0
- package/dist/lib/ixo/chain-write.js.map +1 -1
- package/dist/lib/openhands/explain.d.ts +85 -0
- package/dist/lib/openhands/explain.d.ts.map +1 -0
- package/dist/lib/openhands/explain.js +499 -0
- package/dist/lib/openhands/explain.js.map +1 -0
- package/dist/lib/openhands/routes.d.ts +7 -0
- package/dist/lib/openhands/routes.d.ts.map +1 -1
- package/dist/lib/openhands/routes.js +12 -1
- package/dist/lib/openhands/routes.js.map +1 -1
- package/dist/lib/openhands/why.d.ts +74 -0
- package/dist/lib/openhands/why.d.ts.map +1 -0
- package/dist/lib/openhands/why.js +244 -0
- package/dist/lib/openhands/why.js.map +1 -0
- package/dist/lib/register.d.ts.map +1 -1
- package/dist/lib/register.js +5 -0
- package/dist/lib/register.js.map +1 -1
- package/dist/routes/v1/admin.d.ts.map +1 -1
- package/dist/routes/v1/admin.js +4 -0
- package/dist/routes/v1/admin.js.map +1 -1
- package/dist/routes/v1/collections.d.ts +5 -0
- package/dist/routes/v1/collections.d.ts.map +1 -1
- package/dist/routes/v1/collections.js +12 -0
- package/dist/routes/v1/collections.js.map +1 -1
- package/dist/routes/v1/info.d.ts.map +1 -1
- package/dist/routes/v1/info.js +8 -2
- package/dist/routes/v1/info.js.map +1 -1
- package/dist/runtime.d.ts +3 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +7 -2
- package/dist/runtime.js.map +1 -1
- package/dist/workflows/claim-ingest.d.ts.map +1 -1
- package/dist/workflows/claim-ingest.js +8 -0
- package/dist/workflows/claim-ingest.js.map +1 -1
- package/dist/workflows/openhands-hire.d.ts.map +1 -1
- package/dist/workflows/openhands-hire.js +8 -0
- package/dist/workflows/openhands-hire.js.map +1 -1
- package/docs/api.md +29 -17
- package/docs/guide.md +61 -6
- package/package.json +1 -1
- package/skills/decision-api-oracle/SKILL.md +10 -0
- package/templates/project/.dev.vars.example +4 -0
- package/templates/project/wrangler.jsonc +8 -0
package/docs/api.md
CHANGED
|
@@ -254,10 +254,10 @@ and the database handle from it exactly as the cron does (`cron/scan.ts`'s `cron
|
|
|
254
254
|
| Export | Kind | What it is | Use it when |
|
|
255
255
|
| --- | --- | --- | --- |
|
|
256
256
|
| `signEvaluation` | function | `(env, claimId, amount?) => Promise<ChainWriteOutcome>` — signs a decision the act policy `held` back (the release pass, `lib/ixo/chain-write.ts`). `amount`, when given, is the payout to put on `MsgEvaluateClaim` — the whole point of holding (a payout pooled across several claims) — under the same owner opt-in as the hook's (`operatorAmounts`; off ⇒ the lane's default, logged). Every guard of the lane still applies, so it is safe to call twice: an already-`submitted` row answers `"skipped"`, never a second signature | Your own release trigger for a pooled payout — a batch job, an operator action, a cron of your own |
|
|
257
|
-
| `scanCollection` | function | `(env, collectionId) => Promise<number>` — runs the cron's per-collection discovery pass, right now, for ONE collection. Builds its context with the SAME `cronContext` helper the tick itself uses, so a webhook and the cron racing on one claim start it exactly once. Throws `notFound` if the collection isn't registered, `conflict` if it is paused | Reacting to your own webhook the instant a claim is submitted, instead of waiting for the collection's next turn in the tick's rotation |
|
|
258
|
-
| `registerCollection` | function | `(env, input: RegisterInput) => Promise<WatchedCollection>` — the SAME four gates (WHO/HIRED/PROTOCOL/PAID), delegation resolution, upsert and paused→active notice `POST /v1/collections/register` runs (`lib/register.ts`), as a function. `input.cursor` is the one thing the HTTP body schema never exposes: applied on FIRST registration only — a re-register can never rewind it (it is not in the upsert's conflict `set`). `null` scans the collection from claim zero (the existing NULL-cursor path, blocksync `CLAIMS_FROM_START`) for a collection whose claims may predate this oracle's registration; absent seeds the cursor to now, the HTTP route's own behaviour, unchanged | Your own Workflow registering a collection AS the oracle's own principal, after minting its own evaluate grant — there is no inbound request to authenticate or a body to validate |
|
|
257
|
+
| `scanCollection` | function | `(env, collectionId) => Promise<number>` — runs the cron's per-collection discovery pass, right now, for ONE collection. Builds its context with the SAME `cronContext` helper the tick itself uses, so a webhook and the cron racing on one claim start it exactly once. Throws `notFound` if the collection isn't registered, `conflict` if it is paused, `conflict` with `details.reason: "chain_lane_off"` (before any read) with `CHAIN_LANE` `"false"` | Reacting to your own webhook the instant a claim is submitted, instead of waiting for the collection's next turn in the tick's rotation |
|
|
258
|
+
| `registerCollection` | function | `(env, input: RegisterInput) => Promise<WatchedCollection>` — the SAME four gates (WHO/HIRED/PROTOCOL/PAID), delegation resolution, upsert and paused→active notice `POST /v1/collections/register` runs (`lib/register.ts`), as a function (and the same `409 chain_lane_off` first with `CHAIN_LANE` `"false"`, before any of its reads). `input.cursor` is the one thing the HTTP body schema never exposes: applied on FIRST registration only — a re-register can never rewind it (it is not in the upsert's conflict `set`). `null` scans the collection from claim zero (the existing NULL-cursor path, blocksync `CLAIMS_FROM_START`) for a collection whose claims may predate this oracle's registration; absent seeds the cursor to now, the HTTP route's own behaviour, unchanged | Your own Workflow registering a collection AS the oracle's own principal, after minting its own evaluate grant — there is no inbound request to authenticate or a body to validate |
|
|
259
259
|
| `broadcastAsOracle` | function | `(env, msgs, memo?) => Promise<{ code, transactionHash, rawLog? }>` — signs and broadcasts arbitrary messages as THIS oracle's account, the same sequence-managed signer the act lane uses (`getOracleSigner`, `lib/ixo/chain.ts`). Memo defaults to your oracle's own `name`. Does **not** inspect `res.code` — unlike `signEvaluation` this is a raw broadcast primitive, not a decision lane, so a non-zero code is yours to interpret. Throws `AppError.unavailable` when `ORACLE_SECP_MNEMONIC` is unset, before the signer is ever built | Composing your own `MsgExec` around kit-issued messages |
|
|
260
|
-
| `rehireOpenhands` | function | `(env, collectionId) => Promise<"started" \| "existing" \| "restarted" \| "throttled" \| "failed">` — starts the OpenHands hire for a collection, or reruns its ended instance (spending the collection's backstop restart budget: three a day). A rerun mints only when the chain says the grant is stale or below 50 verdicts | `openhands` on, and your own `act` policy counts verdicts and sees an ask running low before the backstop's next visit |
|
|
260
|
+
| `rehireOpenhands` | function | `(env, collectionId) => Promise<"started" \| "existing" \| "restarted" \| "throttled" \| "failed">` — starts the OpenHands hire for a collection, or reruns its ended instance (spending the collection's backstop restart budget: three a day). A rerun mints only when the chain says the grant is stale or below 50 verdicts. Throws `conflict` (`chain_lane_off`) first with `CHAIN_LANE` `"false"`, no budget spent | `openhands` on, and your own `act` policy counts verdicts and sees an ask running low before the backstop's next visit |
|
|
261
261
|
| `claimFailureReason` | function | `(error: string \| null) => string` — a failed ingest (`claim_ingests.status = "failed"`) in a sentence a claimant may read, from its stored `error`: an owner-billing cause (`OWNER_DELEGATION_CAR` unset, the owner's delegation refused, no IXO subscription) is "The oracle isn't set up to bill this check yet — the operator has been told.", anything else "The check couldn't run; the operator has been told." Never the stored text itself (env var names, RPC detail) | Your own claimant-facing "why" route, so a failed claim says failed — the same words `GET /v1/claims/:claimId` gives its submitter |
|
|
262
262
|
| `AppError` | class | The kit's typed error — `.notFound()`, `.forbidden()`, `.conflict()`, `.unavailable()`, `.internal()`, … (§13) | Catching/rethrowing what the four functions above throw, in your own error handling |
|
|
263
263
|
| `ChainWriteOutcome` | type | `"submitted" \| "skipped" \| "blocked" \| "held" \| "owner_submitted"` — what `signEvaluation` resolves to, and (alongside `"pending"`, the row's initial state before any write is attempted) the full `decisions.chainStatus` enum. A decision the act lane can never sign (an engine `unusable`, a `decided` review verdict) starts `"skipped"` instead; a `pending_review` starts `"pending"` and its fold acts | Branching on the result, or reading `chainStatus` off `GET /v1/claims/:claimId` |
|
|
@@ -466,8 +466,11 @@ collection gets at most three hire reruns a day from the backstop and three from
|
|
|
466
466
|
separate budgets. `POST /openhands/scan/<collectionId>` scans a watched collection at once (at
|
|
467
467
|
most once a minute; extra calls are scanned on the next minute tick). A scan that starts nothing —
|
|
468
468
|
usually because Blocksync has not indexed the claim yet — is looked at again on each of the next
|
|
469
|
-
three minute ticks, and no webhook scan moves the collection's place in the rotation.
|
|
470
|
-
|
|
469
|
+
three minute ticks, and no webhook scan moves the collection's place in the rotation. The third
|
|
470
|
+
route, `GET /openhands/claims/<claimId>/why`, tells the claim's own submitter why it was decided
|
|
471
|
+
the way it was — the per-check reasons the public receipt leaves out (§7 has its auth and body);
|
|
472
|
+
the OpenHands app shows it beside the receipt. `GET /v1/info` advertises the three paths under
|
|
473
|
+
`openhands` (`hire`, `scan`, `why`), which is how the OpenHands app tells a checker from any other oracle.
|
|
471
474
|
|
|
472
475
|
`OpenhandsOptions` (all optional):
|
|
473
476
|
|
|
@@ -479,7 +482,7 @@ both paths under `openhands`, which is how the OpenHands app tells a checker fro
|
|
|
479
482
|
|
|
480
483
|
Bindings, with `openhands` on: the `HIRE_WORKFLOW` workflow (class `OpenhandsHire`) and, outside
|
|
481
484
|
`ENVIRONMENT=development`, the `OPENHANDS_RATE_LIMITER` ratelimit — unbound there, both webhooks
|
|
482
|
-
answer `503` (fail closed). `decision-oracle init --openhands` writes both. State lives in two
|
|
485
|
+
answer `503` and the why route `429` (fail closed). `decision-oracle init --openhands` writes both. State lives in two
|
|
483
486
|
tables of your `DB` (`openhands_state`, `openhands_hires`, shipped migration `0025_openhands`).
|
|
484
487
|
|
|
485
488
|
### The whole-producer escape hatch
|
|
@@ -1604,7 +1607,7 @@ it("bands a vendor score against the rule's own thresholds", async () => {
|
|
|
1604
1607
|
| `CLAIM_WORKFLOW` | `Workflow` | `ClaimIngest` — one instance per claim |
|
|
1605
1608
|
| `REVIEW_WORKFLOW` | `Workflow` | `ReviewComplete` — the review fold |
|
|
1606
1609
|
| `HIRE_WORKFLOW` | `Workflow` (optional) | `OpenhandsHire` — one instance per hired OpenHands collection. Required with `openhands` on |
|
|
1607
|
-
| `OPENHANDS_RATE_LIMITER` | `RateLimit` (optional) | The OpenHands
|
|
1610
|
+
| `OPENHANDS_RATE_LIMITER` | `RateLimit` (optional) | The OpenHands routes' limit, spent per caller address (an IPv6 caller by its /64) and per collection (the webhooks) or per issuer (the why route). Required with `openhands` on outside `ENVIRONMENT=development` (unbound ⇒ `503`) |
|
|
1608
1611
|
| `SEQUENCE_MANAGER` | `DurableObjectNamespace` | The SDK's `SequenceManagerDO`: one instance per signer, allocating account sequence numbers so concurrent chain writes never race |
|
|
1609
1612
|
|
|
1610
1613
|
Your own bindings ride alongside — the type is open (`OracleEnv & YourEnv`); only the names above
|
|
@@ -1650,7 +1653,8 @@ once deployed. Every value is validated by Zod at isolate start, so a bad env fa
|
|
|
1650
1653
|
| `AI_IMAGE_DETAIL` | vars | `high` | `low` / `high` / `auto` — the `image_url.detail` every image rides to the model at (an AI check's photos, the same-event judge's pair evidence). `high` because a photographed screen (moiré, a pixel grid, a cursor) or a generated image (garbled text) is only visible at full resolution. **Cost:** `high` spends more image tokens per call than `low`'s flat handful — set `low` only when no check turns on fine detail |
|
|
1651
1654
|
| `C2PA_IMAGE_CHECK` | vars | `false` | The provenance gate in the stock AI lane. Needs `policies.allowImageChecks` on too |
|
|
1652
1655
|
| `AUTO_SIGN` | vars | `true` | Sign `MsgEvaluateClaim` unattended. Needs `policies.autoSign` on too |
|
|
1653
|
-
| `
|
|
1656
|
+
| `CHAIN_LANE` | vars | `true` | The chain lane: collections hired on chain. `"false"` ⇒ an **API-only oracle**: `POST /v1/collections/register`, `POST /v1/collections/:id/scan`, `POST /v1/collections/:id/resume` (a pause stays allowed), `POST /v1/admin/claims/:claimId/redeliver`, `registerCollection` / `scanCollection` / `rehireOpenhands` and the OpenHands webhooks answer `409 { reason: chain_lane_off }` before any of their own reads (an HTTP caller's UCAN is still resolved first); the cron runs no chain job (no `scan`, `claims-feed`, `claims-replay`, `engine-credits-resume`, `admin-funds-resume`, per-review collection resume or OpenHands job — one `cron.chain-lane.off` debug line per tier); a chain `ClaimIngest` run (in flight, retrying, restarted) throws `unavailable` before its pull — nothing pulled, no AI, no decide — and retries on its backoff (~4 h; past that it ends `failed`); the OpenHands hire Workflow ends `NonRetryable`; `deliverChainEvaluation` / `signEvaluation` sign nothing (`act.skipped`, the row left as it was — `pending` or `blocked`). A review answer on a chain claim already `pending_review` is still folded (`ReviewComplete`: the engine's review call, and a per-review payer is charged) so the decision is recorded; only its signing is skipped. Watched rows are kept as they are and resume from their cursors when it is back on. **Needs `API_LANE`** — off with `API_LANE` off, config refuses to load. Why: a protocol is public on chain, so anyone can create a collection on it and hire this oracle — at no fee where `pricing` charges none — and its claims would spend the owner's engine credits |
|
|
1657
|
+
| `API_LANE` | vars | `false` | Mount the Decision API router (`/v1/api`). Off ⇒ the lane is dormant and the chain lane runs exactly as before. Required for `CHAIN_LANE` `"false"` |
|
|
1654
1658
|
| `API_PAYMENTS` | vars | `false` | The x402 gate. Off ⇒ every new submit answers `503 SOURCE_PAUSED { reason: payments-disabled }` — nothing is accepted unpaid |
|
|
1655
1659
|
| `API_PRICE_USD_CENTS` | vars | unset | The fallback price per decision in whole USD cents, for every protocol `pricing.api` does not price (the config wins). Unset with no `pricing.api` ⇒ `price: null` and the gate refuses loudly at use |
|
|
1656
1660
|
| `X402_FACILITATOR_URL` | vars | unset | The x402 facilitator (verify + settle). No trailing slash. **Required** when `API_PAYMENTS` is on — config refuses to load without it |
|
|
@@ -1781,14 +1785,15 @@ check.**
|
|
|
1781
1785
|
| `GET /health/ready` | public, **rate-limited** (per IP) | `{ ok }` only — `200` when every critical check passes, `503` otherwise. Checks `d1`, `kv`, `engine` (its `/.well-known/did.json`) and, with `API_LANE` and `API_PAYMENTS` on, `facilitator` (`X402_FACILITATOR_URL/supported`), in parallel, 2 s each, plus `identity` (a config read: fails when `ORACLE_ENTITY_DID` is unset on a non-local `ORACLE_HOSTNAME`) and `owner-delegation` (fails when `OWNER_DELEGATION_CAR` is set but is not a UCAN delegation; unset passes); the settled answer is cached per isolate for 15 s. A failure before the route runs (a config that does not load) is also `503 { ok: false }`. Names no check — the detail is `/v1/admin/status`'s |
|
|
1782
1786
|
| `GET /.well-known/did.json` | public, rate-limit exempt | Needs `SERVICE_ED25519_MNEMONIC` |
|
|
1783
1787
|
| `GET /.well-known/oauth-protected-resource/v1/mcp` | public, rate-limit exempt, `Access-Control-Allow-Origin: *`, `cache-control: public, max-age=300` | RFC 9728 metadata for the MCP server: `{ resource: "https://<ORACLE_HOSTNAME>/v1/mcp", authorization_servers: [AUTH_HUB_URL], scopes_supported: ["claims:write", "claims:read"], bearer_methods_supported: ["header"] }`. `404` unless `API_LANE` is on. Outside the `/v1/api` contract |
|
|
1784
|
-
| `GET /v1/info` | **public** (the only public `/v1` path) | —. The marketplace listing: `name`, `description`, `did`, `address`, `entityDid` (the on-chain oracle entity from `ORACLE_ENTITY_DID`, `null` when unset), `serviceDid`, `supportedProtocols`, `protocols` (the same list readably: `[{ did, name, fee }]` — `name` the protocol's `#pro` profile name, else its claim form's title, cached 1 h, `null` when neither reads; `fee` the `{ denom, minAmount }` register charges for it), `pricing`, `policies`, `earnings: { address, baseWallet }` (the operator's `EARNINGS_ADDRESS` / `EARNINGS_BASE_WALLET`, `null` when unset — read-only, set in the oracle's config), `payouts: { decisionToPay, operatorAmount }` — how a payout amount is chosen, for a portal to show before a company opts in: `decisionToPay` is the config's `payouts` map verbatim (per protocol DID, `{ amount: <answer path>, currency: <ISO 4217 code> | { path } | { asset, decimals } }`; `{}` when none, and a protocol absent from it gets no decision to pay), `operatorAmount` is `{ summary }` from `policies.actSummary` when an `act` policy is configured, else `null` (`actSummary` is served only here, never under `policies`), and `reviewBilling: { mode, pricePerReview }` — how a collection pays for its reviews: `mode: "x402"` (per-review billing ACTIVE: IXO's plan for the oracle allows it — the engine's `GET /v1/oracles/me` `reviewBilling`, KV-cached 5 min — on an oracle running `API_LANE`: per review from the company's credits, an empty evaluation leg) with `pricePerReview: { denom: "upay", amount }` the published per-review price (the one the supported protocols share, else `pricing.api.amountMicroUsd` / `API_PRICE_USD_CENTS`; `null` when none is published), or `mode: "escrow"` (the collection's on-chain evaluation payment, `pricing`) with `pricePerReview: null`. `cache-control: public, max-age=300`. With `openhands` set, also `openhands: { hire: "/openhands/hire/:collectionId", scan: "/openhands/scan/:collectionId" }` — absent on any other oracle. A portal is built against these exact field names |
|
|
1785
|
-
| `POST /openhands/hire/:collectionId` | public, **`OPENHANDS_RATE_LIMITER`** (per caller and per collection; unbound outside development ⇒ `503`), `openhands` on only | Starts the collection's hire (`202 { started: true }`) after reading that it exists and its admin granted this oracle a live meta-grant: `404` no such collection (not cached), `409` no meta-grant (cached 10 min). A hire in flight, or an ended one the chain says is healthy, is `200 { existing: true }` (the healthy answer cached 1 min); an ended one the chain says is stale or low is rerun from the webhook's own budget (`200 { restarted: true, reason }`, three a day, then `{ throttled: true }`). `400` a malformed id, `405` not POST, `503` when the chain or Blocksync cannot answer |
|
|
1786
|
-
| `POST /openhands/scan/:collectionId` | public, **`OPENHANDS_RATE_LIMITER`**, `openhands` on only | Scans one watched collection now: `200 { started }`, `202 { coalesced: true }` within a minute of the last (scanned on the next minute tick), `404` not watched, `409` paused. A `200 { started: 0 }` (or a failed scan) is retried on the next three minute ticks |
|
|
1787
|
-
| `
|
|
1788
|
+
| `GET /v1/info` | **public** (the only public `/v1` path) | —. The marketplace listing: `name`, `description`, `did`, `address`, `entityDid` (the on-chain oracle entity from `ORACLE_ENTITY_DID`, `null` when unset), `serviceDid`, `lanes: { chain, api }` (booleans: `CHAIN_LANE` and `API_LANE` — a portal offers an on-chain hire only where `chain` is `true`, a console lists the oracle for the Decision API only where `api` is; the listing is cached 300 s, so a lane flip shows up to 5 min late), `supportedProtocols`, `protocols` (the same list readably: `[{ did, name, fee }]` — `name` the protocol's `#pro` profile name, else its claim form's title, cached 1 h, `null` when neither reads; `fee` the `{ denom, minAmount }` register charges for it), `pricing`, `policies`, `earnings: { address, baseWallet }` (the operator's `EARNINGS_ADDRESS` / `EARNINGS_BASE_WALLET`, `null` when unset — read-only, set in the oracle's config), `payouts: { decisionToPay, operatorAmount }` — how a payout amount is chosen, for a portal to show before a company opts in: `decisionToPay` is the config's `payouts` map verbatim (per protocol DID, `{ amount: <answer path>, currency: <ISO 4217 code> | { path } | { asset, decimals } }`; `{}` when none, and a protocol absent from it gets no decision to pay), `operatorAmount` is `{ summary }` from `policies.actSummary` when an `act` policy is configured, else `null` (`actSummary` is served only here, never under `policies`), and `reviewBilling: { mode, pricePerReview }` — how a collection pays for its reviews: `mode: "x402"` (per-review billing ACTIVE: IXO's plan for the oracle allows it — the engine's `GET /v1/oracles/me` `reviewBilling`, KV-cached 5 min — on an oracle running `API_LANE`: per review from the company's credits, an empty evaluation leg) with `pricePerReview: { denom: "upay", amount }` the published per-review price (the one the supported protocols share, else `pricing.api.amountMicroUsd` / `API_PRICE_USD_CENTS`; `null` when none is published), or `mode: "escrow"` (the collection's on-chain evaluation payment, `pricing`) with `pricePerReview: null`. `cache-control: public, max-age=300`. With `openhands` set, also `openhands: { hire: "/openhands/hire/:collectionId", scan: "/openhands/scan/:collectionId", why: "/openhands/claims/:claimId/why" }` — absent on any other oracle. A portal is built against these exact field names |
|
|
1789
|
+
| `POST /openhands/hire/:collectionId` | public, **`OPENHANDS_RATE_LIMITER`** (per caller and per collection; unbound outside development ⇒ `503`), `openhands` on only | With `CHAIN_LANE` `"false"`, `409 { error, reason: "chain_lane_off" }` before anything else (no limiter token, no read, no start). Starts the collection's hire (`202 { started: true }`) after reading that it exists and its admin granted this oracle a live meta-grant: `404` no such collection (not cached), `409` no meta-grant (cached 10 min). A hire in flight, or an ended one the chain says is healthy, is `200 { existing: true }` (the healthy answer cached 1 min); an ended one the chain says is stale or low is rerun from the webhook's own budget (`200 { restarted: true, reason }`, three a day, then `{ throttled: true }`). `400` a malformed id, `405` not POST, `503` when the chain or Blocksync cannot answer |
|
|
1790
|
+
| `POST /openhands/scan/:collectionId` | public, **`OPENHANDS_RATE_LIMITER`**, `openhands` on only | Scans one watched collection now (with `CHAIN_LANE` `"false"`, `409 { error, reason: "chain_lane_off" }` first): `200 { started }`, `202 { coalesced: true }` within a minute of the last (scanned on the next minute tick), `404` not watched, `409` paused. A `200 { started: 0 }` (or a failed scan) is retried on the next three minute ticks |
|
|
1791
|
+
| `GET /openhands/claims/:claimId/why` | **the claim's own submitter**: a UCAN invocation (`Authorization: Bearer <token>`, `X-Auth-Type: ucan`) carrying exactly `{ can: "claims/why", with: "ixo:claim:<claimId>" }`, addressed to this oracle's did:web, self-issued (no delegation), expiring at most 10 min out, whose issuer is the claim's on-chain `agentDid`. **`OPENHANDS_RATE_LIMITER`** per caller address before the token is checked and per issuer after (`429`; unbound outside development ⇒ `429`). `openhands` on only | Why the claim was decided: `200 { outcome, summary, reasons[], evidenceGrade?, reviewNote?, receiptCid? }` — `outcome` is `approved` (approve or partial) / `rejected` / `review` (held for a person) / `pending`; each reason is `{ code, label, result: pass \| fail \| error \| review, detail, value?, threshold?, unit? }`, one per gate or criterion, explained through the one fact it reads and labelled by the pinned rubric (`#rub`, read for its public sentences only, hash-checked, cached 7 days; unreadable ⇒ codes without labels). An `ext` fact's reason is never echoed (only the stock geo handlers' distances and evidence grade), an AI sentence only scrubbed of identifiers, and no AI instruction is ever read. A claim queued, or on a watched collection and not yet seen, is `pending`; a **failed ingest** is `pending` with a fixed sentence (`claimFailureReason`) — never the stored error. Anyone but the agent, or a claim this oracle never saw, gets the same `404 NOT_FOUND`; `401` no or a bad token, `400` a malformed id, `405` not GET, `503` when Blocksync or the DID resolver cannot answer |
|
|
1792
|
+
| `POST /v1/collections/register` | UCAN `collections/register` | With `CHAIN_LANE` `"false"` (an API-only oracle), `409 chain_lane_off` before any of register's own reads (the UCAN middleware resolves the caller first) — nothing stored. Otherwise the four gates, in order: **who** (the collection entity's chain owner, or a holder of an active on-chain `EvaluateClaimAuthorization` — read uncached) · **hired** (this oracle's `ORACLE_ADDRESS` holds that grant too, else `409`) · **protocol** (in `supportedProtocols`, else `409` — checked before payment) · **paid** (`Payments.evaluation` matches `pricing`, else `402`; while per-review billing is active (IXO's plan), the collection's company — ONE engine read, `GET /v1/collections/:id/company`, its blip or a `disputed` company `503 company_unavailable` — holding a live review-billing delegation on its Decision API source, still signed by the organisation's current chain owner, with a published price and cap left, registers with an EMPTY evaluation leg (`billingSourceId` set, never billed twice) once the payer's credits cover one review: `GET /v1/balances` with the stored delegation in `X-Payment-Delegation`, both `available` and `delegationRemaining` ≥ the price, else `402 insufficient_credits` (`details.price`, `details.fundingUrl`), an unreadable answer `503 credits_unavailable`; no usable delegation ⇒ the escrow gate as before, its refusal `402 no_review_billing` naming both ways in). Body: `{ collectionId, delegationCar?, contactEmail? }`. No `contactEmail` sent and none stored ⇒ the row takes the `contactEmail` of the oldest-activated `active` Decision API source whose `companyEntityDid` is the collection's COMPANY as the engine derives it (`GET /v1/collections/:id/company`, UCAN `collection/company-read`: the collection's entity, or the org a deed is linked to both ways); a `disputed` company, a collection the engine does not know, or none such source ⇒ stays empty. Its `503` or any other failure never fails the register (logged `register.company-unavailable`, the row stays empty for a later register to fill). An explicit or stored contact is never overwritten. The SAME answer (one engine read per register) links the collection to every `active` Decision API source of that company, so its chain claims read with the source's (contracts 1.21.0: `LinkedCollection.linkedBy` = the company's entity DID, audit `source.collection_linked` by `system:company-link`, re-checked hourly against the engine and dropped `not_company` when it names another company or `disputed`; only such a positive finding hides a link — an unreachable engine keeps it in the reads, contracts 1.23.0); idempotent, and only the engine's `ok` links. The engine is asked only when no contact is sent or stored, or an `active` source is not yet linked to the collection. Every refusal carries a stable machine code in `details.reason` beside the human message: `404 collection_not_found`, `403 owner_unresolvable` · `not_owner_or_grantee` · `contact_change_forbidden` · `resume_forbidden`, `409 chain_lane_off` · `not_hired` · `unsupported_protocol`, `402 evaluation_underpaid` · `no_review_billing` · `insufficient_credits`, `503 company_unavailable` · `credits_unavailable`. |
|
|
1788
1793
|
| `GET /v1/collections` | UCAN `collections/read` | The rows the caller owns or enrolled |
|
|
1789
1794
|
| `POST /v1/collections/:id/pause` | UCAN `collections/pause` + read gate + owner, enroller (while it holds a live grant) or `ORACLE_ADMIN_DIDS` (a plain grant holder is `403`) | Sets `status: paused`, clears the reason — the cron enqueues nothing |
|
|
1790
|
-
| `POST /v1/collections/:id/resume` | UCAN `collections/resume` + read gate + owner, enroller (while it holds a live grant) or `ORACLE_ADMIN_DIDS` (a plain grant holder is `403`) | Clears whichever reason the engine set; sends the paired "resumed" notice on a real flip |
|
|
1791
|
-
| `POST /v1/collections/:id/scan` | UCAN `collections/scan` + read gate | Runs the cron's per-collection pass **now**. `409` if paused
|
|
1795
|
+
| `POST /v1/collections/:id/resume` | UCAN `collections/resume` + read gate + owner, enroller (while it holds a live grant) or `ORACLE_ADMIN_DIDS` (a plain grant holder is `403`) | Clears whichever reason the engine set; sends the paired "resumed" notice on a real flip. `409 chain_lane_off` (before the gate) with `CHAIN_LANE` `"false"` — nothing would scan the row |
|
|
1796
|
+
| `POST /v1/collections/:id/scan` | UCAN `collections/scan` + read gate | Runs the cron's per-collection pass **now**. `409` if paused; `409 chain_lane_off` (before the read gate) with `CHAIN_LANE` `"false"`. Same dedupe and cursor rules, so an eager caller can never double-start a claim |
|
|
1792
1797
|
| `PATCH /v1/collections/:id/settings` | UCAN `collections/settings` (never `*`) + the collection's **current** chain owner (the entity's owner, read live off Blocksync; no enroller, grant holder or `ORACLE_ADMIN_DIDS`) — `403` otherwise, `404` unwatched | `{ operatorAmounts: "off" \| "allowed" }` (strict). Whether the act seam's on-chain amount (`policies.act`, `signEvaluation`) may stand on this collection: `"off"`, the default, ⇒ the lane pays its default and logs `act.amount-ignored-not-opted-in`. Recorded with the owner who set it: `"allowed"` stands only while that address still owns the collection's entity, so after a transfer it reads `"off"` until the new owner sets it. Answers the collection row, as `GET /v1/collections` does |
|
|
1793
1798
|
| `GET /v1/collections/:id/settings` | UCAN `collections/settings-read` (never `*`) + the collection's **current** chain owner, as the PATCH — `403` otherwise, `404` unwatched, `503` on chain trouble | `{ operatorAmounts, operatorAmountsBy, effective }`: what the owner set, the address that set it, and whether it is in force NOW — `effective` is the act lane's own reading (`operatorAmountsEffective`, `lib/ixo/chain-write.ts`): `true` only when `operatorAmounts` is `"allowed"` and `operatorAmountsBy` is the live owner, so after a transfer the value reads `"allowed"` with `effective: false` until the new owner says yes |
|
|
1794
1799
|
| `GET /v1/claims?collectionId=&limit=&cursor=` | UCAN `claims/read` + read gate | Summary list, newest first, keyset-paginated (`limit` default 100, max 500) |
|
|
@@ -1801,13 +1806,13 @@ check.**
|
|
|
1801
1806
|
| `GET /v1/admin/status` | UCAN `admin/status` + `ORACLE_ADMIN_DIDS` or `STATUS_READER_DIDS` (`403` otherwise — everyone, while both are unset) | `{ readiness: { ok, checks: { <name>: { ok, ms, critical, detail? } } }, backlog: { review: { pending }, collections: { paused: { total, <reason>: n } }, sources?: { paused }, webhooks?: { due, oldestAgeS } }, engine: { billingStatus, manageSubscriptionUrl } \| null }` — `sources`/`webhooks` only with `API_LANE` on; `engine` is the engine's `GET /v1/oracles/me`, `null` when it did not answer within 2 s; `detail` is set only on a failed `identity` or `owner-delegation` check and names the command to run. Outside the `/v1/api` contract |
|
|
1802
1807
|
| `POST /v1/admin/collections/relink` | UCAN `admin/collections-relink` (strict: never `*`) + `ORACLE_ADMIN_DIDS` (`403` otherwise) | The operator's backfill of register's company links. Body `{ collectionIds?: string[] }` (1–100 ids); absent ⇒ every watched collection, lowest id first, up to 100 (`truncated: true` says more exist — name them). Per collection, the same engine read and the same link register makes (no `active` source left unlinked ⇒ `unchanged`, no engine call) → `{ items: [{ collectionId, outcome: "linked" \| "unchanged" \| "no-company" \| "disputed" \| "unavailable" \| "not-watched", linkedSources }], truncated }`. Idempotent. CLI: `decision-oracle relink [--collection <id>]…`. Outside the `/v1/api` contract |
|
|
1803
1808
|
| `POST /v1/admin/claims/:claimId/reviewer-note` | UCAN `admin/reviewer-note` (strict: never `*`) + `ORACLE_ADMIN_DIDS` (`403` otherwise) | (Re)makes the reviewer note for a claim held for a person — one decided before the oracle made notes, or one whose note failed. Re-pulls the body through the claim's own lane, spends one model call, stores the note. `200 { claimId, written: true }`, `503 { written: false }` when the model gave none, `404` no decision, `409` not `pending_review`. The note itself is read on `GET /v1/reviews/:claimId`. CLI: `decision-oracle reviewer-note <claimId>`. Outside the `/v1/api` contract |
|
|
1804
|
-
| `POST /v1/admin/claims/:claimId/redeliver` | UCAN `admin/redeliver` (strict: never `*`) + `ORACLE_ADMIN_DIDS` (`403` otherwise) | Releases a decided approve/reject with a receipt whose delivery is `pending`/`blocked` — made for the row the evaluate step's recovery parked without facts. With `facts` null it re-pulls the body through the claim's own lane, rebuilds the facts (`buildFacts`, the collection's protocol) and stores their `ext`/`ai` on the row with `chainStatus: pending` in one statement — the act policy's inputs only, never sent to the engine, whose decision and receipt stay as recorded — then runs the idempotent `deliverChainEvaluation`. `200 { claimId, outcome, factsDigestMatches, chainStatus, chainError, txHash }` (`factsDigestMatches` null when the row had facts already, `false` when the rebuild differs from the engine's `factsDigest`, e.g. an AI fact answering differently — stored anyway); `404` no decision; `409` not a receipted approve/reject waiting for the chain, a rubric-unusable / invalid-facts rebuild (`details.reason`, `details.issues`; nothing written), or a protocol now pinned to another rubric than the decision's; transient trouble is a `503` to run again. CLI: `decision-oracle redeliver <claimId> --yes`. Outside the `/v1/api` contract |
|
|
1809
|
+
| `POST /v1/admin/claims/:claimId/redeliver` | UCAN `admin/redeliver` (strict: never `*`) + `ORACLE_ADMIN_DIDS` (`403` otherwise) | With `CHAIN_LANE` `"false"`, `409 chain_lane_off` first (no pull, no rebuild, no write). Releases a decided approve/reject with a receipt whose delivery is `pending`/`blocked` — made for the row the evaluate step's recovery parked without facts. With `facts` null it re-pulls the body through the claim's own lane, rebuilds the facts (`buildFacts`, the collection's protocol) and stores their `ext`/`ai` on the row with `chainStatus: pending` in one statement — the act policy's inputs only, never sent to the engine, whose decision and receipt stay as recorded — then runs the idempotent `deliverChainEvaluation`. `200 { claimId, outcome, factsDigestMatches, chainStatus, chainError, txHash }` (`factsDigestMatches` null when the row had facts already, `false` when the rebuild differs from the engine's `factsDigest`, e.g. an AI fact answering differently — stored anyway); `404` no decision; `409` not a receipted approve/reject waiting for the chain, a rubric-unusable / invalid-facts rebuild (`details.reason`, `details.issues`; nothing written), or a protocol now pinned to another rubric than the decision's; transient trouble is a `503` to run again. CLI: `decision-oracle redeliver <claimId> --yes`. Outside the `/v1/api` contract |
|
|
1805
1810
|
| `GET /v1/oauth/approvals/:approvalId` | UCAN `oauth/approval/read`, invoker AuthHub's `did:web` only (`403` otherwise; `*` is never accepted here); no `X-Auth-Type` needed | What AuthHub's approve page shows: `{ did, grantId, clientName, protocol: { did, name \| null }, price: { display, maxAmountUpay }, billingMode: "x402" \| "none", status, expiresAt }` (`expiresAt` in unix seconds). `404` unless the approval exists, is `pending` and has not expired. Never the delegation. `404` without `API_LANE`. Outside the `/v1/api` contract |
|
|
1806
1811
|
| `POST /v1/oauth/approvals/:approvalId` | UCAN `oauth/approval/approve`, same invoker rule | `{ decision: "approve", did, delegation: string \| null }` or `{ decision: "decline", did }` → `{ status: "approved" \| "declined" }`. `404` unless pending and `did` is the approval's. For billing `x402` the delegation must be a payment delegation (`parsePaymentDelegation`) whose root issuer is `did`, whose `oracles` name this oracle's did:web, whose audience is this oracle's facilitator (`X402_FACILITATOR_URL`'s did.json), whose `maxAmount` is exactly `maxAmountUpay` (a total across rails: more would be standing authority), and which has not expired — else `400 VALIDATION` with `details.reason` one of `invalid_delegation`, `issuer_mismatch`, `oracle_mismatch`, `facilitator_mismatch` (also logged `oauth.approval.facilitator_mismatch` with both DIDs: AuthHub and this oracle name different facilitators), `amount_mismatch`, `expired`, and the approval stays pending. `price.display` is informational; AuthHub computes what it shows from `maxAmountUpay`. For `none` nothing is kept. Approving gives the app 15 minutes to use it. Audited as `oauth.approval_approved` / `oauth.approval_declined` (actor kind `oauth`). Outside the `/v1/api` contract |
|
|
1807
1812
|
| `GET /v1/oauth/standing/:grantId` | UCAN `oauth/standing/read`, AuthHub's `did:web` only, never `*` | A grant's auto-approve: `{ perClaimUpay, totalUpay, spentUpay, until }` (upay strings, unix seconds), never the delegation. `404` when there is none, it is revoked, `until` has passed, or `policies.allowStandingApprovals` is off |
|
|
1808
1813
|
| `PUT /v1/oauth/standing/:grantId` | UCAN `oauth/standing/set`, same | `403 FORBIDDEN` (`details.reason: standing_approvals_off`) unless `policies.allowStandingApprovals`. `{ did, delegation, perClaimUpay, totalUpay, until }` replaces it: the same delegation checks (`maxAmount` = `totalUpay`, expiration ≥ `until`); `did` must be the grant's user as this oracle has seen it (its standing, else an approval) — else `404`. `spentUpay` is never reset (the total is this auto-approve's total across every delegation): a total below it is `400 VALIDATION` (`details.reason: limits_invalid`), nothing changed |
|
|
1809
1814
|
| `DELETE /v1/oauth/standing/:grantId` | UCAN `oauth/standing/revoke`, same | `{ did }`: deletes it, delegation and all; `200` whether or not it existed; another user's `did` is `404` |
|
|
1810
|
-
| `POST /v1/reviews/:claimId` | UCAN `reviews/submit` (never `*`) + **the reviewer gate** (below) — never the read gate: an evaluate grant alone does not answer | `{ answers: { taskId → boolean \| string }, attestation, reviewerGrant }` — the answers must cover **exactly** the pending tasks (all-or-nothing). `202` + the review row, and the `ReviewComplete` workflow spawns. Answered-once until folded: a re-POST before the fold replaces the answers and the grant and re-spawns (that is also how a `foldError` is cleared); after the fold it `409`s |
|
|
1815
|
+
| `POST /v1/reviews/:claimId` | UCAN `reviews/submit` (never `*`) + **the reviewer gate** (below) — never the read gate: an evaluate grant alone does not answer | `{ answers: { taskId → boolean \| string }, attestation, reviewerGrant }` — the answers must cover **exactly** the pending tasks (all-or-nothing). `202` + the review row, and the `ReviewComplete` workflow spawns. Answered-once until folded: a re-POST before the fold replaces the answers and the grant and re-spawns (that is also how a `foldError` is cleared); after the fold it `409`s. With `CHAIN_LANE` `"false"` a chain claim's answer is still accepted and folded (the engine's review call; a per-review payer is charged) — only the signing is skipped |
|
|
1811
1816
|
|
|
1812
1817
|
**The oracle never deletes an organisation's claim data on its own** (owner, 2026-10-11; company
|
|
1813
1818
|
authority D12). Nothing expires a chain claim's transcripts or registry text, nor an API claim's
|
|
@@ -2424,6 +2429,12 @@ trigger is handled in its own `scheduled` before it delegates to the oracle's.
|
|
|
2424
2429
|
Sentry cron monitors: `oracle-scan`, `oracle-claims-feed` and (with `API_LANE`) `oracle-api-lane` per minute,
|
|
2425
2430
|
`oracle-cron-five`, `oracle-cron-hour`.
|
|
2426
2431
|
|
|
2432
|
+
With `CHAIN_LANE` `"false"` every tier keeps only what is not the chain's: the minute tier runs the
|
|
2433
|
+
`API_LANE` share and the backlog gauges, the five-minute `ai-cost-backfill` and the facilitator jobs,
|
|
2434
|
+
the hourly `review-rooms` and the `API_LANE` share (whose per-review collection resume is a no-op:
|
|
2435
|
+
those are chain collections). Each tier logs one `cron.chain-lane.off` at debug. The `oracle-scan`
|
|
2436
|
+
and `oracle-claims-feed` monitors stop checking in, so close them in Sentry if you set one up.
|
|
2437
|
+
|
|
2427
2438
|
1. `resumeEngineCredits` — while any collection is paused `engine-credits`, ask the engine's
|
|
2428
2439
|
`GET /v1/oracles/me` **once** and resume them all when `billingStatus` is `ok`.
|
|
2429
2440
|
2. `resumeAdminFunds` — while any is paused `admin-unfunded`, read the collection admin's spendable
|
|
@@ -2563,7 +2574,8 @@ than a suppressed one.
|
|
|
2563
2574
|
|
|
2564
2575
|
### The chain write
|
|
2565
2576
|
|
|
2566
|
-
`deliverChainEvaluation` reads everything from the decision row and the chain.
|
|
2577
|
+
`deliverChainEvaluation` reads everything from the decision row and the chain. With `CHAIN_LANE`
|
|
2578
|
+
`"false"` it signs nothing (`act.skipped`, the row left as it was). It acts **only** on a
|
|
2567
2579
|
terminal `approve`/`reject`, only when `policies.autoSign` **and** `AUTO_SIGN` are both on, and only
|
|
2568
2580
|
when `ORACLE_SECP_MNEMONIC` is set (otherwise: one log line, skip). `verification_proof` is the
|
|
2569
2581
|
engine's signed receipt CID — a terminal decision without one is parked `blocked`, never delivered
|
package/docs/guide.md
CHANGED
|
@@ -1018,10 +1018,58 @@ Registration is one call with four gates, in this order:
|
|
|
1018
1018
|
`company_unavailable` when the facilitator or the engine cannot say). Without one, the leg gate
|
|
1019
1019
|
above stands, its refusal `402 no_review_billing` naming both ways in.
|
|
1020
1020
|
|
|
1021
|
-
Every refusal carries a stable machine code in `details.reason` (`
|
|
1022
|
-
`not_owner_or_grantee`, `not_hired`, `unsupported_protocol`,
|
|
1023
|
-
`no_review_billing`, `insufficient_credits`, `company_unavailable`,
|
|
1024
|
-
portal never matches a message.
|
|
1021
|
+
Every refusal carries a stable machine code in `details.reason` (`chain_lane_off`,
|
|
1022
|
+
`collection_not_found`, `not_owner_or_grantee`, `not_hired`, `unsupported_protocol`,
|
|
1023
|
+
`evaluation_underpaid`, `no_review_billing`, `insufficient_credits`, `company_unavailable`,
|
|
1024
|
+
`credits_unavailable`, …), so a portal never matches a message.
|
|
1025
|
+
|
|
1026
|
+
### An API-only oracle: turning the chain lane off
|
|
1027
|
+
|
|
1028
|
+
An oracle that serves only the Decision API (`API_LANE` on) still runs the chain lane by default,
|
|
1029
|
+
and that is a door you may not want open: your protocol is public on chain, so anyone can create a
|
|
1030
|
+
claim collection on it, grant your oracle, and register it — at no fee at all where your `pricing`
|
|
1031
|
+
charges none (a `byProtocol` `minAmount: "0"`, or an empty evaluation leg). From then on every claim
|
|
1032
|
+
there is pulled, its facts produced and the engine asked to decide, on **your** engine credits, and
|
|
1033
|
+
its reviews land in your organisation's inbox.
|
|
1034
|
+
|
|
1035
|
+
Close it with one var in `wrangler.jsonc`, per env:
|
|
1036
|
+
|
|
1037
|
+
```jsonc
|
|
1038
|
+
"CHAIN_LANE": "false", // needs "API_LANE": "true" — with no lane at all the worker refuses to start
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Then:
|
|
1042
|
+
|
|
1043
|
+
- `POST /v1/collections/register` (and the `registerCollection` runtime export) answers
|
|
1044
|
+
`409 { reason: "chain_lane_off" }` before any of register's own reads, so nothing is stored;
|
|
1045
|
+
`decision-oracle register` prints that answer as it is.
|
|
1046
|
+
- `POST /v1/collections/:id/scan` (`decision-oracle scan`, the `scanCollection` export), a
|
|
1047
|
+
collection's resume (`POST /v1/collections/:id/resume` — a pause is still allowed), the
|
|
1048
|
+
operator's `POST /v1/admin/claims/:claimId/redeliver` and the `rehireOpenhands` export answer the
|
|
1049
|
+
same `409`, and so do the OpenHands webhooks (`{ error, reason: "chain_lane_off" }`).
|
|
1050
|
+
- The cron runs no chain job: no scan, no claims feed or replay, no chain-collection resume, no
|
|
1051
|
+
OpenHands hire. Each tier logs one `cron.chain-lane.off` line at debug, never one per collection.
|
|
1052
|
+
The Sentry monitors `oracle-scan` and `oracle-claims-feed` stop checking in, so close them if you
|
|
1053
|
+
had them.
|
|
1054
|
+
- A chain claim's run already in flight when you flip it (or retrying, or restarted by hand) stops
|
|
1055
|
+
before its pull: nothing is pulled, no AI is spent, the engine is not asked. The step retries on
|
|
1056
|
+
its backoff, so the run carries on once the lane is back on — within its ~4 hours of retries;
|
|
1057
|
+
past them it ends `failed` and is rerun like any failed claim. One that had already decided
|
|
1058
|
+
keeps its decision and receipt.
|
|
1059
|
+
- The act lane signs no `MsgEvaluateClaim`; a decided row is left as it was (`pending`, or
|
|
1060
|
+
`blocked`) for the owner to submit from the portal.
|
|
1061
|
+
- A reviewer's answer on a chain claim already waiting for review (`pending_review`) is still
|
|
1062
|
+
folded: the engine is asked to complete the review, and a collection billed per review charges
|
|
1063
|
+
its payer for it, so the decision is recorded for the owner. Only the signing is skipped.
|
|
1064
|
+
- `GET /v1/info` says `lanes: { chain: false, api: true }`, so a portal does not offer the oracle
|
|
1065
|
+
for a chain hire (the listing is cached 5 minutes, so a flip shows up to 5 minutes late).
|
|
1066
|
+
|
|
1067
|
+
The Decision API lane is untouched: sources, keys, submits, payments, payouts, reviews, webhooks
|
|
1068
|
+
and its cron share run exactly as before.
|
|
1069
|
+
|
|
1070
|
+
Collections you already watch are not deleted or changed — they are simply not scanned. Set
|
|
1071
|
+
`CHAIN_LANE` back to `"true"` (or remove it) and redeploy, and the next minute tick scans them from
|
|
1072
|
+
their cursors, the claims submitted meanwhile included, and the resumes run again.
|
|
1025
1073
|
|
|
1026
1074
|
### Getting hired on OpenHands
|
|
1027
1075
|
|
|
@@ -1043,9 +1091,15 @@ oracle the right to authorize itself on the new collection, and the app calls
|
|
|
1043
1091
|
own evaluate grant under it, registers the collection through the same four gates above, and scans
|
|
1044
1092
|
it. A grants feed every minute catches a call that never arrived (it follows the meta-grants
|
|
1045
1093
|
made to your oracle on Blocksync), and a backstop every five minutes keeps every hired ask
|
|
1046
|
-
topped up. `GET /v1/info` lists the two webhooks under `openhands` — that is how the app knows
|
|
1094
|
+
topped up. `GET /v1/info` lists the two webhooks (and the why route below) under `openhands` — that is how the app knows
|
|
1047
1095
|
you are an OpenHands checker. (api.md §2, "The OpenHands add-on", has every step and answer.)
|
|
1048
1096
|
|
|
1097
|
+
The add-on also answers the people who submit: `GET /openhands/claims/<claimId>/why` tells a
|
|
1098
|
+
claim's own submitter (a UCAN their app signs as the claim's on-chain agent) which checks passed,
|
|
1099
|
+
which failed and why, in the rubric's words — the reasons the public receipt leaves out — and the
|
|
1100
|
+
OpenHands app shows it on the claim. A claim still running, or one whose check could not run, reads
|
|
1101
|
+
"pending"; the second with a fixed sentence, never your logs' error text. Nothing to configure.
|
|
1102
|
+
|
|
1049
1103
|
Three things to decide:
|
|
1050
1104
|
|
|
1051
1105
|
- **Your protocols.** With `supportedProtocols` set, the app offers your oracle for asks on those
|
|
@@ -1375,7 +1429,8 @@ write, a webhook retry job) is still reported, codes and ids only — answers, f
|
|
|
1375
1429
|
API lane's personal fields are redacted there and in every log line. `SENTRY_TRACES_SAMPLE_RATE`
|
|
1376
1430
|
(default `0.1`) and `SENTRY_ENVIRONMENT` (default `ENVIRONMENT`) tune it. The cron triggers check in
|
|
1377
1431
|
to Sentry cron monitors: `oracle-scan` and — only with `API_LANE` on — `oracle-api-lane` every
|
|
1378
|
-
minute, `oracle-cron-five` every five minutes and `oracle-cron-hour` every hour
|
|
1432
|
+
minute, `oracle-cron-five` every five minutes and `oracle-cron-hour` every hour (with `CHAIN_LANE`
|
|
1433
|
+
off, `oracle-scan` and `oracle-claims-feed` stop: §11, "An API-only oracle"); alert
|
|
1379
1434
|
on a missed check-in and a stopped cron pages you. The conditions that need a person — a collection
|
|
1380
1435
|
or API source paused (per reason), a webhook receiver that refused all 8 attempts, a paid claim whose
|
|
1381
1436
|
settlement was lost or whose receipt the facilitator refused, a facilitator that refuses the
|
package/package.json
CHANGED
|
@@ -148,6 +148,14 @@ courier page answer (`curl -s https://shipment-delivery-oracle.devnet.ixo.earth/
|
|
|
148
148
|
Later deploys: `bunx wrangler deploy --env <env>` (secrets persist); one secret: `bunx wrangler
|
|
149
149
|
secret put <NAME> --env <env>`. The did.json `id` must be `did:web:<host>`.
|
|
150
150
|
|
|
151
|
+
**API-only? Close the chain lane.** An oracle that serves only this lane still takes chain hires by
|
|
152
|
+
default — its protocol is public, so anyone can create a collection on it, hire the oracle (free
|
|
153
|
+
where `pricing` charges nothing) and spend the owner's engine credits. Ask the owner; if they want
|
|
154
|
+
API-only, set `"CHAIN_LANE": "false"` in that env's `vars` (needs `"API_LANE": "true"`), redeploy,
|
|
155
|
+
and check `curl -s https://<host>/v1/info` shows `"lanes":{"chain":false,"api":true}`. Register,
|
|
156
|
+
scan and the OpenHands webhooks then answer `409 chain_lane_off`; the API lane is untouched; setting
|
|
157
|
+
it back to `"true"` resumes any collections already watched (guide §11, "An API-only oracle").
|
|
158
|
+
|
|
151
159
|
### 6. Engine registration — payable at once
|
|
152
160
|
|
|
153
161
|
```bash
|
|
@@ -315,6 +323,8 @@ Decision Console shows the price and card eligibility read-only.
|
|
|
315
323
|
| --- | --- |
|
|
316
324
|
| Console / CLI 401 `UNAUTHORIZED` on a UCAN call | audience mismatch: the invocation names the did:web read from `<host>/.well-known/did.json` — `ORACLE_HOSTNAME` must equal the host the env is served from (its Custom Domain, or its `workers.dev` host), and `--kit` must be that host |
|
|
317
325
|
| 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
|
+
| 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
|
+
| `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 |
|
|
318
328
|
| `PROTOCOL_NOT_ALLOWED` on submit | the source's allow-list lacks it — empty means NONE; `api admin protocols <src> <did> --yes` |
|
|
319
329
|
| 503 `SOURCE_PAUSED` `payments-disabled` | `API_PAYMENTS` is off and the source bills `x402` — turn payments on, or `api admin billing-mode <src> none` |
|
|
320
330
|
| 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 |
|
|
@@ -52,6 +52,10 @@ UNIQUE_HMAC_KEY="change-me-to-a-long-random-string"
|
|
|
52
52
|
# openssl rand -base64 32
|
|
53
53
|
# (deployed: wrangler secret put WEBHOOK_ENCRYPTION_KEY [--env testnet|production])
|
|
54
54
|
# WEBHOOK_ENCRYPTION_KEY="change-me-to-a-long-random-string"
|
|
55
|
+
# An oracle that serves ONLY this lane turns the chain lane off with CHAIN_LANE "false" in
|
|
56
|
+
# wrangler.jsonc (on by default): nobody can then hire it on chain and spend your engine credits.
|
|
57
|
+
# Needs API_LANE on. Not a secret; for `wrangler dev` only:
|
|
58
|
+
# CHAIN_LANE="false"
|
|
55
59
|
# Where your earnings land is NOT a secret: EARNINGS_ADDRESS (ixo1…, PAY and USDC on IXO) and
|
|
56
60
|
# EARNINGS_BASE_WALLET (0x…, USDC on Base) live in wrangler.jsonc `vars`, per env. Each offer names
|
|
57
61
|
# them as its payTo; a way to pay whose address is unset is not offered. For `wrangler dev` only:
|
|
@@ -193,6 +193,11 @@
|
|
|
193
193
|
// Sign `MsgEvaluateClaim` on a terminal verdict automatically. "false" ⇒ decisions are
|
|
194
194
|
// recorded and receipted, but a human submits them from the portal.
|
|
195
195
|
"AUTO_SIGN": "true",
|
|
196
|
+
// The chain lane (collections hired on chain) is ON unless this says "false". Turn it off only
|
|
197
|
+
// for an oracle that serves the Decision API alone: your protocol is public on chain, so anyone
|
|
198
|
+
// could hire this oracle on a collection of their own and spend YOUR engine credits. Off ⇒
|
|
199
|
+
// register, scan and the hire webhooks answer 409 chain_lane_off; needs API_LANE "true".
|
|
200
|
+
// "CHAIN_LANE": "false",
|
|
196
201
|
|
|
197
202
|
// The Decision API lane: paid, off-chain claims over HTTP at /v1/api. Dormant until API_LANE is
|
|
198
203
|
// "true"; with API_PAYMENTS "false" new submits answer 503 payments-disabled, so nothing is
|
|
@@ -261,6 +266,7 @@
|
|
|
261
266
|
// "AI_GATEWAY_ID": "<your gateway>",
|
|
262
267
|
"C2PA_IMAGE_CHECK": "true",
|
|
263
268
|
"AUTO_SIGN": "true",
|
|
269
|
+
// "CHAIN_LANE": "false", // API-only oracle: no chain hires (needs API_LANE "true")
|
|
264
270
|
"API_LANE": "false",
|
|
265
271
|
"API_PAYMENTS": "false"
|
|
266
272
|
// "API_PRICE_USD_CENTS": "120",
|
|
@@ -371,6 +377,7 @@
|
|
|
371
377
|
// "AI_GATEWAY_ID": "<your gateway>",
|
|
372
378
|
"C2PA_IMAGE_CHECK": "true",
|
|
373
379
|
"AUTO_SIGN": "true",
|
|
380
|
+
// "CHAIN_LANE": "false", // API-only oracle: no chain hires (needs API_LANE "true")
|
|
374
381
|
"API_LANE": "false",
|
|
375
382
|
"API_PAYMENTS": "false"
|
|
376
383
|
// "API_PRICE_USD_CENTS": "120",
|
|
@@ -481,6 +488,7 @@
|
|
|
481
488
|
// "AI_GATEWAY_ID": "<your gateway>",
|
|
482
489
|
"C2PA_IMAGE_CHECK": "true",
|
|
483
490
|
"AUTO_SIGN": "true",
|
|
491
|
+
// "CHAIN_LANE": "false", // API-only oracle: no chain hires (needs API_LANE "true")
|
|
484
492
|
"API_LANE": "false",
|
|
485
493
|
"API_PAYMENTS": "false"
|
|
486
494
|
// "API_PRICE_USD_CENTS": "120",
|