@vet402/middleware
Drop-in x402 transaction gate. Score the payment counterparty and
ALLOW / WARN / BLOCK before the payment settles — from Express, Next.js,
or Hono, in about three lines. Fail-closed by default, in both senses: a
score you cannot fetch blocks the payment, and only a clean ALLOW verdict
passes — the default policy blocks WARN too.
BREAKING (v0.2.0): fail-closed by default — money moves only on ALLOW
unless you explicitly opt out. The default policy: "allow-only" blocks
any verdict that is not ALLOW (a blocked WARN carries reason
recommendation_not_allow). The pre-0.2.0 behaviour (WARN allowed
downstream) is now the explicit opt-out policy: "block-only"; custom
blockOn/warnOn banding requires policy: "custom".
This is the productized form of the facilitator-gate and x402-trust-gate
reference adapters. The x402 payment gate stays your beacon; this middleware
reads Vouch before it settles.
npm install @vet402/middleware
Which default do you have? Check with npm ls @vet402/middleware.
0.1.0 ships the old lenient default (a WARN passes downstream) and has
no policy option — set blockOn: ["BLOCK", "WARN"] to get the fail-closed
posture today. 0.2.0 and later are fail-closed out of the box, and the
policy option documented below exists there.
Express — three lines
import { createExpressGate } from "@vet402/middleware/express";
app.use("/api/paid", createExpressGate({
apiUrl: process.env.VOUCH_API_URL!,
apiKey: process.env.VOUCH_API_KEY!,
getAddress: (req) => req.payer,
}));
Anything but ALLOW returns 403 { error: "trust_blocked", ... } before
your handler runs (default policy: "allow-only"). ALLOW continues with
the full decision on req.vouchTrust; under the "block-only" /"custom"
opt-outs a WARN also continues and fires onWarn.
Next.js (App Router)
import { withVouchGate } from "@vet402/middleware/next";
export const POST = withVouchGate(
{
apiUrl: process.env.VOUCH_API_URL!,
apiKey: process.env.VOUCH_API_KEY!,
getAddress: (req) => new URL(req.url).searchParams.get("payer") ?? undefined,
},
async (req, trust) => Response.json({ ok: true, trust }),
);
Or inline with createNextGate(...).check(address) → { decision, response },
returning response when it is non-null.
Hono
import { createHonoGate } from "@vet402/middleware/hono";
app.use("/api/paid/*", createHonoGate({
apiUrl: process.env.VOUCH_API_URL!,
apiKey: process.env.VOUCH_API_KEY!,
getAddress: (c) => c.get("payer"),
}));
Configuration
scoreSource | "wallet" | "wallet" (the x402 beacon) or "payee" (buyer-side receiving history). |
policy | "allow-only" | "allow-only" blocks anything that is not ALLOW (fail-closed). "block-only" lets WARN through (pre-0.2.0 behaviour). "custom" bands with your own blockOn/warnOn. |
blockOn | ["BLOCK"] | Recommendations that block. Requires policy: "custom" (rejected otherwise, never silently ignored). |
warnOn | ["WARN"] | Recommendations that warn (still allowed). Requires policy: "custom". |
minScore | — | Stricter numeric floor (0–100): block below it even on ALLOW. |
failMode | "closed" | "closed" blocks on a lookup failure; "open" allows it (flagged degraded). |
timeoutMs | 5000 | Score-lookup timeout — a hung Vouch never hangs the payment path. |
blockStatus | 403 | HTTP status returned on a block. |
Every decision carries { action, recommendation, score, reason, degraded }.
degraded: true marks a verdict that came from failMode, not a real
score — log or alert on those so trust-blind settlements are visible.
Settlement attestation (optional)
Feed successful settlements back so future scores weight them (10% of the
score). Provide getAttestation on any adapter; it is fire-and-forget — a
failed attestation never fails the paid request.
createExpressGate({
getAttestation: (req) => req.paymentTxHash
? { wallet: req.payer, txHash: req.paymentTxHash, resource: req.path }
: undefined,
});
Non-custodial
The gate reads a score and returns a verdict. It never touches keys, funds,
signing, or transaction submission — settlement stays with your x402 stack.
Links
MIT · vet402