# CardZero Reputation Scoring Rules — v1.0

> **Public commitment**: The keccak256 hash of this document (UTF-8 bytes) is recorded on-chain in `CardZeroReputationRegistry.scoringRulesHash`.
> Anyone can fetch this document from `https://cardzero.ai/SCORING-RULES.md`, compute its keccak256, and verify it matches the on-chain commitment. If the hashes don't match, the document on-chain takes precedence as authoritative.
>
> **Version**: 1.0
> **Effective**: from the time the corresponding hash is set on-chain (initial deployment of CardZeroReputationRegistry).
> **Updated by**: ADMIN_ROLE (deployer) via `updateScoringRules(newURI, newHash)`. Rule changes always emit `ScoringRulesUpdated` event.

---

## What this document covers

CardZero's `CardZeroReputationRegistry` contract on Base mainnet stores **feedback signals** (called *attestations*) about each Agent identity. This document defines:

1. The complete list of event types CardZero attests on-chain
2. The integer score value for each event type
3. How CardZero verifies the underlying fact before attesting (anti-gaming)
4. How third parties can independently audit any attestation

CardZero is the sole `ATTESTOR_ROLE` holder in v1. Future versions may grant ATTESTOR_ROLE to other parties (e.g. the `CardZeroJobs` ERC-8183 contract in Sprint 9).

---

## 1. Event types and score values

All attestations use `valueDecimals = 0` (integer score). Contract enforces `value ∈ [-10, 20]` at write time.

| Event type | Score | Meaning | Triggered by |
|---|---|---|---|
| `payment_success` | **+1** | Wallet completed an on-chain USDC payment (status `confirmed`) | Confirmed `payments` row, sourced from `payments.id` |
| `payment_failure` | **-2** | An on-chain payment was attempted but reverted (status `failed`) | Failed `payments` row, sourced from `payments.id` |
| `wallet_frozen` | **-10** | Owner set `frozen=1` on the wallet (strong negative signal) | `wallets.frozen` state transition |
| `wallet_unfrozen` | **+5** | Owner restored `frozen=0` after a freeze | `wallets.frozen` state transition |
| `lifetime_milestone` (tier $10) | **+5** | Cumulative confirmed payment volume ≥ $10 USDC | `wallet_milestones` row, sourced from `wallet_id:threshold` |
| `lifetime_milestone` (tier $100) | **+10** | Cumulative confirmed payment volume ≥ $100 USDC | `wallet_milestones` row |
| `lifetime_milestone` (tier $1,000) | **+20** | Cumulative confirmed payment volume ≥ $1,000 USDC | `wallet_milestones` row |
| `job_completed` (Sprint 9, ERC-8183) | **+1** | An ERC-8183 Job where this wallet was Provider was approved by Evaluator | `jobs.id` after `complete()` |
| `job_rejected` (Sprint 9, ERC-8183) | **-2** | An ERC-8183 Job where this wallet was Provider was rejected | `jobs.id` after `reject()` |

**Why these values are tight (range [-10, 20])**: Hard contract-level cap prevents any single attestation from dominating the aggregate score. A run of 30 successful payments offsets one freeze (-10 + 30 = +20). A wallet must demonstrate sustained behavior, not single-event dominance.

**Why failure cost (`-2`) > success reward (`+1`)**: Honesty cost should exceed gaming reward. Two successes are needed to offset one failure.

---

## 2. Source-ref invariants (anti–self-attest defense)

Every reputation event written by CardZero **must** be backed by a real DB record that any third party can independently verify by reading CardZero's public payment/job history. The contract does not enforce this directly, but the off-chain audit trail is verifiable.

| `source_kind` | `source_ref` format | What it points to | How to audit |
|---|---|---|---|
| `payment` | `pay_<uuid12>` | Row in `payments` table with matching `id` | Fetch `payments.tx_hash` from CardZero API; verify on Basescan that the tx exists, succeeded/failed, and originated from this wallet |
| `freeze` | `<wallet_id>:<unix_ts>` | Wallet `frozen` state change at that timestamp | Cross-reference with PM2 logs (which include freeze/unfreeze entries) and on-chain `Frozen`/`Unfrozen` events on the wallet |
| `milestone` | `<wallet_id>:<threshold_usdc>` | Row in `wallet_milestones` with matching PK | Sum confirmed `payments.amount` for the wallet; verify total ≥ threshold at trigger time |
| `job` (Sprint 9) | `job_<uuid12>` | Row in `jobs` table with matching `id` | Fetch `jobs.finalize_tx_hash` from CardZero API; verify on Basescan that the ERC-8183 `JobCompleted`/`JobRejected` event was emitted |

