@aibtc/mcp-server 1.75.0 → 1.76.1
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 +2 -2
- package/dist/services/btc-spend.d.ts +4 -3
- package/dist/services/btc-spend.js +14 -6
- package/dist/services/mempool-api.d.ts +4 -0
- package/dist/services/mempool-api.js +15 -1
- package/dist/services/spend-limiter.d.ts +49 -5
- package/dist/services/spend-limiter.js +132 -68
- package/dist/services/state-lock.d.ts +22 -21
- package/dist/services/state-lock.js +106 -24
- package/dist/services/x402.service.d.ts +2 -0
- package/dist/services/x402.service.js +38 -25
- package/dist/tools/endpoint.tools.js +2 -3
- package/dist/tools/inbox-x402.tools.d.ts +7 -5
- package/dist/tools/inbox-x402.tools.js +60 -25
- package/dist/tools/index.js +1 -1
- package/dist/tools/inference-marketplace.tools.d.ts +4 -1
- package/dist/tools/inference-marketplace.tools.js +35 -7
- package/dist/tools/lightning.tools.js +4 -2
- package/dist/tools/profiles.d.ts +2 -2
- package/dist/tools/profiles.js +2 -2
- package/dist/tools/psbt.tools.js +1 -2
- package/dist/tools/sbtc.tools.js +1 -1
- package/dist/tools/yield-dashboard.tools.js +73 -107
- package/dist/transactions/builder.js +51 -34
- package/dist/transactions/sponsor-builder.js +59 -40
- package/package.json +1 -1
- package/skill/SKILL.md +4 -4
|
@@ -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
|
|
@@ -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
|
-
|
|
530
|
-
if
|
|
531
|
-
|
|
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
|
-
//
|
|
664
|
-
//
|
|
665
|
-
|
|
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,13 +672,26 @@ 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
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
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);
|
|
684
696
|
const memo = paymentIdMemo(selectedOption.extra);
|
|
685
697
|
let transaction;
|
|
@@ -706,10 +718,8 @@ export async function createApiClient(baseUrl, options) {
|
|
|
706
718
|
.willSendEq(amount)
|
|
707
719
|
.ft(contracts.SBTC_TOKEN, "sbtc-token"),
|
|
708
720
|
],
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
// STX). The sbtc_transfer clamp caps it at 0.003 STX.
|
|
712
|
-
fee: await resolveDefaultFee(acct.network, "sbtc_transfer"),
|
|
721
|
+
fee,
|
|
722
|
+
sponsored,
|
|
713
723
|
});
|
|
714
724
|
}
|
|
715
725
|
else {
|
|
@@ -719,7 +729,8 @@ export async function createApiClient(baseUrl, options) {
|
|
|
719
729
|
senderKey: acct.privateKey,
|
|
720
730
|
network: networkName,
|
|
721
731
|
memo: memo ?? "",
|
|
722
|
-
fee
|
|
732
|
+
fee,
|
|
733
|
+
sponsored,
|
|
723
734
|
});
|
|
724
735
|
}
|
|
725
736
|
const txHex = "0x" + transaction.serialize();
|
|
@@ -740,9 +751,9 @@ export async function createApiClient(baseUrl, options) {
|
|
|
740
751
|
action: "payment_submitted",
|
|
741
752
|
compatShimUsed: false,
|
|
742
753
|
});
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
|
|
754
|
+
// The signed payment tx is handed to the facilitator from here on, so
|
|
755
|
+
// the booked spend stands.
|
|
756
|
+
reservation = undefined;
|
|
746
757
|
// Retry the original request with the payment header
|
|
747
758
|
const originalRequest = error.config;
|
|
748
759
|
originalRequest.headers = originalRequest.headers || {};
|
|
@@ -750,6 +761,8 @@ export async function createApiClient(baseUrl, options) {
|
|
|
750
761
|
return axiosInstance.request(originalRequest);
|
|
751
762
|
}
|
|
752
763
|
catch (paymentError) {
|
|
764
|
+
if (reservation)
|
|
765
|
+
await getSpendLimiter().release(reservation);
|
|
753
766
|
return Promise.reject(new Error(`x402 payment failed: ${paymentError instanceof Error ? paymentError.message : String(paymentError)}`));
|
|
754
767
|
}
|
|
755
768
|
});
|
|
@@ -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
|
-
//
|
|
358
|
-
|
|
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
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
61
|
-
*
|
|
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
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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;
|
package/dist/tools/index.js
CHANGED
|
@@ -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("
|
|
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)
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* accounts, no API keys. These tools sign locally with the unlocked wallet and
|
|
8
8
|
* call the gateway, so an agent can register/update/check in one step.
|
|
9
9
|
*
|
|
10
|
+
* Each listing charges a flat `pricePerRequest`, set by the provider, in its
|
|
11
|
+
* `acceptedToken` (sBTC or USDCx), as a whole number of the token's base unit.
|
|
12
|
+
*
|
|
10
13
|
* - inference_register_provider: verify + list an endpoint (signed)
|
|
11
|
-
* - inference_update_provider: change name/models/payout/endpoint in place (signed)
|
|
14
|
+
* - inference_update_provider: change name/models/payout/endpoint/price in place (signed)
|
|
12
15
|
* - inference_reveal_key: reveal or rotate the gateway↔endpoint shared key (signed)
|
|
13
16
|
* - inference_check_provider: re-run the health/functional probe (unsigned)
|
|
14
17
|
* - inference_list_providers: list registered providers + health (unsigned)
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* accounts, no API keys. These tools sign locally with the unlocked wallet and
|
|
8
8
|
* call the gateway, so an agent can register/update/check in one step.
|
|
9
9
|
*
|
|
10
|
+
* Each listing charges a flat `pricePerRequest`, set by the provider, in its
|
|
11
|
+
* `acceptedToken` (sBTC or USDCx), as a whole number of the token's base unit.
|
|
12
|
+
*
|
|
10
13
|
* - inference_register_provider: verify + list an endpoint (signed)
|
|
11
|
-
* - inference_update_provider: change name/models/payout/endpoint in place (signed)
|
|
14
|
+
* - inference_update_provider: change name/models/payout/endpoint/price in place (signed)
|
|
12
15
|
* - inference_reveal_key: reveal or rotate the gateway↔endpoint shared key (signed)
|
|
13
16
|
* - inference_check_provider: re-run the health/functional probe (unsigned)
|
|
14
17
|
* - inference_list_providers: list registered providers + health (unsigned)
|
|
@@ -110,10 +113,20 @@ const allowUnsafeGatewayArg = z
|
|
|
110
113
|
.optional()
|
|
111
114
|
.describe("Allow a gateway host outside localhost / *.aibtc.com. Required to send wallet-signed headers " +
|
|
112
115
|
"(and any apiKey) to an arbitrary host — leave unset unless you deliberately self-host the gateway.");
|
|
116
|
+
const acceptedTokenArg = z
|
|
117
|
+
.enum(["sBTC", "USDCx"])
|
|
118
|
+
.describe("Token callers pay in, and the unit of pricePerRequest.");
|
|
119
|
+
/** Whole base units only: the gateway rejects decimals, zero and negatives. */
|
|
120
|
+
const pricePerRequestArg = z
|
|
121
|
+
.string()
|
|
122
|
+
.regex(/^[1-9][0-9]*$/, "pricePerRequest must be a whole number greater than zero, in base units")
|
|
123
|
+
.describe("Flat price per request, as a whole number in the accepted token's base unit: sats for sBTC " +
|
|
124
|
+
"(\"5\" = 5 sats), 0.000001 USDCx for USDCx (\"2000\" = 0.002 USDCx). No decimals.");
|
|
113
125
|
export function registerInferenceMarketplaceTools(server) {
|
|
114
126
|
// Register (verify + list) an endpoint — signed
|
|
115
127
|
server.registerTool("inference_register_provider", {
|
|
116
|
-
description: "List an OpenAI-compatible model endpoint on the AIBTC Inference Marketplace and get paid per request
|
|
128
|
+
description: "List an OpenAI-compatible model endpoint on the AIBTC Inference Marketplace and get paid a flat price per request " +
|
|
129
|
+
"that you set, in sBTC (default) or USDCx. " +
|
|
117
130
|
"Signs a registration message with the unlocked payout wallet (proving ownership) and POSTs it. " +
|
|
118
131
|
"The gateway verifies the signature recovers to payoutAddress, that the endpoint is reachable AND actually " +
|
|
119
132
|
"serving inference, and that model ids are real/commercial Hugging Face repos — then lists it. " +
|
|
@@ -132,6 +145,8 @@ export function registerInferenceMarketplaceTools(server) {
|
|
|
132
145
|
.string()
|
|
133
146
|
.optional()
|
|
134
147
|
.describe("Stacks address that receives sBTC. Defaults to the unlocked wallet's address (must match the gateway network)."),
|
|
148
|
+
pricePerRequest: pricePerRequestArg,
|
|
149
|
+
acceptedToken: acceptedTokenArg.optional().describe("Token callers pay in: sBTC (default) or USDCx. Sets the unit of pricePerRequest."),
|
|
135
150
|
apiKey: z
|
|
136
151
|
.string()
|
|
137
152
|
.optional()
|
|
@@ -139,7 +154,7 @@ export function registerInferenceMarketplaceTools(server) {
|
|
|
139
154
|
gateway: gatewayArg,
|
|
140
155
|
allowUnsafeGateway: allowUnsafeGatewayArg,
|
|
141
156
|
}),
|
|
142
|
-
}, async ({ name, endpoint, models, payoutAddress, apiKey, gateway, allowUnsafeGateway }) => {
|
|
157
|
+
}, async ({ name, endpoint, models, payoutAddress, pricePerRequest, acceptedToken, apiKey, gateway, allowUnsafeGateway }) => {
|
|
143
158
|
try {
|
|
144
159
|
const account = requireUnlockedWallet();
|
|
145
160
|
const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
|
|
@@ -153,6 +168,8 @@ export function registerInferenceMarketplaceTools(server) {
|
|
|
153
168
|
endpoint,
|
|
154
169
|
payoutAddress: payout,
|
|
155
170
|
models,
|
|
171
|
+
pricePerRequest,
|
|
172
|
+
...(acceptedToken ? { acceptedToken } : {}),
|
|
156
173
|
...(apiKey ? { apiKey } : {}),
|
|
157
174
|
}),
|
|
158
175
|
});
|
|
@@ -173,7 +190,8 @@ export function registerInferenceMarketplaceTools(server) {
|
|
|
173
190
|
});
|
|
174
191
|
// Update a listing in place — signed
|
|
175
192
|
server.registerTool("inference_update_provider", {
|
|
176
|
-
description: "Update your marketplace listing in place — name, models, payout address, endpoint,
|
|
193
|
+
description: "Update your marketplace listing in place — name, models, payout address, endpoint, description, price, or accepted " +
|
|
194
|
+
"token — no delete/re-add. Switching acceptedToken requires a new pricePerRequest in the same call. " +
|
|
177
195
|
"Signs an update message with the unlocked payout wallet and PATCHes it. Only fields you pass change. " +
|
|
178
196
|
"Changing the endpoint re-verifies reachability before taking effect. Requires an unlocked wallet.",
|
|
179
197
|
inputSchema: z.object({
|
|
@@ -183,12 +201,17 @@ export function registerInferenceMarketplaceTools(server) {
|
|
|
183
201
|
models: z.array(z.string()).optional().describe("New Hugging Face repo id list."),
|
|
184
202
|
payoutAddress: z.string().optional().describe("New payout address (must match the gateway network)."),
|
|
185
203
|
description: z.string().optional().describe("New description."),
|
|
204
|
+
pricePerRequest: pricePerRequestArg.optional(),
|
|
205
|
+
acceptedToken: acceptedTokenArg.optional().describe("New token callers pay in: sBTC or USDCx. Requires pricePerRequest too."),
|
|
186
206
|
apiKey: z.string().optional().describe("Set/replace the key your endpoint requires (locks the endpoint)."),
|
|
187
207
|
gateway: gatewayArg,
|
|
188
208
|
allowUnsafeGateway: allowUnsafeGatewayArg,
|
|
189
209
|
}),
|
|
190
|
-
}, async ({ providerId, name, endpoint, models, payoutAddress, description, apiKey, gateway, allowUnsafeGateway }) => {
|
|
210
|
+
}, async ({ providerId, name, endpoint, models, payoutAddress, description, pricePerRequest, acceptedToken, apiKey, gateway, allowUnsafeGateway, }) => {
|
|
191
211
|
try {
|
|
212
|
+
if (acceptedToken !== undefined && pricePerRequest === undefined) {
|
|
213
|
+
throw new Error("Changing acceptedToken requires a new pricePerRequest in that token's base unit (sats for sBTC, 0.000001 USDCx for USDCx).");
|
|
214
|
+
}
|
|
192
215
|
const account = requireUnlockedWallet();
|
|
193
216
|
const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
|
|
194
217
|
const patch = {};
|
|
@@ -202,10 +225,14 @@ export function registerInferenceMarketplaceTools(server) {
|
|
|
202
225
|
patch.payoutAddress = payoutAddress;
|
|
203
226
|
if (description !== undefined)
|
|
204
227
|
patch.description = description;
|
|
228
|
+
if (pricePerRequest !== undefined)
|
|
229
|
+
patch.pricePerRequest = pricePerRequest;
|
|
230
|
+
if (acceptedToken !== undefined)
|
|
231
|
+
patch.acceptedToken = acceptedToken;
|
|
205
232
|
if (apiKey !== undefined)
|
|
206
233
|
patch.apiKey = apiKey;
|
|
207
234
|
if (Object.keys(patch).length === 0) {
|
|
208
|
-
throw new Error("Nothing to update — pass at least one of name/endpoint/models/payoutAddress/description/apiKey.");
|
|
235
|
+
throw new Error("Nothing to update — pass at least one of name/endpoint/models/payoutAddress/description/pricePerRequest/acceptedToken/apiKey.");
|
|
209
236
|
}
|
|
210
237
|
const headers = signAuthHeaders(account, "update", providerId);
|
|
211
238
|
const { ok, status, body } = await gatewayFetch(`${base}/v1/providers/${providerId}`, {
|
|
@@ -283,7 +310,8 @@ export function registerInferenceMarketplaceTools(server) {
|
|
|
283
310
|
});
|
|
284
311
|
// List registered providers — unsigned
|
|
285
312
|
server.registerTool("inference_list_providers", {
|
|
286
|
-
description: "List providers registered on the marketplace with their models,
|
|
313
|
+
description: "List providers registered on the marketplace with their models, price per request and accepted token, health/status, " +
|
|
314
|
+
"and flagged state. No signature " +
|
|
287
315
|
"required. Use it to find your provider id (match on payoutAddress) after registering.",
|
|
288
316
|
inputSchema: z.object({
|
|
289
317
|
gateway: gatewayArg,
|
|
@@ -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
|
-
|
|
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,
|
package/dist/tools/profiles.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/profiles.js
CHANGED
|
@@ -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 };
|
package/dist/tools/psbt.tools.js
CHANGED
|
@@ -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().
|
|
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,
|
package/dist/tools/sbtc.tools.js
CHANGED
|
@@ -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, "
|
|
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({
|