Skip to main content
Independent MeasurementInterface: REST v1Auth: Bearer API key
vet402x402 EconomyAugust 2026

API reference

Authenticate with Authorization: Bearer API key. Base URL: https://vet402.com/api/v1

Keys look like vouch_live_… — send them as Authorization: Bearer vouch_live_….

API keys and webhook headers retain the vouch_ / Vouch- prefixes for backward compatibility.

Full machine-readable schema: docs/openapi.yaml on GitHub.

Rate limits

Scoring is synchronous, so plan for both the monthly quota and the burst behaviour below.

Monthly request quota by plan
PlanMonthly requests
Free1,000
Pro50,000
Scale500,000
  • Quota is per calendar month (UTC) and shared across all keys on an account. Each /score call is 1 unit; a /scores/batch of N agents is N units. Every scored response carries X-RateLimit-Limit, X-RateLimit-Used, and X-RateLimit-Remaining headers so you can track consumption without a separate call.
  • No per-second burst throttle on authenticated calls today. Authenticated requests are governed by the monthly quota only — you may spend it as fast as you like — so pace client-side if you must not exhaust the month in one run. A 429 with Retry-After means the monthly quota is spent (retry after the reported seconds, i.e. next month).
  • Abuse throttles (IP-based). To blunt key-guessing and scraping, some paths carry an IP burst cap independent of the quota: authentication failures are limited to 60/minute/IP, the unauthenticated demo scorer to 10/minute/IP, and the public /accuracy and payee-verify endpoints to 20 and 10/minute/IP respectively. Valid authenticated traffic does not hit these.

GET/api/v1/agents/:agentId/score

Score by ERC-8004 agent ID. Pass ?wallet=0x... to verify the agent's registered wallet.

Response

{
  "agentId": "42",
  "wallet": "0x1234...",
  "trustScore": 78,
  "recommendation": "ALLOW",
  "signals": { "identity": {...}, "reputation": {...}, "wallet": {...}, "x402": {...}, "sybil": {...}, "manual": {...} },
  "breakdown": {
    "components": {
      "identity":   { "score": 100, "weight": 0.2, "contribution": 25 },
      "reputation": { "score": 66,  "weight": 0.3, "contribution": 24.75 },
      "wallet":     { "score": 75,  "weight": 0.2, "contribution": 18.75 },
      "x402":       { "score": 83,  "weight": 0.1, "contribution": 10.38 }
    },
    "weightedSubtotal": 79,
    "sybilPenalty": 0,
    "prePolicyScore": 79
  },
  "scoredAt": "2026-07-14T00:00:00Z",
  "cacheExpiresAt": "2026-07-14T00:05:00Z",
  "disclaimer": "Scores are informational only and do not constitute a guarantee, credit assessment, or investment advice."
}

GET/api/v1/wallets/:address/score

Score by wallet address. Primary integration path for x402 API middleware.

Response

{
  "agentId": "0",
  "wallet": "0x1234...",
  "trustScore": 61,
  "recommendation": "WARN",
  "signals": { ... },
  "scoredAt": "2026-07-14T00:00:00Z",
  "cacheExpiresAt": "2026-07-14T00:05:00Z",
  "disclaimer": "Scores are informational only and do not constitute a guarantee, credit assessment, or investment advice."
}

POST/api/v1/scores/batch

Score up to 25 agents in a single request.

Request body

{
  "agents": [
    { "agentId": "1" },
    { "agentId": "2", "wallet": "0x..." }
  ]
}

Response

{
  "results": [
    { "agentId": "1", "trustScore": 78, "recommendation": "ALLOW", ... },
    { "agentId": "2", "error": "invalid_agent_id" }
  ]
}

POST/api/v1/payments/x402

Attest an x402 payment settlement after payment verification. Idempotent on txHash.

Request body

{
  "wallet": "0xpayer...",
  "txHash": "0xabc...",
  "amount": "1000000",
  "network": "base",
  "resource": "/api/premium/data"
}

Response

// 201 Created (first attestation)
// 200 OK (already recorded — idempotent replay on txHash)
{
  "ok": true,
  "created": true,
  "id": "b3f1...",
  "wallet": "0xpayer...",
  "txHash": "0xabc..."
}

GET/api/v1/agents/:agentId/history

Score history snapshots. Requires Pro or Scale plan. Supports ?limit= (1-100, default 20).

Response

{
  "agentId": "42",
  "history": [
    { "trustScore": 78, "recommendation": "ALLOW", "scoredAt": "2026-07-13T00:00:00Z", ... },
    { "trustScore": 74, "recommendation": "ALLOW", "scoredAt": "2026-07-12T00:00:00Z", ... }
  ]
}

GET/api/v1/watchlist

List your watched targets (max 50 per key). POST {targetType, target, chainId?} to add; DELETE /api/v1/watchlist/:id to remove. A daily cron re-scores entries and fires the watch.verdict_changed webhook only when the recommendation changes (score jitter without a verdict change is stored but not pushed).

Response

