Verifies blockchain payments and scans merchant text to prevent agents from signing fraudulent or incorrect transactions.
Also known as runningoffcode/agent-wormhole, wormhole-x402
- Stars
- 6
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- 0.8.0
- Last commit
- Nov 10, 2026
- License
- Apache-2.0
What is Agent Wormhole MCP?
Agent Wormhole verifies blockchain payments and scans merchant text to prevent agents from signing fraudulent or incorrect transactions.
Summary generated from the project's README
About Agent Wormhole MCP server
Agent Wormhole provides a security layer for autonomous agents performing blockchain transactions. It addresses a specific vulnerability where an agent might sign a transaction that is technically valid but does not match the intended merchant or amount. The server functions as a checkpoint to ensure that the payment payload aligns perfectly with a provided quote, preventing attackers from redirecting funds or inflating costs.
Developers can use this as a local MCP tool server via stdio or connect to a hosted verifier using a WORMHOLE_API_KEY. The system supports both Solana and EVM networks. It offers two primary tools: verify_payment, which compares a network quote against a transaction payload, and scan_text, which inspects merchant descriptions for malicious instructions before an AI model processes them. For enhanced security, users can configure a WORMHOLE_VERIFY_PUBKEY to ensure hosted verdicts are cryptographically verified against a known public key.
What you can do with it
- 1Prevent fraudulent payment destinationsAn AI agent should check if a transaction is being sent to an attacker instead of the quoted merchant.
- 2Detect inflated transaction amountsVerify that the amount the agent is about to sign matches the exact amount specified in the merchant quote.
- 3Sanitize merchant descriptionsScan merchant text and agent cards for injected instructions that might manipulate the model's behavior.
- 4Secure headless agent workflowsProvide a security checkpoint for agents that lack a human in the loop to approve transactions.
Written from the project's README. Every use case is checked against a quote from it.
How to install Agent Wormhole MCP
Formats verified 10 Nov 2026Add to CursorAdd to GooseInstall Agent Wormhole MCP in Claude Code
$ claude mcp add --env 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' wormhole-x402 -- npx -y wormhole-x402Install Agent Wormhole MCP in Cursor
~/.cursor/mcp.json
{
"mcpServers": {
"wormhole-x402": {
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}Install Agent Wormhole MCP in VS Code
.vscode/mcp.json
{
"servers": {
"wormhole-x402": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}Install Agent Wormhole MCP in VS Code CLI
$ code --add-mcp '{"name":"wormhole-x402","type":"stdio","command":"npx","args":["-y","wormhole-x402"],"env":{"WORMHOLE_API_KEY":"<YOUR_WORMHOLE_API_KEY>"}}'Install Agent Wormhole MCP in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"wormhole-x402": {
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}Install Agent Wormhole MCP in Codex
$ codex mcp add wormhole-x402 --env 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' -- npx -y wormhole-x402Install Agent Wormhole MCP in Codex config.toml
~/.codex/config.toml
[mcp_servers.wormhole-x402] command = "npx" args = ["-y", "wormhole-x402"] [mcp_servers.wormhole-x402.env] WORMHOLE_API_KEY = "<YOUR_WORMHOLE_API_KEY>"
Install Agent Wormhole MCP in Gemini CLI
$ gemini mcp add -s user -e 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' wormhole-x402 npx -- -y wormhole-x402Install Agent Wormhole MCP in Gemini settings.json
~/.gemini/settings.json
{
"mcpServers": {
"wormhole-x402": {
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}Configuration
WORMHOLE_API_KEYSecretOptional. Hosted-mode key from dashboard.agentwormhole.com: adds URL checks with change history, and the operator's spend policy (budgets, approvals, kill switch) on payments.
WORMHOLE_ADDRESS_LEDGEROptional. Path to the address-provenance ledger written by wormhole-guard's readguard hook (default ~/.wormhole/addresses.jsonl).
WORMHOLE_VERIFY_URLOptional. Override the hosted verifier endpoint (default https://dashboard.agentwormhole.com/api/v1/verify).
| Name | Description | |
|---|---|---|
| WORMHOLE_API_KEY | Optional. Hosted-mode key from dashboard.agentwormhole.com: adds URL checks with change history, and the operator's spend policy (budgets, approvals, kill switch) on payments. | Secret |
| WORMHOLE_ADDRESS_LEDGER | Optional. Path to the address-provenance ledger written by wormhole-guard's readguard hook (default ~/.wormhole/addresses.jsonl). | |
| WORMHOLE_VERIFY_URL | Optional. Override the hosted verifier endpoint (default https://dashboard.agentwormhole.com/api/v1/verify). |
Automated checks
Ran Nov 13, 2026Full report →Passed our automated checks. That is not a security audit: read the code and give the server the narrowest access it needs. How checks work
- Source repository linked and reachablePass
- Open source license (OSI)Pass
- Commit or release in the last yearPass
- Package name not close to a popular packagePass
- Remote endpoint uses HTTPSNot checked
- Destructive tools are labelledNot checked
- No hidden instructions to the modelPass
- No critical advisory in the package or its dependenciesPass
README
View on GitHubwormhole-x402
Integration guide: payment safety, policy, confirmed top-ups and launch attestations.
Your agent is about to sign a payment. Is it the one it was quoted?
ALLOW pays the quoted merchant, quoted amount
REFUSE pays an ATTACKER instead
[X402-001] payment destination is not the account derived from the quote
REFUSE pays the merchant but 900x the amount
[X402-002] payment amount does not match the quoted amount
REFUSE pays correctly + silently approves a delegate
[X402-006] Approve — delegates spending authority over the account
ABSTAIN garbage bytes
could not deserialize — refusing to report it as safe
5 transactions checked in 5ms — no RPC, no network
The gap
Every transaction-security product answers the same question: what will this transaction do? Simulation, asset diffs, address reputation. All useful.
None of it helps an agent, because a payment to an attacker's address simulates perfectly. Correct balances. No revert. Clean verdict. The transaction is entirely valid — it is simply not the one that was asked for.
Nobody answers is this the transaction that was asked for?
In the leading provider's API that question is not merely unimplemented, it is
not expressible. Their Solana scan request carries origin — "DApp domain
proposing these transactions" — and has no field for the agent's instructions
at all. The stack is shaped around a human approving a website's request. A
headless agent has neither a website nor a human.
How it decides
One call, either chain
verify() takes a quote and the payment about to be signed, picks the rail from
the network string, and returns one verdict. It runs the quote-text scan first —
an injected instruction in a merchant's description is refused before its numbers
are ever treated as authoritative — then compares destination, asset and amount.
import { verify } from "wormhole-x402/verify";
const result = await verify(
{
network: "eip155:8453", // or "solana"
quote, // what the merchant's 402 said
payload, // what the agent is about to sign
},
{ quoteProvenance: "caller_asserted", issuedAt: new Date().toISOString() },
);
if (result.decision !== "allow") {
// refuse AND abstain both mean do not sign. "We could not check it" is
// never "it is fine" — that conflation is the bug this library exists for.
throw new Error(result.findings.map((f) => f.code).join(","));
}
Time is an input rather than read from the clock, so a verdict is a pure function of its request and replays identically later.
The other entry points
| Import | What it is |
|---|---|
wormhole-x402/verify |
verify() — the unified check, both rails |
wormhole-x402/client |
guardedPay() / guardedFetch() — one line in front of sign() |
wormhole-x402/receipt |
verifyReceipt() / replayMatches() — check a receipt offline |
wormhole-x402/server |
createVerifyHandler() — host it yourself |
wormhole-x402/metering |
usage accounting and cross-agent correlation |
wormhole-x402/mcp |
the verifier as an MCP tool server — npx wormhole-x402-mcp |
wormhole-x402/provenance |
X402-301 — was this payee introduced by anything legitimate? |
wormhole-x402/delivery |
X402-4xx — did the paid response deliver the quoted resource? |
wormhole-x402/tx |
X402-7xx — is this transaction the one the agent meant to send? |
guardedPay takes the transport as an argument rather than a URL, so the same
call runs against the local verifier or a remote one and the two cannot drift.
A verdict off a socket is a claim, not an answer
guardedPay and guardedFetch require an integrity option. It has no
default, because every default here is either a silent downgrade of your
security or a surprise change to your behaviour:
import { guardedPay } from "wormhole-x402/client";
import { publicKeyFromSpkiBase64 } from "wormhole-x402/receipt";
// The verifier publishes its key at GET <base>/v1/key
const publicKey = publicKeyFromSpkiBase64(publishedKeyBase64);
const res = await guardedPay({
network, quote, payload, transport,
integrity: { mode: "required", publicKey },
});
if (res.allow) await sign(payload);
Under mode: "required" a decisive verdict clears only if a receipt is present,
its signature verifies against your key, the receipt is bound by digest to the
request you actually sent, and the receipt's own decision agrees with the
envelope's. Anything else becomes an abstain — never an allow, and never a
refuse — with integrityFailure naming which check failed. res.verified tells
you whether the cryptography actually ran.
For an in-process transport that never crosses a network, say so explicitly:
integrity: { mode: "trusted_transport", reason: "in-process verify(), no socket" }
verified is then false, because nothing was checked. A verifier running
without a signing key produces abstains under required, which is the honest
answer: an unsigned verdict is one nobody can check.
The MCP server applies the same rule to hosted verdicts. Set
WORMHOLE_VERIFY_PUBKEY to the published key; without it a hosted allow is
downgraded to abstain unless you explicitly accept an unverifiable one with
WORMHOLE_ALLOW_UNSIGNED_HOSTED=1.
Receipts
A decisive verdict carries a receipt: the decision, the rule codes, a coarse amount band and a SHA-256 digest of the request — never the quote text, never the payment bytes. Signed, it is checkable by someone who does not trust you:
import { verifyReceipt, replayMatches } from "wormhole-x402/receipt";
verifyReceipt(receipt, signature, publicKey); // did we really say this?
replayMatches(receipt, request); // is it this exact request?
Neither call touches the network. That is the point of a receipt — an attestation you have to phone someone to check is just their word with extra steps.
Install
Every chain dependency is an optional peer, so you install only the one you
use — and you must install it. The entry point for each chain imports its own SDK
at the top level, so wormhole-x402 alone imports nothing useful:
# Solana
npm install wormhole-x402 @solana/web3.js @solana/spl-token
# EVM
npm install wormhole-x402 viem
# Text scanning only — no chain SDK needed
npm install wormhole-x402
Each entry point is independent. wormhole-x402/evm and
wormhole-x402/quotetext work with no Solana packages present, and
wormhole-x402 works with no viem. Importing an entry point whose SDK is not
installed is a module-resolution error, not a silent no-op.
As an MCP tool server
Agents that cannot be rewired — Claude Code, Cursor, any MCP host — get the
same checkpoint as a tool. Zero extra dependencies: the server is
newline-delimited JSON-RPC over stdio, spoken with Node's own readline.
# full install — verifies payments on both rails
claude mcp add x402-guard -- npx -y -p wormhole-x402 -p @solana/web3.js -p @solana/spl-token -p viem wormhole-x402-mcp
# lightest form — text scanning only; verify_payment abstains with install
# instructions until the chain SDKs for your rail are present
claude mcp add x402-guard -- npx -y wormhole-x402
Two tools:
verify_payment— network, quote, payload (and optionallyexpectedPayer) in;allow/refuse/abstainwith findings out. Anything that is notallowmeans do-not-sign.scan_text— a 402 body, a merchant listing, an agent card, a memo; scanned for injection shapes before the model reads it.
The server always starts and scan_text always works — the verify core
loads lazily, so a missing chain SDK is a per-call abstain naming the install
command, never a startup crash.
Set WORMHOLE_API_KEY and verification runs against the hosted verifier
instead, returning its answer verbatim — including any spend-policy decision
the operator's account enforces there. An unreachable hosted verifier abstains
rather than silently falling back to the local core: the operator configured
hosted mode to get their policy applied, kill switch included. Verdicts carry caller_asserted provenance — the server cannot see
where the quote came from, so its receipts say so rather than implying an
attestation nobody made.
import { guardSigner } from "wormhole-x402";
// `quote` is the PaymentRequirements from the server's HTTP 402 response.
const wallet = guardSigner(myWallet, () => currentQuote);
// Signing now throws unless the transaction matches the quote.
await wallet.signTransaction(tx);
On EVM the payment is a detached EIP-3009 authorization rather than a
transaction, so it has its own wrapper — same guarantee, async because domain
verification can read the chain:
import { guardEvmSigner } from "wormhole-x402/evm";
const signer = guardEvmSigner(myWalletClient, () => currentQuote);
// viem sends this single object; ethers sends the same parts positionally —
// signTypedData(domain, types, value) — and the wrapper recognises both. This
// is the call that CREATES the signature, so there is nothing to recover from
// yet. The wrapper checks the domain against the trusted table for (chainId,
// asset), requires the TransferWithAuthorization struct exactly as verified,
// and compares to, value, the validity window and the nonce to the quote.
await signer.signTypedData({ domain, types, primaryType, message });
The typed-data request is what the wallet signs, so it is the one checked, read where the signer reads it: viem's one request object, the first argument, or ethers' domain, types and value, the first three. A call that could be read either way, or that carries an already-signed payment beside a typed-data request, is refused, with the typed request's own findings when it has any: which one a wallet would sign is the wallet's choice, and one cannot be checked for the other.
Both wrappers are default-deny. Every function property is wrapped, not a
named subset: the ones the wrapper knows how to check are checked, and
everything else is refused rather than handed back. Object properties that
expose signing methods — viem's account, a wallet adapter's inner provider —
are wrapped too, because handing back the raw object is the same hole one level
down.
That means an unmodelled method is unusable rather than unchecked, which is the
trade worth making: "we did not recognise the call so we allowed it" is how
every bypass is written up afterwards. If a method cannot move funds, name it in
allow — an escape hatch with a name on it. Dotted paths work for nested
members (allow: ["account.sign"]).
No quote is a refusal, not a pass. Every optional security parameter with a permissive default ends up unset in production, and then the guard reports green on all traffic and nobody notices.
Or inspect without wrapping:
import { inspectPayment } from "wormhole-x402";
const verdict = inspectPayment(serializedTx, {
payTo: "merchant address from the 402 response",
asset: "token mint",
amount: "1000000", // base units, exact
});
// → { decision: "allow" | "refuse" | "abstain", findings: [...] }
The facilitator flow
Most agents never submit a transaction themselves — they build and partially
sign one (the facilitator pays the fee, which is what makes it feel gasless)
and ship it base64 inside the X-PAYMENT header. The client's key still
touches the bytes exactly once. These are those bytes:
import { inspectPaymentPayload, quoteFromRequirements } from "wormhole-x402";
// `accepts[0]` from the server's 402 response — never model-authored
const quote = quoteFromRequirements(paymentRequired.accepts[0]);
// the X-PAYMENT payload the client is about to sign/ship
const verdict = inspectPaymentPayload(xPaymentHeader, quote);
if (verdict.decision !== "allow") throw new Error(verdict.reason);
EVM (EIP-3009)
On EVM the agent does not sign a transaction — it signs an EIP-712 typed message authorizing the transfer, which a facilitator later submits. The same question applies to those bytes: does the authorization about to be signed match the quote? The EVM verifier lives at a separate entry point so a Solana-only install never pulls an EVM crypto library.
import { inspectAuthorization } from "wormhole-x402/evm";
// quote: { network: "eip155:8453", asset, payTo, amount } from the 402 response
// payload: { signature, authorization: { from, to, value, validAfter, validBefore, nonce } }
const verdict = await inspectAuthorization(quote, payload); // offline, no RPC
if (verdict.decision !== "allow") throw new Error(verdict.reason);
inspectAuthorization recovers the signer from the signature and confirms it is
the stated payer, then compares recipient, amount, token, and chain against the
quote. The EIP-712 domain is built from a curated (chainId, contract) table
of on-chain-verified values — never from the quote's extra, which is
attacker-influenceable. An unknown chain or token abstains rather than guesses.
It refuses standing-authority primitives — an Approve, an EIP-2612
Permit, a Permit2 allowance — that grant a spender ongoing access rather than
making a one-shot payment. This matters on EVM specifically: USDC on Base
implements EIP-2612, so a malicious unbounded Permit validates against the
same domain as a legitimate transfer; the only offline discriminator is the
EIP-712 type. viem is an optional peer dependency (npm install wormhole-x402 viem).
The validity window gets the same treatment as a standing authority, because a
long one is one: an authorization still valid next week can be collected next
week. Before signing (inspectTypedDataRequest, and so guardEvmSigner), the
window may run no longer than the quote's maxTimeoutSeconds plus ten minutes
of clock allowance, and never longer than a day; a quote that names no timeout
gets the day, and one whose timeout cannot be read gets no verdict. Past that,
X402-105 refuses. On a signed payment (inspectAuthorization, verify()), a
window over a day is reported as a medium X402-105 instead: the signature
already exists, and the payer's guard is the place to refuse it. The caller's
clockSkewSeconds widens the other window checks, never these caps.
evmQuoteFromRequirements carries the 402 entry's timeout as a whole number of
seconds, and leaves out any other value, so a converted quote's request digest
is what it was without it.
The one design constraint
Intent must never be something the agent states.
If intent is a field the model fills in, a compromised model fills in both sides of the comparison and validates its own forgery. That is worse than no check at all, because it manufactures confidence at the exact moment funds move irreversibly.
x402 is the case where this works cleanly. The recipient, amount and mint arrive as structured JSON in the server's HTTP 402 response — before the transaction exists, on a channel entirely separate from the model's context. That is a genuinely independent second input.
And the comparison is pure arithmetic: the destination token account derives
deterministically from (recipient, mint). No RPC required.
Agent Wormhole MCP questions
How do I add Agent Wormhole MCP to Claude Code?
Run claude mcp add --env 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' wormhole-x402 -- npx -y wormhole-x402 in your terminal. Add --scope user to make it available in every project.
How do I add Agent Wormhole MCP to Cursor?
Open ~/.cursor/mcp.json and add the server under mcpServers, using the Cursor config shown in the install section above.
Can I use this to scan merchant descriptions?
Yes, the scan_text tool scans 402 bodies, merchant listings, or agent cards for injection shapes before the model reads them.
Does the server require a network connection to verify transactions?
No, it can perform offline verdicts without RPC or network calls.
How does the MCP server handle hosted verdicts?
It applies the same rules as local verdicts; if WORMHOLE_VERIFY_PUBKEY is not set, a hosted allow is downgraded to abstain unless explicitly allowed.