Skip to content
For agent developers
Guides

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.

before.tsts
import { createX402Client } from "x402-client"; // your existing client

const client = createX402Client({
  facilitatorUrl: "https://facilitator.provider.example", // provider's facilitator
  wallet,
});
after.tsts
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.

score.tsts
// 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
}
score.pypython
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 call

Treat 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.

agent.tsts
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;
}
agent.pypython
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 minScore is set and the score has dropped below it, the client stops before paying.
  • Keep an idempotency key. Pass idempotencyKey on 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 escrowId and poll vigil.status(escrowId) rather than blocking the agent loop.

Request options

OptionTypeDefaultMeaning
deadlineMsnumber30000Time the provider has to respond. Maximum 300000. Unresponded escrows refund automatically after this.
minScorenumbernoneRefuse to pay if the Endpoint Score is below this value, or if the endpoint has no public score.
maxPricestring (USDC)noneRefuse to lock funds if the quoted price exceeds this amount. Also the ceiling for the price-integrity check.
schemaJSON SchemanoneSchema the response body must satisfy. If omitted, the schema check falls back to the provider's declared schema or content-type.
idempotencyKeystringgeneratedReuse 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.