jev-risk-check-provider
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Warn
- process.env — Environment variable access in .github/workflows/feeds.yml
- fs module — File system access in .github/workflows/feeds.yml
- crypto private key — Private key handling in .github/workflows/feeds.yml
- process.env — Environment variable access in demo/agent.ts
- network request — Outbound network request in demo/agent.ts
- process.env — Environment variable access in demo/facilitator.ts
- network request — Outbound network request in demo/facilitator.ts
- process.env — Environment variable access in demo/resource-server.ts
- process.env — Environment variable access in demo/run.ts
- network request — Outbound network request in demo/run.ts
- crypto private key — Private key handling in demo/voucher.ts
- network request — Outbound network request in deploy/fresh-feeds.ts
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
x402check — pre-payment risk checks for AI agents and wallets: OFAC SDN, phishing and drainer feeds, transaction simulation, drainer-kit code fingerprints. Signed ES256 attestations (did:web:x402check.xyz), paid per call via x402. Measured limits published.
x402check — pre-payment risk checks for x402 agents and wallets
LIVE: https://x402check.xyz · did:web:x402check.xyz · $0.001 per evaluation ($0.005 with transaction simulation), paid with x402 · 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;
- 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, and the content in front of it reveal. It does not see laundering patterns or other transaction-graph behaviour, and it cannot flag an unknown drainer address that is simply sent funds. A clean verdict means "none of these checks fired", not "safe". Measured limits are in docs/EVIDENCE.md.
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
| Check | Source | Effect on the verdict |
|---|---|---|
| Sanctioned address | Official OFAC SDN XML: 1,056 digital-currency addresses, dated snapshot | score 0 / critical, deterministic, no model call |
| Known phishing domain | MetaMask eth-phishing-detect (~100k hosts, embedded) | capped at 20 (critical) |
| Known drainer / scam address | ScamSniffer (EVM, runtime KV, 7-day publication lag) | capped at 20 |
| Community-flagged domain | ScamSniffer domain list | capped at 40 only when our own domain analysis corroborates it |
| Look-alike domain | public-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 wallet | on-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 recipient | simulation 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 declared | simulation + payment / the explicit transfer in the calldata |
the named payee receives a different asset, or more, than declared → 40 |
| Assets parked in an unverified contract | simulation + Blockscout source verification | nothing in return, contract source not verified → 55 |
| Drainer-kit code | logic-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 |
| Unverified spender | Blockscout source verification | approval or permit to an unverified contract → 75 and at least medium (review) |
| Injected / manipulated intent | Jev typed questions over context (what the agent acted on) |
model penalties and caps |
| New address | on-chain activity | informational 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 per call with x402, so the call is made through an x402 client, which pays when it gets the 402 and retries.
See the price. An unpaid call returns
402with the accepted mainnet options in thePAYMENT-REQUIREDheader:curl -si -X POST https://x402check.xyz/v1/risk-check -H "Content-Type: application/json" -d '{"wallet":"0x7a3e8f0c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f","chain":"base"}' | head -1 # HTTP/2 402Pay 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
| Field | Required | Rules |
|---|---|---|
wallet |
yes | the subject address: EVM 0x…, base58 (Solana/Tron/BTC…), bech32, cashaddr, or CAIP-10. Anything else → 422 {error, field:"wallet"} |
chain |
no | alias (ethereum, base, solana, …) or CAIP-2 (eip155:8453); enables on-chain facts on supported mainnets |
domain |
no | hostname or http(s) URL (normalized server-side); the site the payment or signature is for |
context |
no | ≤ 4096 chars: what the agent acted on (tool output, page text, instruction). Untrusted by design |
interaction |
no | {type, unlimited?}; type ∈ native_transfer, token_transfer, token_approval, nft_approval, permit_signature, order_signature, message_signature, contract_call |
payment |
no | binds the attestation to a payment: {network, pay_to, amount (base units), asset, resource} |
aud |
no | ≤ 256 chars; copied into the attestation, never shown to the model |
transaction |
no | EVM {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, authorization |
no | caller 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: jev-attest-v1), TTL 1 h. Claims:
| Claim | Meaning |
|---|---|
iss, sub, iat, exp, jti |
issuer did:web:x402check.xyz, the subject wallet, times, unique id |
score, tier, categories |
the verdict and the findings behind it |
checks |
what 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) |
asserted |
what the caller claimed (screening / pre-authorization): not verified |
payment, interaction, aud |
what the verdict was issued for |
input_hash |
SHA-256 over the canonical normalized inputs, sources and question set |
request_hash |
SHA-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. Never trust a key URL carried by a response or an intermediary:
npx tsx scripts/verify-attest.ts <jws> --issuer did:web:x402check.xyz [--aud <url>] [--sub <wallet>]
The verifier resolves the key from the issuer's did:web document and checks alg, typ, iss, exp and iat (plus aud and sub when given).
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);
| Tier | UX |
|---|---|
low |
no warning; optional "checked" badge |
medium |
amber: "Some signals suggest caution" |
high |
red: "We recommend you do not proceed" |
critical |
hard block with override; show the categories and evidence |
Payments
Every evaluation is paid; there is no free tier.
- Price: $0.001 per evaluation ($0.002 on Solana), and $0.005 when the request includes a
transactionthat is simulated. 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. - 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. - Facilitators: EVM payments settle through PayAI, and Solana and Monad through Dexter. Dexter's published gas-cost floors are above $0.001 on Base, Polygon, Arbitrum and Avalanche.
/statusshows each network's facilitator and floor against the price. - An unpaid request gets
402with the accepted options inPAYMENT-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).
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: "$0.05" }); // a batch of 25 costs $0.025
const x402check = createClient({ fetch: wrapFetchWithPayment(fetch, payer) });
const verdict = await x402check.check({ wallet: "0x…", chain: "base" });
Architecture
request ─► validate (422 names the field) ─► 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 ─► 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.3)
| What | Result |
|---|---|
| OFAC SDN addresses (external labels) | 24/24 critical |
| MetaMask-listed phishing domains · ScamSniffer drainer addresses | 40/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 block | 18/25 flagged (72%), all as hidden recipients |
| Simulation: real transactions to 19 well-known contracts | 0/84 flagged |
| Code fingerprints: listed drainer contracts matched by earlier kits' code, at creation time | 43/100 |
| Code fingerprints: legitimate contracts (latest blocks + CoinGecko tokens), following delegations and proxies | 0/9,625 matched |
| Plain transfers to unlisted drainers | 0/30: not detectable from the address alone |
| Unlisted phishing domains without a feed | 0–4/60 across four samples: feeds do the heavy lifting |
| Well-known contracts and top dApp domains | 0 false positives (0/22, 0/40) |
| Tranco top 200k, deterministic rules | 22 capped (0.011%): 20 on MetaMask's own list, 2 crypto look-alikes |
| Risky cases with an attacker-written context | 20/100 (only look-alike domains) |
| Injected instructions passed as raw agent content | 40/40 |
| Production, every evaluation paid and settled in USDC on Base | 53/53 correct and 53/53 attestations verified · security:v2 12/12 · security:v3 8/8 |
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 # :8787 local dev server, no paywall (or TYPESAFE_API_KEY=...)
curl localhost:8787/.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/clientis a typed TypeScript client with zero runtime dependencies. It runs on Node ≥ 20, browsers, Cloudflare Workers, Deno and Bun.verifyAttestationchecks the signature against the issuer'sdid:webkey and binds it to the request you made, includingrequest_hash.interpretapplies the fail-closed policy. Its verdicts come from the signed claims only, never from the unsigned body.
@x402check/mcpis an MCP server for any agent (Claude Code, Claude Desktop, other MCP clients), with the toolsx402check_check,x402check_verify_attestationandx402check_methodology.- Every verdict is verified before the agent sees an action.
- It pays each check itself via x402 (USDC on Base, gasless for the payer), with a per-payment cap and a total budget.
# once published to npm; use 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 ready to publish, but not yet published.
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.
Checks are paid per call and a Snap 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_signmessages, 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 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
- Shadow real x402 facilitator traffic, moving the evidence from curated corpora to live flows.
- Proprietary drainer intelligence: scan new contract deployments for known drainer-kit code before any public list names them.
- Fresher address intelligence: real-time drainer feeds and funding-source analytics for plain transfers; EIP-7702 sweeper detection.
- Valuation-aware simulation rules (price data), closing the "return a dust asset" evasion.
- Batch and
uptopayment schemes, to spread settlement gas when facilitators stop sponsoring it. - Wallet-side payment for the Snap, then publish it and request MetaMask allowlisting.
- KMS/HSM custody for the attestation key; key rotation with overlapping
kids. - Kora
decision_providerintegration (issue #682); AP2RiskPayloadonce upstream stabilizes.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found
