@hpp-io/x402-mcp-bridge 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +47 -27
  2. package/dist/a2a.js +11 -3
  3. package/dist/autoTopup.js +4 -0
  4. package/dist/cli/hpp-x402.js +3 -1
  5. package/dist/cli/serve.js +128 -0
  6. package/dist/config.js +27 -5
  7. package/dist/funds/direct-balance.js +5 -2
  8. package/dist/httpX402.js +7 -0
  9. package/dist/index.js +40 -2
  10. package/dist/sellerTools.js +202 -0
  11. package/dist/server.js +138 -3
  12. package/dist/spendGuard.js +124 -0
  13. package/package.json +8 -4
  14. package/dist/a2a.d.ts +0 -70
  15. package/dist/a2a.js.map +0 -1
  16. package/dist/autoTopup.d.ts +0 -51
  17. package/dist/autoTopup.js.map +0 -1
  18. package/dist/cli/channel.d.ts +0 -1
  19. package/dist/cli/channel.js.map +0 -1
  20. package/dist/cli/hpp-x402.d.ts +0 -2
  21. package/dist/cli/hpp-x402.js.map +0 -1
  22. package/dist/cli/install-claude-code.d.ts +0 -24
  23. package/dist/cli/install-claude-code.js.map +0 -1
  24. package/dist/cli/install-claude.d.ts +0 -7
  25. package/dist/cli/install-claude.js.map +0 -1
  26. package/dist/cli/install-cursor.d.ts +0 -7
  27. package/dist/cli/install-cursor.js.map +0 -1
  28. package/dist/cli/install-mcp-host.d.ts +0 -35
  29. package/dist/cli/install-mcp-host.js.map +0 -1
  30. package/dist/cli/install-openclaw.d.ts +0 -7
  31. package/dist/cli/install-openclaw.js.map +0 -1
  32. package/dist/cli/install-windsurf.d.ts +0 -7
  33. package/dist/cli/install-windsurf.js.map +0 -1
  34. package/dist/cli/policy.d.ts +0 -1
  35. package/dist/cli/policy.js.map +0 -1
  36. package/dist/cli/revoke.d.ts +0 -1
  37. package/dist/cli/revoke.js.map +0 -1
  38. package/dist/cli/safe.d.ts +0 -14764
  39. package/dist/cli/safe.js.map +0 -1
  40. package/dist/cli/setup.d.ts +0 -1
  41. package/dist/cli/setup.js.map +0 -1
  42. package/dist/client.d.ts +0 -45
  43. package/dist/client.js.map +0 -1
  44. package/dist/config.d.ts +0 -85
  45. package/dist/config.js.map +0 -1
  46. package/dist/discovery.d.ts +0 -43
  47. package/dist/discovery.js.map +0 -1
  48. package/dist/discoveryTools.d.ts +0 -75
  49. package/dist/discoveryTools.js.map +0 -1
  50. package/dist/funds/direct-balance.d.ts +0 -27
  51. package/dist/funds/direct-balance.js.map +0 -1
  52. package/dist/funds.d.ts +0 -26
  53. package/dist/funds.js.map +0 -1
  54. package/dist/httpX402.d.ts +0 -45
  55. package/dist/httpX402.js.map +0 -1
  56. package/dist/index.d.ts +0 -1
  57. package/dist/index.js.map +0 -1
  58. package/dist/keychain.d.ts +0 -23
  59. package/dist/keychain.js.map +0 -1
  60. package/dist/log.d.ts +0 -16
  61. package/dist/log.js.map +0 -1
  62. package/dist/policy.d.ts +0 -73
  63. package/dist/policy.js.map +0 -1
  64. package/dist/server.d.ts +0 -24
  65. package/dist/server.js.map +0 -1
  66. package/dist/signer.d.ts +0 -45
  67. package/dist/signer.js.map +0 -1
  68. package/dist/signers/raw-eoa.d.ts +0 -12
  69. package/dist/signers/raw-eoa.js.map +0 -1
package/README.md CHANGED
@@ -25,20 +25,35 @@ Two wallet modes:
25
25
  | **light** (default) | fund small = your cap | none (settlement is gasless) | quick start, low value |
