Skip to main content

On-Chain verification guide

Anyone can verify a Mnemom agent’s reputation without trusting Mnemom’s infrastructure. Scores and Merkle roots are published to Base L2 contracts by Mnemom on a cron schedule; you read them back either through the Mnemom API or directly via any Ethereum-compatible client. For the conceptual overview — why on-chain anchoring exists, how the Merkle tree maps to integrity checkpoints, and what ERC-8004 means for interop — see On-Chain Verification.
Anchoring (POST /v1/on-chain/anchor-root) and score publishing (POST /v1/on-chain/publish-scores) are not customer-callable endpoints. Mnemom’s backend runs them on a 6-hour cron under an internal service key. Customers consume on-chain data through the three read endpoints below, or by querying the contracts directly.

Prerequisites

Before using on-chain verification features, ensure you have:
  1. A Mnemom API key — created in your dashboard or via POST /v1/api-keys. Read-only access is enough.
  2. Agents with published reputation scores — agents need at least 50 analyzed integrity checkpoints and a public Mnemom Trust Rating before they appear on-chain.
API key authentication: pass your key in the X-Mnemom-Api-Key header:
X-Mnemom-Api-Key: {api_key}
(Bearer JWTs from /v1/auth/login work too.)

Verifying a score against the chain

Confirm that the score Mnemom reports off-chain matches what’s published on-chain, and that the underlying Merkle root has been anchored. Endpoint: GET /v1/on-chain/verify-proof/{agent_id} — public, no auth required.
curl https://api.mnemom.ai/v1/on-chain/verify-proof/mnm-550e8400-e29b-41d4-a716-446655440000
Response: 200 OK
{
  "agent_id": "mnm-550e8400-e29b-41d4-a716-446655440000",
  "on_chain_score": 782,
  "grade": "A",
  "metadata_hash": "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba",
  "published_at": "2026-02-26T10:35:00.000Z",
  "block_number": 18234589,
  "tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
  "verified": true
}
verified: true means the off-chain Merkle root matches the anchored root and the off-chain score matches the ScoreRecord in the registry. Any false is a tamper signal — open a support ticket.

Checking on-chain status

Lightweight “does this agent have any on-chain data?” check — useful before calling verify-proof in workflows where the agent may be new or pre-eligible. Endpoint: GET /v1/on-chain/status/{agent_id} — public.
curl https://api.mnemom.ai/v1/on-chain/status/mnm-550e8400-e29b-41d4-a716-446655440000
Response: 200 OK
{
  "agent_id": "mnm-550e8400-e29b-41d4-a716-446655440000",
  "has_on_chain_score": true,
  "latest_score": {
    "score": 782,
    "grade": "A",
    "block_number": 18234589,
    "published_at": "2026-02-26T10:35:00.000Z"
  },
  "latest_anchor": {
    "merkle_root": "0xabc123def456789012345678901234567890123456789012345678901234abcd",
    "block_number": 18234567,
    "anchored_at": "2026-02-26T10:30:00.000Z"
  },
  "last_updated": "2026-02-26T10:35:00.000Z"
}

Viewing history

Retrieve the full history of anchoring and publishing events across all agents in your org — useful for compliance audits and trend dashboards. Endpoint: GET /v1/on-chain/history — auth required. Query parameters:
ParameterTypeRequiredDescription
pagenumberNoPage number (default: 1)
per_pagenumberNoResults per page (default: 20, max: 100)
curl "https://api.mnemom.ai/v1/on-chain/history?page=1&per_page=10" \
  -H "X-Mnemom-Api-Key: $MNEMOM_API_KEY"
