For agent developers
Route x402 payments through the VIGIL Facilitator, read Endpoint Scores before calling, and handle Released, Refunded and Disputed outcomes.
This guide is for developers who build agents that pay for APIs with x402 on Base. It shows how to point an existing x402 client at the VIGIL Facilitator, how to read an Endpoint Score before spending USDC, how to make a paid call with a deadline, and what to do with each of the three outcomes an escrowed payment can have.
How x402 works today
An x402 endpoint answers an unpaid request with HTTP 402 and a price; the agent pays that price in USDC on Base and repeats the request with a payment proof. The provider verifies the proof, usually through a facilitator service, and returns the response. Payment settles before the agent sees the content, so an empty body, a wrong schema or a charge above the quote cannot be undone.
Route payments through the Facilitator
Most x402 clients take a facilitator URL as configuration. The VIGIL Facilitator implements the same interface, so switching means changing one value. The Facilitator holds the USDC in escrow, forwards the request, runs the four checks on the response and then releases or refunds.
import { createX402Client } from "x402-client"; // your existing client
const client = createX402Client({
facilitatorUrl: "https://facilitator.provider.example", // provider's facilitator
wallet,
});import { createX402Client } from "x402-client";
const client = createX402Client({
// placeholder, not live
facilitatorUrl: "https://facilitator.vigil.example",
wallet,
});Nothing changes for the provider. The endpoint still receives a valid x402 payment proof once the response passes the checks. From the agent's side, the response body arrives as before; the difference is that the money was held, not spent, while the checks ran.
Read a score before calling
Every scored endpoint has a public Endpoint Score from 0 to 100, aggregated from Watcher attestations and written to the ERC-8004 reputation registry. Reading it is free. The read API mirrors the registry so you do not need an RPC connection for a simple threshold check.
Score bands are 90–100 Reliable, 70–89 Acceptable, 40–69 Degraded and below 40 Unreliable. An endpoint with fewer than 25 attestations from 5 distinct Watchers has no public score and the API returns null. See Checks and scoring for how the number is built.
// placeholder API, not live
const SCORE_API = "https://api.vigil.example/v1/score";
type ScoreResponse = {
endpoint: string;
score: number | null;
attestations: number;
watchers: number;
updatedAtBlock: number;
};
export async function scoreAllows(endpoint: string, minScore = 70): Promise<boolean> {
const res = await fetch(`${SCORE_API}/${encodeURIComponent(endpoint)}`);
if (!res.ok) throw new Error(`score lookup failed: ${res.status}`);
const data = (await res.json()) as ScoreResponse;
if (data.score === null) return false; // not enough attestations yet
return data.score >= minScore;
}
if (await scoreAllows("https://api.weather.example/v1/forecast", 70)) {
// proceed with the paid call
}import httpx
SCORE_API = "https://api.vigil.example/v1/score" # placeholder, not live
def score_allows(endpoint: str, min_score: int = 70) -> bool:
r = httpx.get(f"{SCORE_API}/{httpx.URL(endpoint)}", timeout=5.0)
r.raise_for_status()
data = r.json()
if data["score"] is None: # fewer than 25 attestations from 5 Watchers
return False
return data["score"] >= min_score
if score_allows("https://api.weather.example/v1/forecast", 70):
... # proceed with the paid callTreat a missing score as a decision, not an error. New endpoints are unscored for a while. You can still call them through the Facilitator; escrow protects the individual payment regardless of the score.
Make a paid call with a deadline
The reference clients wrap the fetch-pay-retry loop and return a typed outcome. The deadlineMs option is the time the provider has to respond before the escrow refunds automatically. Default is 30 000 ms, maximum 300 000 ms. Checks finish within 10 s of the response.
import { VigilClient } from "@vigil/x402"; // placeholder package, not published
const vigil = new VigilClient({
facilitatorUrl: "https://facilitator.vigil.example", // placeholder, not live
wallet,
});
const result = await vigil.call("https://api.weather.example/v1/forecast", {
method: "POST",
body: { city: "Lisbon" },
deadlineMs: 30_000,
minScore: 70,
maxPrice: "0.05", // USDC
schema: forecastSchema, // JSON Schema object
});
switch (result.state) {
case "Released":
// checks passed; USDC released to the provider
useForecast(result.body);
break;
case "Refunded":
// a check failed or the deadline passed; USDC returned to your wallet
console.warn("refunded:", result.reason, result.failedChecks);
break;
case "Disputed":
// a Watcher contested the check result; funds stay locked up to 24 h
await queueForLater(result.escrowId);
break;
}from vigil_x402 import VigilClient # placeholder package, not published
vigil = VigilClient(
facilitator_url="https://facilitator.vigil.example", # placeholder, not live
wallet=wallet,
)
result = vigil.call(
"https://api.weather.example/v1/forecast",
method="POST",
body={"city": "Lisbon"},
deadline_ms=30_000,
min_score=70,
max_price="0.05", # USDC
schema=forecast_schema,
)
if result.state == "Released":
use_forecast(result.body)
elif result.state == "Refunded":
log.warning("refunded: %s %s", result.reason, result.failed_checks)
elif result.state == "Disputed":
queue_for_later(result.escrow_id)The full state machine is Quoted, Locked, Responded, Checked, then one of Released, Refunded or Disputed. The client only surfaces the last three. See Escrow flow for the intermediate states.
Handle a refund
A refund is not something you request. When a check fails, or the provider does not respond before the deadline, the Facilitator returns the escrowed USDC to the paying wallet. The client reports Refunded with the failing checks; result.txHash points at the refund transaction.
Guidance for agents:
- Do not treat Refunded as a transport error. The provider answered, or failed to, and the response did not pass. Retrying the same request immediately usually produces the same result.
- Retry with backoff and a cap. A reasonable default is up to 3 attempts with delays of 2 s, 8 s and 30 s. If
minScoreis set and the score has dropped below it, the client stops before paying. - Keep an idempotency key. Pass
idempotencyKeyon the call and reuse it on retries. The Facilitator will not open a second escrow for the same key while the first is not final, so a network drop between your agent and the Facilitator cannot double-lock funds. - Fall back to another endpoint. The score API makes it cheap to keep a ranked list of alternatives for the same capability.
- Disputed is rare and slow. Funds stay locked for up to 24 h while staked Watchers vote. Persist the
escrowIdand pollvigil.status(escrowId)rather than blocking the agent loop.
Request options
| Option | Type | Default | Meaning |
|---|---|---|---|
deadlineMs | number | 30000 | Time the provider has to respond. Maximum 300000. Unresponded escrows refund automatically after this. |
minScore | number | none | Refuse to pay if the Endpoint Score is below this value, or if the endpoint has no public score. |
maxPrice | string (USDC) | none | Refuse to lock funds if the quoted price exceeds this amount. Also the ceiling for the price-integrity check. |
schema | JSON Schema | none | Schema the response body must satisfy. If omitted, the schema check falls back to the provider's declared schema or content-type. |
idempotencyKey | string | generated | Reuse across retries to prevent a second escrow for the same logical request. |
The 0.5% fee on routed volume is taken from the released amount, not added to what you pay. On a refund no fee is charged.
Related pages
- Escrow flow for each state and its timeout.
- Checks and scoring for how the four checks combine into a score.
- Security and risks for what is and is not protected today.