diff --git a/articles/claim-evaluation-protocol.mdx b/articles/claim-evaluation-protocol.mdx
index d87d790..f653714 100644
--- a/articles/claim-evaluation-protocol.mdx
+++ b/articles/claim-evaluation-protocol.mdx
@@ -354,6 +354,10 @@ After the evaluation is repeatable and reviewable, you can decide whether any lo
Design Qi evaluation workflows with UCAN authority, Claims, evidence, rubrics, and UDID records.
+
+ Independently verify signed UDID determinations with the `@ixo/udid-verify` package.
+
+
Build Claim workflows while keeping protocol and service responsibilities separate.
diff --git a/docs.json b/docs.json
index 80d9c2d..8d038ae 100644
--- a/docs.json
+++ b/docs.json
@@ -110,6 +110,7 @@
"pages": [
"guides/dev/ixo-claims",
"guides/dev/agent-evaluations",
+ "guides/dev/verify-udid-receipts",
"guides/digital-mrv"
]
},
diff --git a/guides/dev/agent-evaluations.mdx b/guides/dev/agent-evaluations.mdx
index d0b8c9d..c62a69d 100644
--- a/guides/dev/agent-evaluations.mdx
+++ b/guides/dev/agent-evaluations.mdx
@@ -850,6 +850,10 @@ The agent helped evaluate the Claim, but it did not become the final authority.
Create, process, evaluate, dispute, and automate verifiable Claims.
+
+ Independently verify signed UDID determinations with the `@ixo/udid-verify` package.
+
+
Connect agents to IXO services through secure, capability-scoped tool interfaces.
diff --git a/guides/dev/verify-udid-receipts.mdx b/guides/dev/verify-udid-receipts.mdx
new file mode 100644
index 0000000..2b9e4c6
--- /dev/null
+++ b/guides/dev/verify-udid-receipts.mdx
@@ -0,0 +1,172 @@
+---
+title: "Verify UDID receipts"
+icon: "stamp"
+description: "Verify signed UDID receipts from the IXO Evals Engine end-to-end with the @ixo/udid-verify npm package: JWS signature, audience, canonical bytes."
+---
+
+Use this guide when a third party — a bridge, worker, oracle-to-oracle integration, or any external verifier — needs to check that a presented UDID receipt is authentic, addressed to them, unexpired, byte-canonical, and schema-valid, without running an IXO Evals Engine deployment.
+
+
+ The verification path described here is a standalone extract of the engine's own acceptance rule. Third-party verifiers run byte-identical code to the engine before it acts on a UDID, so a receipt that verifies for you would also verify for the engine.
+
+
+## What `@ixo/udid-verify` is
+
+`@ixo/udid-verify` is a publishable npm package (`Apache-2.0`) that verifies **UDID receipts** — the signed determinations issued by the [IXO Evals Engine](/articles/claim-evaluation-protocol) when a claim-evaluation workflow reaches a decision point.
+
+A UDID receipt is a compact JWS (`EdDSA` / Ed25519, algorithm-pinned) over the canonical JSON encoding of a determination payload (`iss`, `aud`, `sub`, `jti`, `act`, `res`, optional `out`). Verifying one correctly means more than checking the signature. The package ships:
+
+- `EdDSA`-pinned compact-JWS verification with `kid` selection.
+- A verification-only `VerifierKeyring` built from the public `GET /v1/issuer-keys` response (kids are recomputed from key bytes and rejected on mismatch — fail-closed).
+- Canonical-JSON helpers (deep key-sorted `canonicalJsonString`) and CID helpers (`cidV1RawSha256`, `cidV1RawSha256Utf8`).
+- Bundled copies of the normative UDID payload schemas.
+- `out` binding / consistency checks (the `out` narrative is descriptive only; `res` is the trusted decision surface).
+- `fetchIssuerKeys` and `parseIssuerKeysResponse` for issuer-key discovery.
+- A one-call `verifyUdidReceipt` that applies the engine's full acceptance rule and returns a list of named failures.
+
+## Who should use it
+
+Reach for `@ixo/udid-verify` when your service consumes UDID receipts but is **not** the engine that issued them.
+
+
+
+ An `ixo-bridge` or worker that gates on-chain submissions on a valid UDID should reject anything that fails the same acceptance rule as the engine.
+
+
+ An Agentic Oracle consuming another oracle's determination should verify the receipt end-to-end before treating `res` as trustworthy.
+
+
+ Registries, dashboards, or downstream services that show or act on determinations should verify receipts locally so the decision surface can be inspected without trusting an intermediate transport.
+
+
+
+If you are building an engine deployment that signs UDIDs, use the engine's internal `udid-validator` package instead — it keeps the issuer-side helpers (secret-to-key derivation, signing `IssuerKeyring`) and delegates verification to `@ixo/udid-verify` so both sides run identical code.
+
+## Install
+
+```sh
+npm install @ixo/udid-verify
+```
+
+Requires Node.js 20.10 or newer.
+
+## One-call verification
+
+For the common case — you have a compact JWS and know the engine that issued it — pass the engine's base URL and the audience the receipt must be addressed to:
+
+```ts
+import { verifyUdidReceipt } from "@ixo/udid-verify";
+
+const { valid, failures, report } = await verifyUdidReceipt(compactJws, {
+ expectedAud: "did:ixo:my-oracle",
+ issuerKeys: "https://evals.example.org",
+});
+
+if (!valid) {
+ throw new Error(`receipt rejected: ${failures.join(", ")}`);
+}
+
+// report.payload.res is the trusted decision surface (outcome, patch, reason).
+const decision = report.payload.res;
+```
+
+`verifyUdidReceipt` resolves the issuer's public keys from `GET /v1/issuer-keys` on the supplied base URL, verifies the compact JWS with `kid`-selected key material, and applies the engine's full acceptance rule. Canonical-byte enforcement is always on: a receipt whose signed bytes are not the canonical encoding of its payload is not a UDID the engine issued.
+
+## Bring your own keys
+
+If the keys are pinned, cached, or resolved out-of-band, pass a raw published-keys array or a pre-built `VerifierKeyring`:
+
+```ts
+import { VerifierKeyring, verifyUdidJws, verificationReportFailures } from "@ixo/udid-verify";
+
+const keyring = VerifierKeyring.fromPublishedKeys(publishedKeys); // the `keys` array
+const report = await verifyUdidJws(compactJws, keyring, {
+ expectedAud: "did:ixo:my-oracle",
+ enforceCanonicalBytes: true,
+});
+
+const failures = verificationReportFailures(report);
+if (failures.length > 0) throw new Error(`receipt rejected: ${failures.join(", ")}`);
+```
+
+`VerifierKeyring.fromPublishedKeys` holds no private material. It maps each JWS header `kid` to an Ed25519 public `KeyObject`, so historically issued UDIDs keep verifying across signing-key rotations. Every listed key is checked: `kid` is recomputed from the raw public key bytes and a mismatch is rejected. An unknown or missing `kid` fails closed as `unknown_kid`.
+
+## Fetch issuer keys manually
+
+If you want to cache the response, share it across verifications, or apply custom TLS or retry behavior, use `fetchIssuerKeys` (or `parseIssuerKeysResponse` when you already have the body):
+
+```ts
+import { fetchIssuerKeys, parseIssuerKeysResponse } from "@ixo/udid-verify";
+
+// Fetch from the engine's base URL.
+const keyring = await fetchIssuerKeys("https://evals.example.org", {
+ signal: controller.signal,
+});
+
+// Or parse a body you already have.
+const cached = parseIssuerKeysResponse(await response.json());
+```
+
+The `GET /v1/issuer-keys` route needs no auth token and is cacheable (`cache-control: public, max-age=300`). Your trust root is the TLS connection to the deployment you already trust to have evaluated the claim. Pin the keys if you need to remove that runtime dependency.
+
+## The acceptance rule
+
+Only trust `report.payload` after **every** check passes. `verificationReportFailures(report)` returns an empty array exactly when the receipt is trustworthy — the same acceptance rule the engine applies before submitting a determination on-chain, and the same one bridges and workers should apply before acting.
+
+The named failures mirror the engine's own gates:
+
+| Failure code | Meaning |
+| --- | --- |
+| `signature_invalid` | The compact JWS signature did not verify with the resolved `kid`. |
+| `audience_mismatch` | `payload.aud` is missing, malformed, or does not match `expectedAud`. |
+| `replay_rejected` | A supplied `replayCheck` reported the `jti` as already seen for this audience. |
+| `expired` | `payload.exp` is in the past (with optional `clockSkewSec`). |
+| `canonical_bytes_mismatch` | Signed bytes are not the deep-key-sorted canonical JSON encoding of the payload. |
+| `payload_schema_invalid` | Payload failed validation against the bundled UDID schemas. |
+| `missing_payload_after_verify` | Payload could not be parsed as a JSON object after signature verification. |
+
+Additional low-level errors — for example `unknown_kid` when the JWS header refers to a `kid` not in the keyring — surface on `report.errors`.
+
+
+ Do not gate downstream actions on the JWS signature alone. A receipt with a valid signature but a mismatched audience or a non-canonical payload is not a UDID the engine would act on, and it is not one your service should either.
+
+
+## `out` is descriptive, `res` is the decision surface
+
+The optional `out` block in a UDID payload is a human-readable narrative. It should not be treated as the trusted decision. The package exposes helpers to check that `out` stays consistent with `res` — for example, that a summary does not contradict the outcome or claim settlement authority it does not have:
+
+```ts
+import { checkOutConsistency, outIsDescriptiveOnlyNotSettlement } from "@ixo/udid-verify";
+
+const consistency = checkOutConsistency(report.payload);
+if (!consistency.ok) {
+ // Bindings resolve, but `out` conflicts with `res`.
+}
+```
+
+When you extract trusted decision data from a verified receipt, read it from `report.payload.res` — never from `out`.
+
+## Also included
+
+- `canonicalJsonString` / `sortKeysDeep` — the signing canonicalization.
+- `cidV1RawSha256` / `cidV1RawSha256Utf8` — the engine's content-proof convention (CIDv1, `raw` codec, sha2-256, base32) for traces, evidence, and rubric bindings.
+- `validateUdidPayloadSchema` — standalone payload schema validation.
+- `verificationReportMatchesCompactJws` — bind a verification report back to the exact compact bytes it was produced from.
+- `issuerKid` / `issuerPublicKeyFromRaw` — recompute a `kid` from raw Ed25519 public key bytes.
+
+## Related docs
+
+
+
+ The architecture behind Claims, evidence, rubrics, and UDID determinations.
+
+
+ Design Qi evaluation workflows that issue UDID records.
+
+
+ Learn how oracle services fit into the IXO stack.
+
+
+ Build Claim workflows and record evaluations.
+
+