Status: SKELETON (Sprint 0 / Day 1). Sections marked TODO are frozen on the day noted.
ProofGraph V2 produces a task-aware reputation score for an AI agent, derived from verifiable evidence, and exposes it as a machine-readable API and as an ERC-8004 Validation response.
- Agent — identified by ERC-8004
agentId(uint256,IdentityRegistrytokenId). Secondary key: owner address. - Capability — one of a fixed closed set:
Research,Coding,Solidity Audit,Data Analysis. - Evidence — a single verifiable record about one agent performing one task in one capability.
- Score — 0–100, per
(agent, capability), deterministic, explained byreasons[]. - Confidence —
none | low | medium | high, a function of evidence volume and verifier diversity.
On-chain (EvidenceRegistryV2 at 0x99848Ff9527C38c371D5c892a00677b90387aF4a): compact, hash-committed.
| Field | Type | Notes |
|---|---|---|
id |
uint256 | auto-increment |
agentId |
uint256 | ERC-8004 identity |
capability |
string | must be in the closed set |
outcome |
uint8 enum | Success / Failure / Disputed |
counterparty |
address | who the work was for (0 = undisclosed) |
evidenceHash |
bytes32 | keccak256(canonical off-chain JSON) |
uri |
string | where the off-chain JSON lives |
verifier |
address | msg.sender (MVP) |
timestamp |
uint256 | block.timestamp |
Off-chain JSON (public/evidence/<id>.json for MVP): { taskDescription, artifacts[], verifierNote, sourceRef, ... }.
On read, recompute keccak256 and set verified: true|false.
External evidence adapters (read-only): ReputationRegistry feedback, ValidationRegistry responses → normalised into the same internal Evidence shape with source: "erc8004-reputation" | "erc8004-validation".
Pure function score(evidence[], { now, erc8004Signal? }) -> { score, confidence, terms[], reasons[], penalty }.
No network, no Date.now() inside — the caller passes now and (optionally) a pre-fetched
erc8004Signal. Evidence is already filtered to one (agentId, capability).
successes = count(outcome == "Success")
failures = count(outcome == "Failure")
disputed = count(outcome == "Disputed")
total = evidence.length
completed = successes + failures // disputed excluded from the rate
distinctVerifiers = unique(verifier)
distinctCounterparties = unique(counterparty where counterparty != 0x0)
ageDays_i = max(0, (now - (doc.performedAt ?? onchain.timestamp)) / 86400)
| term | value | weight | applicable when |
|---|---|---|---|
success |
(successes + 2) / (completed + 4) — Beta(2,2) shrinkage |
0.45 | always |
verifier |
min(distinctVerifiers, 4) / 4 |
0.20 | always |
volume |
min(total, 8) / 8 |
0.13 | always |
recency |
mean( exp(-ageDays_i / 45) ) |
0.12 | total > 0 |
counterparty |
min(distinctCounterparties, 3) / 3 |
0.05 | distinctCounterparties > 0 |
erc8004 |
0.6 * repMean01 + 0.4 * validationPassRate (from caller) |
0.05 | signal provided |
repMean01 = mean ERC-8004 NewFeedback.value (normalised by its valueDecimals to a
0–1 range, clamped); validationPassRate = passed / total ValidationResponse for the agent.
base = Σ(value_i * weight_i for applicable i) / Σ(weight_i for applicable i)
penalty = (disputed / total) * 0.20 // 0 if total == 0
score = round( 100 * base * (1 - penalty) ) // 0 if total == 0
Weight of a non-applicable term is dropped from both sums (not counted as 0) — e.g. an agent with no disclosed counterparties is not assessed on that axis rather than punished.
none total == 0
low total < 3 OR distinctVerifiers < 2
medium total < 6 OR distinctVerifiers < 3
high total >= 6 AND distinctVerifiers >= 3
"Smoothed success rate 0.71 — 3 successes / 0 failures (Beta(2,2) prior)"
"3 independent verifiers (0.75 of cap 4)"
"Evidence volume 3 / 8"
"Average freshness 0.39 (45-day half-life)"
"Counterparty diversity not assessed — no counterparties disclosed"
"No ERC-8004 reputation signal for this agent"
"1 of 2 outcomes disputed → score x0.90"
"Confidence: medium (4 records, 3 verifiers)"
| agent | capability | ~score | confidence |
|---|---|---|---|
| 42 | Solidity Audit | 63–72 | medium |
| 2 | Research | 58–68 | medium |
| 6 | Coding | 58–68 | medium |
| 7 | Research | 40–52 | low |
| 2 | Solidity Audit | 32–44 | low |
| 6 | Research | 30–42 | low |
(Exact numbers depend on now and the live ERC-8004 signal; tests assert bands + ordering.)
Base: /v2/api (Next route handlers). CORS *. JSON only. Recompute per request (fine at this scale).
All capability scores for one agent + identity block.
Server-side signer (ProofGraph validator key). Guarded, not exposed publicly.
Implemented in src/lib/erc8004.ts + src/lib/agentCard.ts, assembled by
getErc8004Profile(agentId) → { signal, reputation, validations }.
- Identity:
resolveAgent(agentId)→ownerOf,tokenURI.resolveAgentId(str)accepts a numeric id or a0xaddress (address → agentId via explorer mint logs; errors when a wallet owns several agents). - Agent Card:
fetchAgentCard(tokenURI)— best-effort, tries 3 IPFS gateways with a 4 s timeout each, parses{ name, description, url, skills[], endpoints{} }loosely. A fetch failure is surfaced as{ ok: false, reason }and never breaks a score. Known limitation: public IPFS gateways are unreliable; the seed agents' shared test CID is not pinned, so cards resolve asok:falseon testnet. Production would use a dedicated gateway / pinning service. - Reputation:
readReputationSummary→{ total, revoked, active, meanValue01, tags{} }.meanValue01(heuristic 0–1 normalisation ofNewFeedback.value) feeds the scoringerc8004term. The client-defined feedback scale means this is deliberately fuzzy. - Validation:
readValidationHistory→ per-request{ validator, response, tag, responseHash, lastUpdate }. Pass rate (response >= 50) feeds theerc8004term. - Write Validation (DONE, Day 12): ProofGraph's identity is
agentId 889819(src/lib/validatorV2.tsPROOFGRAPH_VALIDATOR; operating wallet = the deployer).respondToValidation({ wallet, requestHash, agentId, capability })computes the task-aware score, builds a canonicalScoreAttestation({ agentId, capability, score, confidence, formulaVersion, evidenceHashes[], computedAt }) + its keccak256, and callsvalidationResponse(requestHash, score, responseURI, attestationHash, tag)—tag= capability,responseURI= the/v2/api/scoreURL.parseRequestURI()recovers(agent, capability)from a request URI.validationRequestis caller-gated to the agent's owner, soscripts/validate-demo.tsregisters a subject agent it controls, seeds it, then runs request → response →getValidationStatusread-back. Requests with no response yet (tag == "") are excluded fromvalidationPassRate.
Contract addresses: see DECISIONS.md §1–2. ABIs: src/lib/abis/ (verified impls, Day 2).
Placeholder. Must state at minimum:
- honest-majority verifier assumption for V2;
- sybil / collusion resistance is out of scope for V2 (V3);
- evidence availability: on-chain hash commits, off-chain blob may disappear →
verified:falseshown; - ProofGraph score is advisory, not a guarantee.
- V1 (
/,EvidenceRegistryat0x865BF223…/0x08bAa6fE…) is frozen and stays live. - V2 is additive: new contract, new routes under
/v2, new libs. No V1 file is modified. - V2.5 is additive: an optional x402 wrapper on the existing
/v2/api/*handlers and a standalone MCP package. No V2 scoring code or route contract changes. See §8. formulaVersionstring is bumped on any scoring change. V2.5 does not touch it.
Optional. Off by default — with X402_FACILITATOR_PRIVATE_KEY unset every wrapper is a
pass-through and /v2/api/* behaves exactly as §4 describes.
- Wrapper:
withFreeAllowanceThenX402(handler, path, priceUsd, description)(src/lib/x402Gate.ts). FirstPROOFGRAPH_X402_FREE_PER_DAYrequests / client / day are free (in-memory counter, keyed onx-forwarded-for/x-real-ip);0⇒ always charge. After that, a valid payment is required.?x402=requireon the query string forces the paid path regardless of remaining allowance (used by the/v2panel, self-test and demo)./v2/api/score→$0.01/v2/api/agents/[id]→$0.02
- Scheme: x402
exactover EIP-3009transferWithAuthorization. Asset = Arc USDC0x3600…0000(6 decimals); network =eip155:5042002. The client signs an authorization, the facilitator submits it. - Facilitator: self-hosted, in-process (
src/lib/x402.ts). Arc is in no hosted facilitator's network list, but@x402/evm'sExactEvmSchemeis chain-agnostic. A viem wallet (X402_FACILITATOR_PRIVATE_KEY, funded with a little Arc USDC — gas on Arc is USDC) runsverify(EIP-712 sig /validAfter/validBefore/ nonce) thensettle(broadcasttransferWithAuthorization→PROOFGRAPH_X402_PAY_TO, default the facilitator address). - Payment requirements carry
extra: { name: "USDC", version: "2" }— Arc USDC's EIP-712 domain, needed because it is not in x402's built-in asset table (verified on-chain: recomputedDOMAIN_SEPARATORmatches). - Response:
200+ the score body (§4) + aPAYMENT-RESPONSEheader — base64 JSON{ success, transaction, network, payer }wheretransactionis the Arc settlement tx hash. CORS exposespayment-response(and legacyx-payment-response). - Client:
makePayingFetch(privateKey)(src/lib/x402Client.ts) — afetchthat auto-signs and retries the 402. Spend controls allow Arc USDC with a $1 / payment cap. - Not on mainnet. Testnet only; a real deployment needs a shared free-tier store and a hardened facilitator key.
packages/proofgraph-mcp — @proofgraph/mcp, stdio transport, wraps
src/lib/proofgraphV2.ts directly (no HTTP, no payment). Tools: get_agent_score,
get_agent_scorecard, resolve_agent, list_capabilities. Read-only; advisory output,
same v2.0 formula.
{ "agent": { "agentId": "42", "address": "0x…", "cardUri": "https://…" }, "capability": "Solidity Audit", "score": 52, "confidence": "medium", "counts": { "evidence": 9, "verifiers": 3, "counterparties": 2, "successes": 7, "failures": 2 }, "successRate": 0.71, "lastEvidenceAt": "2026-08-30T12:00:00Z", "reasons": [ "…", "…" ], "evidence": [ { "id": "7", "source": "EvidenceRegistryV2", "capability": "Solidity Audit", "outcome": "Success", "verifier": "0x…", "counterparty": "0x…", "timestamp": "2026-08-30T12:00:00Z", "uri": "https://…", "verified": true } ], "formulaVersion": "v2.0", "computedAt": "2026-09-10T09:00:00Z" }