@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