Skip to content

Reputation System

RunTimeAdmin edited this page Oct 29, 2026 · 12 revisions

Reputation System

**Referenced Files in This Document** - [reputation-model.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/reputation-model.md) - [architecture.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/architecture.md) - [payment-backed-attestations.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/payment-backed-attestations.md) - [SigvaraReputation.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/SigvaraReputation.sol) - [scoring.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/scoring.js) - [merkle.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/merkle.js) - [payments.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/payments.js) - [external.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/external.js) - [index.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/index.js) - [challenge.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/challenge.ts)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion

Introduction

This document explains Sigvara’s reputation system: how six factors are measured, normalized, and committed on-chain; how evidence is verified through a Merkle tree; how disputes are challenged; and how maturity, decay, and anti-sybil controls prevent farm-and-cash-out attacks. It is intended for both implementers and consumers who need to understand what a score means, how it is computed, and how to validate or gate on it.

The system separates computation from storage: the oracle network computes scores off-chain and proposes them to SigvaraReputation, which stores finalized scores, pending proposals, evidence roots, and maturity state. Consumers read spendable scores via getTotalScore or threshold checks via meetsThreshold.

Section sources

Project Structure

At a high level, the reputation system spans documentation, Solidity contracts, an Oracle service, and an SDK used by consumers:

  • Documentation defines the model, architecture, and payment-backed attestation rules.
  • The Solidity contract stores scores, pending proposals, evidence roots, and maturity.
  • The Oracle service reads chain data, normalizes external feedback, computes scores, builds Merkle proofs, and proposes results.
  • The SDK provides challenge-response utilities for authentication flows that may gate access based on reputation.
graph TB
subgraph "Documentation"
RM["Reputation Model"]
ARCH["Architecture"]
PAYDOC["Payment-Backed Attestations"]
end
subgraph "On-chain"
REP["SigvaraReputation.sol"]
end
subgraph "Oracle Service"
IDX["index.js"]
SCOR["scoring.js"]
PMT["payments.js"]
EXT["external.js"]
MRK["merkle.js"]
end
subgraph "SDK"
CHAL["challenge.ts"]
end
RM --> REP
ARCH --> REP
PAYDOC --> PMT
ARCH --> IDX
IDX --> SCOR
IDX --> PMT
IDX --> EXT
IDX --> MRK
REP --> CHAL
Loading

Diagram sources

Section sources

Core Components

Sigvara’s reputation is a deterministic 0–100 score composed of six factors:

Factor Max Points Purpose Key Inputs Anti-abuse Controls
Fee Activity 20 Measures real economic activity settled to the agent Decayed volume of accepted payments Decay half-life, per-payer cap, self-payment refusal
Success Rate 15 Outcome reported by a credentialed service (CounterAudit today) Age-weighted successful vs total attestations Pseudo-observations (+5) prevent stale perfect ratios; a stranger's attestation records no outcome
Tenure 30 Span of verified paid activity faded by recency First and last verified payment timestamps Starts at first activity, not registration; recency fades abandoned history
External Trust 25 Normalized ERC-8004 feedback for a linked agent Recognized rating tags from ERC-8004 registry Ownership verification; only recognized tags; CounterAudit uses its own tag, so it is not counted
Community Standing 5 Deducted for unresolved flags Flag count Linear penalty capped at zero
Propagation 5 Inherited trust from counterparties’ external standing Distinct counterparties with non-zero external score Uses matured external score; damps reflexivity

A newly registered agent starts with no score and cannot be scored until bonded. Only bonded agents are scorable; un-bonded agents remain PendingBond and are rejected by proposal logic.

Section sources

Architecture Overview

Signals flow into the oracle from multiple sources and are normalized into a single score:

