01 · インストール
1 つの package、API key は不要。
Preflight verification は IFF の公開 production API を使用します。owner authentication は不要です。
npm install @ifandonlyif/x402-preflight
global fetch と WebCrypto を使用します。wallet key を持たず、payment を実行せず、402 response を変更しません。
02 · 直接検証
payment ではなく比較から始める。
この Node 20+ の例は requirement だけを IFF に送信します。URL と payment field は fixture です。client が実際に受け取った endpoint と x402 v2 envelope を使ってください。provider には request せず、payment もしません。
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.
3 秒の deadline は application code です。verify() に built-in timeout や retry はありません。この例は stale と unknown をすべて保留します。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 ではありません。
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 を 1 回行い、provider には request しません。IFF は提出された requirement を HTTPS で受け取ります。helper が制限するのは log の内容だけです。DISCOVERY_ENABLED が有効な場合、unobserved URL は discovery candidate として保持されることがあります。requirement は保存されません。
04 · Payment wrapper
wrapper が通過させる内容を把握する。
unpaid の base fetch をラップしてから payment library に渡してください。payment-enabled の fetch をラップすると比較が遅れることがあります。
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- default は
throwです。mismatch は payment logic が進む前に wrapper を停止します。 onStale- default は
strictです。stale mismatch は throw し、stale でも一致する evidence は warning になります。 onUnobserved- default は
warnです。missing evidence は表示されますが、unsafe とは表示されません。
v0.2.0 では、IFF network error、timeout、429、その他の non-success response があると、warning なしで元の 402 が変更されずに通過します。non-402 とデコードできない 402 も通過します。built-in deadline や automatic retry はありません。unavailability 時に flow を保留する必要がある場合は、直接の例を使用してください。
比較が受け入れられると元の response body は読めます。divergence または stale-mismatch error では fetch promise が reject されます。matches_last_observed がない stale result は warning になります。wrapper は API result 内の cryptographic proof を検証しません。
05 · MCP
MCP で agent を接続する。
3 つの別々の MCP 接続方法があります。
公開 HTTP alpha
evidence、fingerprint、transparency log、receipt check 用の匿名・読み取り専用 tools 13 個(Streamable HTTP)。stateless JSON で、authentication、OAuth、SSE session はありません。verification は hosted service 上で実行されます。payment と 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 の 1 つです。
npx --package=@ifandonlyif/x402-preflight \
iff-x402-preflight-mcp
ローカル Go の内部 alpha プレビュー
完全版の Go stdio server は内部 alpha です。source と binary は配布されておらず、npm package、hosted HTTP service、公開 Apostille repository のいずれにも含まれません。
iff-mcp:15 tools。公開 evidence、requirement comparison、transparency log data、STH・inclusion・consistency・Service Receipt のローカル検証。payment は実行しません。apostille-mcp:20 tools。offline verification と、任意の registration、local signing、submission、private ERC-8004 workflow。authenticated action には明示的な設定と operator 指定の key file が必要です。verification を trust するには独立した完全一致の issuer/key pin が必要です。
06 · Verdict と failure handling
4 つの state、隠れた score はありません。
SDK は trust label を付けず、API vocabulary をそのまま返します。
consistent受け取った requirement は comparison window と一致します。
divergedamount または payee fingerprint が observation と異なります。
staleevidence は存在しますが freshness window 外です。
unobservedIFF には比較可能な fingerprint observation がありません。
| 結果 | 直接の例 | default の wrapFetch |
|---|---|---|
consistent | 自身の policy による判断に進むだけです。 | 元の 402 を返します。 |
diverged | 保留し、観察された変更を確認します。 | Throw X402DivergenceError. |
stale + match | fresh evidence が得られるまで保留します。 | warning を出して元の 402 を返します。 |
stale + mismatch | 保留します。stale でも mismatch は消えません。 | Throw X402StaleMismatchError. |
unobserved | 保留します。比較可能な evidence がないことは risk verdict ではありません。 | warning を出して元の 402 を返します。 |
| IFF timeout/ネットワーク障害 | 保留し、verification unavailable を報告します。 | failure 後に元の 402 を返します。built-in timeout はありません。 |
IFF 429 | 自身の rate-limit policy に従って後で retry します。 | warning なしで元の 402 を返します。 |
429、network error、IFF 5xx は diverged の証拠ではありません。busy-loop は避け、直接 HTTP API では Retry-After を尊重するか、jitter と retry cap を持つ bounded backoff を使います。VerifyRequestError が公開するのは status と body で、header は含みません。400/422 は retry 前に修正してください。これらの snippet は automatic retry を行いません。
pilot を強制適用する前に、両方の stale case を含む 7 行すべてを fixture で replay し、API outage と endpoint evidence を分けて扱ってください。versioned v0.2.0 implementation を確認する。
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 showcase です(local verifier、英語)。x402 payment receipt ではなく、payment、service delivery、caller identity、remote execution、result correctness、safety を証明しません。
- payment policy と risk は application の責任です。
質問またはセキュリティ報告: [email protected]