x402check

Risk-check counterparties before an AI agent pays; pay x402 resources only after a verified allow

AI & MLTypeScriptv0.3.0

x402check — pre-payment risk checks for x402 agents and wallets

LIVE: https://x402check.xyz · did:web:x402check.xyz · $0.001 per evaluation with prepaid credits, or per call via x402 ($0.0035 on Base; $0.005 with transaction simulation) · discovery · DID document · JWKS

x402check is an x402 risk-check provider (wire format of x402 PR #2422). You call it before an agent or a wallet pays or signs, and it checks the counterparty. It combines provider-verified evidence with a typed model:

  • the OFAC SDN list, refreshed daily;
  • curated phishing and drainer feeds;
  • transaction simulation: where the assets actually go, and which approvals are granted;
  • drainer-kit code fingerprints, which recognize redeployed drainer contracts before their address is listed;
  • our own kit watch: every Ethereum and Base block is read as it is produced. It records look-alike wallets that delegate (EIP-7702) to an address-poisoning executor, wallets whose delegate forwards whatever they receive (sweepers), and new contracts running drainer-kit code;
  • look-alike domain analysis;
  • on-chain facts about the counterparty, such as whether an approval is being granted to a plain wallet;
  • a typed model (TypeSafe Jev) that reads the content the agent acted on for injected instructions.

Every verdict is an ES256 attestation. It states which checks the provider actually ran and which fields the caller merely asserted.

Scope, stated plainly. x402check catches what the chain, the lists, its own kit watch, and the content in front of it reveal. It does not see laundering patterns or other transaction-graph behaviour. It cannot flag an unknown drainer address that is simply sent funds, unless the address is one of the look-alikes or compromised wallets the kit watch has seen. A clean verdict means "none of these checks fired", not "safe". Measured limits are in docs/EVIDENCE.md.

x402check demo

The demo video predates v0.2.0. Its evidence slide shows v5 corpus numbers that EVIDENCE.md supersedes, and it predates the v0.3 layers (simulation, code fingerprints) and pricing (every evaluation is paid; there is no free tier).

What it checks

CheckSourceEffect on the verdict
Sanctioned addressOfficial OFAC SDN XML: 1,056 digital-currency addresses, dated snapshotscore 0 / critical, deterministic, no model call
Known phishing domainMetaMask eth-phishing-detect (~100k hosts, embedded)capped at 20 (critical)
Known drainer / scam addressScamSniffer (EVM, runtime KV, 7-day publication lag)capped at 20
Community-flagged domainScamSniffer domain listcapped at 40 only when our own domain analysis corroborates it
Look-alike domainpublic-suffix aware: leet, IDN homoglyphs, typosquats, brand + lure word, official domain reused as a subdomain"strong" impersonation → capped at 40
Approval granted to a plain walleton-chain eth_getCode / activity (EVM), account data (Solana)permits and approvals to an EOA → capped at 55; 40 if the address has no activity
Hidden recipientsimulation of transaction (eth_simulateV1 + traceTransfers)assets leave, nothing comes back, and a wallet the user never named ends up with them → 40 (75, review, when a source-verified contract such as a bridge forwarded them)
Payee gets more than declaredsimulation + payment / the explicit transfer in the calldatathe named payee receives a different asset, or more, than declared → 40
Assets parked in an unverified contractsimulation + Blockscout source verificationnothing in return, contract source not verified → 55
Drainer-kit codelogic-code fingerprints of contracts listed by Forta (embedded) and ScamSniffer (runtime)the subject, or a contract in the simulated transaction, runs a listed drainer's code → 30
Address-poisoning look-alikekit watch (our own scan of every Ethereum and Base block)the wallet delegates (EIP-7702) to an address-poisoning executor → 20 (address_poisoning)
Compromised walletkit watchthe wallet delegates to a labelled sweeper family → 20 (compromised_wallet); to code that forwards what it receives, with no label → 40 (auto_forwarding_wallet)
Drainer operatorkit watchthe address deployed drainer-kit code → 30 (drainer_operator). A forwarder's destinations are not flagged (v0.6.0): whoever deploys a forwarder chooses them
Unverified spenderBlockscout source verificationapproval or permit to an unverified contract → 75 and at least medium (review)
Injected / manipulated intentJev typed questions over context (what the agent acted on)model penalties and caps
New addresson-chain activityinformational new_address category

Caller-supplied screening and authorization fields are recorded as asserted in the attestation and can never lower the score. Prose claims such as "already screened" are unverified by construction.

Quickstart

Every evaluation is paid; there is no free tier. There are two ways to pay:

  • Prepaid credits: one x402 payment buys a balance, and each check then costs $0.001 with no payment round trip. This is the cheapest and fastest option.
  • Per call: an x402 client pays when it gets the 402 and retries.
  1. See the price. An unpaid call returns 402 with the accepted mainnet options in the PAYMENT-REQUIRED header:

    curl -si -X POST https://x402check.xyz/v1/risk-check -H "Content-Type: application/json" -d '{"wallet":"0x7a3e8f0c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f","chain":"base"}' | head -1
    # HTTP/2 402
    
  2. Pay and check. Use the TypeScript SDK with an x402-paying fetch (see Payments), the MCP server, or any x402 client:

    const verdict = await x402check.check({
      wallet: "0x7a3e8f0c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f",
      chain: "eip155:1",
      domain: "https://app.example-dapp.org",
      context: "Permit2 signature: unlimited USDC allowance to this spender",
      interaction: { type: "permit_signature", unlimited: true },
      payment: { network: "eip155:1", asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", pay_to: "0x7a3e8f0c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f" },
    });
    

The same request as the raw body the client sends:

curl -X POST https://x402check.xyz/v1/risk-check \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: <x402 payment payload>" \
  -d '{
    "wallet": "0x7a3e8f0c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f",
    "chain": "eip155:1",
    "domain": "https://app.example-dapp.org",
    "context": "Permit2 signature: unlimited USDC allowance to this spender",
    "interaction": { "type": "permit_signature", "unlimited": true },
    "payment": { "network": "eip155:1", "asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "pay_to": "0x7a3e8f0c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f" }
  }'
{
  "checked": true,
  "score": 40,
  "tier": "high",
  "categories": ["intent_risk", "behavioral", "approval_to_eoa", "new_address"],
  "provider": "did:web:x402check.xyz",
  "evidence": {
    "sanctions": { "list": "ofac-sdn", "as_of": "2026-09-29", "status": "not_listed" },
    "domain": { "host": "app.example-dapp.org", "registrable": "example-dapp.org", "official": false, "impersonation": "none", "signals": [] },
    "onchain": { "status": "ok", "network": "eip155:1", "is_contract": false, "activity": "none", "tx_count": 0 },
    "feeds": [{ "source": "metamask-phishing-detect", "kind": "domain", "as_of": "2026-09-29", "status": "clear" }, "…"],
    "model": "jev-wallet-risk/v6"
  },
  "jws": "eyJhbGciOiJFUzI1NiIsInR5cCI6InJpc2stY2hlY2srand0Ii…",
  "jwks_url": "https://x402check.xyz/.well-known/jwks.json",
  "checked_at": "…", "expires_at": "…"
}

To also check what a transaction will do, send it as transaction ($0.005 per simulated evaluation). The provider simulates it against the latest block and reports the net asset movements, the approvals granted, and any findings:

curl -X POST https://x402check.xyz/v1/risk-check -H "Content-Type: application/json" -d '{
  "wallet": "0x…called contract or decoded counterparty…", "chain": "eip155:1",
  "transaction": { "from": "0x…user…", "to": "0x…contract…", "value": "0x2386f26fc10000", "data": "0x…" }
}'
# evidence.simulation → { "status": "ok", "outflows": [{ "standard": "native", "amount": "10000000000000000",
#   "counterparty": "0x…", "counterparty_is_contract": false }], "inflows": [], "approvals": [],
#   "findings": ["outflow_to_undisclosed_eoa"] }   → score capped at 40

Request fields

FieldRequiredRules
walletyesthe subject address: EVM 0x…, base58 (Solana/Tron/BTC…), bech32, cashaddr, or CAIP-10. Anything else → 422 {error, field:"wallet"}
chainnoalias (ethereum, base, solana, …) or CAIP-2 (eip155:8453); enables on-chain facts on supported mainnets
domainnohostname or http(s) URL (normalized server-side); the site the payment or signature is for
contextno≤ 4096 chars: what the agent acted on (tool output, page text, instruction). Untrusted by design. Leave out secrets and personal data: keys, seed phrases and credit tokens are redacted before the model sees it, but nothing else is (see Data handling)
interactionno{type, unlimited?}; type ∈ native_transfer, token_transfer, token_approval, nft_approval, permit_signature, order_signature, message_signature, contract_call
paymentnobinds the attestation to a payment: {network, pay_to, amount (base units), asset, resource}
audno≤ 256 chars; copied into the attestation, never shown to the model
transactionnoEVM {from, to?, value?, data?} to simulate; needs an eip155 chain. value is decimal or 0x-hex; data is 0x-hex, ≤ 49,152 chars. Simulated on Ethereum, Base, Polygon, Arbitrum, Optimism and BSC
screening, authorizationnocaller assertions, recorded as asserted (can only raise risk)

Batch: POST /v1/risk-check/batch with {"requests": [...]} (≤ 25). It is all-or-nothing: an invalid item returns 422 with its index.

Attestation

A compact JWS (alg: ES256, typ: risk-check+jwt, kid as published in the DID document, today jev-attest-v1), TTL 1 h. Claims:

ClaimMeaning
iss, sub, iat, exp, jtiissuer did:web:x402check.xyz, the subject wallet, times, unique id
score, tier, categoriesthe verdict and the findings behind it
checkswhat the provider verified: sanctions (list, date, status), domain (impersonation), onchain (status, network, activity), feeds (source@date:status, including code-fingerprint sets), simulation (status, network, findings), model (question set, or skipped), model_id (the model id the backend reported; through the AI Gateway this is the alias typesafe-ai/jev)
assertedwhat the caller claimed (screening / pre-authorization): not verified
payment, interaction, audwhat the verdict was issued for
input_hashSHA-256 over the canonical normalized inputs, sources and question set
request_hashSHA-256 over the request fields exactly as sent (RFC 8785): recompute it to prove nothing was dropped or altered in transit

Verify it by pinning the issuer, and bind it to the request it answers. Never trust a key URL carried by a response or an intermediary:

npx tsx scripts/verify-attest.ts <jws> --request '<the exact JSON body>' --max-age 300 [--aud <url>] [--pay-to <addr> --amount <atomic>]

The reference verifier is the SDK's verifyAttestation behind a CLI. It resolves the key from the issuer's did:web document and pins x402check's key by default. It checks alg, typ, iss, exp and iat, and with --request it recomputes request_hash, so a verdict issued for another wallet, payment or transaction is rejected (request_mismatch). --max-age refuses an older verdict (plus a 300 s clock-skew allowance). Without --request, a valid verdict for something else still verifies.

Wallet integration

Check the real counterparty. For approve, a Permit2 signature or a Seaport order, that is the spender, operator or recipient decoded from the calldata or typed data, not the token contract. Send the interaction type too, and the transaction when you want it simulated. The MetaMask Snap in snap/ is a complete reference decoder.

The wallet (or its backend) pays each check with x402, so payingFetch below is a fetch wrapped with an x402 client, as in Payments:

const res = await payingFetch("https://x402check.xyz/v1/risk-check", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    wallet: spenderOrRecipient,              // decoded counterparty
    chain: `eip155:${chainId}`,
    domain: location.origin,                 // the requesting site
    context: "Permit2: unlimited USDC allowance to spender 0x…",
    interaction: { type: "permit_signature", unlimited: true },
  }),
});
if (res.status !== 200) return showNotVerified(res.status);   // 402 = payment not settled: never an all-clear
const v = await res.json();
if (!v.checked) return showNotVerified();                     // fail-closed, never an all-clear
if (v.tier === "high" || v.tier === "critical") warnOrBlock(v);
TierUX
lowno warning; optional "checked" badge
mediumamber: "Some signals suggest caution"
highred: "We recommend you do not proceed"
criticalhard block with override; show the categories and evidence

