@aibtc/mcp-server 1.74.0 → 1.76.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.
@@ -45,6 +45,8 @@ export interface PaymentRequirements {
45
45
  recipient: string;
46
46
  network: string;
47
47
  account: Account;
48
+ /** True when the endpoint advertised `extra.sponsored` — the relay pays the STX fee. */
49
+ sponsored: boolean;
48
50
  }
49
51
  /**
50
52
  * Options for createApiClient
@@ -126,6 +128,12 @@ export type PaymentTokenType = 'STX' | 'sBTC' | 'unsupported';
126
128
  * @param asset - Full contract identifier or token name
127
129
  */
128
130
  export declare function detectTokenType(asset: string): PaymentTokenType;
131
+ /**
132
+ * Memo for an x402 payment from the server's `extra.payment_id`, cut to the
133
+ * first 34 UTF-8 bytes (the Stacks memo limit) on a character boundary.
134
+ * Gateways that bind payments on the memo match that prefix.
135
+ */
136
+ export declare function paymentIdMemo(extra?: Record<string, unknown>): string | undefined;
129
137
  /**
130
138
  * Best-effort display symbol for an asset. Servers advertise a friendly name in
131
139
  * `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";
@@ -525,11 +525,11 @@ export async function createApiClient(baseUrl, options) {
525
525
  }
526
526
  // Cumulative spending limit (sats ledger) — bounds a drain-by-loop of
527
527
  // sub-cap L402 invoices. Keyed by the wallet's Stacks address so all sats
528
- // spends (BTC L1, sBTC, L402) share one ledger.
529
- const l402Addr = getWalletManager().getActiveAccount()?.address;
530
- if (l402Addr) {
531
- await getSpendLimiter().check("sats", BigInt(amountSats), l402Addr);
532
- }
528
+ // spends (BTC L1, sBTC, L402) share one ledger, or the __lightning__
529
+ // bucket when the STX wallet is locked (as lightning_pay_invoice does).
530
+ // Kept even if the payment throws: that does not prove it did not route.
531
+ const l402Addr = getWalletManager().getActiveAccount()?.address ?? "__lightning__";
532
+ await getSpendLimiter().reserve([{ unit: "sats", amount: BigInt(amountSats) }], l402Addr);
533
533
  // Pay the Lightning invoice.
534
534
  let payment;
535
535
  try {
@@ -538,9 +538,6 @@ export async function createApiClient(baseUrl, options) {
538
538
  catch (payErr) {
539
539
  return Promise.reject(new Error(`L402 payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`));
540
540
  }
541
- if (l402Addr) {
542
- await getSpendLimiter().record("sats", BigInt(amountSats), l402Addr);
543
- }
544
541
  cacheL402Auth(method, canonicalUrlKey, challenge.macaroon, payment.preimage);
545
542
  const originalRequest = error.config;
546
543
  originalRequest.headers = originalRequest.headers || {};
@@ -623,6 +620,9 @@ export async function createApiClient(baseUrl, options) {
623
620
  if (error.response?.status !== 402) {
624
621
  return Promise.reject(error);
625
622
  }
623
+ // Booked before signing; given back if the payment fails before the
624
+ // signed transaction is handed to the endpoint.
625
+ let reservation;
626
626
  try {
627
627
  // Decode payment requirements from header
628
628
  const headerValue = error.response?.headers?.[X402_HEADERS.PAYMENT_REQUIRED];
@@ -660,10 +660,9 @@ export async function createApiClient(baseUrl, options) {
660
660
  return Promise.reject(new Error(`Network mismatch: endpoint requires ${paymentNetwork} but wallet is configured for ${acct.network}. ` +
661
661
  `Switch to a ${paymentNetwork} wallet or use a ${acct.network} endpoint.`));
662
662
  }
663
- // Cumulative spending limit: the per-payment cap above bounds a single
664
- // 402, but a malicious endpoint can loop sub-cap payments. This blocks
665
- // once the session/day total would be exceeded.
666
- await getSpendLimiter().check(isSbtc ? "sats" : "ustx", amount, acct.address);
663
+ // The endpoint offers gas sponsorship: sign with `sponsored: true, fee: 0`
664
+ // and its relay co-signs and pays the STX fee at settlement.
665
+ const sponsored = selectedOption.extra?.sponsored === true;
667
666
  // Invoke pre-payment callback (e.g. balance check) before signing/broadcasting.
668
667
  // If the callback throws, the payment is aborted and the error propagates to the caller.
669
668
  if (options?.onBeforePayment) {
@@ -673,14 +672,28 @@ export async function createApiClient(baseUrl, options) {
673
672
  recipient: selectedOption.payTo,
674
673
  network: paymentNetwork ?? acct.network,
675
674
  account: acct,
675
+ sponsored,
676
676
  });
677
677
  }
678
- // Build a non-sponsored signed transaction: the sender pays its own
679
- // STX gas (fee + nonce auto-estimated by @stacks), and the x402
680
- // facilitator simply verifies and broadcasts it. No relay sponsorship
681
- // — this is the same tx shape the x402-stacks lib produces, and the
682
- // balance check in onBeforePayment already accounts for the gas.
678
+ // Clamped fee — do NOT let @stacks auto-estimate, since Hiro's
679
+ // contract_call tiers are polluted by outliers (observed >2000 STX).
680
+ // The sbtc_transfer clamp caps it at 0.003 STX.
681
+ const fee = sponsored
682
+ ? 0n
683
+ : await resolveDefaultFee(acct.network, isSbtc ? "sbtc_transfer" : "token_transfer");
684
+ // Cumulative spending limit: the per-payment cap above bounds a single
685
+ // 402, but a malicious endpoint can loop sub-cap payments. This blocks
686
+ // once the session/day total would be exceeded. A self-paying sender
687
+ // pays the fee, so it is booked in ustx alongside the amount.
688
+ reservation = await getSpendLimiter().reserve([
689
+ { unit: isSbtc ? "sats" : "ustx", amount },
690
+ ...(sponsored ? [] : [{ unit: "ustx", amount: fee }]),
691
+ ], acct.address);
692
+ // Build the signed transaction. Self-paid: the sender pays its own STX
693
+ // gas and the facilitator verifies and broadcasts it. Sponsored: the
694
+ // sponsor slot is left empty for the endpoint's relay to fill.
683
695
  const networkName = getStacksNetwork(acct.network);
696
+ const memo = paymentIdMemo(selectedOption.extra);
684
697
  let transaction;
685
698
  if (tokenType === "sBTC") {
686
699
  const contracts = getContracts(acct.network);
@@ -693,7 +706,7 @@ export async function createApiClient(baseUrl, options) {
693
706
  uintCV(amount),
694
707
  principalCV(acct.address),
695
708
  principalCV(selectedOption.payTo),
696
- noneCV(),
709
+ memo ? someCV(bufferCVFromString(memo)) : noneCV(),
697
710
  ],
698
711
  senderKey: acct.privateKey,
699
712
  network: networkName,
@@ -705,10 +718,8 @@ export async function createApiClient(baseUrl, options) {
705
718
  .willSendEq(amount)
706
719
  .ft(contracts.SBTC_TOKEN, "sbtc-token"),
707
720
  ],
708
- // Clamped fee — do NOT let @stacks auto-estimate, since Hiro's
709
- // contract_call tiers are polluted by outliers (observed >2000
710
- // STX). The sbtc_transfer clamp caps it at 0.003 STX.
711
- fee: await resolveDefaultFee(acct.network, "sbtc_transfer"),
721
+ fee,
722
+ sponsored,
712
723
  });
713
724
  }
714
725
  else {
@@ -717,8 +728,9 @@ export async function createApiClient(baseUrl, options) {
717
728
  amount,
718
729
  senderKey: acct.privateKey,
719
730
  network: networkName,
720
- memo: "",
721
- fee: await resolveDefaultFee(acct.network, "token_transfer"),
731
+ memo: memo ?? "",
732
+ fee,
733
+ sponsored,
722
734
  });
723
735
  }
724
736
  const txHex = "0x" + transaction.serialize();
@@ -739,9 +751,9 @@ export async function createApiClient(baseUrl, options) {
739
751
  action: "payment_submitted",
740
752
  compatShimUsed: false,
741
753
  });
742
- // Record the spend against the cumulative ledger now that the signed
743
- // payment tx is submitted to the facilitator.
744
- await getSpendLimiter().record(isSbtc ? "sats" : "ustx", amount, acct.address);
754
+ // The signed payment tx is handed to the facilitator from here on, so
755
+ // the booked spend stands.
756
+ reservation = undefined;
745
757
  // Retry the original request with the payment header
746
758
  const originalRequest = error.config;
747
759
  originalRequest.headers = originalRequest.headers || {};
@@ -749,6 +761,8 @@ export async function createApiClient(baseUrl, options) {
749
761
  return axiosInstance.request(originalRequest);
750
762
  }
751
763
  catch (paymentError) {
764
+ if (reservation)
765
+ await getSpendLimiter().release(reservation);
752
766
  return Promise.reject(new Error(`x402 payment failed: ${paymentError instanceof Error ? paymentError.message : String(paymentError)}`));
753
767
  }
754
768
  });
@@ -813,6 +827,27 @@ export function detectTokenType(asset) {
813
827
  }
814
828
  return 'unsupported';
815
829
  }
830
+ const STACKS_MEMO_MAX_BYTES = 34;
831
+ /**
832
+ * Memo for an x402 payment from the server's `extra.payment_id`, cut to the
833
+ * first 34 UTF-8 bytes (the Stacks memo limit) on a character boundary.
834
+ * Gateways that bind payments on the memo match that prefix.
835
+ */
836
+ export function paymentIdMemo(extra) {
837
+ const id = extra?.payment_id;
838
+ if (typeof id !== "string" || id.length === 0)
839
+ return undefined;
840
+ let memo = "";
841
+ let bytes = 0;
842
+ for (const ch of id) {
843
+ const size = Buffer.byteLength(ch, "utf8");
844
+ if (bytes + size > STACKS_MEMO_MAX_BYTES)
845
+ break;
846
+ memo += ch;
847
+ bytes += size;
848
+ }
849
+ return memo;
850
+ }
816
851
  /**
817
852
  * Best-effort display symbol for an asset. Servers advertise a friendly name in
818
853
  * `extra.tokenType` (arc0btc sends "USDCx"); otherwise fall back to the contract
@@ -354,9 +354,8 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
354
354
  toolName: "execute_x402_endpoint",
355
355
  asset,
356
356
  onBeforePayment: async (requirements) => {
357
- // Non-sponsored: sender pays its own gas, so validate STX for the
358
- // fee too (sponsored=false is the default, passed explicitly here).
359
- await checkSufficientBalance(requirements.account, requirements.amount, requirements.asset, false);
357
+ // Self-paid payments also need STX for the fee; sponsored ones don't.
358
+ await checkSufficientBalance(requirements.account, requirements.amount, requirements.asset, requirements.sponsored);
360
359
  },
361
360
  });
362
361
  const response = await api.request({ method, url: parsed.requestPath, params, data });
@@ -6,14 +6,16 @@ import { McpServer } from "@modelcontextprotocol/server";
6
6
  */
