Install, one call, done.

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.

verify.mjs@observer-protocol/policy-engine 1.0.0-rc.6 · MIT
$ 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()));
what that printsmeasured 8 August 2026
{
  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.

A verifier that says yes to everything teaches you nothing.

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.

allow: true
verifies-delegation-mandate.json
A real ObserverDelegationCredential, issued 4 August 2026, byte-identical to the copy under /credentials/.
credential verified
Download →
allow: false
must-not-verify-tampered-signature.json
The same credential with one character changed inside proof.proofValue. Every other byte is identical. If this verified, nothing else on this site would be worth reading.
[proof] eddsa-jcs-2022 signature does not verify against the issuer key
Download →
allow: false
must-not-verify-expired-mandate.json
The same credential with its validity window moved into the past. Two defects are present, since the signature no longer covers the altered document either; the window is checked first, so that is what is named.
validity: credential expired (validUntil 2026-02-01T00:00:00Z)
Download →

These two files are deliberately broken

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 ↗

There is no verifier for a PolicyEvaluationCredential. Not here, not hosted, not anywhere.

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.

What each element lets you establish, and what it does not.

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.

issuer · "did:web:bitcoinsingularity.ai"
What it is The identifier of the party that signed this credential. A did:web resolves to a document at that domain's /.well-known/did.json carrying the public keys.
What it proves That whoever controls the signing key for that domain asserted this. Combined with the proof check, the document has not changed since.
How to check Resolve the DID document yourself over HTTPS and confirm the key in proof.verificationMethod appears in its assertionMethod. The engine does this and reports issuerResolution.
Its limit 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.
credentialSubject.id · "did:web:observerprotocol.org:agents:maxi-0001"
What it is The agent this mandate was granted to.
What it proves That the issuer intended this authority for this subject and no other. A credential presented on behalf of a different DID is a different credential.
How to check Compare it against the DID whose action you are examining. The hosted check reports this as identity.valid with the note that the mandate subject binds to the DID.
Its limit The binding is between a mandate and an identifier, not between an identifier and a running process. It does not establish that the software which took an action is the holder of that DID's key. That requires the enforcement point, not the credential.
validFrom / validUntil
What it is The window inside which the issuer intends this authority to exist.
What it proves That authority was time-bounded at the moment of granting, rather than open-ended.
How to check First check the engine runs. Compare against the time of the action being examined, not against now.
Its limit An unexpired credential is not a live one. Expiry and revocation are different mechanisms and this field only covers the first. See credentialStatus below.
credentialSubject.tradingMandate.maxNotionalPerOrder · 10 · unit "USD"
What it is The per-order ceiling, carried alongside the unit it is denominated in.
What it proves The bound the principal signed for. A constraint without its unit is not a constraint, so the unit travels in the credential rather than being assumed by the reader.
How to check Read it directly. Compare against the notional of the order you are examining, converting at a rate you choose rather than one we supply.
Its limit This is a per-order ceiling and nothing else. It does not bound the number of orders, the aggregate across a day, or the total exposure. Those are separate sub-objects and this credential does not carry them.
credentialSubject.tradingMandate.temporal.allowedTimeWindows
What it is Permitted hours and days, each with an explicit IANA timezone: 09:00–17:00 America/New_York, Monday to Friday.
What it proves That the window was specified against a named zone rather than a bare wall-clock string, so it resolves identically for a reader in another jurisdiction.
How to check Convert the timestamp of the action into the named zone before comparing. Comparing timestamp strings across zones is a defect, not a shortcut.
Its limit It states when action was permitted, not when action occurred. Establishing the latter requires the rail's own record or an attestation, both outside this document.
credentialSubject.enforcementMode · "pre_transaction_check"
What it is A declaration of where in the flow this mandate is meant to be applied.
What it proves The issuer's intent that the check happens before the instruction exists, rather than as a review afterwards.
How to check Against the enforcement point's own evidence. The credential states the intent; only the deployment shows the behaviour.
Its limit This is the most important limit on the page. A field in a document cannot enforce anything. It records what the issuer asked for. Whether an enforcement point was actually in the path, and failed closed, is a property of a deployment and must be measured separately from reading this credential.
credentialSubject.delegationScope.may_delegate_further · false
What it is Whether the holder may issue onward authority derived from this one.
What it proves That the authority chain is intended to stop here, so a credential citing this one as its parent contradicts it on its face.
How to check If you are handed a derived credential, check whether its chain passes through a mandate carrying false here.
Its limit It bounds delegation, not sharing. Nothing in the document prevents the subject's key material from being used by another party; that is a custody question, answered by where the key lives.
credentialStatus · ABSENT
What it is A pointer to a revocation status list. This credential does not carry one.
What it proves Nothing, by absence. Both verifiers say so explicitly rather than passing quietly: the engine reports revocation: 'status-absent', and the hosted service returns the note "credential carries no credentialStatus entry — revocation not checkable for this credential".
How to check Where a credential does carry one, the engine fetches it under 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.
Its limit For this artifact, you cannot tell whether the issuer has since revoked it. Treat 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.
proof · DataIntegrityProof · eddsa-jcs-2022
What it is An Ed25519 signature over the JCS-canonicalised document, naming the key that produced it in verificationMethod.
What it proves That not one byte of the document has changed since signing. The tampered sample above differs by a single character and fails here.
How to check The engine reports 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.
Its limit A signature gives continuity, not identity. It establishes that the same key signed this that signed the other things you have from this issuer. Who holds that key, and whether they still should, are questions this field cannot answer.

Convenient, ours, and a different build.

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.

POST /v1/verify · no authenticationverify.observerprotocol.org
$ 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 two verifiers are not the same build

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.

Checking a record is not approving a vendor.

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.