Payments

Every evaluation is paid; there is no free tier.

Prepaid credits (recommended):

  • POST /v1/credits {"amount_usd": 1} returns a 402 for $1.00 on any network. Pay it with any x402 client and the response carries a token (x402c_…, shown once) and its balance. Packs run from $0.10 to $100.
  • Send Authorization: Bearer x402c_… with /v1/risk-check. Each check costs $0.001 ($0.005 when a transaction is simulated), debited atomically.
  • Such a check has no 402 round trip and no on-chain settlement, so it is also several times faster.
  • GET /v1/credits with the token returns the balance. POST /v1/credits with the token tops it up.
  • If a verdict is not produced, the check is refunded.

Per call:

  • Price by payment network (the 402 lists every option): Base $0.0035, Solana $0.002, Sei $0.002, Avalanche $0.001, Monad $0.001, Polygon $0.007, Arbitrum $0.009.

  • A request whose transaction is simulated costs $0.005, or the network's price if that is higher. The simulated price covers the simulation, the classification of every recipient and spender, and code fingerprints through delegations and proxies. It is charged only on chains where simulation runs.

  • A batch is billed per item.

  • Why the prices differ: every x402 payment is an on-chain settlement, and its cost is ours. PayAI settles EVM payments with EIP-3009, which any wallet can pay gaslessly, and bills us the network's gas + 30% per settlement (about $0.0023 on Base). Dexter bills nothing, but on EVM networks it settles only through Permit2, which most payers' wallets cannot use without an on-chain approval. Coinbase CDP settles with EIP-3009 too, at $0.001 per settlement after 1,000 free a month, so it takes the EVM networks where it is cheapest.

  • Routing: each network settles through the facilitator any payer can pay through, and among those the cheapest to us:

    • Coinbase CDP for Base, Polygon and Arbitrum ($0.001 per settlement after 1,000 free a month);
    • PayAI for Avalanche and Sei, and as the fallback for every EVM network;
    • Dexter for Solana and Monad.

    /status shows each network's facilitator, transfer method, fee and margin live. It also lists each facilitator's health and published signers, so anyone can check on-chain who settled a payment.

  • Settlement: USDC via x402 v2 (PAYMENT-SIGNATURE), mainnet only: Base, Polygon, Arbitrum, Avalanche, Monad, Sei and Solana. The x402 "exact" scheme is gasless for the payer, so USDC alone is enough.

  • An unpaid request gets 402 with the accepted options in PAYMENT-REQUIRED. Any x402 client pays and retries.

  • Invalid input is rejected (422/413) before anything is priced.

  • Release after settlement: the attestation is returned only once the payment settles. If the evaluation cannot be produced, nothing is settled (503, no charge).

  • One payment, one evaluation: a payment is claimed once, when it verifies. A copy of the same PAYMENT-SIGNATURE, sent at the same time or later, gets 409 payment_already_used.

  • The payer is screened: a paying wallet on the OFAC SDN list gets 403 payer_sanctioned, and nothing is charged.

  • HTTPS only: a plain-HTTP API call gets 403, before its body or token is read.