7
7
  export declare function checkDirectInboxBalance(address: string, amount: string, sponsored?: boolean): Promise<void>;
8
8
  /**
9
- * x402 inbox messaging via the `x402-stacks` client interceptor
10
- * (`wrapAxiosWithPayment`). The transaction is signed but not broadcast; the
9
+ * x402 inbox messaging. The transaction is signed but not broadcast; the
11
10
  * inbox endpoint settles it through its x402 facilitator.
12
11
  *
13
12
  * 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.
13
+ * the `x402-stacks` interceptor (`wrapAxiosWithPayment`, >=2.1.0) signs a
14
+ * sponsored sBTC transfer with fee 0 and the facilitator co-signs and pays the
15
+ * STX gas. Without it, the payment goes through `createApiClient`, which signs
16
+ * a standard transfer at the `sbtc_transfer` fee clamp (max 0.003 STX) with an
17
+ * exact-amount post-condition. The SDK's standard path leaves the fee to
18
+ * stacks.js auto-estimation, which has no ceiling (#700).
17
19
  *
18
20
  * Server side: `lib/inbox/x402-verify.ts` deserializes the payment tx and
19
21
  * branches on `tx.auth.authType` (sponsored → relay sponsors; standard →
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import axios, { AxiosError } from "axios";
3
3
  import { wrapAxiosWithPayment, decodePaymentRequired, decodePaymentResponse, X402_HEADERS, } from "x402-stacks";
4
- import { getAccount, NETWORK } from "../services/x402.service.js";
4
+ import { createApiClient, getAccount, NETWORK } from "../services/x402.service.js";
5
5
  import { getSpendLimiter } from "../services/spend-limiter.js";
6
6
  import { getSbtcService } from "../services/sbtc.service.js";
7
7
  import { getHiroApi } from "../services/hiro-api.js";
@@ -57,14 +57,32 @@ export async function checkDirectInboxBalance(address, amount, sponsored = false
57
57
  }
58
58
  }
59
59
  /**
60
- * x402 inbox messaging via the `x402-stacks` client interceptor
61
- * (`wrapAxiosWithPayment`). The transaction is signed but not broadcast; the
60
+ * True when a failed x402-stacks request had already carried the signed
61
+ * payment: the retried request's config holds the payment-signature header,
62
+ * either on the error itself (the retry failed outright) or on its `cause`
63
+ * (the interceptor's "402 after payment was sent").
64
+ */
65
+ function paymentWasSent(error) {
66
+ const carried = (e) => {
67
+ const headers = e?.config?.headers;
68
+ if (!headers || typeof headers !== "object")
69
+ return false;
70
+ const wanted = X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase();
71
+ return Object.entries(headers).some(([name, value]) => name.toLowerCase() === wanted && value != null);
72
+ };
73
+ return carried(error) || carried(error?.cause);
74
+ }
75
+ /**
76
+ * x402 inbox messaging. The transaction is signed but not broadcast; the
62
77
  * inbox endpoint settles it through its x402 facilitator.
63
78
  *
64
79
  * 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.
80
+ * the `x402-stacks` interceptor (`wrapAxiosWithPayment`, >=2.1.0) signs a
81
+ * sponsored sBTC transfer with fee 0 and the facilitator co-signs and pays the
82
+ * STX gas. Without it, the payment goes through `createApiClient`, which signs
83
+ * a standard transfer at the `sbtc_transfer` fee clamp (max 0.003 STX) with an
84
+ * exact-amount post-condition. The SDK's standard path leaves the fee to
85
+ * stacks.js auto-estimation, which has no ceiling (#700).
68
86
  *
69
87
  * Server side: `lib/inbox/x402-verify.ts` deserializes the payment tx and
70
88
  * branches on `tx.auth.authType` (sponsored → relay sponsors; standard →
@@ -140,25 +158,42 @@ export function registerInboxX402Tools(server) {
140
158
  // Step 2: Pre-flight balance check — sBTC for the message, plus a
141
159
  // realistic STX gas budget when the sender pays its own fee.
142
160
  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);
146
- // Step 3: Build a payment-enabled axios client. wrapAxiosWithPayment
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.
150
- const stacksAccount = {
151
- address: account.address,
152
- privateKey: account.privateKey,
153
- network: NETWORK,
154
- };
155
- const api = wrapAxiosWithPayment(axios.create({ timeout: 120_000 }), stacksAccount);
156
- // Step 4: Send. The interceptor handles 402 -> sign -> retry.
157
- const response = await api.post(inboxUrl, body, {
158
- headers: { "Content-Type": "application/json" },
159
- });
160
- await spendLimiter.record("sats", BigInt(accept.amount), account.address);
161
- // Step 5: Decode the settlement (txid + payer) from the response header.
161
+ // Step 3: Send. Either interceptor handles 402 -> sign -> retry.
162
+ let response;
163
+ if (sponsored) {
164
+ // The message cost is a signed sBTC spend: book it before signing.
165
+ const spendLimiter = getSpendLimiter();
166
+ const reservation = await spendLimiter.reserve([{ unit: "sats", amount: BigInt(accept.amount) }], account.address);
167
+ const stacksAccount = {
168
+ address: account.address,
169
+ privateKey: account.privateKey,
170
+ network: NETWORK,
171
+ };
172
+ const api = wrapAxiosWithPayment(axios.create({ timeout: 120_000 }), stacksAccount);
173
+ try {
174
+ response = await api.post(inboxUrl, body, {
175
+ headers: { "Content-Type": "application/json" },
176
+ });
177
+ }
178
+ catch (error) {
179
+ // Once the signed payment rode the retried request, the inbox may
180
+ // have settled it, so the booking stands.
181
+ if (!paymentWasSent(error))
182
+ await spendLimiter.release(reservation);
183
+ throw error;
184
+ }
185
+ }
186
+ else {
187
+ // Self-paid: createApiClient clamps the fee and meters the spend.
188
+ const api = await createApiClient(INBOX_BASE, {
189
+ toolName: "send_inbox_message_direct",
190
+ asset: accept.asset,
191
+ });
192
+ response = await api.post(inboxUrl, body, {
193
+ headers: { "Content-Type": "application/json" },
194
+ });
195
+ }
196
+ // Step 4: Decode the settlement (txid + payer) from the response header.
162
197
  const settlement = decodePaymentResponse(response.headers[X402_HEADERS.PAYMENT_RESPONSE]);
163
198
  const txid = settlement?.transaction;
164
199
  const payer = settlement?.payer;
@@ -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
  }
@@ -191,7 +191,7 @@ export function registerAllTools(server, selection) {
191
191
  // At Stake (elsalvadorstakesbtc.com — complete-set prediction market)
192
192
  inGroup("markets", registerAtStakeTools);
193
193
  // At Stake side legions (aibtc.com/legions — weight is the share balance)
194
- inGroup("markets", registerAtStakeLegionTools);
194
+ inGroup("legion", registerAtStakeLegionTools);
195
195
  // Identity (ERC-8004 on-chain agent identity management)
196
196
  inGroup("identity", registerIdentityTools);
197
197
  // Credentials (encrypted credential store — list, get, set, delete, unlock)
@@ -312,9 +312,11 @@ export function registerLightningTools(server) {
312
312
  // user therefore gets a separate Lightning ledger from their STX-address
313
313
  // ledger — see SECURITY.md.
314
314
  const ledgerKey = getWalletManager().getActiveAccount()?.address ?? "__lightning__";
315
- await getSpendLimiter().check("sats", BigInt(amountSats), ledgerKey);
315
+ // Booked before paying, and kept even if payInvoice throws: a failed
316
+ // call does not prove the payment did not route (Spark can settle a
317
+ // payment and still report no preimage).
318
+ await getSpendLimiter().reserve([{ unit: "sats", amount: BigInt(amountSats) }], ledgerKey);
316
319
  const result = await provider.payInvoice(bolt11, maxFeeSats);
317
- await getSpendLimiter().record("sats", BigInt(amountSats), ledgerKey);
318
320
  return createJsonResponse({
319
321
  success: true,
320
322
  preimage: result.preimage,
@@ -29,8 +29,8 @@ export declare const TOOL_GROUPS: {
29
29
  readonly identity: "ERC-8004 identity and reputation, message signing";
30
30
  readonly social: "Nostr, AIBTC inbox";
31
31
  readonly earn: "Bounty board (create, accept, pay, my views)";
32
- readonly legion: "AIBTC News Legion";
33
- readonly markets: "Stacks prediction market, At Stake";
32
+ readonly legion: "AIBTC News Legion, At Stake side legions (El Salvador)";
33
+ readonly markets: "Stacks prediction market, At Stake market";
34
34
  readonly dev: "Scaffolding, OpenRouter, settings, inference marketplace, arXiv";
35
35
  };
36
36
  export type ToolGroup = keyof typeof TOOL_GROUPS;
@@ -59,8 +59,8 @@ export const TOOL_GROUPS = {
59
59
  identity: "ERC-8004 identity and reputation, message signing",
60
60
  social: "Nostr, AIBTC inbox",
61
61
  earn: "Bounty board (create, accept, pay, my views)",
62
- legion: "AIBTC News Legion",
63
- markets: "Stacks prediction market, At Stake",
62
+ legion: "AIBTC News Legion, At Stake side legions (El Salvador)",
63
+ markets: "Stacks prediction market, At Stake market",
64
64
  dev: "Scaffolding, OpenRouter, settings, inference marketplace, arXiv",
65
65
  };
66
66
  export const ALL_TOOLS = { all: true };
@@ -289,8 +289,7 @@ export function registerPsbtTools(server) {
289
289
  // A signed PSBT can be broadcast by anyone (a marketplace, the
290
290
  // counterparty), so the spend is metered here, before it is handed back.
291
291
  const outflowSats = psbtOutflowSats(tx, signedInputs, account, NETWORK);
292
- await getSpendLimiter().check("sats", outflowSats, account.address);
293
- await getSpendLimiter().record("sats", outflowSats, account.address);
292
+ await getSpendLimiter().reserve([{ unit: "sats", amount: outflowSats }], account.address);
294
293
  return createJsonResponse({
295
294
  success: signedInputs.length > 0,
296
295
  network: NETWORK,
@@ -61,7 +61,7 @@ Example: To send 0.001 sBTC, use amount "100000" (satoshis).`,
61
61
  try {
62
62
  const sbtcService = getSbtcService(NETWORK);
63
63
  const account = await getAccount();
64
- const resolvedFee = await resolveFee(fee, NETWORK, "contract_call");
64
+ const resolvedFee = await resolveFee(fee, NETWORK, "sbtc_transfer");
65
65
  const result = await sbtcService.transfer(account, recipient, BigInt(amount), memo, resolvedFee, sponsored);
66
66
  const btcAmount = (BigInt(amount) / BigInt(100_000_000)).toString();
67
67
  return createJsonResponse({