简中

TypeScript SDK · v0.2.0

在 payment wrapper 看到 requirement 前先验证它。

将应用程序收到的 x402 v2 requirement 与 IFF 的独立 observation 比较。 将 consistent、diverged、stale 与 unobserved 作为你自身 payment policy 的个别输入。

  • Node 20+
  • 现代浏览器
  • ES module
  • TypeScript type
从 npm 安装
npm install @ifandonlyif/x402-preflight

包含 browser/Node library、TypeScript declaration 与单一 tool 的 MCP command。

  1. 01收到 402你的 payment client 收到 requirement。
  2. 02解码SDK 读取 x402 v2 envelope。
  3. 03比较IFF 将它与已签署的 observation 比较。
  4. 04套用 policy是否继续由你的代码决定。

01 · 安装

一个 package,不需 API key。

Preflight verification 使用 IFF 的公开 production API,不需要 owner authentication。

终端机
npm install @ifandonlyif/x402-preflight
Runtime contract

使用全局 fetch 与 WebCrypto。不持有 wallet key、不执行 payment,也不修改 402 response。

02 · 直接验证

先从比较开始。

这个 Node 20+ 范例只将 requirement 传给 IFF。URL 与 payment field 是 fixture:请改用 client 实际收到的 endpoint 与 x402 v2 envelope。它不会向 provider 请求,也不付款。

payTo 变更时该比较什么

另存为 preflight.mjs · 以 node preflight.mjs 执行
import { verify, VerifyRequestError } from "@ifandonlyif/x402-preflight";

// Demonstration data, not a request to buy this service.
const endpoint = "https://api.example.com/v1/quote";
const paymentRequired = {
  x402Version: 2,
  accepts: [{
    scheme: "exact",
    network: "eip155:8453",
    asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    amount: "1000",
    payTo: "0x1111111111111111111111111111111111111111",
    maxTimeoutSeconds: 60,
  }],
};

// Example application policy, NOT wrapFetch's default policy.
function decide(result) {
  switch (result?.verdict) {
    case "consistent":
      return result.matches_last_observed === true
        ? { state: "consistent", action: "continue_to_own_policy" }
        : { state: "unexpected_result", action: "hold" };
    case "diverged":
      return { state: "diverged", action: "hold" };
    case "stale":
      return {
        state: result.matches_last_observed === true ? "stale_match"
          : result.matches_last_observed === false ? "stale_mismatch"
          : "stale_unknown",
        action: "hold",
      };
    case "unobserved":
      return { state: "unobserved", action: "hold" };
    default:
      return { state: "unexpected_result", action: "hold" };
  }
}

async function preflightDecision(url, requirement) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 3000);
  try {
    const result = await verify(url, requirement, {
      fetch: (input, init) => fetch(input, {
        ...init, signal: controller.signal, redirect: "error",
      }),
    });
    return decide(result);
  } catch (error) {
    if (error instanceof VerifyRequestError) {
      return error.status === 429
        ? { state: "iff_rate_limited", action: "retry_later" }
        : { state: "iff_request_failed", action: "hold", status: error.status };
    }
    return {
      state: controller.signal.aborted ? "iff_timeout" : "iff_unavailable",
      action: "hold",
    };
  } finally {
    clearTimeout(timer);
  }
}

// Log the decision only, not the requirement or raw error body.
console.log(await preflightDecision(endpoint, paymentRequired));
// No payment follows, including when the state is consistent.
有界且明确的 policy

3 秒 deadline 是 application code:verify() 没有内建 timeout 或 retry。此范例遇到任何 stale 或 unknown result 都会暂停。continue_to_own_policy 只代表比较通过;wallet、budget、merchant 与 payment rule 仍然适用。unknown fixture 通常返回 unobserved。

请使用不含 credentials、query 或 fragment 的完整 public HTTPS endpoint path,以及收到的 envelope(不可用猜测的 price 或 address)。解码 PAYMENT-REQUIRED header 请用 decodePaymentRequiredHeader();browser 需 provider 的 CORS policy 允许读取该 header。

03 · 不强制执行的评估

在既有 policy 旁观察。

在不改变 payment decision 的情况下收集 comparison outcome。Shadow evaluation 是 application-side 对 verify() 的调用,不是 SDK mode 或 wrapFetch() default。

可重用的 observation helper · 不会呼叫 payment client
import { verify, VerifyRequestError } from "@ifandonlyif/x402-preflight";

export async function shadowPreflight(url, paymentRequired, record = console.info) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 3000);
  let event;
  try {
    const result = await verify(url, paymentRequired, {
      fetch: (input, init) => fetch(input, {
        ...init, signal: controller.signal, redirect: "error",
      }),
    });
    const verdicts = ["consistent", "diverged", "stale", "unobserved"];
    event = {
      mode: "shadow",
      state: verdicts.includes(result?.verdict) ? result.verdict : "unexpected_result",
      matches_last_observed: typeof result?.matches_last_observed === "boolean"
        ? result.matches_last_observed : null,
    };
  } catch (error) {
    event = {
      mode: "shadow",
      state: error instanceof VerifyRequestError && error.status === 429
        ? "iff_rate_limited" : controller.signal.aborted ? "iff_timeout" : "iff_unavailable",
    };
  } finally {
    clearTimeout(timer);
  }
  // Do not expose the URL, requirement, API error body or wallet details.
  try { await record(event); } catch { /* Telemetry must not change policy. */ }
}

// At the point where your client already has the actual requirement:
// void shadowPreflight(exactEndpointUrl, actualPaymentRequired);
// Keep the existing payment decision separate; this helper never authorizes it.

