@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.
- package/README.md +47 -27
- package/dist/a2a.js +11 -3
- package/dist/autoTopup.js +4 -0
- package/dist/cli/hpp-x402.js +3 -1
- package/dist/cli/serve.js +128 -0
- package/dist/config.js +27 -5
- package/dist/funds/direct-balance.js +5 -2
- package/dist/httpX402.js +7 -0
- package/dist/index.js +40 -2
- package/dist/sellerTools.js +202 -0
- package/dist/server.js +138 -3
- package/dist/spendGuard.js +124 -0
- package/package.json +8 -4
- package/dist/a2a.d.ts +0 -70
- package/dist/a2a.js.map +0 -1
- package/dist/autoTopup.d.ts +0 -51
- package/dist/autoTopup.js.map +0 -1
- package/dist/cli/channel.d.ts +0 -1
- package/dist/cli/channel.js.map +0 -1
- package/dist/cli/hpp-x402.d.ts +0 -2
- package/dist/cli/hpp-x402.js.map +0 -1
- package/dist/cli/install-claude-code.d.ts +0 -24
- package/dist/cli/install-claude-code.js.map +0 -1
- package/dist/cli/install-claude.d.ts +0 -7
- package/dist/cli/install-claude.js.map +0 -1
- package/dist/cli/install-cursor.d.ts +0 -7
- package/dist/cli/install-cursor.js.map +0 -1
- package/dist/cli/install-mcp-host.d.ts +0 -35
- package/dist/cli/install-mcp-host.js.map +0 -1
- package/dist/cli/install-openclaw.d.ts +0 -7
- package/dist/cli/install-openclaw.js.map +0 -1
- package/dist/cli/install-windsurf.d.ts +0 -7
- package/dist/cli/install-windsurf.js.map +0 -1
- package/dist/cli/policy.d.ts +0 -1
- package/dist/cli/policy.js.map +0 -1
- package/dist/cli/revoke.d.ts +0 -1
- package/dist/cli/revoke.js.map +0 -1
- package/dist/cli/safe.d.ts +0 -14764
- package/dist/cli/safe.js.map +0 -1
- package/dist/cli/setup.d.ts +0 -1
- package/dist/cli/setup.js.map +0 -1
- package/dist/client.d.ts +0 -45
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -85
- package/dist/config.js.map +0 -1
- package/dist/discovery.d.ts +0 -43
- package/dist/discovery.js.map +0 -1
- package/dist/discoveryTools.d.ts +0 -75
- package/dist/discoveryTools.js.map +0 -1
- package/dist/funds/direct-balance.d.ts +0 -27
- package/dist/funds/direct-balance.js.map +0 -1
- package/dist/funds.d.ts +0 -26
- package/dist/funds.js.map +0 -1
- package/dist/httpX402.d.ts +0 -45
- package/dist/httpX402.js.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keychain.d.ts +0 -23
- package/dist/keychain.js.map +0 -1
- package/dist/log.d.ts +0 -16
- package/dist/log.js.map +0 -1
- package/dist/policy.d.ts +0 -73
- package/dist/policy.js.map +0 -1
- package/dist/server.d.ts +0 -24
- package/dist/server.js.map +0 -1
- package/dist/signer.d.ts +0 -45
- package/dist/signer.js.map +0 -1
- package/dist/signers/raw-eoa.d.ts +0 -12
- 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
|
-
##
|
|
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
|
-
#
|
|
32
|
-
|
|
33
|
-
#
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
|
|
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>`
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
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
|
-
|
|
157
|
-
//
|
|
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({
|
package/dist/cli/hpp-x402.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
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.
|
|
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
|