sequenceDiagram
participant Client as "Client / Platform"
participant Oracle as "Oracle index.js"
participant Payments as "payments.js"
participant External as "external.js"
participant Scoring as "scoring.js"
participant Merkle as "merkle.js"
participant Contract as "SigvaraReputation.sol"
Client->>Oracle : Submit attestation with payment txHash
Oracle->>Payments : verifyPayment(txHash, agentAddress)
Payments-->>Oracle : {payer, amount, settledAt}
Oracle->>External : Read ERC-8004 feedback (if linked)
External-->>Oracle : externalScore
Oracle->>Scoring : computeScore(measured inputs)
Scoring-->>Oracle : {feeScore, successScore, ageScore, externalScore, communityScore, propagationScore}
Oracle->>Merkle : buildTree(payment events)
Merkle-->>Oracle : {root, leaves}
Oracle->>Contract : proposeReputation(didHash, data, evidenceRoot)
Contract-->>Oracle : ScoreProposed event
Note over Contract : Challenge window opens
Contract-->>Client : meetsThreshold / getTotalScore
Loading

Diagram sources

Section sources

Detailed Component Analysis

Six-Factor Scoring Model

Fee Activity

Fee activity measures settled volume rather than request counts. When payment verification is required, the oracle verifies each settlement transaction against the chain, extracts the payer from transfer logs, and accumulates age-weighted volume. Volume decays using a half-life; the default is one point per PAYMENT_FEE_UNIT of volume (100 USDC), capped at 20 points. A per-payer cap prevents a small ring of wallets from substituting for a diverse customer base. Self-payments are refused to raise the cost of fake activity.

Key behaviors:

  • Decayed volume uses 0.5^(age/half-life) weighting.
  • Per-payer cap limits contribution regardless of total amount sent: 4 points by default, so five payers with no standing of their own fill the 20-point factor.
  • Trust multiplier can raise caps for counterparties with external standing.
  • If verification is off, fee activity falls back to an attestation-count proxy, which is intentionally weaker.
flowchart TD
Start(["Start fee activity"]) --> Verify["Verify payment txHash on chain"]
Verify --> Valid{"Valid transfer<br/>to agent?"}
Valid --> |No| Reject["Reject attestation"]
Valid --> |Yes| Extract["Extract payer, amount, settledAt"]
Extract --> SelfCheck{"Self-payment?"}
SelfCheck --> |Yes| Reject
SelfCheck --> |No| Group["Group by payer with decay weights"]
Group --> Cap["Apply per-payer cap<br/>and trust multiplier"]
Cap --> Sum["Sum capped volumes"]
Sum --> Points["Convert volume to points<br/>capped at 20"]
Points --> End(["Return feeScore"])
Loading

Diagram sources

Section sources

Success Rate

Success rate is derived from outcomes reported by credentialed services, such as CounterAudit, which audits the work. It divides successful weight by total weight plus pseudo-observations to avoid a single lucky job outscoring a long record and to ensure stale records fade. Both numerator and denominator decay together so a ratio alone does not preserve old success forever.

Key behaviors:

  • Pseudo-observations stabilize low-volume ratios.
  • Decayed weights pull the score toward zero as evidence ages.
  • Only a caller holding an oracle token records an outcome. A stranger's attestation with a verified payment credits the payment as fee volume and tenure and records no outcome, because a settlement hash is public and says nothing about how the work went. A token holder can add the outcome later, once; a recorded outcome is final.
  • Without a token, anything other than success: true is refused, so a negative outcome always needs a credential.
flowchart TD
S(["Start success rate"]) --> Collect["Collect outcome-bearing attestations"]
Collect --> Weight["Apply decay weights"]
Weight --> Ratio["Compute successful / (total + prior)"]
Ratio --> Scale["Scale to 15-point max"]
Scale --> Clamp["Floor result"]
Clamp --> R(["Return successScore"])
Loading

Diagram sources

Section sources

Tenure

Tenure measures the span between an agent’s first and most recent verified payment, multiplied by recency. It deliberately does not measure time since registration because waiting is free and trading is not. With payment verification on, an agent with no verified payments scores zero tenure regardless of registration age; with verification off the factor falls back to the calendar curve over time since registration. Abandoned activity fades quickly.