The DB's `UNIQUE(source_kind, source_ref, event_type)` constraint guarantees each underlying fact contributes exactly one attestation per event type — no duplication, no cumulative inflation.

---

## 3. Aggregate score formula

For a given Agent identity (`agentId`):

```
totalScore   = SUM(value)  over all non-revoked attestations
successCount = COUNT(payment_success) + COUNT(job_completed)
failureCount = COUNT(payment_failure) + COUNT(job_rejected)
successRate  = successCount / (successCount + failureCount)   if denom > 0, else null
```

`getSummary()` returns `(count, summaryValue, summaryValueDecimals)` over all non-revoked attestations matching the given filters. CardZero's public `/v1/reputation/:walletAddress` endpoint additionally returns `successRate`, lifetime volume, and trust signals derived from the wallet state.

---

## 4. Revocation

CardZero may revoke an attestation under two conditions:

1. **Data integrity**: the underlying DB record was found to be incorrect (e.g. payment status mis-recorded, milestone tier miscalculated)
2. **Adversarial signal**: external evidence (legal action, incident response) shows the underlying event was fraudulent or coerced

Revocation is recorded on-chain via `revokeFeedback(agentId, feedbackIndex)` and emits a `FeedbackRevoked` event. Revoked attestations are excluded from `getSummary()` aggregates by default. They remain visible in `readAllFeedback(..., includeRevoked=true)` for audit transparency.

---

## 5. Attestor key isolation

The on-chain `ATTESTOR_ROLE` is granted to a dedicated EOA (`CARDZERO_ATTESTOR_KEY`), separate from CardZero's deployer key (which holds only `ADMIN_ROLE`). This separation:

- Limits the blast radius if either key is compromised
- Allows CardZero to rotate the ATTESTOR_KEY without re-deploying contracts
- In Sprint 9, CardZero will additionally grant `ATTESTOR_ROLE` to the `CardZeroJobs` contract so that ERC-8183 Job lifecycle events automatically write to the Reputation Registry without going through the off-chain backend

The deployer key (ADMIN_ROLE) can update these scoring rules by calling `updateScoringRules(newURI, newHash)`. Such an update emits `ScoringRulesUpdated` and replaces the on-chain commitment. CardZero commits to:

1. Always emit a clear announcement (Twitter, GitHub release, dashboard banner) before any rule change
2. Never silently retroactively modify scores of past attestations (the contract is append-only; existing attestations keep their original value)
3. Document the version number bump and effective date for any rule change

---

## 6. Out-of-scope (intentional gaps for v1)

- **Sybil defense beyond filter-based reads**: CardZero does not currently de-duplicate attestations across multiple wallets controlled by the same human. Third-party indexers may apply such heuristics on their own.
- **Stake-weighted attestations**: All attestations carry equal weight regardless of attestor identity. Future versions may add weighting once `ATTESTOR_ROLE` is opened to non-CardZero parties.
- **Time decay**: Old events count the same as recent events. A wallet inactive for 6 months retains its score. Indexers wanting recency weighting can apply it on their own using `Feedback.timestamp`.
- **Cross-chain reputation merging**: Identities are scoped per-chain. A Base mainnet `agentId` is independent from any other chain's record.

---

## 7. Verifying this document on-chain

```bash
# Fetch the document
curl -s https://cardzero.ai/SCORING-RULES.md > scoring-rules.md

# Compute its keccak256
node -e "import('viem').then(v => console.log(v.keccak256(v.toBytes(require('fs').readFileSync('scoring-rules.md', 'utf8')))))"

# Read the on-chain commitment
cast call 0x... "scoringRulesHash()(bytes32)" \
  --rpc-url https://mainnet.base.org

# The two values must match.
```

---

## Document log

| Version | Effective | Notes |
|---|---|---|
| 1.0 | TBD (Sprint 8 mainnet deploy) | Initial publication. Covers Sprint 8 event types (payment / freeze / milestone) plus reserved Sprint 9 types (job_completed / job_rejected). |
