ブロックチェーン決済の検証および加盟店テキストのスキャンを行い、エージェントが不正または誤ったトランザクションに署名することを防止します。
別名 runningoffcode/agent-wormhole, wormhole-x402
- スター
- 6
- ツール
- —
- ダウンロード数(30日間)
- —
- 最新バージョン
- 0.8.0
- 最終コミット
- 2026年11月10日
- ライセンス
- Apache-2.0
Agent Wormhole MCPとは
Agent WormholeはMCPサーバーです。 ブロックチェーン決済の検証および加盟店テキストのスキャンを行い、エージェントが不正または誤ったトランザクションに署名することを防止します。.
プロジェクトのREADMEから生成された概要
Agent Wormhole MCP server について
Agent Wormhole は、ブロックチェーンのトランザクションを実行する自律型エージェントにセキュリティレイヤーを提供します。これは、エージェントが技術的には有効であるものの、意図した加盟店や金額と一致しないトランザクションに署名してしまうという、特定の脆弱性に対処するものです。このサーバーはチェックポイントとして機能し、決済ペイロードが提供された見積もり(quote)と完全に一致することを確認することで、攻撃者による資金の転送やコストの膨張を防ぎます。
開発者は、stdio を介したローカルの MCP ツールサーバーとして使用するか、WORMHOLE_API_KEY を使用してホストされた検証器に接続できます。システムは Solana と EVM ネットワークの両方をサポートしています。主に 2 つのツールを提供します。1 つは、ネットワークの見積もりとトランザクションペイロードを比較する verify_payment、もう 1 つは、AI モデルが処理する前に加盟店の説明文内の悪意のある指示を検査する scan_text です。セキュリティを強化するために、WORMHOLE_VERIFY_PUBKEY を設定することで、ホストされた判定が既知の公開鍵に対して暗号学的に検証されるように構成できます。
できること
- 1不正な支払い先の防止AI エージェントが、見積もりされた加盟店ではなく攻撃者にトランザクションを送信しようとしていないか確認する必要があります。
- 2トランザクション金額の膨張検知エージェントが署名しようとしている金額が、加盟店の見積もりに指定された正確な金額と一致することを確認します。
- 3加盟店説明文のサニタイズ加盟店のテキストやエージェントカードをスキャンし、モデルの動作を操作する可能性のある注入された指示を検出します。
- 4ヘッドレスエージェントワークフローの保護トランザクションを承認する人間が介在しない(human in the loop がない)エージェントに対して、セキュリティチェックポイントを提供します。
プロジェクトの README から作成されています。すべてのユースケースは、その引用と照らし合わせて確認されています。
Agent Wormhole MCPのインストール方法
フォーマット検証日 2026年11月10日Cursor に追加Gooseに追加Claude CodeにAgent Wormhole MCPをインストール
$ claude mcp add --env 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' wormhole-x402 -- npx -y wormhole-x402CursorにAgent Wormhole MCPをインストール
~/.cursor/mcp.json
{
"mcpServers": {
"wormhole-x402": {
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}VS CodeにAgent Wormhole MCPをインストール
.vscode/mcp.json
{
"servers": {
"wormhole-x402": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}VS Code CLIにAgent Wormhole MCPをインストール
$ code --add-mcp '{"name":"wormhole-x402","type":"stdio","command":"npx","args":["-y","wormhole-x402"],"env":{"WORMHOLE_API_KEY":"<YOUR_WORMHOLE_API_KEY>"}}'Claude DesktopにAgent Wormhole MCPをインストール
claude_desktop_config.json
{
"mcpServers": {
"wormhole-x402": {
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}CodexにAgent Wormhole MCPをインストール
$ codex mcp add wormhole-x402 --env 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' -- npx -y wormhole-x402Codex config.tomlにAgent Wormhole MCPをインストール
~/.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>"
Gemini CLIにAgent Wormhole MCPをインストール
$ gemini mcp add -s user -e 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' wormhole-x402 npx -- -y wormhole-x402Gemini settings.jsonにAgent Wormhole MCPをインストール
~/.gemini/settings.json
{
"mcpServers": {
"wormhole-x402": {
"command": "npx",
"args": [
"-y",
"wormhole-x402"
],
"env": {
"WORMHOLE_API_KEY": "<YOUR_WORMHOLE_API_KEY>"
}
}
}
}設定
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.
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).
| 名前 | 説明 | 種類 |
|---|---|---|
| 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. | シークレット |
| 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). |
自動チェック
実行日 2026年11月13日完全なレポート →自動チェックに合格しました。これはセキュリティ監査ではありません。コードを読み、サーバーに必要最小限のアクセス権を与えてください。 チェックの仕組み
- ソースリポジトリがリンクされており、アクセス可能合格
- オープンソースライセンス (OSI)合格
- 過去1年以内にコミットまたはリリースあり合格
- パッケージ名が有名なパッケージに酷似していない合格
- リモートエンドポイントがHTTPSを使用未チェック
- 破壊的なツールがラベル付けされている未チェック
- モデルへの隠れた指示がない合格
- パッケージおよびその依存関係に重大な勧告なし合格
README
GitHubで見るwormhole-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のよくある質問
Agent Wormhole MCPをClaude Codeに追加するには
ターミナルでclaude mcp add --env 'WORMHOLE_API_KEY=<YOUR_WORMHOLE_API_KEY>' wormhole-x402 -- npx -y wormhole-x402を実行します。すべてのプロジェクトで利用するには--scope userを追加します。
Agent Wormhole MCPをCursorに追加するには
~/.cursor/mcp.jsonを開き、上記のインストールセクションに示すCursor設定を使ってmcpServersの下にサーバーを追加します。
加盟店の説明文をスキャンするために使用できますか?
はい、scan_text ツールは、モデルが読み取る前に、402ボディ、加盟店リスティング、またはエージェントカード内のインジェクションの形跡をスキャンします。
トランザクションを検証するためにネットワーク接続が必要ですか?
いいえ、RPCやネットワークコールを使用せずにオフラインで判定を行うことができます。
MCP server はホストされた判定をどのように処理しますか?
ローカルの判定と同じルールが適用されます。WORMHOLE_VERIFY_PUBKEY が設定されていない場合、ホストされた許可(allow)は、明示的に許可されていない限り、棄権(abstain)へとダウングレードされます。