26
26
  | **Safe** (governance) | on-chain daily cap (AllowanceModule) | delegate pays top-up gas | treasury, teams |
27
27
 
28
- ## Quick start (light mode, ~2 min)
28
+ ## Install
29
29
 
30
+ Three steps: install the CLI, onboard, fund. The CLI does the host wiring for
31
+ you — no config files to edit.
32
+
33
+ **1. Install the CLI** (puts `hpp-x402` on your PATH):
30
34
  ```bash
31
- # 1. Create a wallet + register the bridge into your host in one shot
32
- npx -y -p @hpp-io/x402-mcp-bridge hpp-x402 setup --install claude
33
- # prints your delegate address + a paste-ready config
35
+ # macOS / Linux
36
+ curl -fsSL https://raw.githubusercontent.com/hpp-io/x402-tools/main/install/install.sh | bash
37
+ # Windows (PowerShell)
38
+ irm https://raw.githubusercontent.com/hpp-io/x402-tools/main/install/install.ps1 | iex
39
+ ```
40
+ Or straight from npm (same result): `npm install -g @hpp-io/x402-mcp-bridge`.
34
41
 
35
- # 2. Fund it send USDC.e to that address on HPP (no native gas needed)
36
- npx -y -p @hpp-io/x402-mcp-bridge hpp-x402 fund
42
+ **2. Onboard**create a wallet and register the bridge into your host:
43
+ ```bash
44
+ hpp-x402 setup --install claude-code # or: claude | cursor | windsurf | openclaw
45
+ ```
46
+ This creates a wallet in your OS keychain and writes the correct MCP config for
47
+ that host (on Claude Code it runs `claude mcp add` for you).
37
48
 
38
- # 3. Restart your host. Ask the agent to discover + call a paid service.
49
+ **3. Fund it** — send USDC.e (on HPP, no native gas needed) to your address:
50
+ ```bash
51
+ hpp-x402 fund # prints your wallet address + current balance
39
52
  ```
40
53
 
41
- `--install` accepts `claude | claude-code | cursor | windsurf | openclaw`.
54
+ Restart your host and chat your agent can now discover and pay for services.
55
+ (You can also just ask it *“what's my wallet address?”* — the `wallet_address`
56
+ tool answers.)
42
57
 
43
58
  ## The `hpp-x402` CLI
44
59
 
@@ -55,6 +70,11 @@ hpp-x402 channel <sub> batch-settlement channels (ls/status/refund)
55
70
  hpp-x402 safe <setup|revoke> Safe (governance) wallet + AllowanceModule
56
71
  ```
57
72
 
73
+ Run the CLI as `hpp-x402 <command>` after the installer above, or without
74
+ installing via `npx -y -p @hpp-io/x402-mcp-bridge hpp-x402 <command>`. The `-p`
75
+ matters: a bare `npx @hpp-io/x402-mcp-bridge` launches the MCP **server**, not
76
+ the CLI.
77
+
58
78
  Common flags: `-a, --account <name>` (keychain slot, default `delegate-default`),
59
79
  `-n, --network <caip2>` (`eip155:181228` Sepolia / `eip155:190415` Mainnet).
60
80
 
@@ -81,6 +101,7 @@ Registered by the bridge on top of any proxied upstream tools:
81
101
 
82
102
  | Tool | What it does |
83
103
  |------|--------------|
104
+ | `wallet_address` | report your own wallet address (to fund it with USDC.e) |
84
105
  | `hpp_discover` | list/search the curated HPP discovery directory (read-only) |
85
106
  | `hpp_call` | call a discovered service by `resourceId`; pays via your wallet |
86
107
  | `x402_http_call` | call any allow-listed x402 HTTP endpoint (auth headers from local policy) |
@@ -92,32 +113,31 @@ Non-custodial throughout: discovery never holds funds or sees your keys.
92
113
 
93
114
  ## Configuration (env)
94
115
 
95
- The host config's `env` block drives the bridge. Light mode omits the Safe vars:
116
+ **All env is optional.** A bare entry boots on HPP Sepolia with an auto-created
117
+ keychain wallet:
118
+
119
+ ```jsonc
120
+ { "mcpServers": { "hpp-x402": {
121
+ "command": "npx", "args": ["-y", "@hpp-io/x402-mcp-bridge"]
122
+ } } }
123
+ ```
124
+
125
+ Set env only to override the defaults — mainnet, a specific key, a proxied
126
+ server, or Safe mode:
96
127
 
97
128
  ```jsonc
