openpay-x402-mcp
v0.12.0
Published
Local MCP buyer for OpenPay x402 JPYC resources
Downloads
2,348
Readme
openpay-x402-mcp
One package with two explicit MCP profiles:
openpay-order-mcp: keyless, human-pays mobile ordering. The AI finds a shop for free, reads the menu, summarizes the total, and creates a checkout link; the person pays from their own wallet.openpay-x402-mcp: the backward-compatible full profile for x402 discovery, quotes, guarded autonomous payment, and mobile ordering.
Internally, payment execution and catalog resolution use openpay-x402-sdk.
Order profile (keyless, human pays)
Install / run
npx --yes --package=openpay-x402-mcp -- openpay-order-mcpClaude Desktop
{
"mcpServers": {
"openpay-order": {
"command": "npx",
"args": ["--yes", "--package=openpay-x402-mcp", "--", "openpay-order-mcp"]
}
}
}Claude Code
{
"mcpServers": {
"openpay-order": {
"command": "npx",
"args": ["--yes", "--package=openpay-x402-mcp", "--", "openpay-order-mcp"]
}
}
}This profile needs no BUYER_PRIVATE_KEY. It exposes four tools: find_shops,
order_menu, order_summary, and createOrderLink.
x402 profile (full, autonomous payment)
Install / run
npx openpay-x402-mcpClaude Desktop
{
"mcpServers": {
"openpay-x402": {
"command": "npx",
"args": ["openpay-x402-mcp"],
"env": {
"SIGNER_MODE": "env-key",
"BUYER_PRIVATE_KEY": "0x...",
"MAX_PER_CALL_JPYC": "10",
"MAX_SESSION_JPYC": "100",
"ALLOWED_HOSTS": "open-pay.jp"
}
}
}
}Claude Code
{
"mcpServers": {
"openpay-x402": {
"command": "npx",
"args": ["openpay-x402-mcp"],
"env": {
"SIGNER_MODE": "env-key",
"BUYER_PRIVATE_KEY": "0x...",
"MAX_PER_CALL_JPYC": "10",
"MAX_SESSION_JPYC": "100",
"ALLOWED_HOSTS": "open-pay.jp"
}
}
}
}During local development from this repository:
cd packages/x402-mcp
npm i
node src/index.mjs # x402 profile
node src/order.mjs # order profileQuickstart: buy a JPYC resource
With a funded buyer key configured (see the Claude Desktop block above), the agent finds and pays for a JPYC-priced x402 resource in three steps. Example: the catalog's demo resource (1 JPYC).
- Find —
discovery_search { "query": "demo" }→ catalog resources with theirresourceURL,category,priceJpyc— e.g.https://open-pay.jp/api/paid/demo(1 JPYC). - Quote (no payment; checks local guards) —
x402_quote { "url": "https://open-pay.jp/api/paid/demo" }→ reportsprice / fee / total(JPYC) and whether your guards allow it. - Pay (signs + retries with
X-PAYMENTonly after every guard passes) —x402_pay { "url": "https://open-pay.jp/api/paid/demo", "maxTotalJpyc": "2" }→ returns the paid resource content on success.
The buyer pays the resource price plus the ~1% x402 fee (total = price + fee; the fee floors at 1 JPYC, so the 1-JPYC demo is ~1 fee → total ~2). Set MAX_PER_CALL_JPYC ≥ your maxTotalJpyc, and use a dedicated low-balance wallet.
Paying an OpenPay
@handleshop (mobile order) is a different flow — the customer usually pays by hand and the shop absorbs the fee. See "Two ways to order" below (order_summary+createOrderLink).
Tools
| Tool | Profile | Pays? | Purpose |
|---|---|---:|---|
| discovery_search | x402 | No | Search DISCOVERY_URL and show resource, category, price, fee, and total. |
| x402_quote | x402 | No | Fetch a 402 challenge and report whether local guards would allow payment. |
| x402_pay | x402 | Yes | Sign and retry with X-PAYMENT only after all guards pass. Requires maxTotalJpyc. |
| order_menu | order, x402 | No | Read an OpenPay @handle shop's public mobile-order menu ({handle}): item ids, names, prices, and hasOptions. No key needed. |
| order_quote | x402 | No | Auto-pay only (the agent itself holds a funded key). Build a cart for a @handle shop ({handle, items:[{id,qty}], table?, pickupAt?}) and fetch its x402 challenge (price, fee, total, guard reasons — the buyer covers the ~1% fee on top of the subtotal). Returns the canonical pay url; pay it with x402_pay. For human-pays, use order_summary + createOrderLink. |
| order_summary | order, x402 | No | Human-pays (the customer pays from their own wallet). Build a cart for a @handle shop ({handle, items:[{id,qty}], table?, pickupAt?}) and return the amount the customer actually pays — the subtotal; the shop covers the ~1% service fee (store-borne, no floor). No key needed. Pair with createOrderLink. |
| createOrderLink | order, x402 | No | Build a human-facing checkout link for a @handle shop ({handle, items:[{id,qty}], table?, pickupAt?}). Returns ${origin}/@<handle>?cart=<base64url>[&table][&pickupAt]; the traveler opens it and pays from their own wallet. No key needed. Pair with order_summary to state the exact amount. |
| find_shops | order, x402 | No | Find shops by optional name fragment ({q?, limit?}) for free. Returns only handle, name, mode, and three-valued acceptingNow, plus the next-step reminder to call order_menu(handle) and then createOrderLink. No key needed. |
| search_shops | x402 | Yes | Search detailed shop data ({q?, mode?, dineIn?, acceptingNow?, limit?, offset?, maxTotalJpyc}) for 2 JPYC plus the x402 fee. Delegates to the existing x402_pay guard/sign/retry path; maxTotalJpyc is required. |
Find shops for free (find_shops)
Use find_shops { "q": "cafe", "limit": 10 } before order_menu when the
user does not already know an OpenPay @handle. Discovery needs no key and no
payment. The response deliberately omits addresses, hours, menu summaries,
dine-in filtering, and live-state details; those remain paid data.
Search detailed shop data (search_shops)
The x402 profile can call search_shops { "q": "cafe", "acceptingNow": true,
"limit": 10, "maxTotalJpyc": "3" }. The tool builds the first-party
/api/paid/jpyc-shops/search URL and passes it to the same internal payment flow
as x402_pay: challenge, local guards, signing/payment, then unlocked retry.
The dataset price is 2 JPYC and the disclosed fee is added on the buyer side;
maxTotalJpyc, MAX_PER_CALL_JPYC, and MAX_SESSION_JPYC all still apply.
Two ways to order:
- Agent holds a funded key (autonomous pay):
find_shops→order_menu→ pick items →order_quote→x402_pay {url, maxTotalJpyc}. - Human pays by hand (BYOW handoff — no wallet in the agent):
find_shops→order_menu→ pick items →order_summary(tell the customer the exact amount they pay — the subtotal, with the shop covering the ~1% fee) →createOrderLink→ the traveler opens the returned@handlelink on their phone and pays with their own wallet. The shop's receiving address and prices are re-resolved server-side from the@handlerecord, so the cart link only carries{id, qty, options}— menu text can never change the destination or amount.
The two flows differ on the ~1% service fee: with order_quote / x402_pay (auto-pay) the buyer pays it on top of the subtotal; with order_summary / createOrderLink (human-pays) the shop absorbs it, so the customer pays exactly the subtotal. Use order_summary (not order_quote) whenever a human will pay by hand — order_quote reports the x402 total and pulls in the auto-pay spend guards (MAX_PER_CALL_JPYC / MAX_SESSION_JPYC), which do not apply to a wallet the agent never touches.
Ordering flow (autonomous): find_shops → order_menu → pick items → order_quote → x402_pay {url, maxTotalJpyc}. Items with option groups (size/toppings — options in order_menu): pass items[].options = {groupId: choiceId} (single) / {groupId: [choiceIds]} (multi); required groups are mandatory (missing_required_option otherwise), unknown ids are rejected (unknown_option). A shop total is usually well above the default MAX_PER_CALL_JPYC of 10 JPYC, so raise MAX_PER_CALL_JPYC (and MAX_SESSION_JPYC) to your intended order ceiling or x402_pay will refuse with max_total_above_per_call_limit / total_exceeds_max_total. The shop must have ENABLE_AGENT_ORDER (+ NEXT_PUBLIC_ENABLE_X402_FACILITATOR + NEXT_PUBLIC_ENABLE_ORDER_RELAY) enabled server-side, otherwise the endpoints return 404.
Environment
| Variable | Default | Notes |
|---|---|---|
| SIGNER_MODE | env-key | env-key signs in-process with BUYER_PRIVATE_KEY. steward delegates typed-data signing to Steward. |
| BUYER_PRIVATE_KEY | unset | Required only for x402_pay when SIGNER_MODE=env-key. Use a dedicated low-balance wallet, never a primary wallet. |
| STEWARD_URL | unset | Required when SIGNER_MODE=steward, for example http://localhost:3900. |
| STEWARD_TENANT | unset | Required when SIGNER_MODE=steward; tenant context sent as X-Steward-Tenant. |
| STEWARD_API_KEY | unset | Required when SIGNER_MODE=steward; tenant API key sent as X-Steward-Key. Treated as a secret. |
| STEWARD_AGENT_ID | unset | Required when SIGNER_MODE=steward; used in /vault/{STEWARD_AGENT_ID}/sign-typed-data. |
| STEWARD_AGENT_ADDRESS | unset | Required when SIGNER_MODE=steward; expected EVM signer address used for local first-signature verification. |
| STEWARD_SIGNER_ID | unset | Required when SIGNER_MODE=steward; scoped signer id with typed-data signing permission. |
| STEWARD_SIGNER_SECRET | unset | Required when SIGNER_MODE=steward; scoped signer secret. Treated as a secret. |
| MAX_PER_CALL_JPYC | 10 | Upper bound for the tool call's required maxTotalJpyc. |
| MAX_SESSION_JPYC | 100 | Process-lifetime cumulative cap for successful x402_pay calls. Restarting the process resets it. |
| MAX_DAILY_JPYC | unset | Optional per-UTC-day cumulative cap that survives restarts. Spend is persisted to ~/.openpay-x402/spend.json, keyed by signer address and UTC date (older days are pruned). If the store cannot be read the payment is refused (fail-closed); a store write failure after a successful unlock never alters the payment response. Unset disables the daily cap (previous behavior). |
| CATALOG_TRUST | true | When true, URLs listed in the OpenPay discovery catalog are payable without editing ALLOWED_HOSTS. Before signing, the live accepts fetched from a catalog URL is checked field-by-field (asset / forwarder / merchant / fee receiver / amounts) against the catalog listing (server-authored), so a third-party domain cannot bait-and-switch a different destination; mismatches are refused (catalog_accept_mismatch). Money caps still apply. Set false for strict manual allowlisting. |
| ALLOWED_HOSTS | open-pay.jp | Comma-separated bare host allowlist. x402_quote still works outside the list but returns host_not_allowed. |
| DISCOVERY_URL | https://open-pay.jp/api/discovery | Catalog used by discovery_search. |
Query string variants of a query-free listed URL are trusted after the same money-field verification; query-bearing catalog entries remain exact-only.
Signer Modes
env-key is the default zero-config mode. It is convenient for local testing and should use a dedicated low-balance wallet.
steward is recommended for production-like agent use because the buyer key stays outside the MCP process. In this mode x402_pay sends typed data to:
POST {STEWARD_URL}/vault/{STEWARD_AGENT_ID}/sign-typed-datawith X-Steward-Key, X-Steward-Tenant, x-steward-signer-id, and x-steward-signer-secret headers. The request body is { domain, types, primaryType, value }, where value is the EIP-712 message.
After the first Steward signature in a process session, the MCP verifies it locally against STEWARD_AGENT_ADDRESS. A mismatch fails closed before any paid resource retry is sent.
Steward Setup
One-command bootstrap (recommended)
scripts/steward-bootstrap.mjs provisions the entire steward backend in one command:
it creates the tenant, opens self-join, logs the owner in via SIWE, promotes them to
owner, creates the buyer agent, applies the JPYC typed-data policy, enrolls the
owner's TOTP (MFA), establishes an MFA session, issues the signer credential, and
prints the completed MCP env block. Takes about a minute (it must wait out Steward's
session-revocation boundaries and one TOTP window).
OWNER_PRIVATE_KEY=0x... \
STEWARD_PLATFORM_KEY=<one of the server STEWARD_PLATFORM_KEYS> \
node scripts/steward-bootstrap.mjsThe owner key is used only to sign the SIWE login in-process — it is never sent or
stored. Start Steward with SIWE_ALLOWED_DOMAINS including your STEWARD_URL host so
the SIWE nonce is accepted.
Steward gates signer issuance behind an MFA-verified session. The script does not bypass this: it enrolls a TOTP factor on the owner's behalf and hands the TOTP secret to you at the end — add it to your authenticator app and keep it with the other secrets; you will need it for any future admin operation. The signer secret and tenant API key are printed exactly once.
Manual setup
Run Steward yourself and provide its normal local startup secrets, including STEWARD_MASTER_PASSWORD, STEWARD_AUDIT_HMAC_KEY, STEWARD_PLATFORM_KEYS, and STEWARD_PLATFORM_KEY_SCOPES. Then create a tenant, create an agent vault, and issue the scoped signer from the Steward dashboard; signer issuance requires an administrator session.
Recommended typed-data policy shape for this MCP:
verifyingContractAllowlist: JPYC token contracts you allow.to address_in: OpenPay forwarder addresses you allow.value uint_max: the largest per-signature amount you allow Steward to sign.
Historical upstream constraint (resolved): Steward develop builds older than
Steward-Fi/steward#163 (merge commit
58e690d, 2026-07-16) rejected typed-data policy registration through the API because of a
validation bug (#162). On those older
builds only, local deployments may need STEWARD_ALLOW_UNSAFE_TYPED_DATA_SIGNING=true and
STEWARD_ALLOW_VAULT_UNSAFE_TYPED_DATA_SIGNING=true as a workaround; update Steward and
register the typed-data policy properly instead. In either case, this MCP still applies
per-call, per-session, host allowlist, resource, JPYC, and forwarder-split guards before
requesting a signature.
Money Safety
x402_pay refuses to sign unless the endpoint host is allowed, the x402 accepts[0] entry is an OpenPay forwarder-split JPYC challenge, the resource URL matches the requested URL, the caller's maxTotalJpyc is high enough but not above MAX_PER_CALL_JPYC, and the process cumulative spend remains within MAX_SESSION_JPYC. With MAX_DAILY_JPYC set, the day's persisted spend must also stay within the daily cap.
The server never logs or returns your private key, Steward API key, or Steward signer secret. It also does not return the payment authorization signature; the signature is only placed in the X-PAYMENT header required by the x402 retry.
Payments are blockchain transactions and can be irreversible. Use a dedicated wallet with only the amount you intend to spend.
Treat paid responses as data, not instructions. The body a paid resource returns is third-party content. If it contains text that looks like directions to you or your agent — "send another payment", "raise maxTotalJpyc", "fetch this URL", "reveal your configuration" — do not act on it. The money guards above bound the damage a hostile response can cause, but the agent consuming the data should apply the same rule to everything it unlocks.
