@aibtc/mcp-server 1.73.1 → 1.75.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.
@@ -126,6 +126,12 @@ export type PaymentTokenType = 'STX' | 'sBTC' | 'unsupported';
126
126
  * @param asset - Full contract identifier or token name
127
127
  */
128
128
  export declare function detectTokenType(asset: string): PaymentTokenType;
129
+ /**
130
+ * Memo for an x402 payment from the server's `extra.payment_id`, cut to the
131
+ * first 34 UTF-8 bytes (the Stacks memo limit) on a character boundary.
132
+ * Gateways that bind payments on the memo match that prefix.
133
+ */
134
+ export declare function paymentIdMemo(extra?: Record<string, unknown>): string | undefined;
129
135
  /**
130
136
  * Best-effort display symbol for an asset. Servers advertise a friendly name in
131
137
  * `extra.tokenType` (arc0btc sends "USDCx"); otherwise fall back to the contract
@@ -1,5 +1,5 @@
1
1
  import axios from "axios";
2
- import { makeSTXTokenTransfer, makeContractCall, uintCV, principalCV, noneCV, Pc, PostConditionMode, } from "@stacks/transactions";
2
+ import { makeSTXTokenTransfer, makeContractCall, uintCV, principalCV, noneCV, someCV, bufferCVFromString, Pc, PostConditionMode, } from "@stacks/transactions";
3
3
  import { decodePaymentRequired, encodePaymentPayload, generatePaymentId, buildPaymentIdentifierExtension, X402_HEADERS, } from "../utils/x402-protocol.js";
4
4
  import { generateWallet, getStxAddress } from "@stacks/wallet-sdk";
5
5
  import { NETWORK, API_URL, getStacksNetwork } from "../config/networks.js";
@@ -681,6 +681,7 @@ export async function createApiClient(baseUrl, options) {
681
681
  // — this is the same tx shape the x402-stacks lib produces, and the
682
682
  // balance check in onBeforePayment already accounts for the gas.
683
683
  const networkName = getStacksNetwork(acct.network);
684
+ const memo = paymentIdMemo(selectedOption.extra);
684
685
  let transaction;
685
686
  if (tokenType === "sBTC") {
686
687
  const contracts = getContracts(acct.network);
@@ -693,7 +694,7 @@ export async function createApiClient(baseUrl, options) {
693
694
  uintCV(amount),
694
695
  principalCV(acct.address),
695
696
  principalCV(selectedOption.payTo),
696
- noneCV(),
697
+ memo ? someCV(bufferCVFromString(memo)) : noneCV(),
697
698
  ],
698
699
  senderKey: acct.privateKey,
699
700
  network: networkName,
@@ -717,7 +718,7 @@ export async function createApiClient(baseUrl, options) {
717
718
  amount,
718
719
  senderKey: acct.privateKey,
719
720
  network: networkName,
720
- memo: "",
721
+ memo: memo ?? "",
721
722
  fee: await resolveDefaultFee(acct.network, "token_transfer"),
722
723
  });
723
724
  }
@@ -813,6 +814,27 @@ export function detectTokenType(asset) {
813
814
  }
814
815
  return 'unsupported';
815
816
  }
817
+ const STACKS_MEMO_MAX_BYTES = 34;
818
+ /**
819
+ * Memo for an x402 payment from the server's `extra.payment_id`, cut to the
820
+ * first 34 UTF-8 bytes (the Stacks memo limit) on a character boundary.
821
+ * Gateways that bind payments on the memo match that prefix.
822
+ */
823
+ export function paymentIdMemo(extra) {
824
+ const id = extra?.payment_id;
825
+ if (typeof id !== "string" || id.length === 0)
826
+ return undefined;
827
+ let memo = "";
828
+ let bytes = 0;
829
+ for (const ch of id) {
830
+ const size = Buffer.byteLength(ch, "utf8");
831
+ if (bytes + size > STACKS_MEMO_MAX_BYTES)
832
+ break;
833
+ memo += ch;
834
+ bytes += size;
835
+ }
836
+ return memo;
837
+ }
816
838
  /**
817
839
  * Best-effort display symbol for an asset. Servers advertise a friendly name in
818
840
  * `extra.tokenType` (arc0btc sends "USDCx"); otherwise fall back to the contract
@@ -1,29 +1,23 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
2
  /**
3
- * Pre-flight balance check for a non-sponsored sBTC inbox payment.
4
- * Verifies the wallet holds enough sBTC for the message AND enough STX for a
5
- * realistic transfer fee (the sender pays its own gas).
3
+ * Pre-flight balance check for an sBTC inbox payment.
4
+ * Verifies the wallet holds enough sBTC for the message, and — unless the
5
+ * payment is sponsored — enough STX for a realistic transfer fee.
6
6
  */