Key behaviors:

  • Logarithmic curve over span days.
  • Recency factor scales down abandoned history.
  • Activity window excludes self-payments.
flowchart TD
T(["Start tenure"]) --> Window["Compute activity window<br/>firstActivitySec, lastActivitySec, recency"]
Window --> HasData{"Has first and last activity?"}
HasData --> |No| Zero["Return 0"]
HasData --> |Yes| Span["spanDays = (last - first) / 86400"]
Span --> Curve["ageCurve(spanDays)"]
Curve --> Apply["floor(curve × recency)"]
Apply --> Cap["Cap at 30"]
Cap --> TEnd(["Return ageScore"])
Loading

Diagram sources

Section sources

External Trust

External trust carries ERC-8004 cross-protocol feedback normalized into a 0–25 score. The oracle verifies that the ERC-8004 identity NFT is owned by the same wallet as the Sigvara operator, then reads recognized rating tags and averages them before scaling to 25 points. Only the nine recognised tags are read, and CounterAudit publishes under its own counteraudit tag, so its feedback is not counted a second time.

Key behaviors:

  • Ownership verification ties external reputation to the operator.
  • Only recognized tags are normalized; bespoke scales are ignored.
  • Fail-safe returns zero if configuration is missing or ownership fails.
flowchart TD
E(["Start external trust"]) --> Enabled{"External feed configured?"}
Enabled --> |No| ZeroE["Return 0"]
Enabled --> |Yes| VerifyOwner["Verify ERC-8004 NFT owner == operator"]
VerifyOwner --> OwnerOK{"Owner matches?"}
OwnerOK --> |No| ZeroE
OwnerOK --> |Yes| ReadFeedback["Read clients and feedback rows"]
ReadFeedback --> Normalize["Normalize recognized tags to 0..1"]
Normalize --> Mean["Mean of recognized ratings"]
Mean --> ScaleE["Scale to 25"]
ScaleE --> EEnd(["Return externalScore"])
Loading

Diagram sources

Section sources

Community Standing

Community standing starts at the maximum and deducts points for unresolved flags. Flags are reputational, distinct from slashing. The formula floors the resulting score, which rounds the penalty up, so fractional flag weights do not prematurely forgive bad behavior.

Key behaviors:

  • Linear deduction: max(0, 5 − flags × 2).
  • Floor applied to handle fractional decayed flags.

Section sources

Propagation

Propagation awards up to 5 points based on the breadth of counterparties that have external standing. Each counterparty counts once regardless of payment size. The factor uses matured external scores to damp reflexivity and prevent bootstrapping from fresh identities.

Key behaviors:

  • One point per fully trusted counterparty, pro-rated by external score.
  • Uses matured external score to lag recent gains.
  • Unknown counterparties read as zero.
  • A counterparty the node cannot get an answer for (throttled, timed out or dropped) is retried. If the read still fails, the agent is skipped for that epoch rather than scored on a missing standing.
flowchart TD
P(["Start propagation"]) --> Seen["Iterate distinct counterparties"]
Seen --> AddTrust["Add externalScore / MAX_EXTERNAL_SCORE"]
AddTrust --> CapP["Cap at 5"]
CapP --> PEnd(["Return propagationScore"])
Loading

Diagram sources

Section sources

Evidence Processing and Normalization

Evidence enters through two paths:

  1. Pushed attestations: Clients submit a settlement transaction hash; the oracle verifies the transfer and takes the payer from the chain log. Without an oracle token the payment is credited and no outcome is recorded.
  2. Pulled payments: Operators scan chain Transfer logs for credited payments; these carry no outcome and are excluded from the success ratio.

Normalization steps include:

  • Verifying minimum amount and confirmations.
  • Refusing self-payments.
  • Recording settlement timestamp from block data.
  • Applying decay weights.
  • Capping per-payer contributions.
  • Building Merkle roots over evidence leaves.