98
- // claude_desktop_config.json (generated by `hpp-x402 setup`)
99
- {
100
- "mcpServers": {
101
- "hpp-x402": {
102
- "command": "npx",
103
- "args": ["-y", "@hpp-io/x402-mcp-bridge"],
104
- "env": {
105
- "DELEGATE_PRIVATE_KEY": "keychain://hpp-x402/delegate-default",
106
- "USDCE_ADDRESS": "0x401eCb1D350407f13ba348573E5630B83638E30D",
107
- "HPP_RPC_URL": "https://sepolia.hpp.io",
108
- "HPP_NETWORK": "eip155:181228",
109
- "RESOURCE_SERVER_URL": "https://your-x402-server/mcp/sse" // optional
110
- }
111
- }
112
- }
129
+ "env": {
130
+ "HPP_NETWORK": "eip155:190415", // Mainnet (default: Sepolia)
131
+ "DELEGATE_PRIVATE_KEY": "keychain://hpp-x402/my-wallet", // default: auto-created
132
+ "RESOURCE_SERVER_URL": "https://your-x402-server/mcp/sse" // proxy a seller's MCP tools
113
133
  }
114
134
  ```
115
135
 
116
136
  | Var | Notes |
117
137
  |-----|-------|
118
- | `DELEGATE_PRIVATE_KEY` | `keychain://hpp-x402/<account>` (recommended) or `0x<64 hex>` |
119
- | `USDCE_ADDRESS` | USDC.e token on HPP |
120
- | `HPP_RPC_URL` / `HPP_NETWORK` | chain RPC + CAIP-2 id |
138
+ | `DELEGATE_PRIVATE_KEY` | **optional** — auto-created in the OS keychain if unset. `keychain://hpp-x402/<account>` or `0x<64 hex>` |
139
+ | `HPP_NETWORK` | **optional** CAIP-2 id, default `eip155:181228` (Sepolia); `eip155:190415` = Mainnet |
140
+ | `USDCE_ADDRESS` / `HPP_RPC_URL` | **optional** default per network |
121
141
  | `RESOURCE_SERVER_URL` | optional upstream MCP server to proxy (omit = local tools only) |
122
142
  | `SAFE_ADDRESS` + `ALLOWANCE_MODULE_ADDRESS` | set both = Safe mode; omit both = light mode |
123
143
  | `HPP_X402_DISCOVERY` | `on` (default) / `off` — registers `hpp_discover` / `hpp_call` |
package/dist/a2a.js CHANGED
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { x402Client } from "@x402/core/client";
20
20
  import { ExactEvmScheme } from "@x402/evm/exact/client";
21
+ import { checkWalletSpend, recordWalletSpend } from "./spendGuard.js";
21
22
  import { log } from "./log.js";
22
23
  /**
23
24
  * Pick the single `accept` entry the bridge will actually pay against:
@@ -153,11 +154,16 @@ export async function payA2aAgent(deps, args) {
153
154
  // Reconstruct a single-accept PaymentRequired for the SDK so its
154
155
  // internal selection cannot diverge from what we just chose.
155
156
  const narrowedRequired = { ...required, accepts: [accept] };
156
- // 2. Spend-cap: ensure the delegate holds enough USDC.e (same gate as the
157
- // MCP/HTTP path). Skipped when no funds source is wired.
157
+ const requiredAtomic = BigInt(accept.amount ?? "0");
158
+ // 2a. Wallet-wide guard (per-call + daily ledger) — the same brake as the
159
+ // HTTP/MCP path, so A2A payments are capped too (previously uncapped).
160
+ const walletDeny = checkWalletSpend(requiredAtomic);
161
+ if (walletDeny)
162
+ return errorResult(`blocked: ${walletDeny}`);
163
+ // 2b. Spend-cap: ensure the delegate holds enough USDC.e (Safe autoTopup or
164
+ // light-mode balance check). Skipped when no funds source is wired.
158
165
  if (deps.funds) {
159
166
  try {
160
- const requiredAtomic = BigInt(accept.amount ?? "0");
161
167
  await deps.funds.ensure(requiredAtomic);
162
168
  }
163
169
  catch (err) {
@@ -190,6 +196,8 @@ export async function payA2aAgent(deps, args) {
190
196
  return errorResult(`A2A payment did not complete (state="${finalState}", payment="${payStatus}"): ` +
191
197
  JSON.stringify(task?.status?.message ?? task?.metadata ?? {}).slice(0, 300));
192
198
  }
199
+ // Record the successful spend against the wallet-wide daily ledger.
200
+ recordWalletSpend(requiredAtomic);
193
201
  return {
194
202
  content: [{ type: "text", text: JSON.stringify({ payment: payStatus, ...extractResult(task) }) }],
195
203
  };
package/dist/autoTopup.js CHANGED
@@ -114,6 +114,10 @@ export class AutoTopup {
114
114
  }
115
115
  return this.execute(amount);
116
116
  }
117
+ /** Funds interface: the delegate's immediately-spendable balance. */
118
+ async balance() {
119
+ return this.balanceOfDelegate();
120
+ }
117
121
  /** Read delegate's current ERC-20 balance. */
118
122
  async balanceOfDelegate() {
119
123
  return (await this.publicClient.readContract({
@@ -95,7 +95,7 @@ const program = new Command();
95
95
  program
96
96
  .name("hpp-x402")
97
97
  .description("HPP x402 agent wallet + payment CLI")
98
- .version("0.0.3");
98
+ .version("0.1.1");
99
99
  // ── setup (one-command onboarding) ─────────────────────────────────────
100
100
  program
101
101
  .command("setup")
@@ -334,6 +334,7 @@ program
334
334
  program.command("policy").description("manage the x402_http_call host policy (show/set/list/unset/defaults)").allowUnknownOption();
335
335
  program.command("channel").description("inspect/manage batch-settlement channels (ls/status/refund)").allowUnknownOption();
336
336
  program.command("safe").description("Safe (governance) wallet: setup | revoke").allowUnknownOption();
337
+ program.command("serve").description("run a lightweight x402 seller — a paid HTTP endpoint (--pay-to, --handler)").allowUnknownOption();
337
338
  // Passthrough groups delegate to the existing scripts, which parse process.argv
338
339
  // themselves. Intercept BEFORE commander so their flags aren't consumed, and
339
340
  // rewrite argv so each script sees its sub-args starting at index 2.
@@ -345,6 +346,7 @@ async function runPassthrough(modPath, label, subArgs) {
345
346
  const PASSTHROUGH = {
346
347
  policy: { mod: "./policy.js", label: "hpp-x402-policy" },
347
348
  channel: { mod: "./channel.js", label: "hpp-x402-channel" },
349
+ serve: { mod: "./serve.js", label: "hpp-x402-serve" },
348
350
  };
349
351
  const sub = process.argv[2];
350
352
  if (sub && sub in PASSTHROUGH) {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * hpp-x402 serve — lightweight generic x402 seller (A2 Phase 2).
3
+ *
4
+ * Turns any agent capability into a paid HTTP endpoint. express +
5
+ * @x402/express `paymentMiddleware` run the whole serve-then-settle x402 flow
6
+ * (build requirements → 402 → verify → settle) against our facilitator; the
7
+ * route handler just does the work — either **echo** (default, for testing) or
8
+ * forward the request body to a configured **webhook** (`--handler <url>`).
9
+ * The Noosphere on-chain compute backend is intentionally NOT used — the
10
+ * handler is generic (SELLER_DESIGN layer 2).
11
+ *
12
+ * Usage:
13
+ * hpp-x402 serve --pay-to 0x... [--port 4030] [--path /paid/echo]
14
+ * [--price 10000] [--network eip155:181228] [--asset 0x...]
15
+ * [--facilitator-url https://facilitator-sepolia.hpp.io]
16
+ * [--handler https://my-agent/handle] [--description "..."]
17
+ */
18
+ import express from "express";
19
+ import { paymentMiddleware, x402ResourceServer } from "@x402/express";
20
+ import { ExactEvmScheme } from "@x402/evm/exact/server";
21
+ import { HTTPFacilitatorClient } from "@x402/core/server";
22
+ import { declareDiscoveryExtension } from "@x402/extensions/bazaar";
23
+ const DEFAULT_USDCE = "0x401eCb1D350407f13ba348573E5630B83638E30D";
24
+ const DEFAULT_FACILITATOR = "https://facilitator-sepolia.hpp.io";
25
+ // EIP-712 domain of HPP USDC.e — buyers need (name, version) in the advertised
26
+ // requirements to sign the EIP-3009 authorization. Override via --domain-name /
27
+ // --domain-version for a different asset.
28
+ const DEFAULT_DOMAIN_NAME = "Bridged USDC";
29
+ const DEFAULT_DOMAIN_VERSION = "2";
30
+ function parseArgs(argv) {
31
+ const out = {};
32
+ for (let i = 2; i < argv.length; i++) {
33
+ const a = argv[i];
34
+ if (!a.startsWith("--"))
35
+ continue;
36
+ const k = a.slice(2);
37
+ const next = argv[i + 1];
38
+ if (!next || next.startsWith("--"))
39
+ out[k] = true;
40
+ else {
41
+ out[k] = next;
42
+ i++;
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ export async function run() {
48
+ const a = parseArgs(process.argv);
49
+ const payTo = a["pay-to"];
50
+ if (!payTo)
51
+ throw new Error("--pay-to <address> required (where payments land)");
52
+ const port = Number(a.port ?? 4030);
53
+ const path = a.path ?? "/paid/echo";
54
+ const price = String(a.price ?? "10000");
55
+ const network = (a.network ?? "eip155:181228");
56
+ const asset = a.asset ?? DEFAULT_USDCE;
57
+ const facilitatorUrl = a["facilitator-url"] ?? DEFAULT_FACILITATOR;
58
+ const handlerUrl = a.handler;
59
+ const description = a.description ?? "hpp-x402 paid endpoint";
60
+ const domainName = a["domain-name"] ?? DEFAULT_DOMAIN_NAME;
61
+ const domainVersion = a["domain-version"] ?? DEFAULT_DOMAIN_VERSION;
62
+ // Advertise discovery metadata by default so the facilitator auto-indexes
63
+ // this seller into x402-discovery after the first settlement. --private opts
64
+ // out (unlisted endpoint).
65
+ const discoverable = a.private !== true;
66
+ // SDK server-side scheme builds correct requirements (incl. exact EIP-712
67
+ // domain) + drives verify/settle via the facilitator.
68
+ const facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
69
+ const resourceServer = new x402ResourceServer([facilitator]);
70
+ resourceServer.register(network, new ExactEvmScheme());
71
+ // Bazaar discovery extension — the facilitator picks it up at settlement and
72
+ // forwards it to the discovery indexer, so this seller becomes findable via
73
+ // hpp_discover after its first sale (no explicit register needed).
74
+ const discovery = discoverable
75
+ ? declareDiscoveryExtension({
76
+ bodyType: "json",
77
+ input: {},
78
+ inputSchema: { type: "object", additionalProperties: true },
79
+ output: { example: { ok: true } },
80
+ })
81
+ : {};
82
+ const routes = {
83
+ [`POST ${path}`]: {
84
+ accepts: [
85
+ {
86
+ scheme: "exact",
87
+ network,
88
+ payTo,
89
+ price: { amount: price, asset, extra: { name: domainName, version: domainVersion } },
90
+ maxTimeoutSeconds: 600,
91
+ },
92
+ ],
93
+ description,
94
+ extensions: { ...discovery },
95
+ },
96
+ };
97
+ const app = express();
98
+ app.use(express.json());
99
+ // Serve-then-settle: verifies X-PAYMENT, runs the handler, settles on success.
100
+ app.use(paymentMiddleware(routes, resourceServer));
101
+ app.post(path, async (req, res) => {
102
+ try {
103
+ if (handlerUrl) {
104
+ const r = await fetch(handlerUrl, {
105
+ method: "POST",
106
+ headers: { "content-type": "application/json" },
107
+ body: JSON.stringify(req.body ?? {}),
108
+ });
109
+ const text = await r.text();
110
+ res.status(r.status).type(r.headers.get("content-type") ?? "application/json").send(text);
111
+ }
112
+ else {
113
+ res.json({ ok: true, echo: req.body ?? null, served: "hpp-x402-serve" });
114
+ }
115
+ }
116
+ catch (err) {
117
+ res.status(502).json({ error: `handler failed: ${err.message}` });
118
+ }
119
+ });
120
+ app.get("/healthz", (_req, res) => {
121
+ res.json({ ok: true });
122
+ });
123
+ app.listen(port, () => {
124
+ process.stderr.write(`hpp-x402 serve: POST ${path} @ :${port} price=${price} payTo=${payTo}\n` +
125
+ ` network=${network} facilitator=${facilitatorUrl} handler=${handlerUrl ?? "echo"}\n`);
126
+ });
127
+ }
128
+ //# sourceMappingURL=serve.js.map
package/dist/config.js CHANGED
@@ -8,6 +8,11 @@ import { z } from "zod";
8
8
  const HEX_ADDR = /^0x[0-9a-fA-F]{40}$/;
9
9
  const HEX_KEY = /^0x[0-9a-fA-F]{64}$/;
10
10
  const KEYCHAIN_URI = /^keychain:\/\/[^\/]+\/[a-zA-Z0-9._-]+$/;
11
+ // Per-network defaults so USDCE_ADDRESS / HPP_RPC_URL can be omitted (zero-config).
12
+ const NETWORK_DEFAULTS = {
13
+ "eip155:181228": { rpc: "https://sepolia.hpp.io", usdc: "0x401eCb1D350407f13ba348573E5630B83638E30D" },
14
+ "eip155:190415": { rpc: "https://mainnet.hpp.io", usdc: "0x401eCb1D350407f13ba348573E5630B83638E30D" },
15
+ };
11
16
  const Schema = z.object({
12
17
  // Delegate EOA — signs EIP-3009 payments + executeAllowanceTransfer.
13
18
  // Accepted forms:
@@ -22,17 +27,19 @@ const Schema = z.object({
22
27
  SAFE_ADDRESS: z.string().regex(HEX_ADDR).optional(),
23
28
  // Deployed AllowanceModule (Safe mode only; pairs with SAFE_ADDRESS).
24
29
  ALLOWANCE_MODULE_ADDRESS: z.string().regex(HEX_ADDR).optional(),
25
- // Asset paid in (USDC.e on HPP).
26
- USDCE_ADDRESS: z.string().regex(HEX_ADDR),
30
+ // Asset paid in (USDC.e on HPP). Optional — defaults per network (below).
31
+ USDCE_ADDRESS: z.string().regex(HEX_ADDR).optional(),
27
32
  // Remote MCP server we proxy to (the seller). Optional: when unset the
28
33
  // bridge runs in local-tools-only mode (e.g. credit top-up / A2A) and skips
29
34
  // the upstream connection entirely.
30
35
  RESOURCE_SERVER_URL: z.string().url().optional(),
31
- // Chain RPC.
32
- HPP_RPC_URL: z.string().url(),
36
+ // Chain RPC. Optional — defaults per network (below).
37
+ HPP_RPC_URL: z.string().url().optional(),
33
38
  // CAIP-2 network identifier used by @x402/evm.
34
39
  // e.g. "eip155:181228" (HPP Sepolia) / "eip155:190415" (HPP Mainnet).
35
- HPP_NETWORK: z.string().regex(/^eip155:\d+$/),
40
+ // Optional — defaults to HPP Sepolia so a bare `npx @hpp-io/x402-mcp-bridge`
41
+ // boots with zero config (see NETWORK_DEFAULTS + runBridge auto-wallet).
42
+ HPP_NETWORK: z.string().regex(/^eip155:\d+$/).optional().default("eip155:181228"),
36
43
  // Optional knobs ------------------------------------------------------
37
44
  // How much to pull on each topup, in atomic USDC.e units. If unset,
38
45
  // the bridge tops up "just enough" for the upcoming payment + small
@@ -61,6 +68,15 @@ const Schema = z.object({
61
68
  .url()
62
69
  .optional()
63
70
  .default("https://x402-discovery.hpp.io"),
71
+ // Seller tools (A2 Phase 1). "on" registers seller_* MCP tools so this agent
72
+ // can charge others over x402 (facilitator verify/settle wrappers). Off by
73
+ // default — buyers don't need them.
74
+ HPP_X402_SELLER: z.enum(["on", "off"]).optional().default("off"),
75
+ HPP_X402_FACILITATOR_URL: z
76
+ .string()
77
+ .url()
78
+ .optional()
79
+ .default("https://facilitator-sepolia.hpp.io"),
64
80
  })
65
81
  // Wallet mode is inferred from Safe config presence: both set = Safe mode
66
82
  // (autoTopup from the Safe within an on-chain cap); both omitted = light mode
@@ -88,12 +104,18 @@ export function loadConfig(env = process.env) {
88
104
  const v = parsed.data;
89
105
  const chainId = Number(v.HPP_NETWORK.split(":")[1]);
90
106
  const walletMode = v.SAFE_ADDRESS && v.ALLOWANCE_MODULE_ADDRESS ? "safe" : "light";
107
+ // Zero-config: fill USDC.e + RPC from per-network defaults when omitted.
108
+ const defaults = NETWORK_DEFAULTS[v.HPP_NETWORK] ?? NETWORK_DEFAULTS["eip155:181228"];
91
109
  return {
92
110
  ...v,
111
+ USDCE_ADDRESS: v.USDCE_ADDRESS ?? defaults.usdc,
112
+ HPP_RPC_URL: v.HPP_RPC_URL ?? defaults.rpc,
93
113
  chainId,
94
114
  walletMode,
95
115
  discoveryEnabled: v.HPP_X402_DISCOVERY === "on",
96
116
  discoveryUrl: v.HPP_X402_DISCOVERY_URL,
117
+ sellerEnabled: v.HPP_X402_SELLER === "on",
118
+ facilitatorUrl: v.HPP_X402_FACILITATOR_URL,
97
119
  };
98
120
  }
99
121
  //# sourceMappingURL=config.js.map
@@ -41,13 +41,16 @@ export class DirectBalance {
41
41
  * (sufficient balance) or throws with a funding instruction. It never returns
42
42
  * a tx hash.
43
43
  */
44
- async ensure(requiredAtomic) {
45
- const balance = (await this.publicClient.readContract({
44
+ async balance() {
45
+ return (await this.publicClient.readContract({
46
46
  address: this.token,
47
47
  abi: ERC20_BALANCE_ABI,
48
48
  functionName: "balanceOf",
49
49
  args: [this.delegate],
50
50
  }));
51
+ }
52
+ async ensure(requiredAtomic) {
53
+ const balance = await this.balance();
51
54
  if (balance >= requiredAtomic)
52
55
  return null;
53
56
  throw new Error(`insufficient USDC.e: need ${requiredAtomic}, have ${balance}. ` +
package/dist/httpX402.js CHANGED
@@ -24,6 +24,7 @@ import { x402Client, x402HTTPClient } from "@x402/core/client";
24
24
  import { ExactEvmScheme } from "@x402/evm/exact/client";
25
25
  import { pickExactAccept } from "./a2a.js";
26
26
  import { loadPolicy, checkAccess, checkAmount, resolveCredentials, checkCooldown, recordPaid, acquireHostLock, } from "./policy.js";
27
+ import { checkWalletSpend, recordWalletSpend } from "./spendGuard.js";
27
28
  import { log } from "./log.js";
28
29
  export const X402_HTTP_TOOL = {
29
30
  name: "x402_http_call",
@@ -166,6 +167,11 @@ export async function x402HttpCall(deps, args) {
166
167
  const deny = checkAmount(access.limits, amount);
167
168
  if (deny)
168
169
  return errorResult(`blocked: ${deny}`);
170
+ // wallet-wide guard (per-call + daily ledger) — uniform across all payment
171
+ // tools; blocks a runaway agent before signing.
172
+ const walletDeny = checkWalletSpend(amount);
173
+ if (walletDeny)
174
+ return errorResult(`blocked: ${walletDeny}`);
169
175
  // Ensure the delegate holds enough USDC.e (Safe autoTopup within the
170
176
  // on-chain cap, or a light-mode balance check — same gate either way).
171
177
  if (deps.funds) {
@@ -198,6 +204,7 @@ export async function x402HttpCall(deps, args) {
198
204
  // retries; a failed settle (isError) stays retryable.
199
205
  if (!result.isError) {
200
206
  recordPaid(host, result.content[0].text ?? "");
207
+ recordWalletSpend(amount);
201
208
  }
202
209
  log.info("x402_http_call.done", {
203
210
  host,
package/dist/index.js CHANGED
@@ -14,10 +14,41 @@ import { DirectBalance } from "./funds/direct-balance.js";
14
14
  import { connectUpstream } from "./client.js";
15
15
  import { startBridgeServer } from "./server.js";
16
16
  import { DiscoveryClient } from "./discovery.js";
17
- import { isKeychainURI, resolveKeychain } from "./keychain.js";
17
+ import { isKeychainURI, resolveKeychain, setKeychain, buildKeychainURI, } from "./keychain.js";
18
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
18
19
  const PKG_NAME = "@hpp-io/x402-mcp-bridge";
19
- const PKG_VERSION = "0.0.3"; // mirrors package.json — bump together
20
+ const PKG_VERSION = "0.1.1"; // mirrors package.json — bump together
21
+ const DEFAULT_ACCOUNT = "delegate-default";
22
+ /**
23
+ * Zero-config first run: if no DELEGATE_PRIVATE_KEY is supplied, auto-create a
24
+ * delegate wallet in the OS keychain (idempotent — reused on later runs) and
25
+ * point the env at its keychain URI. Lets a bare
26
+ * `npx -y @hpp-io/x402-mcp-bridge` boot with no config; the user just funds the
27
+ * printed address. Explicitly-set keys are untouched.
28
+ */
29
+ function ensureDelegateKey(env) {
30
+ if (env.DELEGATE_PRIVATE_KEY)
31
+ return;
32
+ const uri = buildKeychainURI(DEFAULT_ACCOUNT);
33
+ let key;
34
+ let created = false;
35
+ try {
36
+ key = resolveKeychain(uri);
37
+ }
38
+ catch {
39
+ key = generatePrivateKey();
40
+ setKeychain(DEFAULT_ACCOUNT, key);
41
+ created = true;
42
+ }
43
+ env.DELEGATE_PRIVATE_KEY = uri;
44
+ const address = privateKeyToAccount(key).address;
45
+ // Written straight to stderr (not the level-gated logger, which isn't
46
+ // configured yet at this point) so the funding address is always visible.
47
+ process.stderr.write(`\nhpp-x402: wallet ${created ? "created" : "ready"} → ${address}\n` +
48
+ ` ▶ Fund it: send USDC.e to this address (no native gas needed).\n\n`);
49
+ }
20
50
  export async function runBridge(env = process.env) {
51
+ ensureDelegateKey(env);
21
52
  const cfg = loadConfig(env);
22
53
  setLogLevel(cfg.LOG_LEVEL);
23
54
  log.info("bridge.start", {
@@ -85,6 +116,12 @@ export async function runBridge(env = process.env) {
85
116
  : undefined;
86
117
  if (discovery)
87
118
  log.info("discovery.enabled", { url: cfg.discoveryUrl });
119
+ // Seller tools — register seller_* when enabled.
120
+ const seller = cfg.sellerEnabled
121
+ ? { network: cfg.HPP_NETWORK, facilitatorUrl: cfg.facilitatorUrl }
122
+ : undefined;
123
+ if (seller)
124
+ log.info("seller.enabled", { facilitator: cfg.facilitatorUrl });
88
125
  await startBridgeServer({
89
126
  upstream,
90
127
  name: PKG_NAME,
@@ -93,6 +130,7 @@ export async function runBridge(env = process.env) {
93
130
  network: cfg.HPP_NETWORK,
94
131
  funds,
95
132
  discovery,
133
+ seller,
96
134
  a2aRpcTimeoutMs,
97
135
  });
98
136
  // Graceful shutdown: when the host closes our stdin (or Claude Desktop