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.

Quickstart

Two of these need no account, no key and no signature — paste them into a terminal as they are.

1 — The public accuracy ledger (no key)

curl "https://vet402.com/api/v1/accuracy"

Aggregate counts only. The same numbers /accuracy renders, including the operator benchmark.

2 — The exact message a payee has to sign (no key)

curl "https://vet402.com/api/v1/payees/verify?wallet=0x4200000000000000000000000000000000000006&name=Acme%20API"

Returns { "message": "…" } — sign it with that wallet and POST it back to the same path to publish a verified-payee page and a badge. Read-only; nothing is written.

3 — Score a payee before paying it (key required)

curl -H "Authorization: Bearer vouch_live_…" \
  "https://vet402.com/api/v1/payees/0x4200000000000000000000000000000000000006/score"

The buyer-side question. Get a key — the free tier is 1,000 lookups a month.

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.
  • Abuse throttles (IP-based), per minute. Key-less and pre-auth paths carry their own IP cap, independent of the quota: authentication failures 60; the unauthenticated demo scorer 10; GET /api/v1/accuracy 20; the badge SVGs 60; the agent passport 20. Verify endpoints split read from write: GET (message preview) 30/IP, while POST is 8/IP and 4 per wallet or agent, so one identity cannot rewrite its public profile in a loop from many IPs. Valid authenticated traffic does not hit any of these.

Two kinds of 429

The two distinct causes of an HTTP 429 and how to tell them apart
CauseBodyHeadersWhat to do
Monthly quota spent (authenticated)error: "rate_limit_exceeded" with retryAfter, usage, limitX-RateLimit-* and Retry-After (seconds to the start of next month, UTC)Stop. Retrying inside the month cannot succeed — raise the plan or wait for the reset.
IP throttle (key-less / pre-auth paths)error: "rate_limited"RateLimit-Limit / -Remaining / -Reset and Retry-After (seconds, always under 60)Sleep for Retry-After and retry. The window is one minute.

The header families are deliberately different names: X-RateLimit-* reports the monthly plan quota, RateLimit-* (IETF draft names) reports the short IP window. No route sets both families on the same response.

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."
}

GET/api/v1/payees/:address/score

Buyer-side screening: should my agent pay this wallet? Never 404s for an unfamiliar wallet — a wallet with no history returns 200 with dataDepth "thin" so you can weigh the confidence yourself. See Payee score below for the composition.

Response

{
  "payee": "0x1234...",
  "score": 52,
  "recommendation": "WARN",
  "dataDepth": "thin",
  "degraded": false,
  "signals": { "receiving": {...}, "walletHealth": {...}, "drainPattern": {...}, "outcomeHistory": {...}, "flags": [...] },
  "scoredAt": "2026-08-13T00:00:00Z",
  "cacheExpiresAt": "2026-08-13T00:05:00Z",
  "disclaimer": "Scores are informational only … it is not an identity or legal-standing check."
}

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.

Payee score

GET /api/v1/payees/:address/score — and the public page at /payee/:address — runs a different engine from the agent/wallet endpoints above and carries no breakdown object. It weighs three tracks, and the weights shift with how much receiving history the wallet actually has, because a cold wallet cannot be judged on a track record it does not have.

Payee score component weights by data depth
dataDepthMeansReceivingWallet healthDrain pattern
thinunder 3 payments received, or from under 2 distinct payers15%45%40%
moderate3+ payments received from 2+ distinct payers35%35%30%
rich10+ payments received across 7+ days from 3+ distinct payers50%25%25%
  • The raw inputs are in the response. signals.receiving reports paymentCount / uniqueDays / distinctPayers, signals.walletHealth reports ageDays / txCount / isBurner, and signals.drainPattern reports the in/out counts and ratio — each with its own 0–100 score, so the weighted arithmetic above can be re-run from the payload.
  • degraded: true is a refusal, not a reading. It means an input could not be read at all. Callers receive a fail-closed BLOCK; the public page prints “Not verifiable right now” rather than a number, because a specific accusation against a named wallet must not rest on an upstream outage.dataDepth answers a different question — how much history exists — and a data-poor wallet read completely is not the same thing.
  • Outcomes adjust the score after weighting. signals.outcomeHistory carries the outcome types on record and the points they moved, which is the same ledger /accuracy aggregates.

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 limitedTwo causes, told apart by the error string: "rate_limit_exceeded" is the monthly quota (retry next month), "rate_limited" is a one-minute IP throttle (retry after the reported seconds). See Two kinds of 429 above.

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

The memoFAQDashboardIntegrations