agenticbtc-mcp 1.0.9 → 1.0.11
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 +141 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agenticbtc-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
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
|
@@ -11,8 +11,8 @@ import crypto from "crypto";
|
|
|
11
11
|
|
|
12
12
|
const API_URL = process.env.AGENTICBTC_API_URL || process.env.AGENTBTC_API_URL || "http://localhost:8000";
|
|
13
13
|
const API_KEY = process.env.AGENTICBTC_API_KEY || process.env.AGENTBTC_API_KEY || "";
|
|
14
|
-
const LND_HOST = process.env.AGENTBTC_LND_HOST || "";
|
|
15
|
-
const LND_MACAROON = process.env.AGENTBTC_LND_MACAROON || "";
|
|
14
|
+
const LND_HOST = process.env.AGENTICBTC_LND_HOST || process.env.AGENTBTC_LND_HOST || "";
|
|
15
|
+
const LND_MACAROON = process.env.AGENTICBTC_LND_MACAROON || process.env.AGENTBTC_LND_MACAROON || "";
|
|
16
16
|
|
|
17
17
|
// X (Twitter) credentials — optional, enables social posting tools
|
|
18
18
|
const X_CONSUMER_KEY = process.env.TWITTER_CONSUMER_KEY || "";
|
|
@@ -771,6 +771,145 @@ server.tool(
|
|
|
771
771
|
}
|
|
772
772
|
);
|
|
773
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
|
+
const satsToSend = amount_sats || 0;
|
|
853
|
+
if (resolvedAgent && satsToSend) {
|
|
854
|
+
const policy = await checkSpendingPolicy(resolvedAgent.id, satsToSend);
|
|
855
|
+
if (!policy.allowed) return { content: [{ type: "text", text: `Payment blocked: ${policy.reason}` }] };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Try Lightning-first: resolve handle@strike.me via LNURL
|
|
859
|
+
if (satsToSend > 0) {
|
|
860
|
+
try {
|
|
861
|
+
const lightningAddr = `${handle}@strike.me`;
|
|
862
|
+
const lnurlRes = await fetch(`https://strike.me/.well-known/lnurlp/${handle}`, { signal: AbortSignal.timeout(5000) });
|
|
863
|
+
if (lnurlRes.ok) {
|
|
864
|
+
const lnurlData = await lnurlRes.json();
|
|
865
|
+
if (lnurlData.status !== "ERROR") {
|
|
866
|
+
const minSats = Math.ceil((lnurlData.minSendable || 1000) / 1000);
|
|
867
|
+
const maxSats = Math.floor((lnurlData.maxSendable || 100000000000) / 1000);
|
|
868
|
+
if (satsToSend >= minSats && satsToSend <= maxSats) {
|
|
869
|
+
let callbackUrl = `${lnurlData.callback}${lnurlData.callback.includes("?") ? "&" : "?"}amount=${satsToSend * 1000}`;
|
|
870
|
+
if (memo && lnurlData.commentAllowed) callbackUrl += `&comment=${encodeURIComponent(memo)}`;
|
|
871
|
+
const invoiceRes = await fetch(callbackUrl, { signal: AbortSignal.timeout(5000) });
|
|
872
|
+
if (invoiceRes.ok) {
|
|
873
|
+
const invoiceData = await invoiceRes.json();
|
|
874
|
+
if (invoiceData.pr) {
|
|
875
|
+
const { status: payStatus, data: payData } = await apiCall("/api/v1/payments", {
|
|
876
|
+
method: "POST",
|
|
877
|
+
body: JSON.stringify({ invoice: invoiceData.pr, fee_limit_sats: Math.max(100, Math.floor(satsToSend * 0.01)) }),
|
|
878
|
+
});
|
|
879
|
+
if (payStatus === 200 && payData.success) {
|
|
880
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, type: "strike_handle_via_lightning", recipient, lightning_address: lightningAddr, amount_sats: satsToSend, fee_sats: payData.fee_sats || 0, payment_hash: payData.payment_hash, message: `Sent ${satsToSend} sats to ${recipient} via Lightning (${lightningAddr}) ⚡ — cheaper than Strike API` }) }] };
|
|
881
|
+
}
|
|
882
|
+
// Lightning failed — fall through to Strike API
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
} catch (_) {
|
|
889
|
+
// LNURL resolution failed — fall through to Strike API
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Fall back to Strike API
|
|
894
|
+
const body = { handle, description: memo || `Payment to $${handle}` };
|
|
895
|
+
if (amount_usd) body.amount_usd = amount_usd;
|
|
896
|
+
if (satsToSend) body.amount_sats = satsToSend;
|
|
897
|
+
if (resolvedAgent) body.agent_id = resolvedAgent.id;
|
|
898
|
+
const { status, data } = await apiCall("/api/v1/strike/pay", {
|
|
899
|
+
method: "POST",
|
|
900
|
+
body: JSON.stringify(body),
|
|
901
|
+
});
|
|
902
|
+
if (status === 200 && data.success) {
|
|
903
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, type: "strike_handle_via_api", recipient, amount_usd: data.amount_usd, fee_usd: data.fee_usd || 0, payment_id: data.payment_id, message: `Sent to ${recipient} via Strike API 💸 (Lightning not available for this recipient)` }) }] };
|
|
904
|
+
}
|
|
905
|
+
return { content: [{ type: "text", text: `Payment failed: ${data?.detail || data?.error || JSON.stringify(data)}` }] };
|
|
906
|
+
|
|
907
|
+
} else {
|
|
908
|
+
return { content: [{ type: "text", text: `Error: Unrecognized recipient format. Use a BOLT11 invoice (lnbc.../lntb...), Lightning address (user@domain.com), or Strike handle ($username).` }] };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
);
|
|
912
|
+
|
|
774
913
|
// Tool: Decode Lightning invoice
|
|
775
914
|
server.tool(
|
|
776
915
|
"decode_invoice",
|