{
  "watchlist": [
    { "id": "…", "targetType": "wallet", "target": "0x…", "chainId": 8453,
      "lastScore": 74, "lastRecommendation": "ALLOW", "lastCheckedAt": "2026-08-05T06:30:00Z" }
  ]
}

POST/api/v1/webhooks

Register a webhook endpoint (max 5 per key). The signing secret is returned ONCE — store it. events must be a non-empty subset of the events list below. URL must be https to a public host (SSRF-guarded at registration AND at every delivery). GET /api/v1/webhooks lists your endpoints (secrets never returned); DELETE /api/v1/webhooks/:id removes one.

Request body

{
  "url": "https://your-host.example/vouch-hook",
  "events": ["watch.verdict_changed", "outcome.recorded"]
}

Response

// 201 Created — secret shown once
{
  "id": "…",
  "url": "https://your-host.example/vouch-hook",
  "events": ["watch.verdict_changed", "outcome.recorded"],
  "secret": "whsec_…"
}

GET/api/v1/payees/verify?wallet=0x…&name=Acme+API

Preview the exact canonical message for a (wallet, name) pair before signing — no API key, no rate limit. The same message is echoed back in a failed POST's expectedMessage field, so you never have to reverse-engineer the format.

Response

{ "message": "Vouch verified payee registration\nwallet: 0x…\nname: Acme API\nThis signature only proves control of the wallet above." }

POST/api/v1/payees/verify

Verified payee registration — free, no API key. Sign the canonical message above (fetch it via GET on this same path, or build it yourself: 4 lines, newline-joined — see the response schema) with the payee wallet; a valid signature proves control and publishes /payee/:address plus an embeddable badge at /api/badge/:address. Verification proves wallet control only; scores stay independent.

Request body

{ "wallet": "0x…", "name": "Acme API", "url": "https://…", "signature": "0x…" }

Response

{ "ok": true, "profile": "/payee/0x…", "badge": "/api/badge/0x…" }

GET/api/v1/agents/verify?agentId=42&name=Acme+Agent

Agent-side twin of payee verify. Preview the exact canonical message to sign for (agentId, name) — no API key. The agent's on-chain wallet is resolved and returned so you sign with the right key.

Response

{ "agentId": "42", "wallet": "0x…", "message": "Vouch agent passport registration\nagentId: 42\nwallet: 0x…\nname: Acme Agent\nThis signature only proves control of the wallet above." }

POST/api/v1/agents/verify

Trust-passport registration — free, no API key. Sign the canonical message above with the agent's on-chain wallet (getAgentWallet(agentId)); a valid signature plus the on-chain wallet binding proves control of the agent identity and publishes /agent/:agentId, a machine-readable passport at /api/v1/agents/:agentId/passport, and a badge at /api/badge/agent/:agentId.

Request body

{ "agentId": "42", "name": "Acme Agent", "url": "https://…", "signature": "0x…" }

Response

{ "ok": true, "agentId": "42", "wallet": "0x…", "profile": "/agent/42", "badge": "/api/badge/agent/42" }

GET/api/v1/agents/42/passport

The portable, third-party-verifiable passport — no API key. Returns the signed identity claim, the verification material (canonical message + signature, so any counterparty can re-run verifyMessage and cross-check the wallet against getAgentWallet on-chain), and a live score with explicit freshness (scoredAt / cacheExpiresAt).

Response

{ "agentId": "42", "verified": true, "identity": { "name": "Acme Agent", "wallet": "0x…", "proof": { "message": "…", "signature": "0x…", "scheme": "eip191-personal-sign" } }, "score": { "trustScore": 78, "recommendation": "ALLOW", "x402": { "paymentCount": 12, "uniqueDays": 6 }, "scoredAt": "…", "cacheExpiresAt": "…" } }

Score breakdown

Every scored verdict (agent and wallet endpoints, and each element of a batch) carries a breakdown object that decomposes the chain score into its four weighted components. It is derived from the same numbers the verdict used, so it can never disagree with trustScore.

  • components — each of identity, reputation, wallet, x402 reports its 0–100 score, its weight, and its contribution (score × weight ÷ 0.8; the four contributions sum to weightedSubtotal). Weights are identity 0.2, reputation 0.3, wallet 0.2, x402 0.1 — divided by 0.8 because the customer whitelist/blacklist is a policy layer, not a signal.
  • weightedSubtotal — the weighted average of the four components, before any sybil adjustment.
  • sybilPenalty — points removed by sybil / data- availability flags (always ≤ 0). The specific flags are in signals.sybil.flags.
  • prePolicyScore weightedSubtotal + sybilPenalty, clamped to 0–100. This equals trustScore unless a manual list moved it, in which case manualOverride is true. The manual layer is deliberately kept out of the breakdown so the chain-derived explanation stays separable from policy.

Hard-blocked verdicts (wallet mismatch, unregistered agent) omit breakdown — no weighting ran — and carry a blockReason instead. Treat the field as optional.

Webhooks

