This page is for the party doing the checking: a supervisor, an external auditor, a counterparty's risk function, someone contesting a determination made about them. It covers delegation credentials, which is what the published engine knows how to read — if you are holding a refusal record or a PolicyEvaluationCredential, see the note in section 02 before you start. You are not our customer and you should not have to become one. Everything below runs on your machine, against artifacts we had no part in putting in your hands.
No API key. No bearer token. No account. Point the verifier at a local copy of the issuer's DID document and it makes no network call at all — not to us, not to anyone. That is the mode to use when the artifact you are holding should not leave your machine.
$ npm install @observer-protocol/policy-engine $ curl -O https://observerprotocol.org/verify-samples/verifies-delegation-mandate.json import { verifyCredentialObject } from '@observer-protocol/policy-engine'; import fs from 'node:fs'; const credential = JSON.parse(fs.readFileSync('./verifies-delegation-mandate.json')); const config = { credentialPath: 'verifies-delegation-mandate.json', // Pin the issuer you expect. A verifier that trusts whoever the // credential names as its issuer is not verifying anything, // it is agreeing. A mismatch here is a denial. issuerDid: 'did:web:bitcoinsingularity.ai', schemaAllowlist: ['https://observerprotocol.org/schemas/delegation/v2.2.json'], revocation: { maxStalenessHours: 24, onUnreachable: 'cache-then-deny', fetchTimeoutMs: 5000 }, didCache: { maxStalenessHours: 24 }, cacheDir: '/tmp/op-cache', auditLog: '/tmp/op-cache/audit.log', rails: {}, allowContractCalls: false, // offline: { didDocumentPath: './did.json' } // no network at all }; console.log(await verifyCredentialObject(credential, config, Date.now()));
{
allow: true,
reason: 'credential verified',
notes: [ 'credential carries no credentialStatus entry —
revocation not checkable for this credential' ],
checks: {
validityWindow: 'passed',
issuerResolution: 'network',
issuerProof: 'eddsa-jcs-2022-verified',
signerBoundary: 'not-configured',
revocation: 'status-absent'
}
}
Read the checks, not the verdict. allow: true on its own tells you almost nothing. This run says the signature verified against a key published at the issuer's own domain, and it also says that no signer boundary was configured and that this credential carries no revocation entry, so neither of those was established. A verifier that returned only the green result would have hidden both. Both are recorded in the section below as limits of this artifact.
The fastest way to establish that a tool is real is to hand you the inputs it must reject. All three files below are served from this domain. The verdicts and reason strings are the engine's own output from a run on 8 August 2026, copied rather than described.
ObserverDelegationCredential, issued 4 August 2026, byte-identical to the copy under /credentials/.proof.proofValue. Every other byte is identical. If this verified, nothing else on this site would be worth reading.They are fixtures for the verifier and are not authorisations for anything. The agent named inside them is real; the documents are not valid credentials about it. The directory README states this too ↗
The refusal record described elsewhere on this site — the signed artifact saying this action was checked and the decision was X — is a PolicyEvaluationCredential. You cannot currently verify one with anything we publish, and every one ever issued fails the same way. Two of them are served under /credentials/ and you will hit this immediately if you run the check above across that directory.
The signatures are sound. They verify under canonical W3C Data Integrity tooling — @digitalbazaar/data-integrity with the eddsa-jcs-2022 cryptosuite — against did:web:observerprotocol.org#key-3, with no Observer endpoint in the path. The documents are intact and the key is the right one. If you want to check an evaluation credential today, that is the route, and it is not ours.
The gap is ours, and it is wider than a missing field. The published engine has no dispatch on credential type at all: the string PolicyEvaluationCredential does not appear in it. Its one structural validator assumes a delegation credential and checks delegation fields in sequence, so an evaluation credential fails at whichever delegation assumption it reaches first. That is reported as a missing credentialSchema, which is misleading — supplying one moves the failure to credentialSubject.id must be a DID, because an evaluation credential's subject has no identifier. It has a decision. The hosted service is built the same way: its endpoint accepts {agentDid, mandate} and has no other shape.
And there is no schema to point at. No PolicyEvaluationCredential schema is published at any URL. AIP v0.8 defines the credential type and requires schema validation when verifying it, without ever assigning it a schema. That is a contradiction in the specification rather than a bug in a deployment, and closing it means publishing a schema at a permanent URL and adding type dispatch to the engine. Both are protocol decisions and neither is a copy change, so neither was done in the pass that wrote this page.
The two artifacts stay published. Removing them would have made this page look clean while leaving the reader to discover the same thing on their own, which is precisely what this page exists to argue against. Everything under /credentials/ and /verify-samples/ is now checked in CI against a recorded expected verdict, in both directions, so the day this is fixed the build will say so.
Every field has a limit. Where a field establishes less than its name suggests, that is said here rather than left for you to discover. Field names and derivations are taken from the published schema at /schemas/delegation/v2.2.json and from the engine source, not from our marketing copy.
did:web resolves to a document at that domain's /.well-known/did.json carrying the public keys.proof.verificationMethod appears in its assertionMethod. The engine does this and reports issuerResolution.did:web binds to domain control, not to a legal entity. It tells you the same party that controls that hostname signed this. It does not tell you who that party is, and if the domain changes hands the historical signature does not change with it.identity.valid with the note that the mandate subject binds to the DID.credentialStatus below.America/New_York, Monday to Friday.false here.revocation: 'status-absent', and the hosted service returns the note "credential carries no credentialStatus entry — revocation not checkable for this credential".onUnreachable: 'cache-then-deny': if the list cannot be reached, a cached answer is used and then the credential is denied. Nothing is let through on a fetch failure.allow: true here as "validly issued and unexpired", not as "currently in force". A status list hosted on an origin other than the pinned issuer's is also refused until you allowlist it — and Observer Protocol's own clause-zero revocation demonstration is exactly such a pair, so it does not verify out of the box.verificationMethod.issuerProof: 'eddsa-jcs-2022-verified'. Canonicalisation matters: two documents that differ only in key order produce the same signature input, and two that differ in any value do not.There is also a hosted verifier. It is open — no token, no account, rate-limited at 60 requests per caller per minute — and it returns a signed result. Use it if you want a second opinion or a quick look. Do not use it for an artifact that should not leave your machine, and do not mistake it for the claim this site makes.
$ curl -X POST https://verify.observerprotocol.org/v1/verify \ -H 'Content-Type: application/json' \ -d '{"agentDid": "<subject DID>", "mandate": <the credential>}' { "identity": { "valid": true, ... }, "mandate": { "valid": true, "notes": ["credential carries no credentialStatus entry — revocation not checkable for this credential"] }, "proof": { "cryptosuite": "eddsa-jcs-2022", "verificationMethod": "did:web:observerprotocol.org#key-7" } }
The hosted service reports engine.running: "0.3.3". The published package is 1.0.0-rc.6. On the artifacts we have put through both they agree, including on the exact reason string for a structural failure — but that is agreement observed on samples, not a shared code path, and you should not treat one as a substitute for the other. Bringing the hosted service onto the published engine is tracked; until it lands, this notice stays.
The service is also candid about a second limit, in its own /version response: the schema and issuer allowlists it enforces come from environment variables on the host, inRepo: false, so a change to what it accepts "leaves no reviewable record". The offline path has no such property, because the allowlist is the one you wrote.
Two further limits, and they are worse than the two above.
A scope answer that does not vary with its input. Attach a proposal and scope.inScope returns false for every USDC amount — a compliant one, an absurd one, any of them. The cause is that engine 0.3.3 cannot scale USDC minor units, so the comparison it is asked to make cannot be made and it fails closed. Failing closed is the correct behaviour and the outcome is still useless: a check that returns the same answer for every input is not telling you about your input. Read a false here as “not evaluated”, never as “out of scope”. The offline package does not share this limit.
A refusal that does not say why. When mandate.valid is false, notes comes back []. The refusal may be entirely correct and you still cannot tell whether the credential was refused for its schema, its signature, its validity window or its issuer. A caller cannot act on it, cannot log anything useful about it, and cannot distinguish a broken credential from a broken deployment. The offline path returns a reason string for the same conditions; that is the one to use if you need to know what happened.
The offline path is the claim. The hosted path is a convenience. If they ever disagree, the offline result is the one to act on, and we would like to hear about it.
Using this tooling implies nothing about Observer Protocol, Agentic Terminal, or any firm whose records you are examining. There is no account to create, no form on this page, and nothing here that asks you for a way to contact you. If you are checking a record because someone is asking you to accept it, the correct outcome of this page is that you can answer that question yourself and then leave.
The verifier is MIT-licensed and self-hostable. A verifier you run from your own infrastructure is worth considerably more than one you load from us, and we would rather you did that. The source, the known limits, and the schemas are all public.
A browser-based verifier that runs entirely client-side is not available yet, and we are not going to ship one that sends your artifact to a server while calling it client-side. The published package depends on Node built-ins — node:crypto, node:fs, node:dns among them — and the DNS use is a live guard against a hostile URL rather than incidental. Porting it means replacing that guard honestly, not stubbing it. Until then, the two paths above are what exist.