Scoring spec
The score is computed onchain, by GroundedReputation.scoreOf, from a 21-bucket histogram. There is no
offchain recompute and no trusted party: the SDK, the explorer and this page only read it.
Constants
| Constant | Value | Where |
|---|---|---|
| Buckets | 21 (scores 0, 5, 10, ... 100) | ScoringLib.BUCKETS |
| Bucket size | 5 | ScoringLib.BUCKET_SIZE |
| Weight cap | 4 | ScoringLib.WEIGHT_CAP |
| Weight unit | 0.10 USDC (100_000 at 6 decimals) |
ScoringLib.WEIGHT_UNIT_6DP |
| "Low confidence" below | 5 live reviewers | ScoringLib.MIN_REVIEWERS_OK (the SDK's confidence) |
| Minimum payment | 0.01 USDC (10_000 at 6 decimals) |
ReceiptRouter.MIN_AMOUNT_6DP |
| Receipt lifetime | 30 days | ReceiptRegistry.RECEIPT_TTL |
| Rating scale | integer 0-100, tag quality |
ERC-8004 feedback, valueDecimals == 0 |
What counts as a rating
A rating counts only if all of these hold when ground runs:
- The deadline has not passed.
- The receipt exists, is unused, and is under 30 days old.
- The caller is the receipt's payer.
- The caller is not the agent's owner, approved operator, operator-for-all, or payout wallet.
- The caller's ERC-8004 feedback at
feedbackIndexexists, is not revoked, hasvalueDecimals == 0, a value in 0-100, and tagquality(ordispute). - The caller has a bound passkey, and it signed exactly this receipt, agent, feedback index, score, whether it is a dispute, reviewer and deadline (EIP-712, verified onchain through the P256 precompile).
Each check has its own custom error. See Contracts.
Weight: what you paid
weight = min(1 + floor(log2(1 + floor(paid / 0.10 USDC))), 4)
| Paid (USDC) | Weight |
|---|---|
| 0.01 - 0.09 | 1 |
| 0.10 - 0.29 | 2 |
| 0.30 - 0.69 | 3 |
| 0.70 and up | 4 |
Logarithmic and capped on purpose: paying 1,000 times more buys 4 times the vote, not 1,000 times. Non-USDC tokens are normalised to 6 decimals first.
Bucket
bucket = floor((score + 2) / 5), so scores round to the nearest multiple of 5, halves up. 22 lands in bucket
4 (20), 28 in bucket 6 (30), 3 in bucket 1 (5).
Score: weighted median
Each reviewer has one live rating per agent. Adding weight w to bucket b for each live rating gives a
histogram; mass is the total weight. The score is the value of the first bucket whose cumulative weight
reaches half the mass:
cumulative(i) * 2 >= mass -> score = i * 5
So the score is always a multiple of 5, and on an exact tie it takes the lower bucket. Worked example: two
reviewers, weight 2 each, rating 22 and 28. Buckets 20 and 30, mass 4. Bucket 20 has cumulative 2, and
2 * 2 >= 4, so the score is 20.
An agent with no live ratings has mass 0 and score 0. That 0 means "unknown", not "bad": read confidence.
Replacing and removing ratings
- Rating again replaces the reviewer's previous live rating. Paying twice never doubles a vote. The new receipt is still consumed.
dispute-tagged ratings are counted indisputesand never move the score.- Revoking the ERC-8004 feedback does not remove a grounded rating by itself. Anyone may call
sweepRevoked(agentId, reviewer): it re-reads the registry and removes the rating if, and only if, it is now revoked. Until someone does, a revoked rating still counts. - Trimming is deliberately absent. Dropping equal weight from both tails cannot move a weighted median, so it would only cost gas.
Reading it
| Call | Returns |
|---|---|
scoreOf(agentId) |
median, reviewers, receipts, paid, weightMass, updatedAt, ownerChangedAt |
meets(agentId, minScore, minReviewers) |
true when reviewers >= minReviewers and median >= minScore |
liveRating(agentId, reviewer) |
that reviewer's bucket, weight, feedbackIndex, isDispute, exists |
disputeCount(agentId) |
count of dispute ratings |
lastOwnerOf(agentId) |
owner at the agent's last grounded rating |
meets is O(21) and callable from another contract; see GroundedGate.
Two quirks worth knowing
ownerChangedAtis lazy. It moves on the next rating after a transfer, not at the transfer. To catch a sale since the last rating, comparelastOwnerOfwith the registry'sownerOf.checkwithratingsAfterOwnerChange: falsedoes this for you.paidandreceiptscount only receipts spent on ratings. A payment nobody rated with is in theReceiptIssuedevents, not inscoreOf.paidalso saturates at the largestuint40(about 1.1 million USDC), because it is a display figure and must not be able to block grounding.