flowchart TD
In(["Evidence intake"]) --> Path{"Path type"}
Path --> |Attestation| Push["verifyPayment(txHash)"]
Path --> |Scan| Pull["Scan Transfer logs"]
Push --> Normalize["Normalize: payer, amount, settledAt"]
Pull --> Normalize
Normalize --> Decay["Apply decay weights"]
Decay --> Cap["Per-payer caps"]
Cap --> Merkle["Build Merkle root"]
Merkle --> Commit["Commit root on-chain"]
Loading

Diagram sources

Section sources

Merkle Tree Proof System

Each proposal commits a Merkle root over the evidence set. Leaves encode:

  • Transaction hash
  • Payer address
  • Amount
  • Settlement time in seconds
  • Outcome code: failed, succeeded, or not reported

The tree uses sorted-pair hashing compatible with OpenZeppelin’s MerkleProof. Odd nodes are promoted rather than duplicated to prevent malleability. Verifiers rebuild leaves from the payment fields on the chain plus the outcome the oracle reports, and check membership against the on-chain root.

classDiagram
class MerkleModule {
+leafFor(event) bytes32
+buildTree(events) Object
+rootFor(events) bytes32
+proofFor(tree, index) bytes32[]
+verifyProof(leaf, proof, root) bool
+hashPair(a, b) bytes32
+settledSeconds(event) number
+outcomeCode(success) uint8
}
class LeafEncoding {
+txHash : bytes32
+payer : address
+amount : uint256
+settledAt : uint256
+outcome : uint8
}
MerkleModule --> LeafEncoding : "encodes"
Loading

Diagram sources

Section sources

Challenge-Response Mechanism for Score Disputes

The challenge mechanism has two layers:

  1. On-chain proposal and rejection: Oracles propose scores; a slashing committee can reject proposals during the challenge window. Finalization is permissionless after the window closes. A proposal remembers its proposer and the end of its window is fixed when it is proposed, so finalization refuses a proposal whose proposer has since lost its authority (operator removed, signer cleared or ORACLE_ROLE revoked) and a later change to the window moves no proposal already pending. This has been live on the Arc testnet proxies since 26 October 2026; a proposal made before then records neither.
  2. Operator divergence: A checker recomputes scores independently and records disagreements without overwriting live proposals. A watcher escalates divergences before the challenge window expires.

Additionally, the SDK provides challenge-response authentication for consumers gating access based on reputation. Challenges bind a DID, audience, nonce, and timestamp; responses are Ed25519 signatures over the payload.

sequenceDiagram
participant Primary as "Primary Oracle"
participant Checker as "Checker Operator"
participant Watcher as "Watcher"
participant Committee as "Slashing Committee"
participant Contract as "SigvaraReputation.sol"
participant Consumer as "Consumer App"
Primary->>Contract : proposeReputation(didHash, data, evidenceRoot)
Contract-->>Primary : ScoreProposed event
Note over Contract : Challenge window open
Checker->>Contract : getPendingScore(didHash)
Contract-->>Checker : Pending score, proposedAt and finalizableAt
Note over Checker : A disagreement is recorded off chain, never overwritten
Watcher->>Contract : Check proposal still live
Contract-->>Watcher : Pending score status
Watcher->>Committee : Alert before the window closes
Committee->>Contract : rejectReputation(didHash)
Contract-->>Committee : ScoreRejected event
Consumer->>Contract : meetsThreshold(didHash, threshold)
Contract-->>Consumer : boolean
Loading

Diagram sources

Section sources

Maturity Calculations and Decay

Maturity prevents farm-and-cash-out by releasing earned scores gradually. The contract stores an anchor matured score and a timestamp; reads extrapolate growth at a configurable rate per day. Falls apply immediately; rises are bounded. Operator transfers restart maturity to prevent buying aged identities. On main the handover is checked before the earned score is compared with the anchor, so a score that had fully matured, one just finalized again and one that had fallen below its anchor all restart the buyer at zero, and a transfer accepted in the same second as a finalize counts as after it.

