@aibtc/mcp-server 1.60.0 → 1.62.0

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 CHANGED
@@ -99,6 +99,8 @@ npx @aibtc/mcp-server@latest bridge --read-only --list-tools
99
99
  npx @aibtc/mcp-server@latest bridge --read-only --allow transfer_stx "send 1 STX to SP3..."
100
100
  ```
101
101
 
102
+ Before any agent loop runs (and on `--list-tools`), the bridge prints a compact **safety receipt** to stderr — network, read-only mode, exposed/write/blocked tool counts, the session spend cap, and the number of known x402 endpoints — so the configured execution boundaries are visible up front. It reports boundaries only; it never claims any value moved.
103
+
102
104
  The allowlist is re-enforced at execution time, so a model can never call a tool outside the exposed set. Any MCP-capable agent framework (`@openrouter/agent`, OpenAI Agents SDK, Claude Agent SDK) can also point at this server directly — the bridge is for driving it through OpenRouter's raw API without adopting a framework.
103
105
 
104
106
  ### Testnet Mode
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
31
31
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
32
+ import { ALL_ENDPOINTS } from "../endpoints/registry.js";
32
33
  const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
33
34
  const DEFAULT_MODEL = "anthropic/claude-3.5-haiku";
34
35
  /**
@@ -136,6 +137,28 @@ function parseArgs(argv) {
136
137
  opts.prompt = promptWords.join(" ").trim();
137
138
  return opts;
138
139
  }
140
+ /**
141
+ * Compact safety receipt printed before any agent loop runs (#583). Makes the
142
+ * configured execution boundaries visible up front so a user can trust the
143
+ * bridge without digging through flags/config. It reports ONLY configured
144
+ * boundaries — it never claims that any value moved. Emitted on stderr so it
145
+ * can't pollute stdout (the final assistant reply / --list-tools piping).
146
+ */
147
+ function printSafetyReceipt(opts, exposed, totalTools) {
148
+ const writeToolCount = exposed.filter((t) => !isReadOnly(t.name)).length;
149
+ const lines = [
150
+ "aibtc-mcp bridge — safety receipt",
151
+ ` network=${opts.network}`,
152
+ ` read_only=${opts.readOnly}`,
153
+ ` exposed_tools=${exposed.length}/${totalTools}`,
154
+ ` write_tool_count=${writeToolCount}`,
155
+ ` blocked_tool_count=${opts.block.size}`,
156
+ ` session_spend_cap=${opts.maxSpendUstx ?? "default"} uSTX / ${opts.maxSpendSats ?? "default"} sats`,
157
+ ` x402_endpoints_known=${ALL_ENDPOINTS.length}`,
158
+ " (reports configured execution boundaries only; no value has moved)",
159
+ ];
160
+ console.error(lines.join("\n"));
161
+ }
139
162
  /** Apply read-only + allow + block to the raw MCP tool list. */
140
163
  function selectTools(tools, opts) {
141
164
  return tools.filter((t) => {
@@ -193,6 +216,7 @@ export async function runBridge(argv) {
193
216
  const { tools: allTools } = await client.listTools();
194
217
  const exposed = selectTools(allTools, opts);
195
218
  if (opts.listTools) {
219
+ printSafetyReceipt(opts, exposed, allTools.length);
196
220
  console.log(`Exposed ${exposed.length}/${allTools.length} tools` +
197
221
  (opts.readOnly ? " (read-only)" : "") + ":");
198
222
  for (const t of exposed)
@@ -211,12 +235,8 @@ export async function runBridge(argv) {
211
235
  await client.close();
212
236
  process.exit(1);
213
237
  }
214
- const writeCount = exposed.filter((t) => !isReadOnly(t.name)).length;
215
- console.error(`Bridge: ${exposed.length} tools -> ${opts.model}` +
216
- (opts.readOnly ? " [read-only]" : ` [${writeCount} can write/spend]`) +
217
- (opts.maxSpendUstx || opts.maxSpendSats
218
- ? ` (cap ${opts.maxSpendUstx ?? "-"} uSTX / ${opts.maxSpendSats ?? "-"} sats)`
219
- : ""));
238
+ printSafetyReceipt(opts, exposed, allTools.length);
239
+ console.error(` model=${opts.model}, max_turns=${opts.maxTurns}`);
220
240
  const openaiTools = exposed.map(toOpenAITool);
221
241
  const messages = [
222
242
  {
@@ -64,13 +64,33 @@ Note: Dual Stacking is only available on mainnet.`,
64
64
  });
