agenticbtc-mcp 1.0.7 → 1.0.10
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/package.json +1 -1
- package/src/server.js +325 -131
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agenticbtc-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Privacy-intelligent payments for AI agents — your privacy, your choice. Universal payment router with Lightning, Strike, Coinbase, PayPal, Venmo support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bitcoin",
|
package/src/server.js
CHANGED
|
@@ -7,12 +7,62 @@
|
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import crypto from "crypto";
|
|
10
11
|
|
|
11
12
|
const API_URL = process.env.AGENTICBTC_API_URL || process.env.AGENTBTC_API_URL || "http://localhost:8000";
|
|
12
13
|
const API_KEY = process.env.AGENTICBTC_API_KEY || process.env.AGENTBTC_API_KEY || "";
|
|
13
14
|
const LND_HOST = process.env.AGENTBTC_LND_HOST || "";
|
|
14
15
|
const LND_MACAROON = process.env.AGENTBTC_LND_MACAROON || "";
|
|
15
16
|
|
|
17
|
+
// X (Twitter) credentials — optional, enables social posting tools
|
|
18
|
+
const X_CONSUMER_KEY = process.env.TWITTER_CONSUMER_KEY || "";
|
|
19
|
+
const X_CONSUMER_SECRET = process.env.TWITTER_CONSUMER_SECRET || "";
|
|
20
|
+
const X_ACCESS_TOKEN = process.env.TWITTER_ACCESS_TOKEN || "";
|
|
21
|
+
const X_ACCESS_SECRET = process.env.TWITTER_ACCESS_SECRET || "";
|
|
22
|
+
const X_BEARER_TOKEN = process.env.TWITTER_BEARER_TOKEN || "";
|
|
23
|
+
const X_USER_ID = X_ACCESS_TOKEN ? X_ACCESS_TOKEN.split("-")[0] : "";
|
|
24
|
+
const X_ENABLED = !!(X_CONSUMER_KEY && X_ACCESS_TOKEN);
|
|
25
|
+
|
|
26
|
+
// OAuth 1.0a helper for X write operations
|
|
27
|
+
function buildXOAuthHeader(method, url) {
|
|
28
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
29
|
+
const nonce = crypto.randomBytes(16).toString("hex");
|
|
30
|
+
const oauthParams = {
|
|
31
|
+
oauth_consumer_key: X_CONSUMER_KEY,
|
|
32
|
+
oauth_nonce: nonce,
|
|
33
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
34
|
+
oauth_timestamp: timestamp,
|
|
35
|
+
oauth_token: X_ACCESS_TOKEN,
|
|
36
|
+
oauth_version: "1.0",
|
|
37
|
+
};
|
|
38
|
+
const sorted = Object.entries(oauthParams).sort(([a], [b]) => a.localeCompare(b));
|
|
39
|
+
const paramStr = sorted.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
40
|
+
const baseStr = `${method.toUpperCase()}&${encodeURIComponent(url)}&${encodeURIComponent(paramStr)}`;
|
|
41
|
+
const signingKey = `${encodeURIComponent(X_CONSUMER_SECRET)}&${encodeURIComponent(X_ACCESS_SECRET)}`;
|
|
42
|
+
const signature = crypto.createHmac("sha1", signingKey).update(baseStr).digest("base64");
|
|
43
|
+
return "OAuth " + Object.entries({ ...oauthParams, oauth_signature: signature })
|
|
44
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
45
|
+
.map(([k, v]) => `${encodeURIComponent(k)}="${encodeURIComponent(v)}"`)
|
|
46
|
+
.join(", ");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function xPost(endpoint, body) {
|
|
50
|
+
const url = `https://api.x.com${endpoint}`;
|
|
51
|
+
const auth = buildXOAuthHeader("POST", url);
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { Authorization: auth, "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
return { status: res.status, data: await res.json() };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function xGet(endpoint) {
|
|
61
|
+
const url = `https://api.x.com${endpoint}`;
|
|
62
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${X_BEARER_TOKEN}` } });
|
|
63
|
+
return { status: res.status, data: await res.json() };
|
|
64
|
+
}
|
|
65
|
+
|
|
16
66
|
// BK Block routing node — all payments route through this node as first hop
|
|
17
67
|
// Routing fee: 0.5% (5000ppm) + 1 sat base
|
|
18
68
|
const BK_BLOCK_NODE_PUBKEY = process.env.BK_BLOCK_NODE_PUBKEY || "031aef3a70c08a6e2d96ba1c78eec66092723cdc41d546329df3f065b0f200bd3b";
|
|
@@ -302,87 +352,42 @@ server.tool(
|
|
|
302
352
|
fee_limit_sats: z.number().optional().default(100).describe("Max fee in sats"),
|
|
303
353
|
},
|
|
304
354
|
async ({ invoice, agent, fee_limit_sats }) => {
|
|
305
|
-
if (!LND_HOST) {
|
|
306
|
-
return { content: [{ type: "text", text: "Error: LND node not configured. Set AGENTBTC_LND_HOST environment variable." }] };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Resolve agent for spending policy enforcement
|
|
310
|
-
let resolvedAgent = null;
|
|
311
|
-
if (agent) {
|
|
312
|
-
resolvedAgent = await resolveAgent(agent);
|
|
313
|
-
if (!resolvedAgent) {
|
|
314
|
-
return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
|
|
315
|
-
}
|
|
316
|
-
if (!resolvedAgent.enabled) {
|
|
317
|
-
return { content: [{ type: "text", text: `Error: Agent '${resolvedAgent.name}' is disabled — payments blocked` }] };
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Auto-detect agent from API key auth level
|
|
322
|
-
if (!resolvedAgent) {
|
|
323
|
-
const auth = await getAuthLevel();
|
|
324
|
-
if (auth.agent_id) {
|
|
325
|
-
resolvedAgent = { id: auth.agent_id, name: auth.agent_name || auth.agent_id };
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
355
|
try {
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
const amountSats = parseInt(decoded.num_satoshis || 0);
|
|
336
|
-
|
|
337
|
-
// Check spending policy if agent is identified
|
|
338
|
-
if (resolvedAgent && amountSats > 0) {
|
|
339
|
-
const policy = await checkSpendingPolicy(resolvedAgent.id, amountSats);
|
|
340
|
-
if (!policy.allowed) {
|
|
341
|
-
return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Pay invoice directly via LND REST API
|
|
346
|
-
const res = await fetch(`${LND_HOST}/v1/channels/transactions`, {
|
|
356
|
+
// Route through the AgenticBTC payment router (POST /api/v1/payments).
|
|
357
|
+
// The router enforces spending policy, collects the 0.5% router fee,
|
|
358
|
+
// performs privacy-aware rail selection, and provides multi-rail failover.
|
|
359
|
+
// Do NOT call LND directly here.
|
|
360
|
+
const { status, data } = await apiCall("/api/v1/payments", {
|
|
347
361
|
method: "POST",
|
|
348
|
-
headers: {
|
|
349
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
350
|
-
"Content-Type": "application/json",
|
|
351
|
-
},
|
|
352
362
|
body: JSON.stringify({
|
|
353
|
-
|
|
354
|
-
|
|
363
|
+
invoice,
|
|
364
|
+
fee_limit_sats: fee_limit_sats ?? 100,
|
|
365
|
+
// Pass agent hint in metadata so the server can scope logging
|
|
366
|
+
...(agent ? { agent_hint: agent } : {}),
|
|
355
367
|
}),
|
|
356
368
|
});
|
|
357
|
-
const data = await res.json();
|
|
358
|
-
|
|
359
|
-
if (res.ok && !data.payment_error) {
|
|
360
|
-
const paidAmount = parseInt(data.value || data.value_sat || amountSats || 0);
|
|
361
|
-
|
|
362
|
-
// Log transaction against agent
|
|
363
|
-
if (resolvedAgent) {
|
|
364
|
-
await logTransaction(resolvedAgent.id, paidAmount, decoded.destination || "", decoded.description || "", "completed");
|
|
365
|
-
}
|
|
366
369
|
|
|
370
|
+
if (status === 200 && data.success) {
|
|
367
371
|
return {
|
|
368
372
|
content: [{
|
|
369
373
|
type: "text",
|
|
370
374
|
text: JSON.stringify({
|
|
371
375
|
success: true,
|
|
372
|
-
payment_hash: data.payment_hash,
|
|
373
|
-
amount_sats:
|
|
374
|
-
fee_sats:
|
|
375
|
-
|
|
376
|
+
payment_hash: data.payment_hash || data.payment_id,
|
|
377
|
+
amount_sats: data.amount_sats,
|
|
378
|
+
fee_sats: data.fee_sats,
|
|
379
|
+
rail: data.rail,
|
|
376
380
|
status: data.status || "SUCCEEDED",
|
|
377
|
-
message: `Paid ${
|
|
381
|
+
message: `Paid ${data.amount_sats} sats via router (${data.rail || "lightning"}) ⚡`,
|
|
378
382
|
}, null, 2),
|
|
379
383
|
}],
|
|
380
384
|
};
|
|
381
385
|
}
|
|
382
|
-
|
|
386
|
+
|
|
387
|
+
const errMsg = data?.detail || data?.error || data?.message || JSON.stringify(data);
|
|
383
388
|
return { content: [{ type: "text", text: `Payment failed: ${errMsg}` }] };
|
|
384
389
|
} catch (e) {
|
|
385
|
-
return { content: [{ type: "text", text: `
|
|
390
|
+
return { content: [{ type: "text", text: `Router connection error: ${e.message}` }] };
|
|
386
391
|
}
|
|
387
392
|
}
|
|
388
393
|
);
|
|
@@ -425,14 +430,13 @@ server.tool(
|
|
|
425
430
|
const macaroon = data.macaroon;
|
|
426
431
|
const amount = data.amount || 0;
|
|
427
432
|
|
|
428
|
-
if (!invoice
|
|
433
|
+
if (!invoice) {
|
|
429
434
|
return {
|
|
430
435
|
content: [{
|
|
431
436
|
type: "text",
|
|
432
437
|
text: JSON.stringify({
|
|
433
438
|
success: false,
|
|
434
|
-
error: "
|
|
435
|
-
invoice,
|
|
439
|
+
error: "No invoice returned from L402 challenge — cannot pay",
|
|
436
440
|
amount,
|
|
437
441
|
}, null, 2),
|
|
438
442
|
}],
|
|
@@ -442,31 +446,29 @@ server.tool(
|
|
|
442
446
|
// Check routing status before paying
|
|
443
447
|
const routingStatus = await checkRoutingStatus();
|
|
444
448
|
if (routingStatus === "suspended") {
|
|
445
|
-
return { content: [{ type: "text", text: "
|
|
449
|
+
return { content: [{ type: "text", text: "Account suspended — routing verification failed. Contact support." }] };
|
|
446
450
|
}
|
|
447
451
|
|
|
448
|
-
//
|
|
449
|
-
|
|
452
|
+
// Pay the L402 invoice via the AgenticBTC payment router (NOT directly via LND).
|
|
453
|
+
// The router collects the 0.5% fee, applies privacy-aware rail selection, and
|
|
454
|
+
// provides multi-rail failover.
|
|
455
|
+
const { status: payStatus, data: payData } = await apiCall("/api/v1/payments", {
|
|
450
456
|
method: "POST",
|
|
451
|
-
headers: {
|
|
452
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
453
|
-
"Content-Type": "application/json",
|
|
454
|
-
},
|
|
455
457
|
body: JSON.stringify({
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
fee_limit: { fixed: 5000 }, // Allow routing fees
|
|
458
|
+
invoice,
|
|
459
|
+
fee_limit_sats: 5000,
|
|
459
460
|
}),
|
|
460
461
|
});
|
|
461
|
-
const payData = await payRes.json();
|
|
462
462
|
|
|
463
|
-
if (payData.
|
|
464
|
-
|
|
463
|
+
if (payStatus !== 200 || !payData.success) {
|
|
464
|
+
const errMsg = payData?.detail || payData?.error || payData?.payment_error || JSON.stringify(payData);
|
|
465
|
+
return { content: [{ type: "text", text: `Lightning payment failed: ${errMsg}` }] };
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
const preimage = payData.payment_preimage;
|
|
468
|
+
const preimage = payData.preimage || payData.payment_preimage;
|
|
468
469
|
if (!preimage) {
|
|
469
|
-
|
|
470
|
+
// Payment succeeded at router level but no preimage returned — report best-effort
|
|
471
|
+
return { content: [{ type: "text", text: "Payment succeeded but no preimage returned from router. L402 access may require preimage — check router logs." }] };
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
// Step 3: Access with L402 token
|
|
@@ -479,22 +481,16 @@ server.tool(
|
|
|
479
481
|
|
|
480
482
|
if (authRes.status === 200) {
|
|
481
483
|
const authData = await authRes.json();
|
|
482
|
-
// Report L402 payment for routing verification
|
|
483
|
-
await reportPayment(
|
|
484
|
-
payData.payment_hash || "",
|
|
485
|
-
amount,
|
|
486
|
-
"",
|
|
487
|
-
[BK_BLOCK_NODE_PUBKEY]
|
|
488
|
-
);
|
|
489
484
|
return {
|
|
490
485
|
content: [{
|
|
491
486
|
type: "text",
|
|
492
487
|
text: JSON.stringify({
|
|
493
488
|
success: true,
|
|
494
489
|
data: authData,
|
|
495
|
-
amount_paid_sats: amount,
|
|
490
|
+
amount_paid_sats: payData.amount_sats || amount,
|
|
496
491
|
payment_preimage: preimage,
|
|
497
|
-
|
|
492
|
+
rail: payData.rail,
|
|
493
|
+
message: `Paid ${payData.amount_sats || amount} sats via router for ${endpoint} API access`,
|
|
498
494
|
}, null, 2),
|
|
499
495
|
}],
|
|
500
496
|
};
|
|
@@ -680,9 +676,9 @@ server.tool(
|
|
|
680
676
|
comment: z.string().optional().default("").describe("Optional comment for the recipient"),
|
|
681
677
|
},
|
|
682
678
|
async ({ address, amount_sats, agent, comment }) => {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
679
|
+
// No LND_HOST check needed — payment routes through the AgenticBTC REST API router.
|
|
680
|
+
// LNURL resolution (steps 1 & 2 below) is external HTTP; only step 3 (the payment)
|
|
681
|
+
// has changed to go through POST /api/v1/payments instead of directly to LND.
|
|
686
682
|
|
|
687
683
|
// Resolve agent for spending policy
|
|
688
684
|
let resolvedAgent = null;
|
|
@@ -739,25 +735,18 @@ server.tool(
|
|
|
739
735
|
return { content: [{ type: "text", text: `Error: No invoice returned from ${domain}` }] };
|
|
740
736
|
}
|
|
741
737
|
|
|
742
|
-
// Step 3: Pay the invoice via LND
|
|
743
|
-
|
|
738
|
+
// Step 3: Pay the invoice via the AgenticBTC payment router (NOT directly via LND).
|
|
739
|
+
// This ensures the 0.5% router fee is collected, privacy-aware rail selection is
|
|
740
|
+
// applied, and multi-rail failover is available.
|
|
741
|
+
const { status: payStatus, data: payData } = await apiCall("/api/v1/payments", {
|
|
744
742
|
method: "POST",
|
|
745
|
-
headers: {
|
|
746
|
-
"Grpc-Metadata-macaroon": LND_MACAROON,
|
|
747
|
-
"Content-Type": "application/json",
|
|
748
|
-
},
|
|
749
743
|
body: JSON.stringify({
|
|
750
|
-
|
|
751
|
-
|
|
744
|
+
invoice: invoiceData.pr,
|
|
745
|
+
fee_limit_sats: Math.max(100, Math.floor(amount_sats * 0.01)),
|
|
752
746
|
}),
|
|
753
747
|
});
|
|
754
|
-
const payData = await payRes.json();
|
|
755
748
|
|
|
756
|
-
if (
|
|
757
|
-
// Log transaction
|
|
758
|
-
if (resolvedAgent) {
|
|
759
|
-
await logTransaction(resolvedAgent.id, amount_sats, address, comment || `Lightning address payment to ${address}`, "completed");
|
|
760
|
-
}
|
|
749
|
+
if (payStatus === 200 && payData.success) {
|
|
761
750
|
return {
|
|
762
751
|
content: [{
|
|
763
752
|
type: "text",
|
|
@@ -765,21 +754,124 @@ server.tool(
|
|
|
765
754
|
success: true,
|
|
766
755
|
recipient: address,
|
|
767
756
|
amount_sats: amount_sats,
|
|
768
|
-
fee_sats:
|
|
757
|
+
fee_sats: payData.fee_sats || 0,
|
|
758
|
+
rail: payData.rail,
|
|
769
759
|
agent: resolvedAgent?.name || null,
|
|
770
|
-
payment_hash: payData.payment_hash,
|
|
771
|
-
message: `Sent ${amount_sats} sats to ${address} ⚡`,
|
|
760
|
+
payment_hash: payData.payment_hash || payData.payment_id,
|
|
761
|
+
message: `Sent ${amount_sats} sats to ${address} via router ⚡`,
|
|
772
762
|
}, null, 2),
|
|
773
763
|
}],
|
|
774
764
|
};
|
|
775
765
|
}
|
|
776
|
-
|
|
766
|
+
const errMsg = payData?.detail || payData?.error || payData?.payment_error || JSON.stringify(payData);
|
|
767
|
+
return { content: [{ type: "text", text: `Payment failed: ${errMsg}` }] };
|
|
777
768
|
} catch (e) {
|
|
778
769
|
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
779
770
|
}
|
|
780
771
|
}
|
|
781
772
|
);
|
|
782
773
|
|
|
774
|
+
// Tool: Universal send_payment — auto-detects recipient type
|
|
775
|
+
server.tool(
|
|
776
|
+
"send_payment",
|
|
777
|
+
"Send a payment to any recipient. Automatically detects recipient type: BOLT11 Lightning invoice (lnbc.../lntb...), Lightning address (user@domain.com), or Strike handle ($username). Routes through the optimal payment rail.",
|
|
778
|
+
{
|
|
779
|
+
recipient: z.string().describe("Where to send: Lightning invoice (lnbc.../lntb...), Lightning address (user@domain.com), or Strike handle ($username)"),
|
|
780
|
+
amount_sats: z.number().optional().describe("Amount in satoshis (required for Lightning address and Strike handle; optional for BOLT11 invoices which encode the amount)"),
|
|
781
|
+
amount_usd: z.number().optional().describe("Amount in USD (for Strike handle payments; alternative to amount_sats)"),
|
|
782
|
+
memo: z.string().optional().default("").describe("Optional payment memo or comment"),
|
|
783
|
+
agent: z.string().optional().describe("Agent wallet name or ID for spending policy enforcement"),
|
|
784
|
+
},
|
|
785
|
+
async ({ recipient, amount_sats, amount_usd, memo, agent }) => {
|
|
786
|
+
// Detect recipient type
|
|
787
|
+
const isBolt11 = /^lnbc|^lntb|^lnbcrt/i.test(recipient);
|
|
788
|
+
const isStrikeHandle = recipient.startsWith("$");
|
|
789
|
+
const isLightningAddress = !isBolt11 && !isStrikeHandle && recipient.includes("@");
|
|
790
|
+
|
|
791
|
+
// Resolve agent for spending policy
|
|
792
|
+
let resolvedAgent = null;
|
|
793
|
+
if (agent) {
|
|
794
|
+
resolvedAgent = await resolveAgent(agent);
|
|
795
|
+
if (!resolvedAgent) return { content: [{ type: "text", text: `Error: Agent '${agent}' not found` }] };
|
|
796
|
+
if (!resolvedAgent.enabled) return { content: [{ type: "text", text: `Error: Agent '${resolvedAgent.name}' is disabled` }] };
|
|
797
|
+
}
|
|
798
|
+
if (!resolvedAgent) {
|
|
799
|
+
const auth = await getAuthLevel();
|
|
800
|
+
if (auth.agent_id) resolvedAgent = { id: auth.agent_id, name: auth.agent_name || auth.agent_id };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (isBolt11) {
|
|
804
|
+
// Delegate to router via pay_lightning_invoice logic
|
|
805
|
+
if (resolvedAgent) {
|
|
806
|
+
const policy = await checkSpendingPolicy(resolvedAgent.id, amount_sats || 0);
|
|
807
|
+
if (!policy.allowed) return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
|
|
808
|
+
}
|
|
809
|
+
const { status, data } = await apiCall("/api/v1/payments", {
|
|
810
|
+
method: "POST",
|
|
811
|
+
body: JSON.stringify({ invoice: recipient, fee_limit_sats: 100 }),
|
|
812
|
+
});
|
|
813
|
+
if (status === 200 && data.success) {
|
|
814
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, type: "lightning_invoice", amount_sats: data.amount_sats, fee_sats: data.fee_sats || 0, rail: data.rail, payment_hash: data.payment_hash }) }] };
|
|
815
|
+
}
|
|
816
|
+
return { content: [{ type: "text", text: `Payment failed: ${data?.detail || data?.error || JSON.stringify(data)}` }] };
|
|
817
|
+
|
|
818
|
+
} else if (isLightningAddress) {
|
|
819
|
+
if (!amount_sats) return { content: [{ type: "text", text: "Error: amount_sats required for Lightning address payments" }] };
|
|
820
|
+
if (resolvedAgent) {
|
|
821
|
+
const policy = await checkSpendingPolicy(resolvedAgent.id, amount_sats);
|
|
822
|
+
if (!policy.allowed) return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
|
|
823
|
+
}
|
|
824
|
+
// LNURL resolve → pay via router
|
|
825
|
+
const [user, domain] = recipient.split("@");
|
|
826
|
+
const lnurlRes = await fetch(`https://${domain}/.well-known/lnurlp/${user}`);
|
|
827
|
+
if (!lnurlRes.ok) return { content: [{ type: "text", text: `Error: Could not resolve Lightning address ${recipient}` }] };
|
|
828
|
+
const lnurlData = await lnurlRes.json();
|
|
829
|
+
const minSats = Math.ceil((lnurlData.minSendable || 1000) / 1000);
|
|
830
|
+
const maxSats = Math.floor((lnurlData.maxSendable || 100000000000) / 1000);
|
|
831
|
+
if (amount_sats < minSats || amount_sats > maxSats) {
|
|
832
|
+
return { content: [{ type: "text", text: `Error: Amount must be between ${minSats} and ${maxSats} sats for this address` }] };
|
|
833
|
+
}
|
|
834
|
+
let callbackUrl = `${lnurlData.callback}${lnurlData.callback.includes("?") ? "&" : "?"}amount=${amount_sats * 1000}`;
|
|
835
|
+
if (memo && lnurlData.commentAllowed) callbackUrl += `&comment=${encodeURIComponent(memo)}`;
|
|
836
|
+
const invoiceRes = await fetch(callbackUrl);
|
|
837
|
+
if (!invoiceRes.ok) return { content: [{ type: "text", text: `Error: Failed to get invoice from ${domain}` }] };
|
|
838
|
+
const invoiceData = await invoiceRes.json();
|
|
839
|
+
if (!invoiceData.pr) return { content: [{ type: "text", text: `Error: No invoice returned from ${domain}` }] };
|
|
840
|
+
const { status, data } = await apiCall("/api/v1/payments", {
|
|
841
|
+
method: "POST",
|
|
842
|
+
body: JSON.stringify({ invoice: invoiceData.pr, fee_limit_sats: Math.max(100, Math.floor(amount_sats * 0.01)) }),
|
|
843
|
+
});
|
|
844
|
+
if (status === 200 && data.success) {
|
|
845
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, type: "lightning_address", recipient, amount_sats, fee_sats: data.fee_sats || 0, payment_hash: data.payment_hash, message: `Sent ${amount_sats} sats to ${recipient} ⚡` }) }] };
|
|
846
|
+
}
|
|
847
|
+
return { content: [{ type: "text", text: `Payment failed: ${data?.detail || data?.error || JSON.stringify(data)}` }] };
|
|
848
|
+
|
|
849
|
+
} else if (isStrikeHandle) {
|
|
850
|
+
const handle = recipient.slice(1); // strip "$"
|
|
851
|
+
if (!amount_usd && !amount_sats) return { content: [{ type: "text", text: "Error: amount_usd or amount_sats required for Strike payments" }] };
|
|
852
|
+
if (resolvedAgent && amount_sats) {
|
|
853
|
+
const policy = await checkSpendingPolicy(resolvedAgent.id, amount_sats);
|
|
854
|
+
if (!policy.allowed) return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
|
|
855
|
+
}
|
|
856
|
+
const body = { handle, description: memo || `Payment to $${handle}` };
|
|
857
|
+
if (amount_usd) body.amount_usd = amount_usd;
|
|
858
|
+
if (amount_sats) body.amount_sats = amount_sats;
|
|
859
|
+
if (resolvedAgent) body.agent_id = resolvedAgent.id;
|
|
860
|
+
const { status, data } = await apiCall("/api/v1/strike/pay", {
|
|
861
|
+
method: "POST",
|
|
862
|
+
body: JSON.stringify(body),
|
|
863
|
+
});
|
|
864
|
+
if (status === 200 && data.success) {
|
|
865
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, type: "strike_handle", recipient, amount_usd: data.amount_usd, fee_usd: data.fee_usd || 0, payment_id: data.payment_id, message: `Sent to ${recipient} via Strike 💸` }) }] };
|
|
866
|
+
}
|
|
867
|
+
return { content: [{ type: "text", text: `Payment failed: ${data?.detail || data?.error || JSON.stringify(data)}` }] };
|
|
868
|
+
|
|
869
|
+
} else {
|
|
870
|
+
return { content: [{ type: "text", text: `Error: Unrecognized recipient format. Use a BOLT11 invoice (lnbc.../lntb...), Lightning address (user@domain.com), or Strike handle ($username).` }] };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
|
|
783
875
|
// Tool: Decode Lightning invoice
|
|
784
876
|
server.tool(
|
|
785
877
|
"decode_invoice",
|
|
@@ -788,43 +880,37 @@ server.tool(
|
|
|
788
880
|
invoice: z.string().describe("BOLT11 Lightning invoice to decode"),
|
|
789
881
|
},
|
|
790
882
|
async ({ invoice }) => {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
883
|
+
// Route through the AgenticBTC REST API (POST /api/v1/decode) instead of calling
|
|
884
|
+
// LND directly, so the server-side LND credentials are used and no client-side
|
|
885
|
+
// LND config is required.
|
|
794
886
|
try {
|
|
795
|
-
const
|
|
796
|
-
|
|
887
|
+
const { status, data } = await apiCall("/api/v1/decode", {
|
|
888
|
+
method: "POST",
|
|
889
|
+
body: JSON.stringify({ invoice }),
|
|
797
890
|
});
|
|
798
|
-
const data = await res.json();
|
|
799
891
|
|
|
800
|
-
if (!
|
|
801
|
-
|
|
892
|
+
if (status !== 200 || !data.success) {
|
|
893
|
+
const errMsg = data?.detail || data?.error || JSON.stringify(data);
|
|
894
|
+
return { content: [{ type: "text", text: `Error decoding invoice: ${errMsg}` }] };
|
|
802
895
|
}
|
|
803
896
|
|
|
804
|
-
const expiry = parseInt(data.expiry || 3600);
|
|
805
|
-
const timestamp = parseInt(data.timestamp || 0);
|
|
806
|
-
const expiresAt = new Date((timestamp + expiry) * 1000).toISOString();
|
|
807
|
-
const isExpired = Date.now() > (timestamp + expiry) * 1000;
|
|
808
|
-
|
|
809
897
|
return {
|
|
810
898
|
content: [{
|
|
811
899
|
type: "text",
|
|
812
900
|
text: JSON.stringify({
|
|
813
901
|
success: true,
|
|
814
|
-
amount_sats:
|
|
902
|
+
amount_sats: data.amount_sats,
|
|
815
903
|
destination: data.destination,
|
|
816
904
|
description: data.description || "",
|
|
817
905
|
payment_hash: data.payment_hash,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
cltv_expiry: parseInt(data.cltv_expiry || 0),
|
|
822
|
-
num_route_hints: (data.route_hints || []).length,
|
|
906
|
+
expires_at: data.expires_at ? new Date(data.expires_at * 1000).toISOString() : null,
|
|
907
|
+
is_expired: data.is_expired,
|
|
908
|
+
cltv_expiry: data.cltv_expiry,
|
|
823
909
|
}, null, 2),
|
|
824
910
|
}],
|
|
825
911
|
};
|
|
826
912
|
} catch (e) {
|
|
827
|
-
return { content: [{ type: "text", text: `
|
|
913
|
+
return { content: [{ type: "text", text: `Router error: ${e.message}` }] };
|
|
828
914
|
}
|
|
829
915
|
}
|
|
830
916
|
);
|
|
@@ -1388,6 +1474,114 @@ server.tool(
|
|
|
1388
1474
|
}
|
|
1389
1475
|
);
|
|
1390
1476
|
|
|
1477
|
+
// ─── X (Twitter) Tools ────────────────────────────────────────────────────────
|
|
1478
|
+
|
|
1479
|
+
server.tool(
|
|
1480
|
+
"post_tweet",
|
|
1481
|
+
"Post a tweet from @agentbtcio. Use for announcing launches, updates, or responding to relevant discussions. Keep under 280 chars.",
|
|
1482
|
+
{
|
|
1483
|
+
text: z.string().max(280).describe("Tweet text (max 280 characters)"),
|
|
1484
|
+
reply_to_tweet_id: z.string().optional().describe("Tweet ID to reply to (optional)"),
|
|
1485
|
+
},
|
|
1486
|
+
async ({ text, reply_to_tweet_id }) => {
|
|
1487
|
+
if (!X_ENABLED) {
|
|
1488
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X credentials not configured. Set TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET in Claude Desktop config." }) }] };
|
|
1489
|
+
}
|
|
1490
|
+
const body = { text };
|
|
1491
|
+
if (reply_to_tweet_id) body.reply = { in_reply_to_tweet_id: reply_to_tweet_id };
|
|
1492
|
+
const { status, data } = await xPost("/2/tweets", body);
|
|
1493
|
+
if (status === 201) {
|
|
1494
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, tweet_id: data.data.id, url: `https://x.com/agentbtcio/status/${data.data.id}`, text }) }] };
|
|
1495
|
+
}
|
|
1496
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.detail || data.title || "Post failed", status }) }] };
|
|
1497
|
+
}
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
server.tool(
|
|
1501
|
+
"get_mentions",
|
|
1502
|
+
"Get recent @mentions and replies to @agentbtcio. Use to monitor engagement and find conversations to join.",
|
|
1503
|
+
{
|
|
1504
|
+
limit: z.number().int().min(1).max(20).default(10).describe("Number of recent mentions to fetch (max 20)"),
|
|
1505
|
+
},
|
|
1506
|
+
async ({ limit }) => {
|
|
1507
|
+
if (!X_BEARER_TOKEN || !X_USER_ID) {
|
|
1508
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X bearer token or user ID not configured." }) }] };
|
|
1509
|
+
}
|
|
1510
|
+
const { status, data } = await xGet(
|
|
1511
|
+
`/2/users/${X_USER_ID}/mentions?max_results=${limit}&tweet.fields=created_at,author_id,conversation_id&expansions=author_id&user.fields=username`
|
|
1512
|
+
);
|
|
1513
|
+
if (status === 200) {
|
|
1514
|
+
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u.username]));
|
|
1515
|
+
const mentions = (data.data || []).map(t => ({
|
|
1516
|
+
id: t.id,
|
|
1517
|
+
text: t.text,
|
|
1518
|
+
from: "@" + (users[t.author_id] || t.author_id),
|
|
1519
|
+
created_at: t.created_at,
|
|
1520
|
+
url: `https://x.com/i/status/${t.id}`,
|
|
1521
|
+
}));
|
|
1522
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, count: mentions.length, mentions }) }] };
|
|
1523
|
+
}
|
|
1524
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.title || "Failed to fetch mentions", status }) }] };
|
|
1525
|
+
}
|
|
1526
|
+
);
|
|
1527
|
+
|
|
1528
|
+
server.tool(
|
|
1529
|
+
"search_x",
|
|
1530
|
+
"Search recent X posts by keyword. Use to find conversations about AI agents, Lightning Network, Bitcoin payments, or AgenticBTC.",
|
|
1531
|
+
{
|
|
1532
|
+
query: z.string().describe("Search query (e.g. 'AI agents Lightning payments', 'agenticbtc', 'MCP tools bitcoin')"),
|
|
1533
|
+
limit: z.number().int().min(1).max(20).default(10).describe("Number of results (max 20)"),
|
|
1534
|
+
},
|
|
1535
|
+
async ({ query, limit }) => {
|
|
1536
|
+
if (!X_BEARER_TOKEN) {
|
|
1537
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X bearer token not configured." }) }] };
|
|
1538
|
+
}
|
|
1539
|
+
const params = new URLSearchParams({ query: `${query} -is:retweet lang:en`, max_results: limit, "tweet.fields": "created_at,author_id", expansions: "author_id", "user.fields": "username" });
|
|
1540
|
+
const { status, data } = await xGet(`/2/tweets/search/recent?${params}`);
|
|
1541
|
+
if (status === 200) {
|
|
1542
|
+
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u.username]));
|
|
1543
|
+
const tweets = (data.data || []).map(t => ({
|
|
1544
|
+
id: t.id,
|
|
1545
|
+
text: t.text,
|
|
1546
|
+
from: "@" + (users[t.author_id] || t.author_id),
|
|
1547
|
+
created_at: t.created_at,
|
|
1548
|
+
url: `https://x.com/i/status/${t.id}`,
|
|
1549
|
+
}));
|
|
1550
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, query, count: tweets.length, tweets }) }] };
|
|
1551
|
+
}
|
|
1552
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.title || "Search failed", status }) }] };
|
|
1553
|
+
}
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
server.tool(
|
|
1557
|
+
"get_own_timeline",
|
|
1558
|
+
"Get recent tweets posted by @agentbtcio. Use to review posting history before drafting new content.",
|
|
1559
|
+
{
|
|
1560
|
+
limit: z.number().int().min(1).max(20).default(10).describe("Number of recent tweets to fetch"),
|
|
1561
|
+
},
|
|
1562
|
+
async ({ limit }) => {
|
|
1563
|
+
if (!X_BEARER_TOKEN || !X_USER_ID) {
|
|
1564
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "X credentials not configured." }) }] };
|
|
1565
|
+
}
|
|
1566
|
+
const { status, data } = await xGet(
|
|
1567
|
+
`/2/users/${X_USER_ID}/tweets?max_results=${limit}&tweet.fields=created_at,public_metrics&exclude=retweets,replies`
|
|
1568
|
+
);
|
|
1569
|
+
if (status === 200) {
|
|
1570
|
+
const tweets = (data.data || []).map(t => ({
|
|
1571
|
+
id: t.id,
|
|
1572
|
+
text: t.text,
|
|
1573
|
+
created_at: t.created_at,
|
|
1574
|
+
likes: t.public_metrics?.like_count || 0,
|
|
1575
|
+
retweets: t.public_metrics?.retweet_count || 0,
|
|
1576
|
+
replies: t.public_metrics?.reply_count || 0,
|
|
1577
|
+
url: `https://x.com/agentbtcio/status/${t.id}`,
|
|
1578
|
+
}));
|
|
1579
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, count: tweets.length, tweets }) }] };
|
|
1580
|
+
}
|
|
1581
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: data.title || "Failed to fetch timeline", status }) }] };
|
|
1582
|
+
}
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1391
1585
|
// Start server
|
|
1392
1586
|
const transport = new StdioServerTransport();
|
|
1393
1587
|
await server.connect(transport);
|