Decay applies to:

  • Payment volume for fee activity.
  • Attestation weights for success rate.
  • Tenure recency.
flowchart TD
M(["Start maturity calculation"]) --> Earned["Get earned score"]
Earned --> Anchor["Get matured anchor and timestamp"]
Anchor --> Handover{"Operator handover since the anchor?"}
Handover --> |Yes| Restart["Anchor = 0, clock restarts at the handover"]
Handover --> |No| Compare{"Earned <= Anchor?"}
Restart --> Compare
Compare --> |Yes| ReturnEarned["Return earned"]
Compare --> |No| TimeSince["Calculate time since anchor"]
TimeSince --> Release["released = anchor + (time × ratePerDay / 1 day)"]
Release --> CapM["Cap at earned"]
CapM --> ReturnM["Return released"]
Loading

Diagram sources

Section sources

Anti-Sybil Measures

Anti-sybil controls target farm-and-cash-out and wash-trading:

  • Bond requirement: Only bonded agents are scorable; registration alone does not activate scoring.
  • Per-payer caps: Prevent a small ring from substituting for diverse counterparties.
  • Self-payment refusal: Raises the floor for fake activity.
  • Decay: Makes farmed volume perishable unless renewed.
  • Maturity: Limits how fast trust can be acquired and spent.
  • Propagation damping: Uses matured external score to prevent reflexivity and bootstrapping.
  • Slash resets: Slashing zeroes all factors and sets terminal status.
flowchart TD
A(["Attack vector"]) --> Control{"Control"}
A --> |Farm volume| Decay["Decay half-life"]
A --> |Wash trade| PerPayer["Per-payer cap"]
A --> |Self-pay| SelfRefuse["Self-payment refusal"]
A --> |Quick cashout| Maturity["Maturity release rate"]
A --> |Sybil ring| PropagationDamp["Propagation uses external score"]
A --> |Buy aged ID| TransferRestart["Transfer restarts maturity"]
Decay --> Safe["Reduced exploitability"]
PerPayer --> Safe
SelfRefuse --> Safe
Maturity --> Safe
PropagationDamp --> Safe
TransferRestart --> Safe
Loading

Diagram sources

Section sources

Concrete Examples of Score Computation and Threshold Validation

Example 1: Fee Activity

Assume:

  • Accepted asset: USDC
  • PAYMENT_FEE_UNIT: 100 USDC per point
  • Default half-life: 90 days
  • Per-payer cap: 4 points

Scenario:

  • Five distinct payers each settle 500 USDC within the current epoch.
  • Each payer is capped at 4 points, which is 400 USDC at 100 USDC per point, so the capped volume sums to 2,000 USDC.
  • Points = min(20, 2000 / 100) = 20.

Four payers of the same size reach only 16 points (4 x 400 USDC), which is why five payers are needed to fill the factor.

If one payer sends 2,000 USDC alone:

  • Capped contribution = 4 points.
  • Total feeScore = 4.

Section sources

Example 2: Success Rate

Assume:

  • Pseudo-observations: 5
  • Decayed successful weight: 8
  • Decayed total weight: 10

Computation:

  • Ratio = 8 / (10 + 5) = 0.533...
  • Score = floor(0.533... × 15) = 8.

As evidence ages, both numerator and denominator decay, pulling the score toward zero unless new evidence accumulates.

Section sources

Example 3: Tenure

Assume:

  • First verified payment: 730 days ago
  • Last verified payment: today
  • Recency: 1.0

Computation:

  • Span days = 730
  • ageCurve(730) = floor(log₂(730 + 1) × 3) ≈ 28
  • ageScore = floor(28 × 1.0) = 28.

If last payment was 365 days ago:

  • Recency < 1.0
  • Score drops proportionally.

