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 ID
  • value - Signed feedback value (positive or negative, range ±1e38)
  • value_decimals - Decimal precision for the value (0-18)
  • tag1 - Primary category tag
  • tag2 - Secondary category tag
  • endpoint - Optional endpoint identifier
  • feedback_uri - Optional URI to detailed feedback document
  • feedback_hash - Optional hash of the feedback document

Validation:

  • Agent must exist in IdentityRegistry
  • value_decimals must 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 ID
  • feedback_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 ID
  • client_address - Address of the original feedback submitter
  • feedback_index - Index of the feedback to respond to
  • response_uri - URI to the response document
  • response_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 ID
  • client_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 ID
  • client_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 clients
  • client_limit - Max clients to process
  • feedback_offset - Skip first N feedback entries per client
  • feedback_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 ID
  • client_addresses - Filter to specific clients (empty for all)
  • tag1, tag2 - Filter by tags (empty for all)
  • include_revoked - Whether to include revoked feedback
  • client_offset, client_limit - Pagination for clients
  • feedback_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) -> u64

get_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>
) -> u64

Authorization 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,
  }),
});