verify402

Preflight safety gate and x402 on-chain truth verification tools for AI agents

AI & MLJavaScriptv1.0.3

verify402

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.

npm version MCP Server MCP Registry OpenAPI 3.1 x402 v2 License: Apache-2.0


30-Second Quickstart

1. Free Preflight Gate (No wallet, no key)

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.


πŸ€– Using verify402 with AI Agents (Step-by-Step)

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.

Step 1: Add the MCP Server to Your Agent

Option A: Free Mode (Zero Config, No Wallet, No Key)

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:

  • Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json (Mac) or %APPDATA%\Claude\claude_desktop_config.json (Windows)
  • Cursor: .cursor/mcp.json or Cursor Settings β†’ MCP
  • Windsurf: ~/.codeium/windsurf/mcp_config.json
  • Cline: cline_mcp_settings.json
{
  "mcpServers": {
    "verify402": {
      "command": "npx",
      "args": ["-y", "verify402-mcp@latest"],
      "env": {
        "VERIFY402_URL": "https://verify402.onrender.com"
      }
    }
  }
}
Option B: Paid Mode (Unlocks 5 Deep Truth Checks)

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"
      }
    }
  }
}

Step 2: How to Set Up the Agent's Hot Wallet

If you want your agent to use the paid truth tools, follow these 4 steps to set up its wallet safely:

  1. Create a Dedicated Hot Wallet:
    • Generate a brand new EVM address using MetaMask, Rabby, or CLI (cast wallet new).
    • ⚠️ NEVER USE YOUR MAIN WALLET OR COLD VAULT. Treat this wallet like physical petty cash.
  2. Fund with USDC on Base (EIP-155:8453):
    • Send $2 to $10 of USDC on the Base network to your new hot wallet.
    • The tools cost tiny fixed amounts per call: $0.03 (route), $0.05 (arbitrage/quotecheck/depth), $0.10 (token safety).
  3. Fund with Gas (ETH on Base):
    • Send ~$0.50 worth of ETH on Base for token approvals / gas overhead.
  4. Export the Private Key:
    • Copy the wallet's private key (hex string starting with 0x) into the EVM_PRIVATE_KEY field in your MCP config.
    • Client-Side Guardrails: The bridge enforces 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.

Step 3: Prompting the AI Agent to Use the Tools

Once configured, restart your AI client. The agent automatically has access to the tools. You can prompt it naturally:

What you want to doExample Prompt to your AI AgentTool CalledCost
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_gateFree
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

Machine-Readable Discovery

Human Pages: Home Β· Start Β· Preflight Gate Β· MCP Server Β· Checks Β· Live Findings Β· Proof & Receipts Β· Docs Β· Donate


Why an agent would pay for this

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.

The measurement discipline

Quotes come from the canonical on-chain sources β€” never our own reimplementation of venue math:

  • V3: Uniswap's deployed QuoterV2 (its answers are the router's answers by construction)
  • V2: each router's own getAmountsOut

Both 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:

  1. Invented profit (the big one). 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.
  2. Basis points off by 100x. βˆ’292 USDC on a 100,000 USDC round trip rendered as βˆ’0.29 bps instead of βˆ’29. A rounding trick multiplying by 10_000n instead of 1_000_000n when keeping two decimals of precision.
  3. Venue that doesn't exist still gets reported. Started as an unasserted assumption; 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.

Run it

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

Endpoints

RoutePriceWhat it does
POST /preflightfreepre-signing gate: validate a payment request or transaction, return PASS / CAUTION / BLOCK + a short-lived receipt
POST /preflight/verifyfreeverify 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 /findingsfreelive no_edge verdicts on Ethereum majors
GET /infofreemachine-readable service description
GET /healthfreeliveness

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.

The preflight gate