vet402 is otherwise a pull API. Webhooks turn it into a monitoring service: register an endpoint once and we POST you a signed event when something you care about changes — most importantly a watched target whose verdict moved (e.g. an ALLOW you gated a payment on becoming a BLOCK). Register with POST /api/v1/webhooks (above); up to 5 endpoints per key.

Events

Webhook event types and their payloads
EventFires whendata fields
watch.verdict_changedA watchlist target's recommendation changes on a re-scan (daily cron). Verdict changes only — not score jitter.watchId, targetType, target, chainId, previous{score,recommendation}, current{score,recommendation}
outcome.recordedAn outcome (auto-detected or partner-reported) lands on a verdict you requested.trustEventId, outcomeType, source, wallet, agentId
list.changedYour own manual whitelist/blacklist changes (also on import) — a team audit trail.action, wallet, listType

A score is never pushed — scores are computed on demand and pushing a cached one would invite treating a stale number as fresh.

Delivery payload

Every delivery is a JSON POST with this envelope. id is unique per event — dedupe on it (see idempotency below).

POST https://your-host.example/vouch-hook
Content-Type: application/json
Vouch-Signature: t=1723000000,v1=5f2b…   (hex HMAC-SHA256)
User-Agent: vouch-webhooks/1

{
  "id": "evt_9f8a…",
  "type": "watch.verdict_changed",
  "createdAt": "2026-08-06T09:30:00.000Z",
  "data": {
    "watchId": "…",
    "targetType": "wallet",
    "target": "0x…",
    "chainId": 8453,
    "previous": { "score": 74, "recommendation": "ALLOW" },
    "current":  { "score": 31, "recommendation": "BLOCK" }
  }
}

Verifying the signature

The Vouch-Signature header is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256(secret, `${t}.${rawBody}`) — the timestamp, a literal dot, then the raw request body. Recompute it with your whsec_… secret, compare in constant time, and reject if the timestamp is more than 5 minutes from now (replay guard). The reference implementation is below — copy it as-is.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, header, toleranceSec = 300) {
  const parts = new Map(header.split(",").map(p => {
    const i = p.indexOf("="); return [p.slice(0, i), p.slice(i + 1)];
  }));
  const t = Number(parts.get("t"));
  const v1 = parts.get("v1");
  if (!Number.isFinite(t) || !v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay guard
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

Delivery, retries & idempotency

  • At-most-once, no retry. Each event is delivered once with a 5-second timeout. A non-2xx response or timeout is not re-delivered — it increments a failure counter instead. A 2xx resets that counter to zero. (Design your handler to catch up by polling the watchlist / outcome endpoints, not by relying on redelivery.)
  • Auto-disable. After 20 consecutive failed deliveries the endpoint is disabled to stop wasting egress on a dead URL. Re-create it (POST /api/v1/webhooks) to re-enable — a new secret is issued.
  • Idempotency. Treat id as an idempotency key: store processed ids and ignore a repeat, so a duplicate dispatch (e.g. overlapping cron passes) is a no-op on your side.
  • SSRF safety / redirects. The target URL is re-validated at delivery time and redirects are rejected (a redirect at delivery is an SSRF vector, not a feature). Point the endpoint at its final https URL directly.

Availability

vet402 is in closed beta, run by a single operator. We publish our real operating posture rather than a contractual uptime figure we can't yet stand behind:

  • No SLA credits during beta. Service is best-effort, with no financial uptime guarantee. When we commit to a numeric target it will be backed by measured operating history — we would rather under-promise than publish a number the way some vendors publish accuracy claims they never measured.
  • Infrastructure. Serverless compute (Vercel), managed Postgres (Neon), and Base RPC. Availability inherits from these providers; there is no independent multi-region failover today.
  • Fail-closed, not fail-wrong. When an upstream (RPC, indexer, settlement store) is unavailable, the affected signal is marked with an *_unavailable flag and penalized rather than guessed — a degraded lookup returns a more cautious verdict, not a confidently wrong one. Each response's dataCoverage reports indexer and settlement freshness so you can see what the score could draw on.
  • Monitoring. A public health endpoint, GET /api/health, returns 200/503 for uptime pollers. A deeper env/DB/RPC probe runs on a daily cron and returns 503 only on a critical failure (indexer catch-up lag is reported, not alerted, to avoid backfill alert fatigue).
  • Status & incidents. No hosted status page yet; during beta, material incidents are communicated to integrators directly. Point your own uptime monitor at /api/health in the meantime.

Error codes

HTTP error codes returned by the vet402 API
StatusMeaningDetail
400Bad requestMalformed body/params (e.g. invalid wallet format, empty batch).
401UnauthorizedMissing or invalid API key on the Authorization: Bearer header.
403Forbidden / plan upgrade requirede.g. score history on a plan below Pro.
429Rate limitedMonthly quota spent (or an IP abuse throttle tripped). Response includes "retryAfter" (seconds); see Rate limits above.

Error bodies are shaped as { "error": string, "details"?: object }.

The memoFAQDashboardIntegrations