Reputation Registry
The ReputationRegistry provides an on-chain feedback system for AI agents. It uses cryptographic authorization to prevent spam while allowing legitimate users to submit scored feedback.
Overview
The ReputationRegistry provides:
- Value-based feedback with signed integers (i128) and configurable decimal precision
- Tag-based categorization with two tags per feedback entry
- Feedback revocation by the original submitter
- Response appending for bi-directional communication
- On-chain aggregation with pagination support for gas-bounded queries
Data Structures
FeedbackCore
Represents the core data of a feedback entry.
pub struct FeedbackCore {
pub value: i128, // Signed value (positive or negative)
pub value_decimals: u8, // Decimal precision (0-18)
pub is_revoked: bool, // Whether feedback has been revoked
}Value Range: ±1e38 (MAX_ABS_VALUE)
Decimals: 0-18 (validated on submission)
Tags are stored separately as ByteArray due to storage constraints.
Events
NewFeedback
Emitted when feedback is submitted.
pub struct NewFeedback {
#[key]
pub agent_id: u256,
#[key]
pub client_address: ContractAddress,
#[key]
pub tag1: ByteArray,
pub value: i128,
pub value_decimals: u8,
pub tag2: ByteArray,
pub endpoint: ByteArray,
pub feedback_uri: ByteArray,
pub feedback_hash: u256,
}FeedbackRevoked
Emitted when feedback is revoked.
pub struct FeedbackRevoked {
#[key]
pub agent_id: u256,
#[key]
pub client_address: ContractAddress,
pub feedback_index: u64,
}ResponseAppended
Emitted when a response is added to feedback.
pub struct ResponseAppended {
#[key]
pub agent_id: u256,
#[key]
pub client_address: ContractAddress,
pub feedback_index: u64,
pub responder: ContractAddress,
pub response_uri: ByteArray,
pub response_hash: u256,
}Interface
IReputationRegistry
#[starknet::interface]
pub trait IReputationRegistry<TContractState> {
// Write functions
fn give_feedback(
ref self: TContractState,
agent_id: u256,
value: i128,
value_decimals: u8,
tag1: ByteArray,
tag2: ByteArray,
endpoint: ByteArray,
feedback_uri: ByteArray,
feedback_hash: u256
);
fn revoke_feedback(
ref self: TContractState,
agent_id: u256,
feedback_index: u64
);
fn append_response(
ref self: TContractState,
agent_id: u256,
client_address: ContractAddress,
feedback_index: u64,
response_uri: ByteArray,
response_hash: u256
);
// Read functions
fn get_summary(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray
) -> (u64, i128, u8);
fn get_summary_paginated(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray,
client_offset: u64,
client_limit: u64,
feedback_offset: u64,
feedback_limit: u64
) -> (u64, i128, u8, bool);
fn read_feedback(
self: @TContractState,
agent_id: u256,
client_address: ContractAddress,
index: u64
) -> (i128, u8, ByteArray, ByteArray, bool);
fn read_all_feedback(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray,
include_revoked: bool
) -> (Array<ContractAddress>, Array<i128>, Array<u8>, Array<ByteArray>, Array<ByteArray>, Array<bool>);
fn read_all_feedback_paginated(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray,
include_revoked: bool,
client_offset: u64,
client_limit: u64,
feedback_offset: u64,
feedback_limit: u64
) -> (Array<ContractAddress>, Array<i128>, Array<u8>, Array<ByteArray>, Array<ByteArray>, Array<bool>, bool);
fn get_response_count(
self: @TContractState,
agent_id: u256,
client_address: ContractAddress,
feedback_index: u64,
responders: Span<ContractAddress>
) -> u64;
fn get_clients(self: @TContractState, agent_id: u256) -> Array<ContractAddress>;
fn get_last_index(self: @TContractState, agent_id: u256, client_address: ContractAddress) -> u64;
fn get_identity_registry(self: @TContractState) -> ContractAddress;
}Functions
give_feedback
Submit feedback for an agent.
fn give_feedback(
ref self: TContractState,
agent_id: u256,
value: i128,
value_decimals: u8,
tag1: ByteArray,
tag2: ByteArray,
endpoint: ByteArray,
feedback_uri: ByteArray,
feedback_hash: u256
)Parameters:
agent_id- Target agent's token IDvalue- Signed feedback value (positive or negative, range ±1e38)value_decimals- Decimal precision for the value (0-18)tag1- Primary category tagtag2- Secondary category tagendpoint- Optional endpoint identifierfeedback_uri- Optional URI to detailed feedback documentfeedback_hash- Optional hash of the feedback document
Validation:
- Agent must exist in IdentityRegistry
value_decimalsmust be ≤ 18- Caller must not be the agent owner (no self-feedback)
Example:
import { Account, CallData } from "starknet";
// Submit positive feedback with 2 decimal precision
// value = 85.50 represented as 8550 with decimals = 2
await clientAccount.execute({
contractAddress: reputationRegistryAddress,
entrypoint: "give_feedback",
calldata: CallData.compile({
agent_id: agentId,
value: 8550,
value_decimals: 2,
tag1: "reliability",
tag2: "speed",
endpoint: "https://agent.example.com/api",
feedback_uri: "ipfs://QmDetailedReview",
feedback_hash: reviewHash,
}),
});revoke_feedback
Revoke previously submitted feedback. Only the original feedback submitter can revoke.
fn revoke_feedback(ref self: TContractState, agent_id: u256, feedback_index: u64)Parameters:
agent_id- Agent's token IDfeedback_index- Index of the feedback to revoke
Access Control: Only the client who submitted the feedback
Example:
await clientAccount.execute({
contractAddress: reputationRegistryAddress,
entrypoint: "revoke_feedback",
calldata: CallData.compile({
agent_id: agentId,
feedback_index: 0,
}),
});append_response
Append a response to existing feedback. Any party can respond.
fn append_response(
ref self: TContractState,
agent_id: u256,
client_address: ContractAddress,
feedback_index: u64,
response_uri: ByteArray,
response_hash: u256
)Parameters:
agent_id- Agent's token IDclient_address- Address of the original feedback submitterfeedback_index- Index of the feedback to respond toresponse_uri- URI to the response documentresponse_hash- Hash of the response document
Example:
// Agent owner responds to feedback
await ownerAccount.execute({
contractAddress: reputationRegistryAddress,
entrypoint: "append_response",
calldata: CallData.compile({
agent_id: agentId,
client_address: clientAddress,
feedback_index: 0,
response_uri: "ipfs://QmResponseDocument",
response_hash: responseHash,
}),
});get_summary
Get aggregated feedback summary for an agent.
fn get_summary(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray
) -> (u64, i128, u8)Parameters:
agent_id- Agent's token IDclient_addresses- Filter to specific clients (empty for all)tag1- Filter by primary tag (empty for all)tag2- Filter by secondary tag (empty for all)
Returns: Tuple of (feedback count, summary value, summary decimals)
Example:
const registry = new Contract(abi, reputationRegistryAddress, provider);
// Get overall summary
const [count, summaryValue, decimals] = await registry.get_summary(agentId, [], "", "");
console.log(`${count} reviews, total value: ${summaryValue / 10 ** decimals}`);
// Filter by tag
const [reliabilityCount, reliabilityValue, reliabilityDecimals] = await registry.get_summary(
agentId,
[],
"reliability",
""
);get_summary_paginated
Get aggregated feedback with pagination for gas-bounded queries.
fn get_summary_paginated(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray,
client_offset: u64,
client_limit: u64,
feedback_offset: u64,
feedback_limit: u64
) -> (u64, i128, u8, bool)Parameters:
agent_id- Agent's token IDclient_addresses- Filter to specific clients (empty for all)tag1- Filter by primary tag (empty for all)tag2- Filter by secondary tag (empty for all)client_offset- Skip first N clientsclient_limit- Max clients to processfeedback_offset- Skip first N feedback entries per clientfeedback_limit- Max feedback entries per client
Returns: Tuple of (count, summary value, decimals, truncated)
The truncated flag indicates if additional data exists outside the pagination window.
Example:
// Paginated query with 100 clients, 50 feedback per client
const [count, value, decimals, truncated] = await registry.get_summary_paginated(
agentId,
[],
"",
"",
0, // client_offset
100, // client_limit
0, // feedback_offset
50 // feedback_limit
);
if (truncated) {
// More data exists - make another call with offset
}read_feedback
Read a single feedback entry.
fn read_feedback(
self: @TContractState,
agent_id: u256,
client_address: ContractAddress,
index: u64
) -> (i128, u8, ByteArray, ByteArray, bool)Returns: Tuple of (value, value_decimals, tag1, tag2, is_revoked)
read_all_feedback
Read all feedback matching filters.
fn read_all_feedback(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray,
include_revoked: bool
) -> (Array<ContractAddress>, Array<i128>, Array<u8>, Array<ByteArray>, Array<ByteArray>, Array<bool>)Returns: Six parallel arrays (clients, values, value_decimals, tag1s, tag2s, revoked statuses)
read_all_feedback_paginated
Read feedback with pagination for gas-bounded queries.
fn read_all_feedback_paginated(
self: @TContractState,
agent_id: u256,
client_addresses: Span<ContractAddress>,
tag1: ByteArray,
tag2: ByteArray,
include_revoked: bool,
client_offset: u64,
client_limit: u64,
feedback_offset: u64,
feedback_limit: u64
) -> (Array<ContractAddress>, Array<i128>, Array<u8>, Array<ByteArray>, Array<ByteArray>, Array<bool>, bool)Parameters:
agent_id- Agent's token IDclient_addresses- Filter to specific clients (empty for all)tag1,tag2- Filter by tags (empty for all)include_revoked- Whether to include revoked feedbackclient_offset,client_limit- Pagination for clientsfeedback_offset,feedback_limit- Pagination for feedback per client
Returns: Six parallel arrays plus a truncated flag indicating if more data exists.
get_clients
Get all addresses that have submitted feedback for an agent.
fn get_clients(self: @TContractState, agent_id: u256) -> Array<ContractAddress>get_last_index
Get the last feedback index for a specific client.
fn get_last_index(self: @TContractState, agent_id: u256, client_address: ContractAddress) -> u64get_response_count
Count responses to a specific feedback entry.
fn get_response_count(
self: @TContractState,
agent_id: u256,
client_address: ContractAddress,
feedback_index: u64,
responders: Span<ContractAddress>
) -> u64Authorization Flow
The reputation system uses a two-step authorization flow:
Agent Owner Creates Authorization
The agent owner signs a FeedbackAuth struct specifying:
- Which client can give feedback
- How many feedback entries they can submit
- When the authorization expires
Client Submits Feedback
The client submits feedback along with the signed authorization. The contract verifies:
- The signature is valid
- The client address matches
- The authorization hasn't expired
- The index limit hasn't been exceeded
Contract Records Feedback
If all checks pass, the feedback is recorded on-chain and an event is emitted.
Tag Encoding
Tags are stored as u256 (bytes32). You can encode string tags using Poseidon or keccak hashing:
import { hash } from "starknet";
function encodeTag(tagString: string): bigint {
// Simple approach: convert short strings directly
if (tagString.length <= 31) {
return BigInt("0x" + Buffer.from(tagString).toString("hex"));
}
// For longer strings, use Poseidon hash
return BigInt(hash.computePoseidonHash(tagString));
}
// Common tags
const RELIABILITY = encodeTag("reliability");
const SPEED = encodeTag("speed");
const ACCURACY = encodeTag("accuracy");
const COST = encodeTag("cost");
const SECURITY = encodeTag("security");Security Considerations
Security Model
- Self-feedback prevention - The caller cannot be the agent owner
- Signature verification - Uses SNIP-12 TypedData with domain separator
- Chain ID binding - Signatures include chain ID to prevent cross-chain replay
- Expiry enforcement - Authorizations expire at the specified timestamp
- Index limits - Maximum number of feedback entries per authorization
- Reentrancy guards - All state-changing functions are protected
Complete Example
import { Account, RpcProvider, Contract, CallData, constants } from "starknet";
// Setup
const provider = new RpcProvider({ nodeUrl: rpcUrl });
const ownerAccount = new Account(provider, ownerAddress, ownerPrivateKey, undefined, constants.TRANSACTION_VERSION.V3);
const clientAccount = new Account(provider, clientAddress, clientPrivateKey, undefined, constants.TRANSACTION_VERSION.V3);
// 1. Owner creates authorization for client
const feedbackAuth = {
agent_id: agentId,
client_address: clientAddress,
index_limit: 10,
expiry: Math.floor(Date.now() / 1000) + 86400, // 24 hours
chain_id: "0x534e5f5345504f4c4941", // SN_SEPOLIA
identity_registry: identityRegistryAddress,
signer_address: ownerAddress,
};
const signature = await ownerAccount.signMessage(createTypedData(feedbackAuth));
// 2. Client submits feedback
await clientAccount.execute({
contractAddress: reputationRegistryAddress,
entrypoint: "give_feedback",
calldata: CallData.compile({
agent_id: agentId,
score: 92,
tag1: encodeTag("reliability"),
tag2: encodeTag("speed"),
fileuri: "ipfs://QmDetailedReview",
filehash: reviewHash,
feedback_auth: feedbackAuth,
signature: signature,
}),
});
// 3. Query reputation
const registry = new Contract(abi, reputationRegistryAddress, provider);
const [count, avgScore] = await registry.get_summary(agentId, [], 0, 0);
console.log(`Agent has ${count} reviews with average score ${avgScore}/100`);
// 4. Owner responds to feedback
await ownerAccount.execute({
contractAddress: reputationRegistryAddress,
entrypoint: "append_response",
calldata: CallData.compile({
agent_id: agentId,
client_address: clientAddress,
feedback_index: 0,
response_uri: "ipfs://QmThankYouResponse",
response_hash: 0,
}),
});