Preflight safety gate and x402 on-chain truth verification tools for AI agents
Machine-payable on-chain truth & pre-signing checks for AI agents. Free fail-closed preflight gate + five verifiable truth checks paid per call via x402 in USDC. No API key, no account, no wallet required for the free gate.
Verify an x402 payment challenge before signing it:
curl -sS -X POST https://verify402.onrender.com/preflight \
-H 'content-type: application/json' \
-d '{
"kind": "payment",
"payment": {
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xAeEF086DD4e436c69d6d0F684b4c9F43392df9cb",
"resource": "https://verify402.onrender.com/verify"
},
"policy": { "maxAmountUsd": 0.25 }
}'
Returns PASS / CAUTION / BLOCK, typed reasons, exact checked and notChecked lists, pinned block number, and a verifiable 90-second HMAC receipt.
A PASS means the listed checks passed at that block. It does not mean the
transaction is safe, cannot drain funds, or that the token is not a honeypot.
Read notChecked in the response.
The MCP server is live on npm as verify402-mcp and indexed on the official Model Context Protocol Registry (io.github.a32116150-ctrl/verify402-mcp).
It works with Claude Desktop, Cursor, Windsurf, Cline, OpenCode, and any MCP client over stdio.
Gives your AI agent the free preflight_gate tool. It checks payment challenges, token approvals, payee/amount, and transaction calldata at a fresh block before signing. Honeypot/sellability evidence is the paid check_token_safety check, not a free-gate guarantee.
Paste this into your client config file:
~/Library/Application Support/Claude/claude_desktop_config.json (Mac) or %APPDATA%\Claude\claude_desktop_config.json (Windows).cursor/mcp.json or Cursor Settings β MCP~/.codeium/windsurf/mcp_config.jsoncline_mcp_settings.json{
"mcpServers": {
"verify402": {
"command": "npx",
"args": ["-y", "verify402-mcp@latest"],
"env": {
"VERIFY402_URL": "https://verify402.onrender.com"
}
}
}
}
Unlocks the 5 on-chain truth checks (verify_arbitrage, check_token_safety, get_best_route, verify_quote_fairness, get_trade_depth). The agent pays per call via x402 in USDC.
{
"mcpServers": {
"verify402": {
"command": "npx",
"args": ["-y", "verify402-mcp@latest"],
"env": {
"VERIFY402_URL": "https://verify402.onrender.com",
"EVM_PRIVATE_KEY": "0xYOUR_HOT_WALLET_PRIVATE_KEY",
"MAX_PAYMENT_USD": "0.25",
"MAX_DAILY_USD": "1.00",
"VERIFY402_NETWORKS": "eip155:8453",
"VERIFY402_PAY_TO": "0xAeEF086DD4e436c69d6d0F684b4c9F43392df9cb"
}
}
}
}
If you want your agent to use the paid truth tools, follow these 4 steps to set up its wallet safely:
cast wallet new).$0.03 (route), $0.05 (arbitrage/quotecheck/depth), $0.10 (token safety).0x) into the EVM_PRIVATE_KEY field in your MCP config.MAX_PAYMENT_USD (hard cap per call, default $0.25) and MAX_DAILY_USD (daily budget cap) before any signature can be created. This caps what the bridge can authorize; it is not protection against a compromised private key.Once configured, restart your AI client. The agent automatically has access to the tools. You can prompt it naturally:
| What you want to do | Example Prompt to your AI Agent | Tool Called | Cost |
|---|---|---|---|
| Verify a transaction before signing | "Before signing, run preflight_gate on the calldata to check payee, amount, approvals, and block freshness. If it returns BLOCK, stop; a PASS is not a guarantee." | preflight_gate | Free |
| Check measured token safety / sellability | "Run check_token_safety on token 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE and tell me whether it is sellable at a realistic size, what the round-trip cost is, and whether the result is TRUST / CAUTION / AVOID. This is not a code audit." | check_token_safety | $0.10 |
| Verify an arbitrage opportunity | "Use verify_arbitrage to check if the claimed arb between Uniswap V3 and Sushiswap on WETH/USDC is real right now on chain 1." | verify_arbitrage | $0.05 |
| Audit a swap quote | "I was quoted 0.35 WETH for 1,000 USDC. Call verify_quote_fairness to see if this quote is honest or if I'm being overcharged." | verify_quote_fairness | $0.05 |
| Check market depth & slippage | "Check get_trade_depth for WETH on Uniswap V3 to see how much slippage I would take on a $25,000 swap." | get_trade_depth | $0.05 |
| Find true on-chain swap route | "Query get_best_route to find the exact net payout for swapping 2,000 USDC to WETH across all DEX pools." | get_best_route | $0.03 |
/.well-known/verify402.json (Documentation)/openapi.jsonserver.json (Publishing Guide)docs/x402-discovery.md/llms.txtdocs/discovery-audit.mddocs/distribution-directory-checklist.mdHuman Pages: Home Β· Start Β· Preflight Gate Β· MCP Server Β· Checks Β· Live Findings Β· Proof & Receipts Β· Docs Β· Donate
Models hallucinate profit. Most arb "opportunities" an LLM surfaces are stale, ignore fees, or come from a quote that was never checked against the venue that would execute the trade. This service exists to be the check an agent cannot do itself.
Quotes come from the canonical on-chain sources β never our own reimplementation of venue math:
QuoterV2 (its answers are the router's answers by construction)getAmountsOutBoth legs are measured against one block number, so the round trip reflects a single instant rather than two prices that never coexisted.
Three bugs were caught by the test suite before this ever served a paid response, and they are the reason the tests exist:
decodeFunctionResult on getAmountsOut β a
single declared output, which viem unwraps β was indexed with decoded[0],
silently grabbing the array's first element: the input amount. On the sell leg
that turned 41 WETH into 41 trillion USDC of "profit". Now the decoder finds
the uint256[] structurally and refuses a result equal to the input.10_000n instead of
1_000_000n when keeping two decimals of precision.all configured venues exist on-chain is now a test.node test/measure.test.mjs # 5 checks, needs a mainnet RPC
One check is the headline property: a 100k USDC round trip on Ethereum must NOT come out profitable. If it ever does, the engine is wrong until proven otherwise.
npm install
npm test # engine checks (network required)
node src/server.mjs # testnet payment mode
BYPASS_PAYMENT=1 node src/server.mjs # local dev only - loud warning, no billing
X402_MODE=mainnet FACILITATOR_URL=... node src/server.mjs
| Route | Price | What it does |
|---|---|---|
POST /preflight | free | pre-signing gate: validate a payment request or transaction, return PASS / CAUTION / BLOCK + a short-lived receipt |
POST /preflight/verify | free | verify a preflight receipt (HMAC, intent hash, expiry) |
POST /verify | $0.05 (x402) | the measurement, with verdict + evidence |
POST /safety | $0.10 (x402) | pre-trade token check: TRUST / CAUTION / AVOID + evidence; not a code audit |
POST /route | $0.03 (x402) | what would the chain ACTUALLY pay for this swap? best route + every quoted venue |
POST /quotecheck | $0.05 (x402) | audit a quote someone else gave you: fair / overpaying / too_good_to_be_true |
POST /depth | $0.05 (x402) | position sizing: how big can I trade before the round trip eats me? |
GET /findings | free | live no_edge verdicts on Ethereum majors |
GET /info | free | machine-readable service description |
GET /health | free | liveness |
POST /depth is the pre-trade sizing question nothing on-chain answers.
Pool-reserve "liquidity: $2.7M" tells an agent nothing about the block it
will actually trade in. The depth engine buys the target with the best route
at each rung of a USD ladder, immediately sells the exact position back, and
reports per-rung impact %, round-trip cost %, and capacityUsd β the largest
size that stays under the caller's cost tolerance. Live example (SHIB,
2.5% tolerance): "$2.7M liquidity" resolves to a real capacity of $1,000
($100 β 1.20% round trip, $10k β 6.57%). The ladder is the honest answer.
POST /preflight is the free control an agent runs immediately before signing.
It accepts one of two intents:
payTo, resource, timeout, authorization window.chainId, to, data, value, optional from (EVM:
Ethereum 1, Arbitrum 42161, Base 8453).Response: PASS / CAUTION / BLOCK, reasons[], checked[],
notChecked[], and a receipt:
{
"verdict": "CAUTION",
"sign_allowed": true,
"requires_ack": true,
"reasons": [{ "code": "unlimited_approval", "severity": "caution", "message": "..." }],
"checked": ["scheme", "network", "asset", "amount", "payee", "resource", "payer_balance", "block_freshness"],
"notChecked": ["recipient reputation", "sanctions screening", "payment signature / signed payload", "facilitator settlement", "off-chain promises", "counterparty identity / tool metadata", "prompt injection / hidden Unicode in tool output"],
"receipt": {
"spec": "verify402/preflight-receipt@1",
"intentHash": "β¦",
"blockNumber": 48277717,
"blockHash": "0xβ¦",
"expiresAt": "2026-10-19Tβ¦Z",
"signature": "β¦"
}
}
POST /preflight/verify recomputes it.CAUTION; older than 120s is BLOCK. The response carries
block.timestamp and block.ageSeconds. A block with no timestamp is a
503, not a PASS.503 and
no receipt. An executor must refuse to sign without a verifiable receipt.The MCP server exposes it as preflight_gate. It runs with no wallet (free gate
only); add EVM_PRIVATE_KEY for the five paid tools. If sign_allowed is
false, do not sign; if requires_ack is true, surface the reasons to the
operator. Client configs and the install command are at
/mcp.
examples/preflight-guard.mjs is a dependency-free, deny-by-default wrapper:
it calls the gate, refuses BLOCK, requires an explicit acknowledgement for
CAUTION, verifies the receipt, and only then calls your signer. It holds
no key and sends nothing itself. The intent is cloned and deep-frozen before the
gate, and that same object is passed to the signer. If you do not want to trust
the public instance, run your own and pin baseUrl.
POST /verify request:
{ "chainId": 1, "target": "WETH", "base": "USDC", "size": 100000, "minNet": 0 }
Response (abridged):
{
"verdict": "no_edge",
"blockNumber": 26214368,
"buy": { "venue": "uni-v3-0.05%", "amountOutHuman": 41.366 },
"sell": { "venue": "uni-v3-0.05%", "amountOutHuman": 99707.9 },
"gross": { "human": -292.08, "bps": -29.23 },
"net": { "human": -342.14, "bps": -34.23 },
"costs": { "flashLoanPremiumBps": 5, "gasUsd": 0.0561 },
"venues": { "quoted": 12, "buySide": [...], "sellSide": [...], "unavailable": [...] },
"caveats": ["..."]
}
verdict is one of edge (net cleared minNet), no_edge_costs_ate_it (gross
positive, costs larger), or no_edge (unprofitable before costs).
POST /safety request (accepts any ERC-20 address, not just registry symbols):
{ "chainId": 1, "tokenAddress": "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", "intendedUsd": 500 }
Response (abridged):
{
"verdict": "TRUST",
"checked": ["contract", "market", "sellability", "liquidity"],
"notes": ["sellable via uni-v3-0.3% via hop, uniswap-v2 via hop, ...",
"round trip in and out of $500 costs 1.42%"],
"evidence": { "contract": {...}, "market": {...},
"sellability": { "canSell": true, "probedInUsd": 25 },
"liquidity": { "roundTripCostPct": 1.42 } },
"elapsedMs": 1434
}
verdict is one of:
AVOID β positively dangerous: cannot be exited at a realistic size, or the
contract is not a token at allCAUTION β tradable, but with a stated cost or problem (thin pool,
manufactured price, expensive round trip)TRUST β every check passed; still not a promise of profit, only a record
that nothing measured was wrongProbes are USD-sized on purpose: an earlier version probed "half a token" and flagged SHIB β a $1B token β as a honeypot, because half a SHIB is dust no pool can quote. The test suite and live calls caught that before it launched; every future verdict change must be explainable, not silent.
A request without payment gets 402Payment Required with instructions in the
PAYMENT-REQUIRED header; the client pays and retries with proof β that is the
entire x402 flow, and it is what makes the caller a program rather than a person.
See DEPLOY.md β free-tier container deployment, ERC-8004 identity mint
(npm run register, owner's key, on-chain data URI), mainnet facilitator
wiring, and the distribution order.
mcp-bridge/ is packaged as verify402-mcp (stdio). It exposes the free
pre-signing gate (preflight_gate) and the five paid endpoints
(verify_arbitrage, check_token_safety, get_best_route,
verify_quote_fairness, get_trade_depth) to Claude Desktop, Cursor, OpenCode,
or any MCP client.
EVM_PRIVATE_KEY; the server registers only
preflight_gate and nothing can be charged.MAX_PAYMENT_USD (default $0.25)
inside the x402 client before a signature exists. It also requires an
allowlisted EVM network, the verify402 payee, and a known paid resource on the
configured server, and refuses redirects. MAX_DAILY_USD is optional.npx -y verify402-mcp@latest from the public npm registry.
The pinned GitHub release tarball remains an optional fallback:
npx -y --package=https://github.com/a32116150-ctrl/verify402/releases/download/mcp-v1.0.2/verify402-mcp-1.0.2.tgz verify402-mcp.
No npm account is required to run the published package. Client configs, the
exact OpenCode V2 shape, and the release command are at
/mcp. See also
mcp-bridge/README.md.0x11121007bafec47f132763f42e2f58af6ebba7fba9c7ed1b5980d3f595a9324a).x-idempotency-key): send the header on the FIRST call of an
operation, minting ONE key per operation (not per retry β though with double-keying even
fresh ids per retry are protected, see below). If the response is lost after settlement,
retrying returns the SAME verdict without requiring payment again, under two locks:
10 minutes (max 3 replays). Caveats stated plainly: the key is the caller's secret
(whoever presents it replays for free), the window is short by design so the 402 paywall
stays intact, and replayed responses carry recovery: { replayed: true, responseHash, replays }.
onAfterSettle
hook). Promotion is keyed on a server-generated operationId carried in the
response (recovery.operationId, excluded from the projection) and requires a
confirmed settle result with a non-empty transaction reference; the projection hash
of the served bytes is an additional check. Equal output does not identify an
operation β two requests that both resolve to {"verdict":"no_match"} are still
distinct, and settling one never promotes the other. A failed, aborted or pending
settlement promotes nothing. The replay cap (3) is per operation, shared between the
caller key and the content-hash companion key β two aliases are ONE budget, not two.payload.authorization.from), the payer's scope. Presenting the same key with
changed request content or from a different payer returns 409 recovery_conflict
(conflict: "request_mismatch" | "scope_mismatch") before the paywall β never the
other operation's verdict, never a charge.responseHash for 6 hours. A retry within the tombstone window re-measures with
the paywall re-armed (there is no free measure after the window β anything else would be a
loophole), but the response carries recovery: { remeasured: true, previousResponseHash, expiredAt }, so the calling agent can branch on "this operation was charged before and
could not be replayed" instead of guessing.test/recovery.test.mjs includes the review-requested controls
β two staged operations with byte-identical output do not cross-promote; promotion is
refused on failed/pending settle, a missing/empty transaction, or a missing operationId;
same key + changed content or scope is a conflict; a companion requires a matching scope.verify402/evidence-bundle@1): every paid response
carries evidence_bundle: { requestHash, responseHash, pinnedBlock, mode, createdAt, payment.in }. Both hashes are sha256 over canonical JSON (keys sorted recursively, no
whitespace). requestHash covers { route, body }; responseHash covers the response
body excluding evidence_bundle, recovery, cache, elapsedMs. Reference
implementation of the canonical form: src/recovery.mjs (canonicalStable), tests in
test/recovery.test.mjs.MAX_PAYMENT_USD caps one payment at signature time;
MAX_DAILY_USD (optional, mcp-bridge/budget.mjs) caps the whole UTC day across
restarts via a local ledger. Attempted charges are counted before the payment is built β
a spending guard should over-count, never under-count.GET https://verify402.onrender.com/info β prices, endpoints, recovery contract.GET https://verify402.onrender.com/findings β free verdicts on majors; see the honest no_edge baseline.POST /verify with your question ({"chainId":1,"target":"WETH","size":100000}) and no payment β HTTP 402.payment-required response header.mcp-bridge/first-payment.mjs; MCP users: mcp-bridge/).payment header. The verdict arrives with the settlement response.x-idempotency-key: <random-32-hex> to step 3 so lost responses replay free instead of paying twice.evidence_bundle.responseHash must equal sha256(canonical JSON of the body minus evidence_bundle, recovery, cache, elapsedMs).MAX_PAYMENT_USD (per call) and MAX_DAILY_USD (per day) in the bridge .env.evidence_bundle.responseHash β determinism of the measurement. An edge verdict should be raced, not trusted.no_edge β that is the market's fee boundary, not a failure of the tool.Apache License 2.0. See LICENSE and NOTICE.
You may use, modify, and distribute this software, including in commercial agent
systems and bots. If you redistribute it or a product containing it, keep the
copyright notice, the license text, and the NOTICE file. The verify402 name
and logo are not licensed for endorsement.
Source-derived launch command. Check the maintainerβs required arguments and credentials before running:
npx -y verify402-mcpMerge 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.
{
"mcpServers": {
"io-github-a32116150-ctrl-verify402-mcp": {
"command": "npx",
"args": [
"-y",
"verify402-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 referenceverify402 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.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..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.