Skip to Content
Smart ContractsDecisionAnchor

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

PropertyValue
Hash Algorithmkeccak256
GranularityOne anchor per (agentTokenId, epochNumber)
MutabilityImmutable after write — cannot be overwritten
Write AccessAuthorized anchors + owner
Read AccessPublic (anyone can verify)

How It Works

  1. After each decision round, the backend canonicalizes the decision payload (role outputs + CEO decision + RLAIF logs) and computes its keccak256 hash.
  2. The backend calls anchorDecision(agentTokenId, decisionHash, epochNumber).
  3. The hash is stored permanently on-chain, indexed by agent token ID and epoch number.
  4. Anyone can call verifyDecision() or getDecisionHash() 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:

  • decisionHash cannot be bytes32(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 owner

Verification 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:

  1. Retrieve the original decision payload from the database.
  2. Canonicalize the payload (deterministic JSON serialization).
  3. Compute keccak256(canonicalPayload).
  4. Call verifyDecision() with the computed hash.
  5. 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 Indexed

The anchoring transaction is signed by a dedicated backend wallet (DECISION_ANCHOR_PRIVATE_KEY), which must be registered via setAuthorizedAnchor().

Security

  • ReentrancyGuard on anchorDecision()
  • One-write-per-slot: once anchored, a decision hash cannot be changed or deleted
  • Zero hash rejection prevents accidental empty anchors
  • onlyAnchor modifier: only authorized backend wallets or the owner can write
  • All verification functions are public view — no gas cost for callers