블록체인 결제를 검증하고 가맹점 텍스트를 스캔하여 에이전트가 사기성 또는 잘못된 트랜잭션에 서명하는 것을 방지합니다.
다른 이름 runningoffcode/agent-wormhole, wormhole-x402
- 스타
- 6
- Tools
- —
- 다운로드, 30일
- —
- 최신 버전
- 0.8.0
- 마지막 커밋
- 2026년 11월 10일
- 라이선스
- Apache-2.0
Agent Wormhole MCP란?
Agent Wormhole은(는) MCP server입니다. 블록체인 결제를 검증하고 가맹점 텍스트를 스캔하여 에이전트가 사기성 또는 잘못된 트랜잭션에 서명하는 것을 방지합니다.
프로젝트 README에서 생성된 요약
Agent Wormhole MCP server 정보
Agent Wormhole은 블록체인 트랜잭션을 수행하는 자율 에이전트를 위한 보안 계층을 제공합니다. 에이전트가 기술적으로는 유효하지만 의도한 가맹점이나 금액과 일치하지 않는 트랜잭션에 서명할 수 있는 특정 취약점을 해결합니다. 이 server는 결제 페이로드가 제공된 견적과 완벽하게 일치하는지 확인하는 체크포인트 역할을 하여, 공격자가 자금을 가로채거나 비용을 부풀리는 것을 방지합니다.
개발자는 이를 stdio를 통해 로컬 MCP 도구 server로 사용하거나 WORMHOLE_API_KEY를 사용하여 호스팅된 검증기에 연결할 수 있습니다. 이 시스템은 Solana 및 EVM 네트워크를 모두 지원합니다. 주요 도구로는 네트워크 견적과 트랜잭션 페이로드를 비교하는 verify_payment와, 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를 열고 mcpServers 아래에 서버를 추가합니다. 위 설치 섹션에 표시된 Cursor 구성을 사용합니다.
가맹점 설명을 스캔하는 데 사용할 수 있나요?
네, scan_text 도구를 사용하면 모델이 읽기 전에 402 body, 가맹점 목록 또는 에이전트 카드를 스캔하여 인젝션 형태를 찾아냅니다.
트랜잭션 검증을 위해 서버에 네트워크 연결이 필요한가요?
아니요, RPC나 네트워크 호출 없이 오프라인 판정을 수행할 수 있습니다.
MCP server는 호스팅된 판정을 어떻게 처리하나요?
로컬 판정과 동일한 규칙을 적용합니다. WORMHOLE_VERIFY_PUBKEY가 설정되지 않은 경우, 명시적으로 허용되지 않는 한 호스팅된 허용(allow)은 기권(abstain)으로 강등됩니다.