Section sources

Example 4: External Trust

Assume:

  • Recognized tags: quality=80/100, rating=4/5, win-rate=0.8
  • Normalized values: 0.8, 0.8, 0.8
  • Mean = 0.8
  • externalScore = round(0.8 × 25) = 20.

If no recognized tags exist, externalScore = 0.

Section sources

Example 5: Propagation

Assume:

  • Five distinct counterparties with externalScore = 25 each.
  • MAX_EXTERNAL_SCORE = 25.

Computation:

  • Each contributes 25 / 25 = 1.0.
  • Sum = 5.0.
  • propagationScore = min(5, floor(5.0)) = 5.

If ten counterparties have externalScore = 12.5:

  • Each contributes 12.5 / 25 = 0.5.
  • Sum = 5.0.
  • propagationScore = 5.

Section sources

Example 6: Threshold Validation

On-chain consumer checks:

  • Call meetsThreshold(didHash, 50).
  • Returns true if getTotalScore(didHash) >= 50.

Consumers should use getTotalScore for spendable thresholds, not getEarnedScore, because maturity gates when a score becomes usable.

meetsThreshold compares the spendable score and nothing else. It does not look at the agent's status or bond, so a suspended agent passes it, and at a threshold of 0 so does an address that was never registered. On main, SigvaraReader.isTrusted(agent, minScore) (not deployed on any chain yet) adds the Active-status check and the current minimum stake.

Section sources

Dependency Analysis

The reputation system depends on several modules and contracts:

graph TB
REP["SigvaraReputation.sol"]
ORACLE["oracle/index.js"]
SCORING["oracle/scoring.js"]
PAYMENTS["oracle/payments.js"]
EXTERNAL["oracle/external.js"]
MERKLE["oracle/merkle.js"]
CHALLENGE["packages/sdk/src/challenge.ts"]
ORACLE --> SCORING
ORACLE --> PAYMENTS
ORACLE --> EXTERNAL
ORACLE --> MERKLE
ORACLE --> REP
CHALLENGE --> REP
Loading

Diagram sources

Section sources

Performance Considerations

  • Decay arithmetic: Uses scaled integers to preserve precision for 18-decimal tokens.
  • Grouping by payer: Reduces repeated traversal of payment events across factors.
  • Merkle tree construction: O(E log E); cached per evidence set. Each leaf is built directly and cached against its frozen event, and each root against its frozen event array, so an epoch in which an agent's evidence has not changed reuses its root.
  • External feedback reads: Per-client iteration with timeouts; fail-safe returns zero.
  • Maturity reads: Extrapolated on read to allow accrual even if oracle stops proposing.

Troubleshooting Guide

Common issues and resolutions:

  • Attestation rejected due to RPC error: Provider unavailable; retry later. Do not treat as negative outcome.
  • Attestation accepted but no outcome recorded: The request carried no oracle token, so only the payment was credited (outcomeRecorded: false). Outcomes come from a token holder such as CounterAudit.
  • Settlement hash not found or unconfirmed: Ensure minimum confirmations met and transaction mined.
  • Self-payment detected: Payer equals operator or agent address; use a distinct counterparty.
  • External trust zero despite ERC-8004 setup: Verify environment configuration and NFT ownership.
  • Merkle proof verification fails: Check leaf encoding, settledAt units (seconds), and outcome codes.
  • Challenge response replayed: Nonce reuse is rejected; generate a fresh challenge.

Section sources

Conclusion

Sigvara’s reputation system combines on-chain economic signals, external trust registries, and community feedback into a robust 0–100 score. Its design emphasizes verifiability through Merkle proofs, resistance to sybil and farm-and-cash-out attacks through decay, maturity, and bonding, and transparency through public evidence and challenge mechanisms. Consumers should gate on getTotalScore and meetsThreshold, while auditors can reconstruct evidence from chain data and compare against on-chain roots.

Overview

Clone this wiki locally