POST /preflight is the free control an agent runs immediately before signing. It accepts one of two intents:

  • payment β€” an x402 402 challenge (EVM or Solana): scheme, CAIP-2 network, asset, raw amount, payTo, resource, timeout, authorization window.
  • transaction β€” 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": "…"
  }
}
  • The receipt is HMAC-SHA256 signed, bound to the canonical normalized intent, a fresh pinned block where the chain is readable, and an expiry (default 90s, max 300s). POST /preflight/verify recomputes it.
  • Fresh block, not just any block: on EVM chains, a latest block older than 30s is 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.
  • Fail-closed: if the chain cannot be read, the endpoint returns 503 and no receipt. An executor must refuse to sign without a verifiable receipt.
  • Not checked, on purpose: recipient reputation, sanctions, contract audits, full balance-delta simulation, MEV, the payment signature/signed payload, gas price/limit chosen by the signer, raw signed-transaction binding, facilitator settlement, counterparty identity/tool metadata, and prompt injection or hidden Unicode in tool output. They are named in every response instead of being implied. A PASS is not a counterparty audit.
  • Free is rate-limited (60/min per IP, 8 concurrent) so it cannot become a public RPC proxy.

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.

Reference executor

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 all
  • CAUTION β€” 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 wrong

Probes 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.

Deploying

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: how AI agents consume this

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.

  • Free gate only: omit EVM_PRIVATE_KEY; the server registers only preflight_gate and nothing can be charged.
  • Paid tools: add a dedicated hot wallet with USDC. The bridge signs the x402 payment only β€” never a trade β€” and enforces 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.
  • Install: 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.

Settlement, response delivery, and recovery (honest contract)

  • Delivery: the verdict is returned inside the settlement response β€” the paid answer arrives together with the payment confirmation (testnet receipt: tx 0x11121007bafec47f132763f42e2f58af6ebba7fba9c7ed1b5980d3f595a9324a).
  • Recovery β€” implemented (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 }.
    • Settlement-gated (fail-closed): a verdict record only becomes replayable after the facilitator confirms the settlement (official x402 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.
    • Key/scope conflicts (rejected before payment or replay): a key is bound to one operation β€” its canonical request content plus, when the rail exposes it (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.
    • Double-keying: the verdict is recorded under the caller's key and an implicit key derived from the canonical request content. A caller that mints a fresh id on each retry still replays free under the content hash. Two distinct asks that canonicalise identically are the same question β€” sharing one verdict inside the window is determinism, not a bug. The companion is a same-caller recovery alias, not a public cache: a companion hit is only returned to a caller whose declared scope matches the scope recorded at staging. Equal content alone is never permission to see another account's result.
    • After the window (typed, not silent): expired verdicts leave a tombstone carrying the previous 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.
    • Conformance controls: 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.
  • Evidence bundle β€” implemented (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.
  • Payment caps (bridge/client): 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.

First paid call, in ten lines

  1. GET https://verify402.onrender.com/info β€” prices, endpoints, recovery contract.
  2. GET https://verify402.onrender.com/findings β€” free verdicts on majors; see the honest no_edge baseline.
  3. POST /verify with your question ({"chainId":1,"target":"WETH","size":100000}) and no payment β†’ HTTP 402.
  4. Read the payment instructions from the payment-required response header.
  5. Build an x402 payment for ≀ the stated price (Node example: mcp-bridge/first-payment.mjs; MCP users: mcp-bridge/).
  6. Re-send the same request with the payment header. The verdict arrives with the settlement response.
  7. On a retry/restart, add x-idempotency-key: <random-32-hex> to step 3 so lost responses replay free instead of paying twice.
  8. Verify the receipt: evidence_bundle.responseHash must equal sha256(canonical JSON of the body minus evidence_bundle, recovery, cache, elapsedMs).
  9. Cap your own wallet: MAX_PAYMENT_USD (per call) and MAX_DAILY_USD (per day) in the bridge .env.
  10. Repeat the call in the same block and compare evidence_bundle.responseHash β€” determinism of the measurement. An edge verdict should be raced, not trusted.

Honesty

  • No profit is promised. The measured answer for mainstream pairs is almost always no_edge β€” that is the market's fee boundary, not a failure of the tool.
  • The service reports what it measured, not what it guarantees.
  • Not financial advice.

License

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.

Installation

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

bash
npx -y verify402-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-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 reference

Package

verify402-mcpnpm

Compatible MCP Clients

verify402 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