@vet402/sdk
Thin TypeScript client for the vet402 Trust API. Node 20+,
ESM, zero runtime dependencies.
npm install @vet402/sdk
Get a key at vet402.com/dashboard/keys,
export it as VOUCH_API_KEY, and this runs as-is:
import { createVouchClient } from "@vet402/sdk";
const vouch = createVouchClient({ apiKey: process.env.VOUCH_API_KEY! });
const score = await vouch.getWalletScore("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
console.log(score.trustScore, score.recommendation);
const payee = await vouch.getPayeeScore("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
console.log(payee.score, payee.recommendation, payee.dataDepth);
await vouch.attestX402Payment({
wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
txHash: "0x" + "11".repeat(32),
resource: "/api/premium/data",
});
Methods: getAgentScore, getWalletScore, getPayeeScore,
getPayeeVerdictFast, batchScore, attestX402Payment, createSpendGuard.
Two fields that outrank recommendation
A payee score always carries degraded and signalsUnavailable, and both
override the recommendation:
degraded: true | An input could not be read at all. The body is a refusal, not a measurement. | Never treat as ALLOW. |
signalsUnavailable non-empty | Some inputs were not measured (wallet_metrics, native_drain, usdc_drain, outcome_history) — a real but partial view, capped below ALLOW for that reason. | Never treat as ALLOW. |
SpendGuard already refuses both (payee_score_degraded /
payee_partial_measurement). If you read getPayeeScore directly, make the
same two checks yourself before moving money.
Timeouts
Every request is bounded by AbortSignal.timeout, default
DEFAULT_REQUEST_TIMEOUT_MS (10 s), overridable per client:
const vouch = createVouchClient({ apiKey: process.env.VOUCH_API_KEY!, timeoutMs: 3000 });
There is no way to disable it. SpendGuard is fail-closed, and a fail-closed
judgement that never runs is not a judgement — a timeout surfaces as
payee_trust_unavailable (a deny), never as a hang.
verify-at-settle (getPayeeVerdictFast)
For facilitators and payment middleware that need the check inside the
settlement flow. This surface never computes: it returns the engine's already
pinned verdict, or an honest cache_cold.
import { payeeVerdictFastAllows } from "@vet402/sdk";
const verdict = await vouch.getPayeeVerdictFast(payee);
if (!payeeVerdictFastAllows(verdict)) {
void vouch.getPayeeScore(payee);
return queueForRetry();
}
SpendGuard deliberately does not use it: the guard enforces minPayeeScore
bands and its own maxScoreAgeMs and reports the full payeeScore, none of
which the fast body can supply. It is a pre-check, not a replacement for
getPayeeScore.
On 0.1.0? Check with npm ls @vet402/sdk. That release predates
three things documented here: the apiUrl default (pass
apiUrl: "https://vet402.com/api/v1" explicitly), the VouchApiError class
(errors are plain Errors whose message is the API's code), and the
fail-closed trustPolicy default (see SpendGuard below). Everything else
below is the same on both.
Errors
A non-2xx answer throws a VouchApiError carrying the API's machine-readable
code and the HTTP status, so you can tell your key is wrong from we are
having a bad day without parsing strings:
import { VouchApiError } from "@vet402/sdk";
try {
await vouch.getPayeeScore("0x...");
} catch (err) {
if (err instanceof VouchApiError && err.status === 401) {
}
}
SpendGuard — pre-payment policy for agents
Buyer-side counterpart to the score lookups above: before your agent pays
someone, ask the guard. It returns an allow/deny decision plus
machine-readable reasons — and nothing else. SpendGuard is strictly
non-custodial: it never touches keys, funds, signing, or transaction
submission. Execution stays with your wallet stack (Coinbase AgentKit,
Privy, ...).
BREAKING (v0.2.0): fail-closed by default. Money moves only on a clean
ALLOW verdict unless you explicitly opt out. With no trustPolicy set,
every evaluate() performs the payee trust lookup and denies when:
Recommendation is WARN or BLOCK | payee_recommendation_not_allow |
| The score came from a degraded read | payee_score_degraded |
Partial measurement (signalsUnavailable non-empty) | payee_partial_measurement |
| The lookup was refused for your key (401/403) | payee_trust_unauthenticated |
| The lookup failed on our side (5xx, timeout, rate limit) | payee_trust_unavailable |
Opt-outs: trustPolicy: "block-only" (WARN passes; BLOCK, degraded and
failed lookups still deny) or trustPolicy: "custom" (pre-0.2.0 behaviour —
only the rules you set apply, and the lookup only runs when
minPayeeScore / blockOnRecommendation is set).
Which default do you have? Check with npm ls @vet402/sdk. 0.1.0
has no trustPolicy option: the lookup runs only when you set
minPayeeScore / blockOnRecommendation, which is what "custom" means
here. 0.2.0 and later are fail-closed out of the box.
const guard = vouch.createSpendGuard({
maxPerTxUsd: 10,
dailyBudgetUsd: 50,
minPayeeScore: 40,
});
const decision = await guard.evaluate({ payee: "0x...", amountUsd: 5 });
if (decision.allow) {
} else {
console.error(decision.reasons);
}
How it works:
- The local rules (
maxPerTxUsd, dailyBudgetUsd) are optional — set only
the ones you want. Under the default trustPolicy: "allow-only" the payee
trust lookup (GET /v1/payees/{address}/score) always runs, but is skipped
when a local rule already denied, so no quota is burned on a dead payment.
Only trustPolicy: "custom" makes the lookup conditional on
minPayeeScore / blockOnRecommendation being set — with "custom" and
neither set, no API calls happen at all.
- Everything the guard cannot vet fails closed: a WARN/BLOCK verdict, a
degraded read, a partial measurement, or a failed lookup all deny (reason
codes in the table above). A failed lookup names whose problem it is:
payee_trust_unauthenticated means the API key is missing or invalid and
retrying will not help; payee_trust_unavailable means the upstream is
unhappy and retrying might.
- Budget reservation is optimistic: once the local rules pass, the amount is
reserved before the trust lookup awaits and returned automatically if the
trust rules deny — so concurrent
evaluate calls within one process cannot
race past the daily budget together. If an allowed transfer then fails or
is skipped, call guard.release(amountUsd) to give the reservation back.
- The daily budget counter lives in this process's memory (UTC day): it
resets on process restart and is not shared across replicas. Treat it as a
runaway-agent brake, not an accounting system — persist your own ledger if
you need durable budgets.
Links
Absolute, because relative repo paths do not resolve on the npm package page.
MIT · vet402
What the default actually does today (measured 2026-08-25)
Under the default trustPolicy: "allow-only", SpendGuard currently denies
every payee that exists. Know this before you wire it into a payment path.
It is not a lookup failure — it is the engine's banding working as designed:
- an unregistered bare wallet is capped at 62 by the wallet engine;
- a payee with no independent receiving record is capped at 69
(
PAYEE_THIN_SCORE_CEILING, the 2026-08-13 score-manipulation ruling);
- the ALLOW line is 70.
On the operator benchmark at https://vet402.com/accuracy the 17 known-good
addresses (Vitalik, the Ethereum Foundation, Coinbase, Kraken, the ENS DAO
treasury, Gitcoin) score 0 ALLOW / 17 WARN / 0 BLOCK, and a live payee
with 48 delivery-verified L1 receipts and zero failures still scores WARN.
The default is deliberate and is not changing. "We could not verify this"
must keep meaning "do not pay". The mistake to avoid is reaching for
trustPolicy: "custom" to unblock yourself — "custom" also disables the
staleness (H-2), degraded and partial-measurement refusals, which is almost
certainly not what you wanted.
trustPolicy: "evidence" — accept a WARN you can justify
const guard = vouch.createSpendGuard({
maxPerTxUsd: 10,
trustPolicy: "evidence",
requireEvidence: { minL1Deliveries: 3, minL1DistinctBuyers: 2 },
});
A WARN passes only when the payee's measured signals.receiving record
clears the floors you name. Everything else behaves exactly like
"allow-only":
| BLOCK | deny (payee_recommendation_block) — evidence never overturns a refusal |
| WARN, floors cleared | allow |
| WARN, floors not cleared | deny (payee_insufficient_evidence) |
| Evidence field absent from the response | counts as 0 — absence is not a pass |
| Degraded read | deny (payee_score_degraded) |
| Stale score | deny (payee_score_stale) |
| Partial measurement | deny (payee_partial_measurement) |
| Lookup failed | deny (payee_trust_unavailable / ..._unauthenticated) |
Floors available: minL1Deliveries, minL1DistinctBuyers,
minX402Payments, minDistinctPayers. At least one must be >= 1 —
all-zero floors would accept every WARN, which is "block-only" and has to
be spelled that way. requireEvidence is rejected under any other policy, so
the opt-out is always visible at the call site.
The Python SDK (vet402) does not yet have evidence; its
trust_policy options remain allow-only / block-only / custom.