import { createClient } from "@x402check/client";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";

const payer = new x402Client();
payer.register("eip155:*", new ExactEvmScheme(privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`)));
payer.setSpendControls({ maxAmountPerPayment: "$1" });

// Once: buy credits (one x402 payment). Store the token like a password.
const { token } = await createClient({ fetch: wrapFetchWithPayment(fetch, payer) }).buyCredits(1);

// Every check after that: $0.001 from the balance, no payment round trip.
const x402check = createClient({ creditToken: token });
const verdict = await x402check.check({ wallet: "0x…", chain: "base" });

// Or pay per call instead: createClient({ fetch: wrapFetchWithPayment(fetch, payer) }).

Architecture

request ─► validate (422 names the field) ─► credits debit, or x402 payment (per item) ─► Provider
                                                                         │
      deterministic, provider-side ──────────────────────────────────────┤
        OFAC SDN screen ── listed? ──► score 0 · critical (no model call)│
        domain analysis (PSL, homoglyph, typosquat, lure)                │
        threat feeds (MetaMask + OFAC refreshed daily, ScamSniffer KV)   │
        on-chain facts + code fingerprints (JSON-RPC)                    │
        transaction simulation (eth_simulateV1) + contract verification  │
      model ─ Jev typed questions over provider checks + context ────────┤
      code  ─ weights, deterministic caps, tiers (src/scoring.ts) ───────┤
                                                                         ▼
         settle payment (per call) ─► release ES256 attestation: checks · asserted · payment · jti

Source layout:

  • src/: provider, validation, enrichment, simulation, code fingerprints, scoring and JWS.
  • deploy/: the Cloudflare Worker: the x402 paywall and pricing, facilitator routing, feed refresh and /status. See deploy/README.md.
  • packages/: the TypeScript SDK (client) and the MCP server (mcp).
  • snap/: the MetaMask Snap.
  • eval/: evaluation layers and production probes.
  • scripts/: data refresh, the feeds publisher and the verifier.
  • docs/: METHODOLOGY, EVIDENCE, STRATEGY.

Evidence (v0.4–v0.6)

WhatResult
Single-use payments (v0.6.0, production): three copies of one payment sent at onceone evaluated and settled, two refused (409); a later copy refused too
Kit watch, Ethereum, a 24 h backfill: addresses flagged (6,232 poisoning look-alikes, 575 wallets delegated to sweepers or forwarders, 24 destinations)6,831, none of them on ScamSniffer's list, which publishes with a 7-day delay: lead time not yet measured
Kit watch: sampled poisoning look-alikes confirmed by a victim's history (lower bound)27/37 (73%)
Kit watch: new contracts matching an old drainer kit (Ethereum 24 h + Base 6 h, from the backfill's logs, not a tracked report)0/2,966: drainer infrastructure moved to EIP-7702
Code sets on held-out legitimate code (verified contracts + callees)Before the v0.4 collision gate, the v0.4 kit families matched 16/2,500 held-out contracts on Ethereum; the v0.3 production Forta set matched exchange deposit fleets (Luno, Poloniex, BitGo) that Forta labels as phishing. Found and fixed. After the gate, on a second held-out set: 1 match in 1,737 + 0 in 2,935 (Ethereum + Base), and that match was a real, unlisted drainer on our own review
PayAI shadow: 7 days of PayAI-settled payments on Base, deterministic layers3,132 payments, 207 payees, 0 flagged; $3.13 to check them all
OFAC SDN addresses (external labels)24/24 critical
MetaMask-listed phishing domains · ScamSniffer drainer addresses40/40 · 30/30
Drainer permits, drainer feed switched off (approval-to-EOA rule)27/30
Simulation: real drainer transactions that still move assets at the latest block18/25 flagged (72%), all as hidden recipients
Simulation: real transactions to 19 well-known contracts0/84 flagged
Code fingerprints: listed drainer contracts matched by earlier kits' code, at creation time40/82 (Ethereum and Base; 43/100 in v0.3 on Ethereum only)
Plain transfers to unlisted drainers0/30: not detectable from the address alone, unless the kit watch has seen the wallet
Unlisted phishing domains without a feed0–4/60 across four samples: feeds do the heavy lifting
Well-known contracts and top dApp domains0 false positives (0/22, 0/40)
Tranco top 200k, deterministic rules22 capped (0.011%): 20 on MetaMask's own list, 2 crypto look-alikes
Risky cases with an attacker-written context20/100 (only look-alike domains)
Injected instructions passed as raw agent content40/40
Signing guard (v0.6.0): real drainer transactions that still move the victim's assets, refused before the key signs (ScamSniffer feed off)22/25 (88%; by contract 10/11): 18 by simulation, 4 by drainer code; the 3 misses are one contract · legitimate: 0/49 that move assets and 0/84 that execute refused; 1/228 in all (a reverting transaction too large to simulate)
Payments in production: a check paid per call on Base, and one paid from prepaid credits$0.0035, 2.7–4.7 s end to end (single samples: PayAI, Coinbase CDP, CDP on v0.6.0) · $0.001, 0.5–0.8 s, with no settlement per check
Production, every evaluation paid (settled in USDC on Base, or from credits bought that way)53/53 correct and 53/53 attestations verified · security:v2 12/12 (re-run on v0.5) · security:v3 8/8 · security:v4 7/7 · security:v5 9/9, 11/11 through Coinbase CDP (v0.5.1), and 11/11 on v0.6.0

Full methodology, confidence intervals and what each number does not show: docs/EVIDENCE.md. How each verdict is formed, with every cap: docs/METHODOLOGY.md. Earlier evidence documents are kept as historical records with correction notes.

Run locally

npm install
npm test                           # provider unit tests
npm run typecheck                  # root + deploy + scripts + snap
AI_GATEWAY_API_KEY=... npm start   # :8799 local dev server, no paywall (or TYPESAFE_API_KEY=...)
curl localhost:8799/.well-known/risk-check.json
npm run eval:suite -- --seed 200   # full evaluation (~$0.15 of model calls)

Worker: npm run dev:worker, or wrangler dev --local in deploy/. See deploy/README.md for secrets, feeds, deploy and rollback.

For agents: SDK and MCP server

  • @x402check/client is a typed TypeScript client with zero runtime dependencies. It runs on Node ≥ 20, browsers, Cloudflare Workers, Deno and Bun.
    • verifyAttestation checks the signature against the issuer's did:web key and binds it to the request you made, including request_hash.
    • interpret applies the fail-closed policy. Its verdicts come from the signed claims only, never from the unsigned body.
    • guardAccount (@x402check/client/guard) makes the check enforced, not advisory. It wraps the account an agent signs with, and every transaction, permit, order, x402 payment or EIP-7702 delegation is signed only after a verified allow bound to that exact request. Otherwise it throws, and the key never signs. The exceptions are stated: an x402 payment of up to $0.25 to x402check's own pay_to (paying for a check) is not checked; a request with no counterparty and nothing to simulate (e.g. a zero-value deployment) is signed as inert; and a warn is signed when your onWarn approves it. See the signing guard.
  • @x402check/mcp is an MCP server for any agent (Claude Code, Claude Desktop, other MCP clients), with the tools x402check_check, x402check_pay, x402check_verify_attestation and x402check_methodology.
    • Every verdict is verified before the agent sees an action.
    • x402check_pay fetches an x402 resource and pays only after x402check clears the exact payee, right before signing. A warn goes to the user in the client, never to the agent.
    • It pays each check from prepaid credits (X402CHECK_CREDIT_TOKEN) or itself via x402 (USDC on Base, gasless for the payer), with a per-payment cap and a total budget. Both secrets are redacted from every output.
# with prepaid credits ($0.001 a check, no payment round trip):
claude mcp add x402check -e X402CHECK_CREDIT_TOKEN=x402c_… -- npx -y @x402check/mcp
# or paying per call from a dedicated wallet with a small USDC balance on Base:
claude mcp add x402check -e X402CHECK_PAYER_KEY=0x… -e X402CHECK_BUDGET_USD=1 -- npx -y @x402check/mcp

Both packages are on npm:

The MCP server is also in the official MCP Registry as io.github.caiovicentino/x402check.

Discovery: agents and their developers can find x402check in three catalogs.

  • The x402 Bazaar, which Coinbase CDP builds from the payments it settles. It lists /v1/risk-check and its batch endpoint, with the input schema, a callable example and the price on each network (GET https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources).
  • x402scan, which reads /openapi.json. That document declares each paid operation's price range, schemas and agent guidance.
  • The official MCP Registry, where the MCP server is listed as io.github.caiovicentino/x402check (npm @x402check/mcp).

Opening an endpoint in a browser returns an example request and the prices.

MetaMask Snap (preview)

snap/ has onTransaction / onSignature insights that decode the request locally and show the real counterparty, the amounts (including UNLIMITED approvals) and local danger findings.

Every check is paid, and this Snap version cannot pay yet. So 0.3.0 sends nothing to x402check.xyz and has no network permission. Every insight says "NOT verified by x402check" and never shows an all-clear.

The paid mode is complete and tested behind a single flag (src/config.ts), ready for when wallet-side payment exists. It checks the counterparty, simulates the transaction ("You send 1.5 ETH → 0x… (wallet)"), and renders the signed verdict with its evidence and warnings for hidden recipients and known drainer code.

Supported decoding:

  • calldata: ERC-20 approve / transfer, Permit2, EIP-2612, setApprovalForAll, NFT transfers;
  • typed data: v1, v3 and v4, including Permit2 and Seaport;
  • personal_sign messages, decoded to text.

On install and on update it shows a disclosure that states exactly what happens. In this version nothing is sent. It stores only which disclosure you have seen.

cd snap && npm install && npm test          # builds, then 322 tests (285 run; paid-mode scenarios also run in Node) incl. the built bundle in SES
npx mm-snap serve                           # then wallet_requestSnaps "local:http://localhost:8062" in MetaMask Flask

The Snap is not yet published to npm nor allowlisted by MetaMask, so there is no one-click install in regular MetaMask yet.

Data handling

What a check sends where, so you can decide what to put in a request:

  • The model vendor. TypeSafe's Jev model, reached through the Vercel AI Gateway, receives the request's fields (including context) and the provider's own findings. Before that, the server redacts private keys, seed phrases and x402check credit tokens from context. Nothing else is redacted. We have not yet confirmed the vendor's retention terms, so leave secrets and personal data out of context.
  • Public RPC operators and Blockscout receive the addresses and, for a simulation, the transaction.
  • Cloudflare runs the Worker. Workers Logs keep the Worker's own log lines, which by design carry no request bodies or tokens, and Cloudflare's invocation log of each request (method, URL and request metadata).
  • x402check's KV keeps a record of each settlement (network, transaction, route) for 400 days, for reconciliation. Credit tokens are stored only as their SHA-256.
  • Attestations carry what the verdict covers (sub, payment, interaction, aud) and hashes of the request (input_hash, request_hash), not the context itself.

Data sources and licenses

Code is MIT. Data sources:

  • OFAC SDN (U.S. Treasury);
  • MetaMask eth-phishing-detect (DBAD-1.2, embedded as a derived hash set, attributed);
  • ScamSniffer scam-database (GPL-3.0, runtime only: never committed or bundled);
  • Forta labelled datasets (MIT, derived code fingerprints);
  • Blockscout and public JSON-RPC endpoints.

See THIRD_PARTY_NOTICES.md. OFAC and MetaMask are refreshed daily by .github/workflows/feeds.yml and swapped in at runtime after verification. /status shows the versions in use. Manual refresh: npm run ofac:update, npm run feeds:update.

Roadmap

The full roadmap is in docs/STRATEGY.md.

  1. Shadow facilitator traffic continuously. The first week of PayAI-settled payments is replayed (v0.4), and a weekly report and an inline, log-only integration are offered to PayAI.
  2. Measure the kit watch's lead time over the public lists as their listings arrive (they lag 7 days), and extend it to factory deployments (CREATE2 inside a contract) and to more chains.
  3. Fresher address intelligence: funding-source analytics for plain transfers to wallets the watch has not seen.
  4. Valuation-aware simulation rules (price data), closing the "return a dust asset" evasion.
  5. Batch and upto payment schemes, to spread settlement gas when facilitators stop sponsoring it.
  6. Wallet-side payment for the Snap, then publish it and request MetaMask allowlisting.
  7. KMS/HSM custody for the attestation key. (Rotation with overlapping kids shipped in v0.6.0: a next key is published before it signs.)
  8. Kora decision_provider integration (issue #682); AP2 RiskPayload once upstream stabilizes.

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
npx -y @x402check/mcp

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-caiovicentino-x402check": {
      "command": "npx",
      "args": [
        "-y",
        "@x402check/mcp"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

@x402check/mcpnpm

Compatible MCP Clients

x402check works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More