Response: 200 OK
{
  "anchors": [
    {
      "anchor_id": "anc-7f8a9b2c",
      "merkle_root": "0xabc123def456789012345678901234567890123456789012345678901234abcd",
      "leaf_count": 347,
      "tree_depth": 9,
      "tx_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "block_number": 18234567,
      "anchored_at": "2026-02-26T10:30:00.000Z"
    }
  ],
  "publications": [
    {
      "publication_id": "pub-3e4f5a6b",
      "agent_count": 2,
      "tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
      "block_number": 18234589,
      "published_at": "2026-02-26T10:35:00.000Z"
    }
  ],
  "page": 1,
  "per_page": 10,
  "total_anchors": 42,
  "total_publications": 18
}

How anchoring + publishing actually happen

Mnemom’s backend runs a 6-hour cron that:
  1. Computes agent-level Merkle trees from the latest integrity checkpoints.
  2. Aggregates agent-level roots into a global root.
  3. Calls MnemoMerkleAnchor.anchorRoot(root, leafCount, treeDepth) on Base.
  4. Publishes eligible agents’ reputation scores in a batch to MnemoReputationRegistry.publishBatch(records) (up to 200 agents per tx per contract limit).
This runs under an internal service key — customers never call POST /v1/on-chain/anchor-root or POST /v1/on-chain/publish-scores themselves; those endpoints are gated behind X-Service-Key and reject any other auth. Gas is paid from the Mnemom operational wallet; there is no customer-facing billing line for on-chain operations. You do not need to hold ETH on Base to use any part of this system. If you need an off-cycle anchor or publish (e.g., for a time-critical compliance filing), contact support — the backend team can run the cron manually.

Verifying directly against Base L2

Everything above goes through the Mnemom API. If you prefer a fully trust-minimized path, read the contracts directly. Deployed addresses (Base mainnet, chain ID 8453):
ContractAddressBasescan
MnemoReputationRegistry0xfba717a6c4eb481a74f6911954a625242a048425view
MnemoMerkleAnchor0xdfdbc9374907d5adea8f100a0d1e07e16c99816dview
Agent ID encoding: human-readable IDs like mnm-550e8400-... are stored on-chain as keccak256(abi.encodePacked(agentId)). Compute the hash client-side before querying:
import { keccak256, toUtf8Bytes } from 'ethers';

const agentId = 'mnm-550e8400-e29b-41d4-a716-446655440000';
const agentIdBytes32 = keccak256(toUtf8Bytes(agentId));
// → 0x3a8c...
Query the registry:
import { Contract, JsonRpcProvider } from 'ethers';

const provider = new JsonRpcProvider('https://mainnet.base.org');
const registryAbi = [
  'function getScore(bytes32 agentId) view returns (tuple(uint16 score, bytes3 grade, uint64 publishedAt, uint64 blockNumber, bytes32 metadataHash))',
  'function getScoreHistory(bytes32 agentId) view returns (tuple(uint16,bytes3,uint64,uint64,bytes32)[])',
];
const registry = new Contract(
  '0xfba717a6c4eb481a74f6911954a625242a048425',
  registryAbi,
  provider,
);

const latest = await registry.getScore(agentIdBytes32);
console.log('score:', latest.score, 'grade:', decodeGrade(latest.grade));
Query the Merkle anchor:
const anchorAbi = [
  'function isRootAnchored(bytes32 merkleRoot) view returns (bool)',
  'function getLatestRoot() view returns (tuple(bytes32 merkleRoot, uint64 anchoredAt, uint64 blockNumber, uint32 leafCount, uint16 treeDepth))',
];
const anchor = new Contract(
  '0xdfdbc9374907d5adea8f100a0d1e07e16c99816d',
  anchorAbi,
  provider,
);

const latestRoot = await anchor.getLatestRoot();
const anchored = await anchor.isRootAnchored(latestRoot.merkleRoot);
Both getScore and isRootAnchored are view calls — zero gas, no wallet required. Any public Base RPC endpoint works.
Use the API path when possible — it’s cached and doesn’t hit the RPC on every request. Drop to direct RPC when you need trust-minimized verification or are composing with other smart contracts.

See also