7
- export declare function checkDirectInboxBalance(address: string, amount: string): Promise<void>;
7
+ export declare function checkDirectInboxBalance(address: string, amount: string, sponsored?: boolean): Promise<void>;
8
8
  /**
9
- * Direct (non-sponsored) x402 inbox messaging.
9
+ * x402 inbox messaging via the `x402-stacks` client interceptor
10
+ * (`wrapAxiosWithPayment`). The transaction is signed but not broadcast; the
11
+ * inbox endpoint settles it through its x402 facilitator.
10
12
  *
11
- * Unlike `send_inbox_message` — which builds a *sponsored* sBTC transfer
12
- * (`sponsored: true`, `fee: 0n`) so the aibtc relay co-signs and pays the STX
13
- * gas — this tool uses the standard `x402-stacks` client interceptor
14
- * (`wrapAxiosWithPayment`). That signs a normal standard-auth sBTC transfer
15
- * where the *sender pays its own STX gas*. The transaction is signed but not
16
- * broadcast; the inbox endpoint settles it via the x402 facilitator's
17
- * `/settle` flow (no relay sponsorship).
13
+ * The inbox decides who pays gas. When its 402 advertises `extra.feePayer`,
14
+ * x402-stacks (>=2.1.0) signs a sponsored sBTC transfer with fee 0 and the
15
+ * facilitator co-signs and pays the STX gas. Without it, the SDK signs a
16
+ * standard transfer and the sender pays its own gas.
18
17
  *
19
18
  * Server side: `lib/inbox/x402-verify.ts` deserializes the payment tx and
20
- * branches on `tx.auth.authType`. A standard-auth tx (what x402-stacks signs)
21
- * takes the non-sponsored `X402PaymentVerifier.settle()` path — the relay never
22
- * adds a sponsor signature or pays gas.
23
- *
24
- * Trade-off vs the sponsored tool: the sender must hold STX to cover the
25
- * transfer fee (the pre-flight check enforces this). In exchange there is no
26
- * relay in the middle of the payment authorization.
19
+ * branches on `tx.auth.authType` (sponsored → relay sponsors; standard →
20
+ * relay `/settle` broadcasts as-is).
27
21
  */
28
22
  export declare function registerInboxX402Tools(server: McpServer): void;
29
23
  //# sourceMappingURL=inbox-x402.tools.d.ts.map
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import axios, { AxiosError } from "axios";
3
3
  import { wrapAxiosWithPayment, decodePaymentRequired, decodePaymentResponse, X402_HEADERS, } from "x402-stacks";
4
4
  import { getAccount, NETWORK } from "../services/x402.service.js";
5
+ import { getSpendLimiter } from "../services/spend-limiter.js";
5
6
  import { getSbtcService } from "../services/sbtc.service.js";
6
7
  import { getHiroApi } from "../services/hiro-api.js";
7
8
  import { getExplorerTxUrl } from "../config/networks.js";
@@ -20,11 +21,11 @@ const INBOX_BASE = "https://aibtc.com/api/inbox";
20
21
  */
