DecisionAnchor
DecisionAnchor provides on-chain cryptographic provenance for AI agent decisions. It stores decision hashes per agent per epoch, enabling anyone to verify that a specific decision existed at a specific time without exposing the raw decision data.
Deployed at: 0x89A9D203686Bd881F5dD12c7dEcA5f49572AA94e (Base Sepolia)
Key Properties
| Property | Value |
|---|---|
| Hash Algorithm | keccak256 |
| Granularity | One anchor per (agentTokenId, epochNumber) |
| Mutability | Immutable after write — cannot be overwritten |
| Write Access | Authorized anchors + owner |
| Read Access | Public (anyone can verify) |
How It Works
- After each decision round, the backend canonicalizes the decision payload (role outputs + CEO decision + RLAIF logs) and computes its keccak256 hash.
- The backend calls
anchorDecision(agentTokenId, decisionHash, epochNumber). - The hash is stored permanently on-chain, indexed by agent token ID and epoch number.
- Anyone can call
verifyDecision()orgetDecisionHash()to confirm provenance.
The raw decision data (prompts, outputs, RLAIF feedback) is never stored on-chain. Only the hash is anchored, providing verifiability without data exposure.
Interface
Write Functions
/// @notice Anchor a decision hash for a specific agent and epoch.
/// Reverts if a hash already exists for this (agentTokenId, epochNumber) pair.
/// @param agentTokenId The agent's ERC-8004 token ID
/// @param decisionHash The keccak256 hash of the canonical decision payload
/// @param epochNumber The epoch number this decision belongs to
function anchorDecision(
uint256 agentTokenId,
bytes32 decisionHash,
uint256 epochNumber
) external;Constraints:
decisionHashcannot bebytes32(0)- Each
(agentTokenId, epochNumber)pair can only be anchored once - Caller must be an authorized anchor or the contract owner
View Functions
/// @notice Get the anchored decision hash for an agent at a specific epoch.
/// @param agentTokenId The agent's ERC-8004 token ID
/// @param epochNumber The epoch number to query
/// @return The anchored hash (bytes32(0) if not anchored)
function getDecisionHash(
uint256 agentTokenId,
uint256 epochNumber
) external view returns (bytes32);
/// @notice Verify that a given hash matches the anchored decision.
/// @param agentTokenId The agent's ERC-8004 token ID
/// @param epochNumber The epoch number to verify
/// @param expectedHash The hash to compare against
/// @return True if the hashes match
function verifyDecision(
uint256 agentTokenId,
uint256 epochNumber,
bytes32 expectedHash
) external view returns (bool);
/// @notice Total number of decisions anchored across all agents.
function totalAnchored() external view returns (uint256);
/// @notice Direct mapping access: agentTokenId -> epochNumber -> decisionHash.
function decisions(uint256 agentTokenId, uint256 epochNumber) external view returns (bytes32);Admin Functions (Owner Only)
/// @notice Authorize or deauthorize an address to anchor decisions.
/// @param anchor The address to authorize/deauthorize
/// @param authorized Whether the address should be authorized
function setAuthorizedAnchor(address anchor, bool authorized) external;Events
event DecisionAnchored(
uint256 indexed agentTokenId,
bytes32 indexed decisionHash,
uint256 epochNumber,
uint256 timestamp
);
event AnchorAuthorized(address indexed anchor, bool authorized);Errors
error AlreadyAnchored(); // Hash already exists for this agent+epoch
error InvalidHash(); // decisionHash is bytes32(0)
error ZeroAddress(); // anchor address is address(0)
error UnauthorizedAnchor(); // Caller not authorized and not ownerVerification Flow
On-chain verification is public and permissionless:
// Check if a decision was anchored
bytes32 hash = decisionAnchor.getDecisionHash(agentTokenId, epochNumber);
bool exists = hash != bytes32(0);
// Verify a specific hash
bool valid = decisionAnchor.verifyDecision(agentTokenId, epochNumber, expectedHash);Off-chain verification:
- Retrieve the original decision payload from the database.
- Canonicalize the payload (deterministic JSON serialization).
- Compute
keccak256(canonicalPayload). - Call
verifyDecision()with the computed hash. - If true, the decision is cryptographically proven to have existed at the anchored timestamp.
Integration
The backend anchoring process runs in the decision worker after each decision round completes:
Decision Round → Canonicalize Payload → keccak256 → anchorDecision() → Event IndexedThe anchoring transaction is signed by a dedicated backend wallet (DECISION_ANCHOR_PRIVATE_KEY), which must be registered via setAuthorizedAnchor().
Security
ReentrancyGuardonanchorDecision()- One-write-per-slot: once anchored, a decision hash cannot be changed or deleted
- Zero hash rejection prevents accidental empty anchors
onlyAnchormodifier: only authorized backend wallets or the owner can write- All verification functions are public
view— no gas cost for callers