65
65
  }
66
66
  const resolvedAddress = address || (await getWalletAddress());
67
- const [enrolledThisCycleRaw, enrolledNextCycleRaw, minimumAmountRaw, aprDataRaw, cycleOverviewRaw,] = await Promise.all([
67
+ // Use allSettled, not all: `is-enrolled-this-cycle` fails contract-side
68
+ // on mainnet with RuntimeCheck(AtBlockUnavailable) (it reads historical
69
+ // block state Hiro has pruned). With Promise.all a single failing read
70
+ // sinks the whole response, hiding the other 4 useful fields. Recover
71
+ // per-call and surface a `warnings` array instead. (#554)
72
+ const warnings = [];
73
+ const settled = await Promise.allSettled([
68
74
  callDualStackingReadOnly("is-enrolled-this-cycle", [principalCV(resolvedAddress)]),
69
75
  callDualStackingReadOnly("is-enrolled-in-next-cycle", [principalCV(resolvedAddress)]),
70
76
  callDualStackingReadOnly("get-minimum-enrollment-amount", []),
71
77
  callDualStackingReadOnly("get-apr-data", []),
72
78
  callDualStackingReadOnly("current-overview-data", []),
73
79
  ]);
80
+ const readNames = [
81
+ "is-enrolled-this-cycle",
82
+ "is-enrolled-in-next-cycle",
83
+ "get-minimum-enrollment-amount",
84
+ "get-apr-data",
85
+ "current-overview-data",
86
+ ];
87
+ const values = settled.map((r, i) => {
88
+ if (r.status === "fulfilled")
89
+ return r.value;
90
+ warnings.push(`${readNames[i]}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
91
+ return null;
92
+ });
93
+ const [enrolledThisCycleRaw, enrolledNextCycleRaw, minimumAmountRaw, aprDataRaw, cycleOverviewRaw,] = values;
74
94
  // Parse APR data — returns {min-apr: uint, max-apr: uint} divided by 1_000_000 for %
75
95
  let apr = {
76
96
  minApr: 0,
@@ -107,10 +127,11 @@ Note: Dual Stacking is only available on mainnet.`,
107
127
  const raw = minimumAmountRaw;
108
128
  minimumEnrollmentSats = raw.value !== undefined ? Number(raw.value) : 0;
109
129
  }
110
- // Parse boolean enrollment flags
130
+ // Parse boolean enrollment flags. A failed read stays null (unknown)
131
+ // rather than collapsing to a misleading `false`.
111
132
  const parseBoolean = (raw) => {
112
133
  if (raw === null || raw === undefined)
113
- return false;
134
+ return null;
114
135
  if (typeof raw === "boolean")
115
136
  return raw;
116
137
  const obj = raw;
@@ -126,6 +147,7 @@ Note: Dual Stacking is only available on mainnet.`,
126
147
  minimumEnrollmentSats,
127
148
  apr,
128
149
  cycleOverview,
150
+ ...(warnings.length > 0 && { warnings }),
129
151
  });
130
152
  }
131
153
  catch (error) {
@@ -34,6 +34,7 @@ import { registerOrdinalsMarketplaceTools } from "./ordinals-marketplace.tools.j
34
34
  import { registerTaprootMultisigTools } from "./taproot-multisig.tools.js";
35
35
  import { registerJingswapTools } from "./jingswap.tools.js";
36
36
  import { registerSigningTools } from "./signing.tools.js";
37
+ import { registerInferenceMarketplaceTools } from "./inference-marketplace.tools.js";
37
38
  import { registerNewsTools } from "./news.tools.js";
38
39
  import { registerIdentityTools } from "./identity.tools.js";
39
40
  import { registerCredentialsTools } from "./credentials.tools.js";
@@ -160,6 +161,8 @@ export function registerAllTools(server) {
160
161
  registerJingswapTools(server);
161
162
  // Message Signing (BTC BIP-322, Stacks SIWS, SIP-018 structured data, Nostr NIP-01)
162
163
  registerSigningTools(server);
164
+ // AIBTC Inference Marketplace (list/manage a paid model endpoint via wallet signature)
165
+ registerInferenceMarketplaceTools(server);
163
166
  // AIBTC News (signal feed, leaderboard, file signals)
164
167
  registerNewsTools(server);
165
168
  // Identity (ERC-8004 on-chain agent identity management)
@@ -0,0 +1,30 @@
1
+ /**
2
+ * AIBTC Inference Marketplace Tools
3
+ *
4
+ * List an OpenAI-compatible model endpoint on the marketplace and manage it, all
5
+ * authorized by a wallet signature (the same wallet that receives sBTC payouts).
6
+ * Ownership is proven by signing a short message with the payout wallet — no
7
+ * accounts, no API keys. These tools sign locally with the unlocked wallet and
8
+ * call the gateway, so an agent can register/update/check in one step.
9
+ *
10
+ * - inference_register_provider: verify + list an endpoint (signed)
11
+ * - inference_update_provider: change name/models/payout/endpoint in place (signed)
12
+ * - inference_reveal_key: reveal or rotate the gateway↔endpoint shared key (signed)
13
+ * - inference_check_provider: re-run the health/functional probe (unsigned)
14
+ * - inference_list_providers: list registered providers + health (unsigned)
15
+ *
16
+ * The gateway verifies the signature recovers to `payoutAddress` (which also
17
+ * enforces the network), that the endpoint is reachable AND serving inference,
18
+ * and that model ids are real, text-generation, commercially-licensed HF repos.
19
+ */
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ type AuthAction = "register" | "update" | "reveal-key";
22
+ /** The exact message the gateway rebuilds and verifies. Must match byte-for-byte. */
23
+ export declare function authMessage(action: AuthAction, providerId: string, timestamp: number): string;
24
+ /** Sign an auth message with the unlocked wallet and return the gateway headers. */
25
+ export declare function signAuthHeaders(account: {
26
+ privateKey: string;
27
+ }, action: AuthAction, providerId: string): Record<string, string>;
28
+ export declare function registerInferenceMarketplaceTools(server: McpServer): void;
29
+ export {};
30
+ //# sourceMappingURL=inference-marketplace.tools.d.ts.map
@@ -0,0 +1,303 @@
1
+ /**
2
+ * AIBTC Inference Marketplace Tools
3
+ *
4
+ * List an OpenAI-compatible model endpoint on the marketplace and manage it, all
5
+ * authorized by a wallet signature (the same wallet that receives sBTC payouts).
6
+ * Ownership is proven by signing a short message with the payout wallet — no
7
+ * accounts, no API keys. These tools sign locally with the unlocked wallet and
8
+ * call the gateway, so an agent can register/update/check in one step.
9
+ *
10
+ * - inference_register_provider: verify + list an endpoint (signed)
11
+ * - inference_update_provider: change name/models/payout/endpoint in place (signed)
12
+ * - inference_reveal_key: reveal or rotate the gateway↔endpoint shared key (signed)
13
+ * - inference_check_provider: re-run the health/functional probe (unsigned)
14
+ * - inference_list_providers: list registered providers + health (unsigned)
15
+ *
16
+ * The gateway verifies the signature recovers to `payoutAddress` (which also
17
+ * enforces the network), that the endpoint is reachable AND serving inference,
18
+ * and that model ids are real, text-generation, commercially-licensed HF repos.
19
+ */
20
+ import { z } from "zod";
21
+ import { signMessageHashRsv, privateKeyToPublic, compressPublicKey, } from "@stacks/transactions";
22
+ import { hashMessage } from "@stacks/encryption";
23
+ import { bytesToHex } from "@stacks/common";
24
+ import { createJsonResponse, createErrorResponse } from "../utils/index.js";
25
+ import { getWalletManager } from "../services/wallet-manager.js";
26
+ /** Default marketplace gateway; override with the `gateway` arg for local dev. */
27
+ const DEFAULT_GATEWAY = "https://inference.aibtc.com";
28
+ /** Bound network calls so a hung endpoint can't block the tool call forever. */
29
+ const GATEWAY_TIMEOUT_MS = 15_000;
30
+ /** Loopback hosts always allowed for local dev. */
31
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
32
+ /**
33
+ * These tools sign an ownership message with the payout wallet and may forward
34
+ * the endpoint's own `apiKey` — both secrets. If the gateway destination were
35
+ * free-form, a prompt-injected "use gateway https://attacker.example" could
36
+ * redirect that signed request and leak the signature + apiKey. So the override
37
+ * is restricted to localhost and the marketplace (`*.aibtc.com`); any other host
38
+ * requires an explicit `allowUnsafeGateway: true`, which natural language alone
39
+ * won't set.
40
+ */
41
+ function isAllowedGatewayHost(hostname) {
42
+ const host = hostname.toLowerCase();
43
+ if (LOOPBACK_HOSTS.has(host))
44
+ return true;
45
+ return host === "aibtc.com" || host.endsWith(".aibtc.com");
46
+ }
47
+ /** Validate + normalize the gateway base URL (trailing slash stripped). */
48
+ function resolveGateway(gateway, allowUnsafe) {
49
+ const base = (gateway ?? DEFAULT_GATEWAY).replace(/\/$/, "");
50
+ let url;
51
+ try {
52
+ url = new URL(base);
53
+ }
54
+ catch {
55
+ throw new Error(`Invalid gateway URL: ${base}`);
56
+ }
57
+ if (!allowUnsafe && !isAllowedGatewayHost(url.hostname)) {
58
+ throw new Error(`Gateway host "${url.hostname}" is not allowed. Wallet-signed headers and your endpoint apiKey ` +
59
+ `are only sent to the marketplace (*.aibtc.com) or localhost. To target another host on ` +
60
+ `purpose, pass allowUnsafeGateway: true.`);
61
+ }
62
+ return base;
63
+ }
64
+ function requireUnlockedWallet() {
65
+ const account = getWalletManager().getActiveAccount();
66
+ if (!account) {
67
+ throw new Error("Wallet is not unlocked. Use wallet_unlock first to enable signing.");
68
+ }
69
+ return account;
70
+ }
71
+ /** The exact message the gateway rebuilds and verifies. Must match byte-for-byte. */
72
+ export function authMessage(action, providerId, timestamp) {
73
+ return `Inference Marketplace\nAction: ${action}\nProvider: ${providerId}\nTimestamp: ${timestamp}`;
74
+ }
75
+ /** Sign an auth message with the unlocked wallet and return the gateway headers. */
76
+ export function signAuthHeaders(account, action, providerId) {
77
+ const timestamp = Math.floor(Date.now() / 1000);
78
+ const message = authMessage(action, providerId, timestamp);
79
+ const signature = signMessageHashRsv({
80
+ messageHash: bytesToHex(hashMessage(message)),
81
+ privateKey: account.privateKey,
82
+ });
83
+ const publicKey = compressPublicKey(privateKeyToPublic(account.privateKey));
84
+ return {
85
+ "Content-Type": "application/json",
86
+ "X-Stacks-Signature": signature,
87
+ "X-Stacks-Public-Key": publicKey,
88
+ "X-Stacks-Timestamp": String(timestamp),
89
+ };
90
+ }
91
+ async function gatewayFetch(url, init) {
92
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS) });
93
+ const text = await res.text();
94
+ let body;
95
+ try {
96
+ body = JSON.parse(text);
97
+ }
98
+ catch {
99
+ body = { raw: text };
100
+ }
101
+ return { ok: res.ok, status: res.status, body };
102
+ }
103
+ const gatewayArg = z
104
+ .string()
105
+ .url()
106
+ .optional()
107
+ .describe(`Marketplace gateway base URL. Defaults to ${DEFAULT_GATEWAY}. Use http://localhost:8787 for local dev.`);
108
+ const allowUnsafeGatewayArg = z
109
+ .boolean()
110
+ .optional()
111
+ .describe("Allow a gateway host outside localhost / *.aibtc.com. Required to send wallet-signed headers " +
112
+ "(and any apiKey) to an arbitrary host — leave unset unless you deliberately self-host the gateway.");
113
+ export function registerInferenceMarketplaceTools(server) {
114
+ // Register (verify + list) an endpoint — signed
115
+ server.registerTool("inference_register_provider", {
116
+ description: "List an OpenAI-compatible model endpoint on the AIBTC Inference Marketplace and get paid per request in sBTC. " +
117
+ "Signs a registration message with the unlocked payout wallet (proving ownership) and POSTs it. " +
118
+ "The gateway verifies the signature recovers to payoutAddress, that the endpoint is reachable AND actually " +
119
+ "serving inference, and that model ids are real/commercial Hugging Face repos — then lists it. " +
120
+ "Requires an unlocked wallet on the gateway's network.",
121
+ inputSchema: {
122
+ name: z.string().describe("Display name for your node, e.g. \"Alice's Qwen node\"."),
123
+ endpoint: z
124
+ .string()
125
+ .url()
126
+ .describe("OpenAI-compatible base URL, e.g. https://my-host/v1 (must expose /models and /chat/completions)."),
127
+ models: z
128
+ .array(z.string())
129
+ .min(1)
130
+ .describe("Hugging Face repo ids you serve, e.g. [\"Qwen/Qwen2.5-7B-Instruct\"]."),
131
+ payoutAddress: z
132
+ .string()
133
+ .optional()
134
+ .describe("Stacks address that receives sBTC. Defaults to the unlocked wallet's address (must match the gateway network)."),
135
+ apiKey: z
136
+ .string()
137
+ .optional()
138
+ .describe("Optional: a key your endpoint requires. Locks the endpoint so only the gateway (which presents it) can call it."),
139
+ gateway: gatewayArg,
140
+ allowUnsafeGateway: allowUnsafeGatewayArg,
141
+ },
142
+ }, async ({ name, endpoint, models, payoutAddress, apiKey, gateway, allowUnsafeGateway }) => {
143
+ try {
144
+ const account = requireUnlockedWallet();
145
+ const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
146
+ const payout = payoutAddress ?? account.address;
147
+ const headers = signAuthHeaders(account, "register", endpoint);
148
+ const { ok, status, body } = await gatewayFetch(`${base}/v1/providers`, {
149
+ method: "POST",
150
+ headers,
151
+ body: JSON.stringify({
152
+ name,
153
+ endpoint,
154
+ payoutAddress: payout,
155
+ models,
156
+ ...(apiKey ? { apiKey } : {}),
157
+ }),
158
+ });
159
+ return createJsonResponse({
160
+ success: ok,
161
+ status,
162
+ payoutAddress: payout,
163
+ gateway: base,
164
+ result: body,
165
+ note: ok
166
+ ? "Registered and verified. Your endpoint is listed and callable."
167
+ : "Registration failed — see result.error (e.g. wrong network, endpoint unreachable, or invalid model id).",
168
+ });
169
+ }
170
+ catch (error) {
171
+ return createErrorResponse(error);
172
+ }
173
+ });
174
+ // Update a listing in place — signed
175
+ server.registerTool("inference_update_provider", {
176
+ description: "Update your marketplace listing in place — name, models, payout address, endpoint, or description — no delete/re-add. " +
177
+ "Signs an update message with the unlocked payout wallet and PATCHes it. Only fields you pass change. " +
178
+ "Changing the endpoint re-verifies reachability before taking effect. Requires an unlocked wallet.",
179
+ inputSchema: {
180
+ providerId: z.string().describe("The provider id returned at registration (from inference_list_providers)."),
181
+ name: z.string().optional().describe("New display name."),
182
+ endpoint: z.string().url().optional().describe("New OpenAI-compatible base URL (re-verified before it takes effect)."),
183
+ models: z.array(z.string()).optional().describe("New Hugging Face repo id list."),
184
+ payoutAddress: z.string().optional().describe("New payout address (must match the gateway network)."),
185
+ description: z.string().optional().describe("New description."),
186
+ apiKey: z.string().optional().describe("Set/replace the key your endpoint requires (locks the endpoint)."),
187
+ gateway: gatewayArg,
188
+ allowUnsafeGateway: allowUnsafeGatewayArg,
189
+ },
190
+ }, async ({ providerId, name, endpoint, models, payoutAddress, description, apiKey, gateway, allowUnsafeGateway }) => {
191
+ try {
192
+ const account = requireUnlockedWallet();
193
+ const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
194
+ const patch = {};
195
+ if (name !== undefined)
196
+ patch.name = name;
197
+ if (endpoint !== undefined)
198
+ patch.endpoint = endpoint;
199
+ if (models !== undefined)
200
+ patch.models = models;
201
+ if (payoutAddress !== undefined)
202
+ patch.payoutAddress = payoutAddress;
203
+ if (description !== undefined)
204
+ patch.description = description;
205
+ if (apiKey !== undefined)
206
+ patch.apiKey = apiKey;
207
+ if (Object.keys(patch).length === 0) {
208
+ throw new Error("Nothing to update — pass at least one of name/endpoint/models/payoutAddress/description/apiKey.");
209
+ }
210
+ const headers = signAuthHeaders(account, "update", providerId);
211
+ const { ok, status, body } = await gatewayFetch(`${base}/v1/providers/${providerId}`, {
212
+ method: "PATCH",
213
+ headers,
214
+ body: JSON.stringify(patch),
215
+ });
216
+ return createJsonResponse({
217
+ success: ok,
218
+ status,
219
+ gateway: base,
220
+ result: body,
221
+ note: ok ? "Listing updated." : "Update failed — see result.error.",
222
+ });
223
+ }
224
+ catch (error) {
225
+ return createErrorResponse(error);
226
+ }
227
+ });
228
+ // Reveal or rotate the shared key — signed
229
+ server.registerTool("inference_reveal_key", {
230
+ description: "Reveal or rotate your provider's shared key — the gateway↔endpoint credential the gateway presents when it " +
231
+ "calls your endpoint (NOT an ownership token; ownership is your wallet). The gateway stores it write-only, so a " +
232
+ "wallet signature is the only way to read it back. Pass rotate=true to issue a new one. Requires an unlocked wallet.",
233
+ inputSchema: {
234
+ providerId: z.string().describe("The provider id (from inference_list_providers)."),
235
+ rotate: z.boolean().optional().describe("true = generate and return a NEW key (invalidates the old one). false/omitted = reveal the current key."),
236
+ gateway: gatewayArg,
237
+ allowUnsafeGateway: allowUnsafeGatewayArg,
238
+ },
239
+ }, async ({ providerId, rotate, gateway, allowUnsafeGateway }) => {
240
+ try {
241
+ const account = requireUnlockedWallet();
242
+ const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
243
+ const headers = signAuthHeaders(account, "reveal-key", providerId);
244
+ const { ok, status, body } = await gatewayFetch(`${base}/v1/providers/${providerId}/key`, {
245
+ method: "POST",
246
+ headers,
247
+ body: JSON.stringify(rotate ? { rotate: true } : {}),
248
+ });
249
+ return createJsonResponse({
250
+ success: ok,
251
+ status,
252
+ gateway: base,
253
+ result: body,
254
+ note: ok
255
+ ? "Your endpoint must REQUIRE this key (proxy / Cloudflare Access) for it to prevent freeloading — an open endpoint ignores it."
256
+ : "Reveal/rotate failed — see result.error.",
257
+ });
258
+ }
259
+ catch (error) {
260
+ return createErrorResponse(error);
261
+ }
262
+ });
263
+ // Re-run the health/functional probe — unsigned
264
+ server.registerTool("inference_check_provider", {
265
+ description: "Re-run the marketplace's health + functional probe for a provider on demand (checks the endpoint is reachable and " +
266
+ "actually returns a completion). No signature required. Useful right after (re)starting your node or tunnel.",
267
+ inputSchema: {
268
+ providerId: z.string().describe("The provider id to check."),
269
+ gateway: gatewayArg,
270
+ allowUnsafeGateway: allowUnsafeGatewayArg,
271
+ },
272
+ }, async ({ providerId, gateway, allowUnsafeGateway }) => {
273
+ try {
274
+ const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
275
+ const { ok, status, body } = await gatewayFetch(`${base}/v1/providers/${providerId}/check`, {
276
+ method: "POST",
277
+ });
278
+ return createJsonResponse({ success: ok, status, gateway: base, result: body });
279
+ }
280
+ catch (error) {
281
+ return createErrorResponse(error);
282
+ }
283
+ });
284
+ // List registered providers — unsigned
285
+ server.registerTool("inference_list_providers", {
286
+ description: "List providers registered on the marketplace with their models, health/status, and flagged state. No signature " +
287
+ "required. Use it to find your provider id (match on payoutAddress) after registering.",
288
+ inputSchema: {
289
+ gateway: gatewayArg,
290
+ allowUnsafeGateway: allowUnsafeGatewayArg,
291
+ },
292
+ }, async ({ gateway, allowUnsafeGateway }) => {
293
+ try {
294
+ const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
295
+ const { ok, status, body } = await gatewayFetch(`${base}/v1/providers`, { method: "GET" });
296
+ return createJsonResponse({ success: ok, status, gateway: base, result: body });
297
+ }
298
+ catch (error) {
299
+ return createErrorResponse(error);
300
+ }
301
+ });
302
+ }
303
+ //# sourceMappingURL=inference-marketplace.tools.js.map
@@ -7,9 +7,11 @@
7
7
  * same LightningProvider singleton to auto-pay invoice challenges on 402s.
8
8
  */
9
9
  import { z } from "zod";
10
+ import { decode as decodeBolt11 } from "light-bolt11-decoder";
10
11
  import { createJsonResponse, createErrorResponse } from "../utils/index.js";
11
12
  import { getLightningManager } from "../services/lightning-manager.js";
12
13
  import { getWalletManager } from "../services/wallet-manager.js";
14
+ import { getSpendLimiter } from "../services/spend-limiter.js";
13
15
  import { MempoolApi, getMempoolTxUrl, } from "../services/mempool-api.js";
14
16
  import { buildAndSignBtcTransaction } from "../transactions/bitcoin-builder.js";
15
17
  import { UnisatIndexer } from "../services/unisat-indexer.js";
@@ -284,11 +286,39 @@ export function registerLightningTools(server) {
284
286
  if (!provider) {
285
287
  throw new Error("Lightning wallet is locked. Use lightning_unlock first.");
286
288
  }
289
+ // Meter the invoice face value against the cumulative sats spend limit
290
+ // (#572). maxFeeSats only bounds the routing fee, not the payable amount,
291
+ // so without this a single invoice can pay out an unbounded amount.
292
+ // Decode the invoice to read the amount (millisats), mirroring the L402
293
+ // auto-pay path in x402.service.ts.
294
+ let decoded;
295
+ try {
296
+ decoded = decodeBolt11(bolt11);
297
+ }
298
+ catch (decodeErr) {
299
+ throw new Error(`BOLT-11 invoice could not be decoded: ${decodeErr instanceof Error ? decodeErr.message : String(decodeErr)}`);
300
+ }
301
+ const amountSection = decoded.sections.find((s) => s.name === "amount");
302
+ const amountMsat = amountSection?.value ? BigInt(amountSection.value) : 0n;
303
+ const amountSats = Number(amountMsat / 1000n);
304
+ if (amountSats === 0) {
305
+ throw new Error("Invoice has no amount; refusing to pay amountless invoices for safety (an unmeterable spend defeats the spending cap).");
306
+ }
307
+ // Key the sats ledger by the active Stacks address so BTC L1 / sBTC /
308
+ // L402 / Lightning spends share one budget. When the main wallet is
309
+ // locked (Lightning has its own session), fall back to a dedicated
310
+ // Lightning bucket so the pay is always metered. Note: a locked-STX
311
+ // user therefore gets a separate Lightning ledger from their STX-address
312
+ // ledger — see SECURITY.md.
313
+ const ledgerKey = getWalletManager().getActiveAccount()?.address ?? "__lightning__";
314
+ await getSpendLimiter().check("sats", BigInt(amountSats), ledgerKey);
287
315
  const result = await provider.payInvoice(bolt11, maxFeeSats);
316
+ await getSpendLimiter().record("sats", BigInt(amountSats), ledgerKey);
288
317
  return createJsonResponse({
289
318
  success: true,
290
319
  preimage: result.preimage,
291
320
  feesPaidSats: result.feesPaid,
321
+ amountSats,
292
322
  });
293
323
  }
294
324
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibtc/mcp-server",
3
- "version": "1.60.0",
3
+ "version": "1.62.0",
4
4
  "description": "Bitcoin-native MCP server for AI agents: BTC/STX wallets, DeFi yield, sBTC peg, NFTs, and x402 payments.",
5
5
  "mcpName": "io.github.aibtcdev/mcp-server",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Bitcoin L1 wallet for agents - check balances, send BTC, manage UTX
4
4
  license: MIT
5
5
  metadata:
6
6
  author: aibtcdev
7
- version: 1.60.0 # x-release-please-version
7
+ version: 1.62.0 # x-release-please-version
8
8
  npm: "@aibtc/mcp-server"
9
9
  github: https://github.com/aibtcdev/aibtc-mcp-server
10
10
  ---
@@ -214,6 +214,33 @@ Always probe before executing paid endpoints. Never call `execute_x402_endpoint`
214
214
  See: [references/stacks-defi.md](references/stacks-defi.md) for endpoint catalog
215
215
  See: [references/x402-inbox.md](references/x402-inbox.md) for inbox-specific flow details
216
216
 
217
+ ### Inference Marketplace (earn sBTC serving models)
218
+
219
+ List an OpenAI-compatible model endpoint on the AIBTC Inference Marketplace and
220
+ get paid per request in sBTC. Ownership is proven by a wallet signature — the
221
+ same wallet that receives payouts — so there are no accounts or API keys. The
222
+ tools sign locally with the unlocked wallet and call the gateway, so an agent can
223
+ register and manage a listing in one step.
224
+
225
+ ```
226
+ "Register my endpoint https://my-host/v1 serving Qwen/Qwen2.5-7B-Instruct on the inference marketplace"
227
+ ```
228
+
229
+ | Tool | Description | Signed |
230
+ |------|-------------|:------:|
231
+ | `inference_register_provider` | Verify + list an endpoint (name, models, payout, optional apiKey) | ✅ |
232
+ | `inference_update_provider` | Change name/models/payout/endpoint/description/apiKey in place | ✅ |
233
+ | `inference_reveal_key` | Reveal or rotate the gateway↔endpoint shared key (`rotate: true`) | ✅ |
234
+ | `inference_check_provider` | Re-run the health/functional probe | — |
235
+ | `inference_list_providers` | List providers + health (find your id by `payoutAddress`) | — |
236
+
237
+ The gateway verifies the signature recovers to `payoutAddress` (which also fixes
238
+ the network), that the endpoint is reachable AND actually serving inference, and
239
+ that model ids are real, commercially-licensed Hugging Face repos. Requires an
240
+ unlocked wallet on the gateway's network. Every tool takes an optional `gateway`
241
+ arg (defaults to `https://inference.aibtc.com`; use `http://localhost:8787` for
242
+ local dev).
243
+
217
244
  ### Genesis Lifecycle
218
245
 
219
246
  Agent identity and reputation on Bitcoin and Stacks: