402coffee-verify 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # 402coffee-verify
2
+
3
+ Gate payments on **verified x402 agents**. One free call to [402.coffee](https://api.402.coffee/docs) tells you whether a paying agent holds a current certificate proving it pays correctly, refuses deliberately over-priced orders, and checks who it's paying — before you transact with it.
4
+
5
+ - **Free** — the `/verify` check costs nothing. (An optional paid `/score` gives a decision-grade 0–100 risk number.)
6
+ - **Zero dependencies**, Node 18+.
7
+ - Composes with your existing trust/reputation stack: those tell you the counterparty was worth paying; this tells you the agent won't get swapped at signing.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install 402coffee-verify
13
+ ```
14
+
15
+ ## Library
16
+
17
+ ```js
18
+ import { verify, passesGate, verifyGate } from "402coffee-verify";
19
+
20
+ // Raw status
21
+ const result = await verify("0xAGENT…");
22
+ result.verified; // true = ≥1 current PASS, no current FAIL
23
+ result.capabilities.scam_resistance; // { tested, result, fresh, expired, valid_until }
24
+
25
+ // One-shot gate
26
+ const { ok } = await verifyGate("0xAGENT…", {
27
+ require: ["scam_resistance", "recipient_awareness"], // must hold these
28
+ fresh: true, // non-expired cert required
29
+ });
30
+ if (!ok) throw new Error("agent not certified safe");
31
+ ```
32
+
33
+ ## CLI
34
+
35
+ ```bash
36
+ npx 402coffee-verify 0xAGENT… --require scam_resistance,recipient_awareness
37
+ # exit 0 = pass, 1 = fail, 2 = usage/network error → drop straight into CI/scripts
38
+ ```
39
+
40
+ ## Middleware (Express)
41
+
42
+ ```js
43
+ import { requireVerifiedAgent } from "402coffee-verify/middleware";
44
+
45
+ // Reads the wallet from the `x-agent-wallet` header (override with getWallet)
46
+ app.post("/paid-endpoint",
47
+ requireVerifiedAgent({ require: ["scam_resistance"], fresh: true }),
48
+ handler
49
+ );
50
+ ```
51
+
52
+ ## Next.js App Router / hono / edge
53
+
54
+ ```js
55
+ import { guardAgent } from "402coffee-verify/middleware";
56
+
57
+ export async function POST(req) {
58
+ const wallet = req.headers.get("x-agent-wallet");
59
+ const gate = await guardAgent(wallet, { require: ["recipient_awareness"] });
60
+ if (!gate.ok) return gate.response; // ready 402 Response
61
+ // …serve the paid action…
62
+ }
63
+ ```
64
+
65
+ ## Paid score (optional, $0.10)
66
+
67
+ `/score` returns a deterministic 0–100 risk score with itemized on-chain evidence. It costs $0.10 in USDC on Base, so you must pass an **x402-paying fetch** (e.g. `wrapFetchWithPayment` from `@x402/fetch`) — with a plain fetch you get back the 402 payment terms, not a score.
68
+
69
+ ```js
70
+ import { score } from "402coffee-verify";
71
+ import { wrapFetchWithPayment } from "@x402/fetch";
72
+
73
+ const payingFetch = wrapFetchWithPayment(fetch, wallet);
74
+ const s = await score("0xAGENT…", { fetchImpl: payingFetch });
75
+ // s.score, s.tier, s.components, s.payment_history …
76
+ ```
77
+
78
+ ## What "verified" means
79
+
80
+ `verified === true` only when the agent has **at least one current (non-expired) PASS and no current FAIL**. Certificates are valid for 30 days (behaviour drifts); require a fresh one for anything that matters. Every result 402.coffee returns is a fact it observed on-chain — never a subjective grade or a safety guarantee.
81
+
82
+ Docs: <https://api.402.coffee/docs> · Test your own agent: <https://api.402.coffee/inspect>
83
+
84
+ MIT © Agent Café / 402.coffee
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // CLI: gate on an agent's 402.coffee verification status.
3
+ //
4
+ // npx 402coffee-verify 0x<wallet> [--require scam_resistance,recipient_awareness]
5
+ // [--no-fresh] [--json] [--base <url>]
6
+ //
7
+ // Exit codes: 0 = gate passes 1 = gate fails 2 = usage / network error
8
+
9
+ import { verify, passesGate, capabilityHeld, DEFAULT_BASE_URL } from "../index.js";
10
+
11
+ function parseArgs(argv) {
12
+ const args = { require: [], fresh: true, json: false, baseUrl: DEFAULT_BASE_URL, wallet: null };
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const a = argv[i];
15
+ if (a === "--json") args.json = true;
16
+ else if (a === "--no-fresh") args.fresh = false;
17
+ else if (a === "--require") args.require = (argv[++i] || "").split(",").map((s) => s.trim()).filter(Boolean);
18
+ else if (a === "--base") args.baseUrl = argv[++i];
19
+ else if (a === "--help" || a === "-h") args.help = true;
20
+ else if (!a.startsWith("-") && !args.wallet) args.wallet = a;
21
+ }
22
+ return args;
23
+ }
24
+
25
+ const HELP = `402coffee-verify — check if a paying agent is certified safe (free)
26
+
27
+ Usage:
28
+ 402coffee-verify 0x<wallet> [options]
29
+
30
+ Options:
31
+ --require <caps> comma list that MUST be held, e.g.
32
+ scam_resistance,recipient_awareness
33
+ --no-fresh accept expired certs (default: require a current one)
34
+ --json print the full /verify JSON
35
+ --base <url> API base (default ${DEFAULT_BASE_URL})
36
+ -h, --help this help
37
+
38
+ Exit: 0 gate passes | 1 gate fails | 2 usage/network error
39
+ Docs: https://api.402.coffee/docs`;
40
+
41
+ async function main() {
42
+ const args = parseArgs(process.argv.slice(2));
43
+ if (args.help || !args.wallet) {
44
+ console.log(HELP);
45
+ process.exit(args.wallet ? 0 : 2);
46
+ }
47
+ let result;
48
+ try {
49
+ result = await verify(args.wallet, { baseUrl: args.baseUrl });
50
+ } catch (e) {
51
+ console.error(`error: ${e.message}`);
52
+ process.exit(2);
53
+ }
54
+
55
+ if (args.json) {
56
+ console.log(JSON.stringify(result, null, 2));
57
+ } else {
58
+ const caps = ["scam_resistance", "recipient_awareness", "can_pay", "full_suite"];
59
+ console.log(`wallet ${result.wallet}`);
60
+ console.log(`verified ${result.verified ? "yes" : "no"}${result.tested ? "" : " (never tested)"}`);
61
+ for (const c of caps) {
62
+ const held = capabilityHeld(result, c, { fresh: args.fresh });
63
+ const cap = result.capabilities?.[c];
64
+ const detail = !cap || cap.tested !== true ? "not tested"
65
+ : `${cap.result}${cap.fresh === false ? " (stale)" : ""}`;
66
+ console.log(` ${held ? "✓" : "·"} ${c.padEnd(20)} ${detail}`);
67
+ }
68
+ }
69
+
70
+ const ok = passesGate(result, { require: args.require, fresh: args.fresh });
71
+ if (!args.json) {
72
+ console.log(ok ? "\nGATE: PASS" : "\nGATE: FAIL");
73
+ if (!ok) console.log(`Not certified for your requirement. Test at ${result.how_to_test || "https://api.402.coffee/inspect"}`);
74
+ }
75
+ process.exit(ok ? 0 : 1);
76
+ }
77
+
78
+ main();
package/index.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ // Type definitions for 402coffee-verify
2
+
3
+ export const DEFAULT_BASE_URL: string;
4
+
5
+ export type CapabilityName =
6
+ | "scam_resistance"
7
+ | "recipient_awareness"
8
+ | "can_pay"
9
+ | "full_suite";
10
+
11
+ export interface Capability {
12
+ tested: boolean;
13
+ result?: "pass" | "fail";
14
+ fresh?: boolean;
15
+ expired?: boolean;
16
+ valid_until?: string;
17
+ }
18
+
19
+ export interface Cert {
20
+ product: string;
21
+ level: string;
22
+ passed: number;
23
+ total: number;
24
+ result: "pass" | "fail";
25
+ test_version: number;
26
+ test_version_means: string;
27
+ issued: string;
28
+ age_days: number;
29
+ fresh: boolean;
30
+ expired: boolean;
31
+ valid_until: string;
32
+ cert_url: string;
33
+ badge_url: string;
34
+ }
35
+
36
+ export interface VerifyResult {
37
+ service: string;
38
+ wallet: string;
39
+ tested: boolean;
40
+ verified: boolean;
41
+ verified_means: string;
42
+ capabilities: Record<CapabilityName, Capability>;
43
+ certs: Cert[];
44
+ freshness_policy: { valid_for_days: number; note: string };
45
+ how_to_test: string;
46
+ paid_score?: { note: string; url: string };
47
+ checked_at: string;
48
+ }
49
+
50
+ export interface VerifyOptions {
51
+ baseUrl?: string;
52
+ fetchImpl?: typeof fetch;
53
+ timeoutMs?: number;
54
+ }
55
+
56
+ export interface GateOptions {
57
+ requireVerified?: boolean;
58
+ require?: CapabilityName[];
59
+ fresh?: boolean;
60
+ }
61
+
62
+ export function verify(wallet: string, opts?: VerifyOptions): Promise<VerifyResult>;
63
+ export function capabilityHeld(
64
+ result: VerifyResult,
65
+ name: CapabilityName,
66
+ opts?: { fresh?: boolean }
67
+ ): boolean;
68
+ export function passesGate(result: VerifyResult, opts?: GateOptions): boolean;
69
+ export function verifyGate(
70
+ wallet: string,
71
+ opts?: VerifyOptions & GateOptions
72
+ ): Promise<{ ok: boolean; result: VerifyResult }>;
73
+ export function score(wallet: string, opts?: VerifyOptions): Promise<Record<string, unknown>>;
package/index.js ADDED
@@ -0,0 +1,134 @@
1
+ // 402coffee-verify — gate payments on verified x402 agents.
2
+ // Zero dependencies. Node 18+ (uses global fetch / AbortController).
3
+ //
4
+ // The primitive is the FREE GET /verify endpoint on 402.coffee. It tells you
5
+ // whether a paying agent holds a current certificate proving it pays correctly,
6
+ // refuses over-priced orders, and checks who it is paying.
7
+ //
8
+ // Docs: https://api.402.coffee/docs
9
+
10
+ export const DEFAULT_BASE_URL = "https://api.402.coffee";
11
+
12
+ const WALLET_RE = /^0x[0-9a-fA-F]{40}$/;
13
+
14
+ /**
15
+ * Look up an agent's public verification status. Free, no payment.
16
+ * @param {string} wallet 0x-prefixed 40-hex address
17
+ * @param {object} [opts]
18
+ * @param {string} [opts.baseUrl] default https://api.402.coffee
19
+ * @param {typeof fetch} [opts.fetchImpl] custom fetch (e.g. for tests)
20
+ * @param {number} [opts.timeoutMs] default 8000
21
+ * @returns {Promise<object>} the raw /verify JSON (see index.d.ts VerifyResult)
22
+ */
23
+ export async function verify(wallet, opts = {}) {
24
+ if (typeof wallet !== "string" || !WALLET_RE.test(wallet.trim())) {
25
+ throw new Error(`invalid wallet address: ${wallet} (expected 0x + 40 hex)`);
26
+ }
27
+ const base = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
28
+ const fetchImpl = opts.fetchImpl || globalThis.fetch;
29
+ if (!fetchImpl) throw new Error("no fetch available; pass opts.fetchImpl on Node < 18");
30
+ const timeoutMs = opts.timeoutMs ?? 8000;
31
+
32
+ const ctrl = new AbortController();
33
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
34
+ try {
35
+ const res = await fetchImpl(
36
+ `${base}/verify?wallet=${encodeURIComponent(wallet.trim())}`,
37
+ { headers: { accept: "application/json" }, signal: ctrl.signal }
38
+ );
39
+ if (!res.ok) throw new Error(`verify failed: HTTP ${res.status}`);
40
+ return await res.json();
41
+ } finally {
42
+ clearTimeout(t);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Is a single named capability currently held (tested + pass + not expired)?
48
+ * @param {object} result a VerifyResult from verify()
49
+ * @param {"scam_resistance"|"recipient_awareness"|"can_pay"|"full_suite"} name
50
+ * @param {object} [opts]
51
+ * @param {boolean} [opts.fresh] require a non-expired cert (default true)
52
+ * @returns {boolean}
53
+ */
54
+ export function capabilityHeld(result, name, opts = {}) {
55
+ const fresh = opts.fresh !== false;
56
+ const c = result?.capabilities?.[name];
57
+ if (!c || c.tested !== true) return false;
58
+ if (c.result !== "pass") return false;
59
+ if (c.expired === true) return false;
60
+ if (fresh && c.fresh !== true) return false;
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Decide whether an agent passes your gate.
66
+ * @param {object} result a VerifyResult from verify()
67
+ * @param {object} [opts]
68
+ * @param {boolean} [opts.requireVerified] require top-level verified=true (default true)
69
+ * @param {Array<string>} [opts.require] capabilities that must be held, e.g.
70
+ * ["scam_resistance","recipient_awareness"]
71
+ * @param {boolean} [opts.fresh] require non-expired certs (default true)
72
+ * @returns {boolean}
73
+ */
74
+ export function passesGate(result, opts = {}) {
75
+ const requireVerified = opts.requireVerified !== false;
76
+ const fresh = opts.fresh !== false;
77
+ const required = Array.isArray(opts.require) ? opts.require : [];
78
+ if (requireVerified && result?.verified !== true) return false;
79
+ for (const cap of required) {
80
+ if (!capabilityHeld(result, cap, { fresh })) return false;
81
+ }
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Convenience: look up + gate in one call. Returns { ok, result }.
87
+ * Never throws on a failed gate — only on a network/validation error.
88
+ * @param {string} wallet
89
+ * @param {object} [opts] merged verify() + passesGate() options
90
+ * @returns {Promise<{ok: boolean, result: object}>}
91
+ */
92
+ export async function verifyGate(wallet, opts = {}) {
93
+ const result = await verify(wallet, opts);
94
+ return { ok: passesGate(result, opts), result };
95
+ }
96
+
97
+ /**
98
+ * Paid decision-grade score ($0.10 via x402). Requires an x402-paying fetch
99
+ * (e.g. wrapFetchWithPayment from @x402/fetch) passed as opts.fetchImpl — with a
100
+ * plain fetch this returns the 402 payment terms, not a score. Honest by design:
101
+ * we never fake a paid result.
102
+ * @param {string} wallet
103
+ * @param {object} [opts]
104
+ * @param {string} [opts.baseUrl]
105
+ * @param {typeof fetch} [opts.fetchImpl] an x402-paying fetch to actually pay
106
+ * @param {number} [opts.timeoutMs] default 15000
107
+ * @returns {Promise<object>} the score JSON, or the 402 terms if unpaid
108
+ */
109
+ export async function score(wallet, opts = {}) {
110
+ if (typeof wallet !== "string" || !WALLET_RE.test(wallet.trim())) {
111
+ throw new Error(`invalid wallet address: ${wallet} (expected 0x + 40 hex)`);
112
+ }
113
+ const base = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
114
+ const fetchImpl = opts.fetchImpl || globalThis.fetch;
115
+ if (!fetchImpl) throw new Error("no fetch available; pass opts.fetchImpl");
116
+ const timeoutMs = opts.timeoutMs ?? 15000;
117
+
118
+ const ctrl = new AbortController();
119
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
120
+ try {
121
+ const res = await fetchImpl(`${base}/score`, {
122
+ method: "POST",
123
+ headers: { "content-type": "application/json", accept: "application/json" },
124
+ body: JSON.stringify({ wallet: wallet.trim() }),
125
+ signal: ctrl.signal,
126
+ });
127
+ // 402 (unpaid) still returns useful JSON (the payment terms) — return it as-is.
128
+ return await res.json();
129
+ } finally {
130
+ clearTimeout(t);
131
+ }
132
+ }
133
+
134
+ export default { verify, capabilityHeld, passesGate, verifyGate, score, DEFAULT_BASE_URL };
package/middleware.js ADDED
@@ -0,0 +1,79 @@
1
+ // Drop-in middleware for gating routes on 402.coffee verification.
2
+ // import { requireVerifiedAgent } from "402coffee-verify/middleware";
3
+
4
+ import { verify, passesGate, DEFAULT_BASE_URL } from "./index.js";
5
+
6
+ /**
7
+ * Express middleware. Blocks the request unless the agent's wallet is verified.
8
+ *
9
+ * app.post("/paid", requireVerifiedAgent({ require: ["scam_resistance"] }), handler)
10
+ *
11
+ * @param {object} [opts]
12
+ * @param {(req:any)=>string|undefined} [opts.getWallet] how to read the wallet
13
+ * (default: header `x-agent-wallet`, then `?wallet=`)
14
+ * @param {string[]} [opts.require] capabilities that must be held
15
+ * @param {boolean} [opts.fresh] require non-expired certs (default true)
16
+ * @param {string} [opts.baseUrl]
17
+ */
18
+ export function requireVerifiedAgent(opts = {}) {
19
+ const baseUrl = opts.baseUrl || DEFAULT_BASE_URL;
20
+ const getWallet =
21
+ opts.getWallet || ((req) => req.headers?.["x-agent-wallet"] || req.query?.wallet);
22
+ return async function (req, res, next) {
23
+ const wallet = getWallet(req);
24
+ if (!wallet) {
25
+ return res.status(400).json({ error: "missing agent wallet (send header x-agent-wallet)" });
26
+ }
27
+ try {
28
+ const result = await verify(wallet, { baseUrl });
29
+ if (passesGate(result, { require: opts.require, fresh: opts.fresh })) return next();
30
+ return res.status(402).json({
31
+ error: "agent not verified for this action",
32
+ verify: `${baseUrl}/verify?wallet=${wallet}`,
33
+ get_certified: "https://api.402.coffee/inspect",
34
+ });
35
+ } catch (e) {
36
+ // Fail closed on a paid path: if we cannot verify, do not let it through.
37
+ return res.status(502).json({ error: "verification check failed", detail: String(e) });
38
+ }
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Framework-agnostic guard for Next.js App Router / hono / edge handlers.
44
+ * Returns { ok, result, response? } — if !ok, `response` is a ready Response.
45
+ *
46
+ * const gate = await guardAgent(wallet, { require: ["recipient_awareness"] });
47
+ * if (!gate.ok) return gate.response;
48
+ */
49
+ export async function guardAgent(wallet, opts = {}) {
50
+ const baseUrl = opts.baseUrl || DEFAULT_BASE_URL;
51
+ if (!wallet) {
52
+ return {
53
+ ok: false,
54
+ response: Response.json({ error: "missing agent wallet" }, { status: 400 }),
55
+ };
56
+ }
57
+ try {
58
+ const result = await verify(wallet, { baseUrl });
59
+ const ok = passesGate(result, { require: opts.require, fresh: opts.fresh });
60
+ if (ok) return { ok: true, result };
61
+ return {
62
+ ok: false,
63
+ result,
64
+ response: Response.json(
65
+ {
66
+ error: "agent not verified for this action",
67
+ verify: `${baseUrl}/verify?wallet=${wallet}`,
68
+ get_certified: "https://api.402.coffee/inspect",
69
+ },
70
+ { status: 402 }
71
+ ),
72
+ };
73
+ } catch (e) {
74
+ return {
75
+ ok: false,
76
+ response: Response.json({ error: "verification check failed", detail: String(e) }, { status: 502 }),
77
+ };
78
+ }
79
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "402coffee-verify",
3
+ "version": "0.1.0",
4
+ "description": "Gate payments on verified x402 agents. One free call to 402.coffee — check if a paying agent is certified safe (pays correctly, refuses over-priced scams, verifies the recipient). Zero dependencies.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "bin": {
9
+ "402coffee-verify": "bin/402coffee-verify.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./index.d.ts",
14
+ "import": "./index.js"
15
+ },
16
+ "./middleware": {
17
+ "import": "./middleware.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "index.js",
22
+ "index.d.ts",
23
+ "middleware.js",
24
+ "bin/",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "keywords": [
31
+ "x402",
32
+ "agent",
33
+ "payments",
34
+ "verification",
35
+ "trust",
36
+ "usdc",
37
+ "base",
38
+ "conformance",
39
+ "402.coffee"
40
+ ],
41
+ "license": "MIT",
42
+ "homepage": "https://api.402.coffee/integrations"
43
+ }