Checks and scoring
The four checks VIGIL runs on every response, the reason codes, the Attestation format, and how attestations become a 0–100 Endpoint Score.
This page defines the four checks that decide whether an escrowed payment is released, how a check is evaluated against the provider's own 402 quote, the reason codes a failed check produces, the shape of an Attestation, and the formula that turns attestations into a public Endpoint Score. The same checks run for escrowed calls and for Watcher probes.
The four checks
Schema validity
The response body must match what the 402 quote declared. If the quote carries a JSON Schema, the body is parsed and validated against it. If the quote declares only a content type, the body must be non-empty and parse as that type (for example, valid JSON for application/json). A body that is empty, unparseable, or valid but of the wrong shape fails this check.
Size
The quote declares a minimum and a maximum body size in bytes. The check reads the delivered body length, not the Content-Length header. A body below the minimum usually signals a truncated or placeholder response; a body above the maximum signals padding or an unexpected payload. Either fails.
Deadline
The complete response must arrive before the agent's deadline (default 30 s, maximum 300 s, or the provider's declared maximum timeout if shorter). The clock starts when the escrow lock is confirmed and stops when the last byte arrives. A response that arrives late is treated the same as no response.
Price integrity
The amount actually charged must equal the amount quoted in the 402 header. The Facilitator compares the settlement amount against the quote it locked. For probes, the Watcher compares the amount debited from its probe budget against the quote. Overcharging fails; undercharging also fails, because it means the quote cannot be relied on.
How a check is evaluated
Every check compares a declaration with an observation.
- Declaration. The 402 response is the source of truth: price, schema or content type, size bounds, maximum timeout. VIGIL never invents expectations; it holds the provider to what the provider said.
- Observation. A probe request (or the agent's escrowed request) is made and the response is measured: bytes, arrival time, parsed body, charged amount.
- Verdict. Each check yields
passorfailwith a reason code. The overall result ispassonly if all four pass. The first failing check in the order schema, size, deadline, price supplies the primary code; all failing codes are kept.
Probes run this every 60 s from at least 3 regions, spending the quoted price per probe. Escrow verdicts run it once per paid call.
Reason codes
| Code | Check | Meaning |
|---|---|---|
SCHEMA_MISMATCH | schema | Body parsed but did not validate against the declared schema or shape |
EMPTY_BODY | schema | Body had zero bytes or only whitespace |
UNPARSEABLE_BODY | schema | Body could not be parsed as the declared content type |
SIZE_BELOW_MIN | size | Body shorter than the declared minimum |
SIZE_ABOVE_MAX | size | Body longer than the declared maximum |
DEADLINE_EXCEEDED | deadline | Response completed after the deadline |
TIMEOUT | deadline | No response at all before the deadline; escrow auto-refunded |
PRICE_ABOVE_QUOTE | price | Charged amount greater than the quoted amount |
PRICE_BELOW_QUOTE | price | Charged amount less than the quoted amount |
HTTP_ERROR | schema | Provider returned a 4xx or 5xx status after payment |
Attestation example
An Attestation is one signed observation, written onchain by the Watcher (or by the Facilitator for an escrow verdict). Prices are USDC atomic units as strings (6 decimals, so "1000" is 0.001 USDC). The watcher address, block and signature below are placeholders.
{
"endpoint": "https://api.example-provider.com/v1/quote",
"method": "GET",
"quotedPrice": "1000",
"chargedPrice": "1000",
"checks": {
"schema": { "pass": true },
"size": { "pass": true },
"deadline": { "pass": true },
"price": { "pass": true }
},
"reasons": [],
"latencyMs": 412,
"result": "pass",
"watcher": "0x0000000000000000000000000000000000000000",
"region": "eu-west",
"block": 0,
"timestamp": "2026-09-26T10:14:03Z",
"signature": "0x<placeholder>"
}A failed attestation has "result": "fail", pass: false on the failing checks, and the codes in reasons, for example ["SIZE_BELOW_MIN", "SCHEMA_MISMATCH"]. Bodies are never stored onchain; the Facilitator keeps the response hash in the escrow record for disputes.
From attestations to a score
Each attestation contributes four component values in the range 0 to 1:
- uptime: 1 if a response arrived before the deadline, 0 for
TIMEOUT,DEADLINE_EXCEEDEDorHTTP_ERROR. - schema: 1 if the schema and size checks both passed, otherwise 0.
- price: 1 if the price check passed, otherwise 0.
- latency:
1 − latencyMs / deadlineMs, clamped to 0..1, so a fast response scores near 1 and a response at the deadline scores 0.
Check weights are uptime 30, schema 30, price 25, latency 15, so a single attestation's value is 30·uptime + 30·schema + 25·price + 15·latency, a number from 0 to 100.
Attestations are then combined into a weighted mean:
- Stake weighting. Each attestation is weighted by its Watcher's stake, with a per-Watcher cap: one Watcher's attestations can carry at most 10% of the total weight for an endpoint, however much it stakes.
- Time decay. Each attestation is also weighted by
0.5 ^ (age / 7 days). An attestation from a week ago counts half as much as one from now; one from a month ago counts about 6%.
In plain text: the Endpoint Score is the sum over attestations of (stake weight × decay × attestation value), divided by the sum of (stake weight × decay).
// Reference formula. Not the production aggregator.
const HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;
const WATCHER_CAP = 0.1; // max share of total weight per Watcher
const W = { uptime: 30, schema: 30, price: 25, latency: 15 };
type Attestation = {
watcher: string;
stake: number;
timestamp: number; // ms
uptime: 0 | 1;
schema: 0 | 1;
price: 0 | 1;
latency: number; // 0..1
};
export function endpointScore(atts: Attestation[], now: number): number | null {
const watchers = new Set(atts.map((a) => a.watcher));
if (atts.length < 25 || watchers.size < 5) return null; // not public yet
const totalStake = [...watchers].reduce(
(sum, w) => sum + (atts.find((a) => a.watcher === w)?.stake ?? 0),
0,
);
let num = 0;
let den = 0;
for (const a of atts) {
const stakeWeight = Math.min(a.stake, WATCHER_CAP * totalStake);
const decay = Math.pow(0.5, (now - a.timestamp) / HALF_LIFE_MS);
const value =
W.uptime * a.uptime + W.schema * a.schema + W.price * a.price + W.latency * a.latency;
num += stakeWeight * decay * value;
den += stakeWeight * decay;
}
return den === 0 ? null : Math.round(num / den);
}The aggregator runs this deterministically over the public registry and writes the result to the ERC-8004 Reputation Registry with the attestation count, last block and a content hash of the attestation set, so anyone can recompute and verify it. Attestations marked false by a dispute are excluded.
Minimum before public
A score is published only after at least 25 attestations from at least 5 distinct Watchers. Until then the registry returns no score and the read endpoint https://api.vigil.example/v1/score/{endpoint} (placeholder) reports "status": "insufficient" with the current count. At the reference cadence, a newly listed endpoint probed by 5 Watchers crosses the threshold within its first hour.
Score bands
| Score | Band | What it means for an agent |
|---|---|---|
| 90–100 | Reliable | Responses match the quote almost always. Safe default for unattended calls. |
| 70–89 | Acceptable | Occasional failures or slow responses. Use escrow; consider a fallback. |
| 40–69 | Degraded | Frequent failures or price drift. Escrow strongly advised; prefer alternatives. |
| 0–39 | Unreliable | Most recent attestations failed. Avoid unless no alternative exists. |
Bands are a reading aid. The registry stores the number, and agents can set their own thresholds.