Every verdict carries an append-only, Ed25519-signed, hash-chained proof. This page is the complete recipe to verify one yourself, with no trust in us and no AI Verify software — just a published key and a few lines of standard crypto.
A passing check means the bytes below were signed by the named key and have not changed since. It says nothing about whether the verdict is right — that is the verdict’s own determination, not what the signature attests.
1. The signed envelope — the exact bytes that were hashed and signed, plus the signature. The proof owner downloads it with their API key (raw storage stays private; this is the public door to it) and can hand the result to anyone.
GET /v1/verifications/{id}/proof/envelope (Authorization: Bearer <your API key>)
{
"verification_id": "…",
"format": "acretix-verify/proof/2",
"signing_domain": "acretix-verify/proof-sig/2:",
"jwks_uri": "/.well-known/acretix-verify/keys.json",
"entries": [
{
"seq": 0,
"key_id": "ed25519-…",
"algo": "ed25519",
"content_hash": "<sha256 hex>",
"prev_hash": null,
"signature": "<base64 Ed25519>",
"envelope": "<the exact canonical JSON bytes that were hashed>"
}
]
}2. The public key — published as a JWKS. Pick the key whose kid equals the entry’s key_id. No private key is ever served.
GET /.well-known/acretix-verify/keys.json (public, no auth)
{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "<base64url public key>", "kid": "ed25519-…", "use": "sig", "alg": "EdDSA" } ] }acretix-verify/proof/2, algorithm Ed25519 (EdDSA, no prehash). Reject anything else.{ format, algo, key_id, verification_id, seq, rubric_hash, submission_hash, prev_hash, payload, created_at }. format/algo/key_id are bound inside the signed body, so the scheme cannot be silently downgraded or confused.acretix-verify/proof-sig/2: immediately followed by the content_hash hex. The signature is Ed25519 over those UTF-8 bytes, base64-encoded. The domain tag is what makes a proof signature unusable for anything but a proof.prev_hash equals the previous entry’s content_hash, and seq runs contiguously from 0.Save the envelope JSON to a file; pass it and the JWKS URL. Runs on stock Node (≥ 18), no dependencies. Exits non-zero if any entry fails.
import { readFileSync } from 'node:fs';
import { createHash, createPublicKey, verify } from 'node:crypto';
const envelope = JSON.parse(readFileSync(process.argv[2], 'utf8')); // .../proof/envelope output
const jwks = await fetch(process.argv[3]).then((r) => r.json()); // the public JWKS URL
for (const e of envelope.entries) {
const hash = createHash('sha256').update(e.envelope).digest('hex'); // 1. hash the bytes
const key = createPublicKey({ key: jwks.keys.find((k) => k.kid === e.key_id), format: 'jwk' }); // 2. the key
const ok = hash === e.content_hash && // 3. integrity
verify(null, Buffer.from('acretix-verify/proof-sig/2:' + hash), key, Buffer.from(e.signature, 'base64')); // 4. signature
console.log('seq ' + e.seq + ': ' + (ok ? 'VERIFIED' : 'FAILED'));
}The signature is checked over the hash you compute from the envelope, so editing any byte of the verdict breaks the check. This is the same logic the standalone verifier (tools/verify-proof/from-endpoints.ts) runs in our own test suite.