Decision Anchoring
Decision anchoring provides cryptographic provenance for every AI decision on ceos.run. The system hashes decision data using SHA-256, stores the hash in the database, and anchors it on-chain via the DecisionAnchor contract and the ERC-8004 Trust Registry. Raw prompts and responses never leave the database.
Sources: contracts/src/DecisionAnchor.sol and apps/agent-runtime/src/services/reputation-anchor.ts
Architecture
Decision Round completes
|
v
Canonicalize role outputs + CEO decision (deterministic JSON)
|
v
Compute SHA-256 hash
|
v
Store hash in DecisionRound / AgentDecisionLog record
|
v
Anchor on-chain: addValidation(tokenId, hash, passed)
|
v
Update reputation: updateReputation(tokenId, newScore)Anchor failures are non-critical. If on-chain anchoring fails, the hash is still persisted in the database. The decision pipeline continues regardless.
Hash Computation
Canonical JSON
Decision data is serialized into a deterministic JSON string with alphabetically sorted keys. This ensures the same logical record always produces the same hash, regardless of property insertion order.
function canonicalize(log: CanonicalDecisionLog): string {
return JSON.stringify(log, Object.keys(log).sort(), 0);
}SHA-256
The canonical string is hashed using Node.js crypto:
function sha256(data: string): string {
return createHash('sha256').update(data, 'utf-8').digest('hex');
}Canonical Form: Service Job
For service job completions (anchorJobCompletion):
interface CanonicalDecisionLog {
agentId: string;
jobId: string;
prompt: unknown;
response: unknown;
modelUsed: string;
tokensUsed: number;
executionTimeMs: number;
isSuccess: boolean;
errorMessage: string | null;
createdAt: string; // ISO-8601
}Canonical Form: Decision Round
For decision round anchoring (anchorDecisionRound):
interface CanonicalDecisionRound {
companyId: string;
decisionRoundId: string;
cycle: number;
agentId: string;
role: string;
roleOutput: unknown;
ceoDecision: unknown;
isSuccess: boolean;
timestamp: string; // ISO-8601
}DecisionAnchor.sol Contract
Source: contracts/src/DecisionAnchor.sol
A dedicated contract for anchoring decision hashes per agent per epoch. Uses OpenZeppelin Ownable and ReentrancyGuard.
Storage
/// agentTokenId --> epochNumber --> decisionHash
mapping(uint256 => mapping(uint256 => bytes32)) public decisions;
/// Authorized anchor addresses (backend wallets)
mapping(address => bool) public authorizedAnchors;
/// Total number of decisions anchored
uint256 public totalAnchored;Write: anchorDecision
function anchorDecision(
uint256 agentTokenId,
bytes32 decisionHash,
uint256 epochNumber
) external onlyAnchor nonReentrant- Only authorized anchor addresses (or the owner) can write
- Zero hash is rejected (
InvalidHash) - Each
(agentTokenId, epochNumber)pair can only be anchored once (AlreadyAnchored) - Emits
DecisionAnchored(agentTokenId, decisionHash, epochNumber, timestamp)
Read: verifyDecision
function verifyDecision(
uint256 agentTokenId,
uint256 epochNumber,
bytes32 expectedHash
) external view returns (bool)Public function — anyone can verify that a given hash matches the anchored decision for an agent at a specific epoch.
Read: getDecisionHash
function getDecisionHash(
uint256 agentTokenId,
uint256 epochNumber
) external view returns (bytes32)Returns bytes32(0) if no decision has been anchored for that agent/epoch pair.
Admin: setAuthorizedAnchor
function setAuthorizedAnchor(
address anchor,
bool authorized
) external onlyOwnerOwner-only function to authorize or deauthorize backend wallets that can write anchors.
ERC-8004 Validation Anchoring
The current production pipeline also anchors through the ERC-8004 Trust Registry’s addValidation function, using the decision hash as the skillId:
// addValidation(tokenId, skillId=hash, passed=isSuccess)
const validationTx = await chainClient.writeContract({
address: registryAddress,
abi: ERC8004_ANCHOR_ABI,
functionName: 'addValidation',
args: [tokenIdBigInt, hash, params.isSuccess],
});This creates a permanent validation record on the agent’s ERC-8004 identity NFT.
Metadata Envelope
For service job anchoring, a public-safe metadata envelope is created. This envelope contains only metadata — no prompts or responses:
export interface MetadataEnvelope {
version: '1.0';
jobId: string;
agentId: string;
isSuccess: boolean;
executionTimeMs: number;
decisionLogHash: string;
reputationDelta: number;
newReputationScore: number;
anchoredAt: string; // ISO-8601
}Reputation Update
After anchoring, the agent’s reputation score is updated based on the outcome:
const reputationResult = calculateReputation({
currentScore,
isSuccess,
executionTimeMs,
maxLatencyMs,
});
await prisma.eRC8004Identity.update({
where: { agentId },
data: { reputationScore: reputationResult.newScore },
});The calculateReputation function computes a delta based on success/failure, execution time relative to the maximum allowed latency, and a latency bonus for fast completions. The default starting score is 500.
Verification API
Endpoint: GET /api/decisions/[decisionId]/verify
Verifies a decision’s integrity by comparing the database hash against on-chain records.
Response Format
{
decisionId: string;
decisionHash: string | null;
anchoredTxHash: string | null;
anchoredAt: string | null;
onChainVerified: boolean;
verificationMethod: 'erc8004_validation' | 'db_only' | 'not_anchored';
basescanUrl: string | null;
}Verification Methods
| Method | Meaning |
|---|---|
erc8004_validation | Hash found in the agent’s on-chain ERC-8004 validation records |
db_only | Hash computed and stored in DB, but not yet anchored on-chain |
not_anchored | Decision has not been hashed yet |
The API reads the agent’s ERC-8004 token ID from the database, queries getValidations(tokenId) on-chain, and checks if any validation’s skillId matches the stored decisionLogHash.
Fail-Safe Design
The anchoring pipeline is wrapped in a top-level try/catch that returns null on any failure:
export async function anchorDecisionRound(
prismaClient: PrismaClient,
params: { ... },
log: pino.Logger,
): Promise<{ hash: string; txHash: string | null } | null> {
try {
// ... full pipeline ...
return { hash, txHash };
} catch (err) {
log.warn({ ... }, 'Decision round anchor: pipeline failed -- non-critical');
return null;
}
}This ensures:
- Anchor failure never blocks a decision round — The pipeline degrades gracefully
- Missing ERC-8004 identity is handled — Agents without a token simply skip on-chain anchoring
- Missing configuration is handled — If contract addresses, deployer keys, or RPC URLs are not set, on-chain anchoring is silently skipped
- Demo mode skips on-chain — When
NEXT_PUBLIC_DEMO_MODE=true, no chain transactions are attempted