Skip to main content

On-Chain Anchoring

Anchorer batches memory hashes into a Merkle tree and records the root on Monad via MemoryAnchor.sol. This creates an immutable, on-chain audit trail for every memory batch.

How It Works​

1. remember() → Memory stored in PlugPort (hash computed, anchorId=null)
2. anchorPending()
→ find all memories with anchorId=null
→ build Merkle tree from their hashes
→ send recordRoot(root, count) to MemoryAnchor.sol on Monad
→ store AnchorRecord in PlugPort (root, txHash, blockNumber, count, hashes[])
→ update each memory: set anchorId = root
3. verify(id)
→ recompute memory hash from stored content
→ find AnchorRecord containing this memory's hash
→ rebuild Merkle proof for this memory's leaf
→ call getRootByAnchor(root) on Monad
→ return { verified, hashValid, proofValid, onChain, explorerUrl }

Configuration​

MONAD_RPC_URL=https://testnet-rpc.monad.xyz
ANCHOR_CONTRACT=0x12a49Ee0c56dA65Eb519cC612Ee4D3A4A089c1f4
ANCHOR_PRIVATE_KEY=0x... # signs the anchor() transaction
EXPLORER_TX_URL=https://testnet.monadscan.com/tx/

Without these variables, anchor in agentGraphFromEnv() is null. All functionality except anchor_now and verify_memory works without anchoring configured.

anchorPending()​

const { batch } = await anchor.anchorPending();
if (!batch) {
console.log('No pending memories — all already anchored');
} else {
console.log(batch.count, 'memories anchored');
console.log('root:', batch._id);
console.log('tx:', batch.txHash);
console.log('explorer:', `${process.env.EXPLORER_TX_URL}${batch.txHash}`);
}

batch is null when there are no un-anchored memories.

verify(id)​

const v = await anchor.verify('mem_abc123');
if (!v) {
console.log('Memory not found or not yet anchored');
} else {
console.log(v.verified); // true if all checks pass
console.log(v.hashValid); // stored hash matches current content
console.log(v.anchored); // memory belongs to an anchor record
console.log(v.proofValid); // Merkle proof reconstructs the root
console.log(v.onChain); // root recorded on MemoryAnchor.sol
console.log(v.explorerUrl); // Monad explorer link
console.log(v.problem); // first failure reason, or null if verified
}

VerifyResult fields​

FieldTypeDescription
verifiedbooleantrue iff all four checks below pass
hashValidbooleanRecomputed hash matches memory.hash
anchoredbooleanMemory is in an AnchorRecord
proofValidbooleanMerkle proof reconstructs the batch root
onChainbooleanRoot recorded on MemoryAnchor.sol
rootstringThe Merkle root
anchoredAtnumberBlock timestamp of the anchor tx
anchoredBystringAddress that sent the anchor tx
txHashstring | nullAnchor transaction hash
explorerUrlstring | nullBlock explorer link for the tx
computedHashstringThe hash that was recomputed
proofstring[]The Merkle proof path
problemstring | nullFirst failure reason, or null

MemoryAnchor Contract​

MemoryAnchor.sol is a minimal Solidity contract that maps Merkle roots to metadata:

function recordRoot(bytes32 root, uint256 count) external onlyOwner
function getRootByAnchor(bytes32 root) external view returns (uint256 timestamp, address anchoredBy, uint256 count, bool exists)

Source: contracts/MemoryAnchor.sol.

Gas Costs​

Each recordRoot() call on Monad testnet costs approximately 45,000 gas regardless of how many memories are in the batch. This means anchoring 1 or 300 memories costs the same — anchor frequently in large batches.

Idempotency & Crash Recovery​

The contract reverts if anchor() is called with a root already recorded (AlreadyAnchored error). Anchorer.anchorPending() handles crash recovery: if the anchor tx was sent but the process crashed before the receipt was saved, the next anchorPending() call catches the AlreadyAnchored revert, reads anchoredAt[root] and anchoredBy[root] directly, and reconstructs the record — without sending a second transaction.