21
22
  const MAX_REALISTIC_FEE_USTX = 3000n; // 0.003 STX — the sbtc_transfer fee ceiling
22
23
  /**
23
- * Pre-flight balance check for a non-sponsored sBTC inbox payment.
24
- * Verifies the wallet holds enough sBTC for the message AND enough STX for a
25
- * realistic transfer fee (the sender pays its own gas).
24
+ * Pre-flight balance check for an sBTC inbox payment.
25
+ * Verifies the wallet holds enough sBTC for the message, and — unless the
26
+ * payment is sponsored — enough STX for a realistic transfer fee.
26
27
  */
27
- export async function checkDirectInboxBalance(address, amount) {
28
+ export async function checkDirectInboxBalance(address, amount, sponsored = false) {
28
29
  const sbtcService = getSbtcService(NETWORK);
29
30
  const sbtcBalance = BigInt((await sbtcService.getBalance(address)).balance);
30
31
  const required = BigInt(amount);
@@ -33,6 +34,9 @@ export async function checkDirectInboxBalance(address, amount) {
33
34
  throw new InsufficientBalanceError(`Insufficient sBTC balance: need ${formatSbtc(amount)}, have ${formatSbtc(sbtcBalance.toString())} (shortfall: ${formatSbtc(shortfall.toString())}). ` +
34
35
  `Deposit more sBTC via the bridge at https://bridge.stx.eco or use a different wallet.`, "sBTC", sbtcBalance.toString(), amount, shortfall.toString());
35
36
  }
37
+ // Sponsored: the facilitator pays the gas, so no STX is needed.
38
+ if (sponsored)
39
+ return;
36
40
  const hiro = getHiroApi(NETWORK);
37
41
  const stxBalance = BigInt((await hiro.getStxBalance(address)).balance);
38
42
  const fees = await hiro.getMempoolFees();
@@ -53,33 +57,28 @@ export async function checkDirectInboxBalance(address, amount) {
53
57
  }
54
58
  }
55
59
  /**
56
- * Direct (non-sponsored) x402 inbox messaging.
60
+ * x402 inbox messaging via the `x402-stacks` client interceptor
61
+ * (`wrapAxiosWithPayment`). The transaction is signed but not broadcast; the
62
+ * inbox endpoint settles it through its x402 facilitator.
57
63
  *
58
- * Unlike `send_inbox_message` — which builds a *sponsored* sBTC transfer
59
- * (`sponsored: true`, `fee: 0n`) so the aibtc relay co-signs and pays the STX
60
- * gas — this tool uses the standard `x402-stacks` client interceptor
61
- * (`wrapAxiosWithPayment`). That signs a normal standard-auth sBTC transfer
62
- * where the *sender pays its own STX gas*. The transaction is signed but not
63
- * broadcast; the inbox endpoint settles it via the x402 facilitator's
64
- * `/settle` flow (no relay sponsorship).
64
+ * The inbox decides who pays gas. When its 402 advertises `extra.feePayer`,
65
+ * x402-stacks (>=2.1.0) signs a sponsored sBTC transfer with fee 0 and the
66
+ * facilitator co-signs and pays the STX gas. Without it, the SDK signs a
67
+ * standard transfer and the sender pays its own gas.
65
68
  *
66
69
  * Server side: `lib/inbox/x402-verify.ts` deserializes the payment tx and
67
- * branches on `tx.auth.authType`. A standard-auth tx (what x402-stacks signs)
68
- * takes the non-sponsored `X402PaymentVerifier.settle()` path — the relay never
69
- * adds a sponsor signature or pays gas.
70
- *
71
- * Trade-off vs the sponsored tool: the sender must hold STX to cover the
72
- * transfer fee (the pre-flight check enforces this). In exchange there is no
73
- * relay in the middle of the payment authorization.
70
+ * branches on `tx.auth.authType` (sponsored → relay sponsors; standard →
71
+ * relay `/settle` broadcasts as-is).
74
72
  */
75
73
  export function registerInboxX402Tools(server) {
76
74
  server.registerTool("send_inbox_message_direct", {
77
75
  description: "Send a paid x402 message to another agent's inbox on aibtc.com. This is the canonical inbox " +
78
76
  "send tool (the older sponsored send_inbox_message is deprecated).\n\n" +
79
- "It signs a standard sBTC transfer with the x402-stacks client interceptor — you pay BOTH the sBTC " +
80
- "message cost AND your own STX gas fee. No relay sits in the middle of the payment; the inbox settles " +
81
- "the signed transaction via the x402 facilitator.\n\n" +
82
- "Requires an unlocked wallet holding sBTC (message cost) and STX (gas). Mainnet only.",
77
+ "It signs an sBTC transfer with the x402-stacks client interceptor and the inbox settles it via its " +
78
+ "x402 facilitator. When the inbox offers sponsorship (402 `extra.feePayer`), the transfer is gasless — " +
79
+ "you pay only the sBTC message cost. Otherwise you also pay your own STX gas fee.\n\n" +
80
+ "Requires an unlocked wallet holding sBTC (message cost), plus STX for gas when the inbox does not " +
81
+ "sponsor. Mainnet only.",
83
82
  inputSchema: z.object({
84
83
  recipientBtcAddress: z
85
84
  .string()
@@ -135,13 +134,19 @@ export function registerInboxX402Tools(server) {
135
134
  throw new Error("402 response missing or empty payment-required header");
136
135
  }
137
136
  const accept = paymentRequired.accepts[0];
138
- // Step 2: Pre-flight balance check — sBTC for the message AND a
139
- // realistic STX gas budget (sender pays its own fee).
140
- await checkDirectInboxBalance(account.address, accept.amount);
137
+ // The SDK signs a sponsored (fee 0) transfer exactly when the
138
+ // requirement names a fee payer — mirror its check.
139
+ const sponsored = typeof accept.extra?.feePayer === "string";
140
+ // Step 2: Pre-flight balance check — sBTC for the message, plus a
141
+ // realistic STX gas budget when the sender pays its own fee.
142
+ await checkDirectInboxBalance(account.address, accept.amount, sponsored);
143
+ // The message cost is a signed sBTC spend: meter it before signing.
144
+ const spendLimiter = getSpendLimiter();
145
+ await spendLimiter.check("sats", BigInt(accept.amount), account.address);
141
146
  // Step 3: Build a payment-enabled axios client. wrapAxiosWithPayment
142
- // installs the x402-stacks interceptor: on a 402 it signs a standard
143
- // (non-sponsored) sBTC transfer and retries with the payment-signature
144
- // header. The sender's wallet pays the gas.
147
+ // installs the x402-stacks interceptor: on a 402 it signs the sBTC
148
+ // transfer (sponsored or standard, per `extra.feePayer`) and retries
149
+ // with the payment-signature header.
145
150
  const stacksAccount = {
146
151
  address: account.address,
147
152
  privateKey: account.privateKey,
@@ -152,13 +157,16 @@ export function registerInboxX402Tools(server) {
152
157
  const response = await api.post(inboxUrl, body, {
153
158
  headers: { "Content-Type": "application/json" },
154
159
  });
160
+ await spendLimiter.record("sats", BigInt(accept.amount), account.address);
155
161
  // Step 5: Decode the settlement (txid + payer) from the response header.
156
162
  const settlement = decodePaymentResponse(response.headers[X402_HEADERS.PAYMENT_RESPONSE]);
157
163
  const txid = settlement?.transaction;
158
164
  const payer = settlement?.payer;
159
165
  return createJsonResponse({
160
166
  success: true,
161
- message: "Message delivered (direct non-sponsored x402)",
167
+ message: sponsored
168
+ ? "Message delivered (x402, gas sponsored)"
169
+ : "Message delivered (x402, sender paid gas)",
162
170
  recipient: {
163
171
  btcAddress: recipientBtcAddress,
164
172
  stxAddress: recipientStxAddress,
@@ -166,14 +174,16 @@ export function registerInboxX402Tools(server) {
166
174
  contentLength: content.length,
167
175
  inbox: response.data,
168
176
  payment: {
169
- mode: "direct-x402-nonsponsored",
177
+ mode: sponsored ? "x402-sponsored" : "direct-x402-nonsponsored",
170
178
  amount: accept.amount + " sats sBTC",
171
179
  ...(payer && { payer }),
172
180
  ...(txid && {
173
181
  txid,
174
182
  explorer: getExplorerTxUrl(txid, NETWORK),
175
183
  }),
176
- note: "Sender paid its own STX gas — no relay sponsorship.",
184
+ note: sponsored
185
+ ? `STX gas paid by the inbox facilitator (${accept.extra?.feePayer}).`
186
+ : "Sender paid its own STX gas — no relay sponsorship.",
177
187
  },
178
188
  });
179
189
  }
@@ -7,9 +7,9 @@ import { createJsonResponse } from "../utils/index.js";
7
7
  // was unstable: a burst of sends with an uninitialized local nonce could wedge
8
8
  // the relay's sponsor queue (nonces stuck `held(gap)`), leaving payments
9
9
  // accepted but messages undelivered (issue #540/#592). All inbox sends now go
10
- // through send_inbox_message_direct, which signs a standard non-sponsored sBTC
11
- // transfer and settles directly through the x402 facilitator with no relay in
12
- // the middle.
10
+ // through send_inbox_message_direct, which is gasless: x402-stacks signs a
11
+ // sponsored transfer when the inbox advertises a fee payer, and the relay
12
+ // sponsors it in one step (aibtcdev/x402-sponsor-relay#436).
13
13
  //
14
14
  // This tool is retained only as a redirect so existing agents that still call
15
15
  // send_inbox_message get pointed to the working tool instead of an
@@ -17,10 +17,8 @@ import { createJsonResponse } from "../utils/index.js";
17
17
  // ============================================================================
18
18
  export function registerInboxTools(server) {
19
19
  server.registerTool("send_inbox_message", {
20
- description: "⛔ DEPRECATED — do not use. The sponsored (relay) inbox send has been removed. " +
21
- "Use send_inbox_message_direct instead. It signs a standard sBTC transfer and settles directly " +
22
- "through the x402 facilitator (no relay). Requires an unlocked wallet holding sBTC (message cost) " +
23
- "and STX (gas). Mainnet only.",
20
+ description: "⛔ DEPRECATED — do not use. Use send_inbox_message_direct instead: it is gasless (the relay " +
21
+ "pays the STX gas), so you need only the sBTC message cost in an unlocked wallet. Mainnet only.",
24
22
  inputSchema: z.object({
25
23
  recipientBtcAddress: z
26
24
  .string()
@@ -37,12 +35,10 @@ export function registerInboxTools(server) {
37
35
  return createJsonResponse({
38
36
  success: false,
39
37
  deprecated: true,
40
- error: "send_inbox_message has been removed. The sponsored relay path was unstable " +
41
- "(payments could be accepted but never delivered when the relay's sponsor nonce " +
42
- "queue wedged).",
38
+ error: "send_inbox_message has been removed.",
43
39
  useInstead: "send_inbox_message_direct",
44
40
  note: "Call send_inbox_message_direct with the same recipientBtcAddress, recipientStxAddress, " +
45
- "and content. It pays both the sBTC message cost and its own STX gas, with no relay in the middle.",
41
+ "and content. It is gasless — the relay pays the STX gas; you need only the sBTC message cost.",
46
42
  });
47
43
  });
48
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibtc/mcp-server",
3
- "version": "1.73.1",
3
+ "version": "1.75.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",
@@ -74,7 +74,7 @@
74
74
  "sbtc": "^0.3.2",
75
75
  "tslib": "^2.8.1",
76
76
  "ws": "^8.21.0",
77
- "x402-stacks": "^2.0.3",
77
+ "x402-stacks": "^2.1.0",
78
78
  "zod": "^4.6.5"
79
79
  },
80
80
  "devDependencies": {
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.73.1 # x-release-please-version
7
+ version: 1.75.0 # x-release-please-version
8
8
  npm: "@aibtc/mcp-server"
9
9
  github: https://github.com/aibtcdev/aibtc-mcp-server
10
10
  ---
@@ -206,8 +206,9 @@ Always probe before executing paid endpoints. Never call `execute_x402_endpoint`
206
206
 
207
207
  **send_inbox_message_direct** — dedicated tool for aibtc.com inbox messages:
208
208
  - Parameters: `recipientBtcAddress` (bc1...), `recipientStxAddress` (SP...), `content` (max 500 chars)
209
- - Direct (non-sponsored) payment: signs a standard sBTC transfer and settles through the x402 facilitator — no relay in the middle
210
- - Sender pays BOTH the sBTC message cost AND its own STX gas; requires an unlocked wallet holding sBTC and STX (mainnet only)
209
+ - Gasless: the inbox advertises a fee payer, so the tool signs a sponsored sBTC transfer (fee 0) and the relay pays the STX gas
210
+ - Requires an unlocked wallet holding the sBTC message cost (100 sats) — no STX needed (mainnet only)
211
+ - One pending payment per sender: wait for the previous message's payment to confirm before sending the next
211
212
  - Implements the full x402 v2 payment flow with a balance pre-check
212
213
  - Note: the older sponsored `send_inbox_message` tool is deprecated and no longer sends — use this tool instead
213
214
 
@@ -115,15 +115,20 @@ send_inbox_message_direct({
115
115
  })
116
116
  ```
117
117
 
118
- It signs a standard sBTC transfer with the x402-stacks client interceptor and settles the
119
- payment directly through the x402 facilitator — there is no relay in the middle. The sender
120
- pays BOTH the sBTC message cost AND its own STX gas, so the wallet must be unlocked and hold
121
- sBTC and STX. Mainnet only. The tool runs a balance pre-check before signing and fails clearly
122
- if STX gas is short.
123
-
124
- > **Deprecated:** the older sponsored `send_inbox_message` tool no longer sends. It returns a
125
- > message directing callers to `send_inbox_message_direct`. The relay-sponsored path was removed
126
- > because sponsored transactions were unstable.
118
+ Inbox sends are **gasless**. The inbox's 402 advertises `extra.feePayer` (the relay's sponsor
119
+ address), so the x402-stacks interceptor signs a sponsored sBTC transfer with fee 0 and the relay
120
+ pays the STX gas. The wallet must be unlocked and hold the sBTC message cost (100 sats) — no STX
121
+ needed. Mainnet only. The tool runs a balance pre-check before signing. If the inbox ever stops
122
+ advertising `feePayer`, the same tool signs a standard transfer and the sender pays its own gas.
123
+
124
+ Sponsored sends follow a few rules (the inbox returns a clear error otherwise):
125
+ - **One pending payment per sender** — wait for the previous inbox payment to confirm (seconds)
126
+ before sending the next; a second one while it's pending gets `SENDER_NONCE_DUPLICATE` (409).
127
+ - **Up to 10 sponsored payments per sender per minute** (`RATE_LIMITED`, 429).
128
+ - **No messages to yourself** — a transfer to your own address fails on-chain, so it's refused.
129
+
130
+ > **Deprecated:** the older `send_inbox_message` tool no longer sends. It returns a message
131
+ > directing callers to `send_inbox_message_direct`, which is now the gasless path.
127
132
 
128
133
  ## More Information
129
134