只进行一次 IFF comparison request,不向 provider request。IFF 通过 HTTPS 接收提交的 requirement;helper 只限制写入 log 的内容。启用 DISCOVERY_ENABLED 时,unobserved URL 可能保留为 discovery candidate;requirement 不会存储。

04 · Payment wrapper

了解 wrapper 放行的内容。

封装未付款的 base fetch,再交给 payment library。封装已启用 payment 的 fetch 可能使比较太晚执行。

TypeScript
import {
  wrapFetch, X402DivergenceError, X402StaleMismatchError,
} from "@ifandonlyif/x402-preflight";

const preflightFetch = wrapFetch(fetch, {
  onDiverged: "throw",
  onStale: "strict",
  onUnobserved: "warn",
});

export async function fetchWithPreflight(url, init) {
  try {
    return await preflightFetch(url, { ...init, redirect: "error" });
  } catch (error) {
    if (error instanceof X402DivergenceError) {
      console.warn("IFF requirement diverged; payment flow stopped.");
    } else if (error instanceof X402StaleMismatchError) {
      console.warn("IFF stale evidence mismatched; payment flow stopped.");
    }
    // Do not turn a rejected preflight into a bare-fetch/payment retry.
    throw error;
  }
}

// Give fetchWithPreflight to your payment library as its base fetch.
// This snippet alone does not send a request or enable payments.
onDiverged
预设为 throw。mismatch 会在 payment logic 继续前停止 wrapper。
onStale
预设为 strict。stale mismatch 会 throw;stale matching evidence 会警告。
onUnobserved
预设为 warn。missing evidence 会显示,但不标为 unsafe。
重要:此 wrapper 对 IFF error 采 fail-open。

在 v0.2.0,IFF network error、timeout、429 或其他非成功 response 都会原样放行原始 402,且不警告。非 402 与无法解码的 402 也会放行。没有内建 deadline 或自动 retry。若服务不可用时必须暂停流程,请使用直接验证范例。

比较获接受时,原始 response body 仍可读取;divergence 或 stale-mismatch error 会 reject fetch promise。stale result 缺少 matches_last_observed 时只警告。wrapper 不验证 API result 内的 cryptographic proof。

05 · MCP

通过 MCP 连接 agent。

三种各自独立的 MCP 接入方式。

公开 HTTP alpha

13 个匿名、只读 tools(Streamable HTTP),用于 evidence、fingerprint、transparency log 与 receipt check。返回 stateless JSON,无 authentication、OAuth 或 SSE session。verification 在托管服务执行。不付款,也不公开 submit_discovery_candidate。

https://iff-mcp-api-production.up.railway.app/mcp/iff

公开 npm command

package 包含 stdio command iff-x402-preflight-mcp,只有一个 tool:verify_x402_endpoint。

无需 global install 即可执行
npx --package=@ifandonlyif/x402-preflight \
  iff-x402-preflight-mcp

本地 Go 内部 alpha 预览

完整 Go stdio server 是内部 alpha。源代码与二进制文件未发行,也不在 npm package、托管 HTTP service 或公开 Apostille repository 中。

  • iff-mcp:15 个 tools,用于公开 evidence、requirement comparison、transparency log data,以及本地验证 STH、inclusion、consistency 与 Service Receipt。不付款。
  • apostille-mcp:20 个 tools,用于 offline verification,以及可选的 registration、本地签名、submission 与私有 ERC-8004 流程。authenticated action 需明确配置与 operator 指定的 key file。信任 verification 需独立取得且完全匹配的 issuer/key pin。

阅读 Apostille MCP security model。

06 · Verdict 与失败处理

四种 state,没有隐藏 score。

SDK 返回 API vocabulary,不加 trust label。

consistent

收到的 requirement 与 comparison window 相符。

diverged

amount 或 payee fingerprint 与 observation 不同。

stale

evidence 存在但已超出 freshness window。

unobserved

IFF 没有可供比较的 fingerprint observation。

应用程序范例与 v0.2.0 wrapper 预设行为比较
结果直接验证范例预设 wrapFetch
consistent仅继续交由你自身的 policy 决定。返回原始 402。
diverged暂停;检查观测到的变更。掷出 X402DivergenceError.
stale + match暂停,等候新鲜的 evidence。警告并返回原始 402。
stale + mismatch暂停;stale 不会消除 mismatch。掷出 X402StaleMismatchError.
unobserved暂停;没有可比较的 evidence 并非风险 verdict。警告并返回原始 402。
IFF timeout/网络失败暂停;回报 verification 不可用。失败后返回原始 402;没有内建 timeout。
IFF 429依你的 rate-limit policy 稍后重试。返回原始 402,不会警告。

429、network error 与 IFF 5xx 不是 divergence 的证据。请勿连续重试;直接使用 HTTP API 时遵循 Retry-After,或采用有上限的 backoff、jitter 与 retry 次数限制。VerifyRequestError 提供 status 与 body,不含 header。400/422 请先修正再重试。这些范例不执行自动 retry。

强制套用 policy 前,请以 fixture 重播全部七种情况(含两种 stale),并将 API outage 与 endpoint evidence 分开判读。查看 v0.2.0 版本的实作.

07 · 界线

evidence 是输入,不是保证。

  • SDK 绝不提交 paid canary transaction。
  • match 不保证 payment 后 delivery,也不是 payment-safety 主张。没有 composite score。
  • monitor signature 显示 provenance 与 tamper evidence,不是 attestation,也不证明 trusted execution。
  • Service Receipt v2 是 core preflight flow 以外的实验性 direct-API 展示(local verifier,英文)。它不是 x402 payment receipt,不证明 payment、service delivery、caller identity、remote execution、result correctness 或 safety。
  • payment policy 与风险由 application 负责。

问题或安全性回报: [email protected]