@fairseal/verify
Independent, fail-closed verification for FairSeal receipts (VEO objects).
Every check runs from public inputs. You do not need to trust FairSeal — or this
package's authors — to re-verify a receipt: signatures verify against the keys
embedded or supplied, and anchors verify against Base mainnet through any RPC
you choose.
npm install @fairseal/verify
Requires Node >= 18. Both ESM (import) and CJS (require) are supported
on all Node versions from 18 onward. @noble v2 cryptography is bundled into
the dist so CJS consumers do not need --experimental-require-module.
Quick start (VEO-2)
import { verifyVEO } from '@fairseal/verify';
const result = await verifyVEO(receipt);
if (result.valid) {
} else {
console.log(result.checks);
console.log(result.errors);
}
Result semantics (fail-closed)
valid | true only when all applicable checks pass and, if an anchor is present, it verified on-chain. Never true for an anchored object whose chain check was skipped or failed. |
structural | Structure + integrity + signature only. Can be true while valid is false (anchor pending/failed). |
anchored | true (verified on-chain) / false (check ran and failed) / "unknown" (chain unreachable) / "not_present" (no anchor on this object) / "not_checked" (structuralOnly mode) |
checks.* | Each check is pass, fail, or skipped with a detail string. |
"We could not verify" and "we verified it is wrong" are different states —
this package never collapses them.
Offline pre-screening
const result = await verifyVEO(receipt, { structuralOnly: true });
VEO-1 receipts (EIP-191 / secp256k1)
FairSeal API receipts (VEO-1) are signed with personal_sign (EIP-191) over a
canonical serialization:
import { verifyEIP191Signature, canonicalizeVEO1 } from '@fairseal/verify';
const check = verifyEIP191Signature(veo1Object);
Merkle and on-chain anchor utilities
import { verifyMerklePath, verifyAnchor, DEFAULT_CONTRACT_ADDRESSES } from '@fairseal/verify';
const ok = verifyMerklePath(leafHash, merklePath, expectedRoot);
const anchor = await verifyAnchor(anchorInfo, { rpcUrl: 'https://mainnet.base.org' });
verifyMerklePath alone proves inclusion against a root offline; it does
not prove the root is on-chain. Use verifyAnchor (or call
MerkleAnchor.getBatchRoot on any Base RPC yourself) for chain confirmation.
Verifying a notarize receipt directly (v0.4.0+)
From v0.4.0, verifyAnchor accepts the raw response from
GET /v1/notarize/:receipt_id — no manual field remapping needed:
import { verifyAnchor } from '@fairseal/verify';
const receipt = await fetch('https://api.fairseal.io/v1/notarize/nr_<id>').then(r => r.json());
const result = await verifyAnchor(receipt, { rpcUrl: 'https://mainnet.base.org' });
if (result.valid) {
console.log('✓ Anchored at block', result.onChain?.blockNumber);
} else {
console.log('✗', result.errors);
}
The adapter maps proof.anchor_tx → tx_hash, proof.anchor_chain → chain,
proof.merkle_root → merkle_root, and the other proof.* fields automatically.
The three accepted schemas and their field sources:
agent_decision | /v1/notarize/:id | proof.anchor_chain | proof.anchor_tx | proof.merkle_root |
fairseal-anchor-receipt-v1 | /v2/anchor/:id/receipt | chain | tx_hash | merkle_root |
| VEO-2 | /v1/rng/latest etc. | anchor.chain | anchor.tx_hash | anchor.merkle_root |
Note on Merkle path (multi-leaf batches): notarize receipts encode
proof.merkle_path as a pair-sorted string[]. For single-receipt batches
(the common case) this is always [] and verification runs normally. For
multi-leaf batches the path check is skipped (surfaced as a warnings[] entry)
but the on-chain transaction and contract-state checks still apply — the
anchor is confirmed via the chain, not just the path.
What verification proves — and what it does not
Proves:
- the object's content matches its hashes (integrity),
- the declared signer produced the signature (EIP-191 secp256k1 for VEO-1,
Ed25519 for VEO-2),
- when anchored: the receipt's hash is included in a Merkle batch whose root
exists on Base mainnet.
Does not prove:
- that the decision or output described by the receipt was correct, safe, or
compliant,
- that the issuing agent recorded all of its actions (capture completeness),
- signer identity beyond the key itself, unless you bind keys to identities
out-of-band.
A fully receipted agent can still be wrong. Audit reasoning separately.
API surface
verifyVEO(veo, options) / verifyVEOSync(veo, options) — top-level VEO-2 verification
verifyStructure, verifyIntegrity, verifySignature, isSigned — individual VEO-2 checks
verifyEIP191Signature, canonicalizeVEO1, eip191Hash, pubKeyToAddress — VEO-1
computeMerkleRoot, verifyMerklePath, normalizeHex, toBytes32Hex — Merkle
verifyAnchor, DEFAULT_CONTRACT_ADDRESSES, DEFAULT_RPC_URLS — on-chain anchor
- RPC constants:
TOPIC_BATCH_ANCHORED, SELECTOR_GET_BATCH_ROOT, SELECTOR_BATCH_EXISTS
Links
License
MIT