@aibtc/mcp-server 1.77.0 → 1.79.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.
- package/README.md +3 -1
- package/dist/services/inbox-auto-reply.service.d.ts +188 -0
- package/dist/services/inbox-auto-reply.service.js +528 -0
- package/dist/services/x402-post.d.ts +50 -0
- package/dist/services/x402-post.js +108 -0
- package/dist/services/x402.service.d.ts +30 -2
- package/dist/services/x402.service.js +22 -1
- package/dist/tools/endpoint.tools.js +41 -3
- package/dist/tools/inbox-auto-reply.tools.d.ts +14 -0
- package/dist/tools/inbox-auto-reply.tools.js +177 -0
- package/dist/tools/inbox-x402.tools.d.ts +2 -7
- package/dist/tools/inbox-x402.tools.js +20 -97
- package/dist/tools/index.js +3 -0
- package/dist/tools/profiles.js +3 -0
- package/dist/tools/skill-mappings.js +2 -0
- package/dist/utils/storage.d.ts +9 -0
- package/dist/utils/x402-recovery.d.ts +4 -1
- package/dist/utils/x402-recovery.js +8 -2
- package/package.json +1 -1
- package/skill/SKILL.md +67 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AxiosInstance } from "axios";
|
|
1
|
+
import { type AxiosInstance, type AxiosRequestConfig } from "axios";
|
|
2
2
|
import { type PaymentRequirementsV2 } from "../utils/x402-protocol.js";
|
|
3
3
|
import { NETWORK, API_URL, type Network } from "../config/networks.js";
|
|
4
4
|
import type { Account } from "../transactions/builder.js";
|
|
@@ -45,9 +45,29 @@ 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. */
|
|
48
|
+
/** True when the endpoint advertised `extra.feePayer` or `extra.sponsored` — the relay pays the STX fee. */
|
|
49
49
|
sponsored: boolean;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Who pays STX gas for the payment the interceptor signed. Sponsored: fee 0,
|
|
53
|
+
* the endpoint's relay pays at settlement. Self-paid: the sender's own fee.
|
|
54
|
+
*/
|
|
55
|
+
export interface X402PaymentGas {
|
|
56
|
+
sponsored: boolean;
|
|
57
|
+
feeUstx: string;
|
|
58
|
+
}
|
|
59
|
+
declare module "axios" {
|
|
60
|
+
interface AxiosRequestConfig {
|
|
61
|
+
/** Set by the x402 interceptor on the paid retry; read with getPaymentGas. */
|
|
62
|
+
x402PaymentGas?: X402PaymentGas;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read the gas decision the interceptor attached to a paid request's config
|
|
67
|
+
* (`response.config` on success, `error.config` on failure). Undefined when
|
|
68
|
+
* no payment was signed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function getPaymentGas(config: AxiosRequestConfig | undefined): X402PaymentGas | undefined;
|
|
51
71
|
/**
|
|
52
72
|
* Options for createApiClient
|
|
53
73
|
*/
|
|
@@ -128,6 +148,14 @@ export type PaymentTokenType = 'STX' | 'sBTC' | 'unsupported';
|
|
|
128
148
|
* @param asset - Full contract identifier or token name
|
|
129
149
|
*/
|
|
130
150
|
export declare function detectTokenType(asset: string): PaymentTokenType;
|
|
151
|
+
/**
|
|
152
|
+
* Whether an x402 payment requirement offers gas sponsorship (sign with fee 0,
|
|
153
|
+
* the relay co-signs and pays the STX fee). x402-stacks documents a non-empty
|
|
154
|
+
* `extra.feePayer` as the signal; `extra.sponsored === true` is also honored
|
|
155
|
+
* for endpoints that send only that, and an explicit `sponsored: false` wins.
|
|
156
|
+
* Shared by createApiClient and x402-post so both paths use one rule.
|
|
157
|
+
*/
|
|
158
|
+
export declare function isSponsoredRequirement(extra?: Record<string, unknown>): boolean;
|
|
131
159
|
/**
|
|
132
160
|
* Memo for an x402 payment from the server's `extra.payment_id`, cut to the
|
|
133
161
|
* first 34 UTF-8 bytes (the Stacks memo limit) on a character boundary.
|
|
@@ -367,6 +367,14 @@ export async function mnemonicToAccount(mnemonic, network) {
|
|
|
367
367
|
network,
|
|
368
368
|
};
|
|
369
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Read the gas decision the interceptor attached to a paid request's config
|
|
372
|
+
* (`response.config` on success, `error.config` on failure). Undefined when
|
|
373
|
+
* no payment was signed.
|
|
374
|
+
*/
|
|
375
|
+
export function getPaymentGas(config) {
|
|
376
|
+
return config?.x402PaymentGas;
|
|
377
|
+
}
|
|
370
378
|
/**
|
|
371
379
|
* Build a canonical absolute URL for a request so that the L402 attempt
|
|
372
380
|
* counter and macaroon cache are keyed by the actual endpoint, not the
|
|
@@ -662,7 +670,7 @@ export async function createApiClient(baseUrl, options) {
|
|
|
662
670
|
}
|
|
663
671
|
// The endpoint offers gas sponsorship: sign with `sponsored: true, fee: 0`
|
|
664
672
|
// and its relay co-signs and pays the STX fee at settlement.
|
|
665
|
-
const sponsored = selectedOption.extra
|
|
673
|
+
const sponsored = isSponsoredRequirement(selectedOption.extra);
|
|
666
674
|
// Invoke pre-payment callback (e.g. balance check) before signing/broadcasting.
|
|
667
675
|
// If the callback throws, the payment is aborted and the error propagates to the caller.
|
|
668
676
|
if (options?.onBeforePayment) {
|
|
@@ -758,6 +766,7 @@ export async function createApiClient(baseUrl, options) {
|
|
|
758
766
|
const originalRequest = error.config;
|
|
759
767
|
originalRequest.headers = originalRequest.headers || {};
|
|
760
768
|
originalRequest.headers[X402_HEADERS.PAYMENT_SIGNATURE] = encodedPayload;
|
|
769
|
+
originalRequest.x402PaymentGas = { sponsored, feeUstx: fee.toString() };
|
|
761
770
|
return axiosInstance.request(originalRequest);
|
|
762
771
|
}
|
|
763
772
|
catch (paymentError) {
|
|
@@ -827,6 +836,18 @@ export function detectTokenType(asset) {
|
|
|
827
836
|
}
|
|
828
837
|
return 'unsupported';
|
|
829
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* Whether an x402 payment requirement offers gas sponsorship (sign with fee 0,
|
|
841
|
+
* the relay co-signs and pays the STX fee). x402-stacks documents a non-empty
|
|
842
|
+
* `extra.feePayer` as the signal; `extra.sponsored === true` is also honored
|
|
843
|
+
* for endpoints that send only that, and an explicit `sponsored: false` wins.
|
|
844
|
+
* Shared by createApiClient and x402-post so both paths use one rule.
|
|
845
|
+
*/
|
|
846
|
+
export function isSponsoredRequirement(extra) {
|
|
847
|
+
if (extra?.sponsored === false)
|
|
848
|
+
return false;
|
|
849
|
+
return (typeof extra?.feePayer === "string" && extra.feePayer !== "") || extra?.sponsored === true;
|
|
850
|
+
}
|
|
830
851
|
const STACKS_MEMO_MAX_BYTES = 34;
|
|
831
852
|
/**
|
|
832
853
|
* Memo for an x402 payment from the server's `extra.payment_id`, cut to the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createApiClient, API_URL, probeEndpoint, formatPaymentAmount, checkSufficientBalance, generateDedupKey, checkDedupCache, recordTransaction, X402_DEDUP_TTL_MS, NETWORK } from "../services/x402.service.js";
|
|
2
|
+
import { createApiClient, API_URL, probeEndpoint, formatPaymentAmount, checkSufficientBalance, generateDedupKey, checkDedupCache, recordTransaction, X402_DEDUP_TTL_MS, NETWORK, getPaymentGas } from "../services/x402.service.js";
|
|
3
3
|
import { STATIC_ENDPOINTS, formatEndpointsTable } from "../endpoints/registry.js";
|
|
4
4
|
import { discoverOpenApiEndpoints, fetchDirectoryEntries, } from "../services/x402-discovery.service.js";
|
|
5
5
|
import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
@@ -127,6 +127,18 @@ function formatProbeResponse(result, method, fullUrl, callWithOptions, messagePr
|
|
|
127
127
|
callWith: buildCallWith(callWithOptions),
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* One line saying who pays STX gas for a payment that was signed but whose
|
|
132
|
+
* call failed, or "" when none was signed. The fee is only charged if the
|
|
133
|
+
* payment settles on chain, which a failed call does not tell us.
|
|
134
|
+
*/
|
|
135
|
+
function formatGasLine(gas) {
|
|
136
|
+
if (!gas)
|
|
137
|
+
return "";
|
|
138
|
+
return gas.sponsored
|
|
139
|
+
? "\n\nGas: sponsored (the endpoint's relay pays the STX fee if the payment settles)."
|
|
140
|
+
: `\n\nGas: self-paid; the payment was signed with a ${gas.feeUstx} uSTX fee, charged only if it settles on chain.`;
|
|
141
|
+
}
|
|
130
142
|
export function registerEndpointTools(server) {
|
|
131
143
|
// List x402 endpoints
|
|
132
144
|
server.registerTool("list_x402_endpoints", {
|
|
@@ -273,6 +285,8 @@ Supported sources:
|
|
|
273
285
|
|
|
274
286
|
Use list_x402_endpoints to discover available endpoints.
|
|
275
287
|
|
|
288
|
+
A paid result includes txid and gas: {sponsored, feeUstx}. Sponsored means the endpoint's relay pays the STX fee (feeUstx "0"); otherwise this wallet paid feeUstx on top of the price.
|
|
289
|
+
|
|
276
290
|
For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs a standard sBTC transfer and settles directly through the x402 facilitator.`,
|
|
277
291
|
inputSchema: z.object({
|
|
278
292
|
method: z
|
|
@@ -402,10 +416,13 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
|
|
|
402
416
|
: txid !== null
|
|
403
417
|
? { txid }
|
|
404
418
|
: {};
|
|
419
|
+
// Who paid STX gas for the payment, as decided when it was signed.
|
|
420
|
+
const gas = getPaymentGas(response.config);
|
|
405
421
|
return createJsonResponse({
|
|
406
422
|
endpoint: `${method} ${fullUrl}`,
|
|
407
423
|
response: response.data,
|
|
408
424
|
...txidFields,
|
|
425
|
+
...(gas && { gas }),
|
|
409
426
|
});
|
|
410
427
|
}
|
|
411
428
|
catch (error) {
|
|
@@ -423,6 +440,9 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
|
|
|
423
440
|
// Mirrors the success path, including the synthetic pending marker
|
|
424
441
|
// used when the txid is not yet observable.
|
|
425
442
|
const paymentSigHeader = axiosError.config?.headers?.[X402_HEADERS.PAYMENT_SIGNATURE];
|
|
443
|
+
// A signed payment may still settle after the call failed, so say who
|
|
444
|
+
// pays gas on every error path too.
|
|
445
|
+
const gasLine = formatGasLine(getPaymentGas(axiosError.config));
|
|
426
446
|
if (paymentSigHeader && dedupKey) {
|
|
427
447
|
const broadcastTxid = extractTxidFromPaymentSignature(paymentSigHeader);
|
|
428
448
|
await recordTransaction(dedupKey, broadcastTxid ?? `pending:${dedupKey}`);
|
|
@@ -453,7 +473,7 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
|
|
|
453
473
|
}
|
|
454
474
|
return {
|
|
455
475
|
...baseError,
|
|
456
|
-
content: [{ type: "text", text }],
|
|
476
|
+
content: [{ type: "text", text: text + gasLine }],
|
|
457
477
|
};
|
|
458
478
|
}
|
|
459
479
|
if (paymentSigHeader) {
|
|
@@ -480,11 +500,29 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
|
|
|
480
500
|
`Transaction recovery info:\n` +
|
|
481
501
|
` txid: ${confirmation.txid}\n` +
|
|
482
502
|
` status: ${confirmation.status}\n` +
|
|
483
|
-
` explorer: ${confirmation.explorer}
|
|
503
|
+
` explorer: ${confirmation.explorer}` +
|
|
504
|
+
gasLine,
|
|
484
505
|
},
|
|
485
506
|
],
|
|
486
507
|
};
|
|
487
508
|
}
|
|
509
|
+
// No txid from the header: a sponsored payment (the relay assigns its
|
|
510
|
+
// txid when it sponsors) or one that didn't decode. It may still have
|
|
511
|
+
// settled, so point at where the real txid can be found.
|
|
512
|
+
const baseError = formatEndpointError(error, label);
|
|
513
|
+
return {
|
|
514
|
+
...baseError,
|
|
515
|
+
content: [
|
|
516
|
+
{
|
|
517
|
+
type: "text",
|
|
518
|
+
text: baseError.content[0].text +
|
|
519
|
+
`\n\nA payment was signed, but its txid is not known here` +
|
|
520
|
+
(fallbackPaymentId ? ` (paymentId: ${fallbackPaymentId})` : "") +
|
|
521
|
+
`. It may have settled: query get_account_transactions for this wallet before paying again.` +
|
|
522
|
+
gasLine,
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
};
|
|
488
526
|
}
|
|
489
527
|
return formatEndpointError(error, label);
|
|
490
528
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox auto-reply tools.
|
|
3
|
+
*
|
|
4
|
+
* - inbox_auto_reply_configure: store enabled / model / systemPrompt / dailyCapSats /
|
|
5
|
+
* maxSatsPerReply in ~/.aibtc/config.json (no args = show the current settings)
|
|
6
|
+
* - inbox_auto_reply: answer paid, unanswered inbox messages with a reply
|
|
7
|
+
* written by an AIBTC Inference Marketplace model (x402, sBTC), then sign and
|
|
8
|
+
* post it to the outbox. Built to be called from an agent's autonomy loop.
|
|
9
|
+
*
|
|
10
|
+
* The run logic, ledger and safety rules live in services/inbox-auto-reply.service.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
13
|
+
export declare function registerInboxAutoReplyTools(server: McpServer): void;
|
|
14
|
+
//# sourceMappingURL=inbox-auto-reply.tools.d.ts.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox auto-reply tools.
|
|
3
|
+
*
|
|
4
|
+
* - inbox_auto_reply_configure: store enabled / model / systemPrompt / dailyCapSats /
|
|
5
|
+
* maxSatsPerReply in ~/.aibtc/config.json (no args = show the current settings)
|
|
6
|
+
* - inbox_auto_reply: answer paid, unanswered inbox messages with a reply
|
|
7
|
+
* written by an AIBTC Inference Marketplace model (x402, sBTC), then sign and
|
|
8
|
+
* post it to the outbox. Built to be called from an agent's autonomy loop.
|
|
9
|
+
*
|
|
10
|
+
* The run logic, ledger and safety rules live in services/inbox-auto-reply.service.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { getAccount, NETWORK } from "../services/x402.service.js";
|
|
14
|
+
import { DEFAULT_AUTO_REPLY_MODEL, DEFAULT_DAILY_CAP_SATS, DEFAULT_MAX_REPLIES_PER_RUN, DEFAULT_MAX_SATS_PER_REPLY, listLiveModels, marketplaceReplyGenerator, runAutoReply, } from "../services/inbox-auto-reply.service.js";
|
|
15
|
+
import { readAppConfig, writeAppConfig } from "../utils/storage.js";
|
|
16
|
+
import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* The stored sat limits, defaulted and checked. The config file is plain JSON
|
|
19
|
+
* (shared with the skills CLI), so a missing or hand-edited value must not
|
|
20
|
+
* turn into "no limit" at run time.
|
|
21
|
+
*/
|
|
22
|
+
function autoReplyLimits(config) {
|
|
23
|
+
const limit = (name, value, fallback) => {
|
|
24
|
+
if (value === undefined || value === null)
|
|
25
|
+
return fallback;
|
|
26
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
27
|
+
throw new Error(`inboxAutoReply.${name} in ~/.aibtc/config.json must be a non-negative integer, got ${JSON.stringify(value)}. ` +
|
|
28
|
+
`Fix it with inbox_auto_reply_configure { ${name}: <sats> }.`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
dailyCapSats: limit("dailyCapSats", config.dailyCapSats, DEFAULT_DAILY_CAP_SATS),
|
|
34
|
+
maxSatsPerReply: limit("maxSatsPerReply", config.maxSatsPerReply, DEFAULT_MAX_SATS_PER_REPLY),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function requireMainnet() {
|
|
38
|
+
if (NETWORK !== "mainnet") {
|
|
39
|
+
throw new Error("Inbox auto-reply is mainnet only: the aibtc.com inbox and the inference marketplace settle in mainnet sBTC. Set NETWORK=mainnet.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function registerInboxAutoReplyTools(server) {
|
|
43
|
+
server.registerTool("inbox_auto_reply_configure", {
|
|
44
|
+
description: "Configure inbox auto-reply (stored in ~/.aibtc/config.json). Call with no arguments to see the current " +
|
|
45
|
+
"settings. A systemPrompt (the agent's voice / personality / what it can help with) is required before " +
|
|
46
|
+
"auto-reply can be enabled. The model must be live on inference.aibtc.com; it is checked against the " +
|
|
47
|
+
"catalog when set. dailyCapSats caps sats spent on reply inference per UTC day, on top of the wallet-wide " +
|
|
48
|
+
"spending limit. maxSatsPerReply is the most one reply may cost; a higher quote stops the run unpaid.",
|
|
49
|
+
inputSchema: z.object({
|
|
50
|
+
enabled: z.boolean().optional().describe("Allow inbox_auto_reply to post replies. Dry runs work while disabled."),
|
|
51
|
+
model: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe(`Inference marketplace model id (see GET https://inference.aibtc.com/v1/models). Default ${DEFAULT_AUTO_REPLY_MODEL}.`),
|
|
55
|
+
systemPrompt: z
|
|
56
|
+
.string()
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(4000)
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("System prompt for reply generation: who the agent is, its tone, what it offers, what it won't do."),
|
|
61
|
+
dailyCapSats: z
|
|
62
|
+
.number()
|
|
63
|
+
.int()
|
|
64
|
+
.min(0)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe(`Most sats spent on reply inference per UTC day. Default ${DEFAULT_DAILY_CAP_SATS}.`),
|
|
67
|
+
maxSatsPerReply: z
|
|
68
|
+
.number()
|
|
69
|
+
.int()
|
|
70
|
+
.min(0)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe(`Most sats paid for one reply. Default ${DEFAULT_MAX_SATS_PER_REPLY}.`),
|
|
73
|
+
}),
|
|
74
|
+
}, async ({ enabled, model, systemPrompt, dailyCapSats, maxSatsPerReply }) => {
|
|
75
|
+
try {
|
|
76
|
+
const config = await readAppConfig();
|
|
77
|
+
const current = config.inboxAutoReply;
|
|
78
|
+
if (enabled === undefined &&
|
|
79
|
+
model === undefined &&
|
|
80
|
+
systemPrompt === undefined &&
|
|
81
|
+
dailyCapSats === undefined &&
|
|
82
|
+
maxSatsPerReply === undefined) {
|
|
83
|
+
return createJsonResponse({
|
|
84
|
+
configured: Boolean(current),
|
|
85
|
+
settings: current ?? null,
|
|
86
|
+
...(!current && {
|
|
87
|
+
note: "Not configured. Set at least systemPrompt, then enabled: true.",
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (model !== undefined) {
|
|
92
|
+
const live = await listLiveModels();
|
|
93
|
+
if (!live.includes(model)) {
|
|
94
|
+
throw new Error(`Model "${model}" is not live on the inference marketplace. Live models: ${live.join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const next = {
|
|
98
|
+
enabled: enabled ?? current?.enabled ?? false,
|
|
99
|
+
model: model ?? current?.model ?? DEFAULT_AUTO_REPLY_MODEL,
|
|
100
|
+
systemPrompt: systemPrompt ?? current?.systemPrompt ?? "",
|
|
101
|
+
dailyCapSats: dailyCapSats ?? current?.dailyCapSats ?? DEFAULT_DAILY_CAP_SATS,
|
|
102
|
+
maxSatsPerReply: maxSatsPerReply ?? current?.maxSatsPerReply ?? DEFAULT_MAX_SATS_PER_REPLY,
|
|
103
|
+
};
|
|
104
|
+
if (next.enabled && !next.systemPrompt) {
|
|
105
|
+
throw new Error("Set a systemPrompt before enabling auto-reply.");
|
|
106
|
+
}
|
|
107
|
+
await writeAppConfig({ ...config, inboxAutoReply: next });
|
|
108
|
+
return createJsonResponse({ success: true, settings: next });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return createErrorResponse(error);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
server.registerTool("inbox_auto_reply", {
|
|
115
|
+
description: "Answer this agent's paid aibtc.com inbox messages automatically. In one call: reads the inbox, picks paid, " +
|
|
116
|
+
"settled, unanswered messages (oldest first), writes each reply with an AIBTC Inference Marketplace model " +
|
|
117
|
+
"(paid per request in sBTC via x402, ~1 sat), signs it (BIP-322) and posts it to the outbox (free). " +
|
|
118
|
+
"Never answers a message twice (overlapping runs skip a message another run is answering), stops at the " +
|
|
119
|
+
"daily cap or on a quote above maxSatsPerReply, gives up on a message after 2 paid failures, and returns a summary: checked, replied, " +
|
|
120
|
+
"skipped (with reason), failed, inference sats spent and inference cap remaining.\n\n" +
|
|
121
|
+
"Posting a reply is free; the only cost is the inference call that writes it. " +
|
|
122
|
+
"Call it from the autonomy loop when the heartbeat or inbox shows unread messages. Configure first with " +
|
|
123
|
+
"inbox_auto_reply_configure. dryRun writes (and pays for) replies without posting them, so the operator " +
|
|
124
|
+
"can review the voice; each draft is saved and reused by later dry runs and the real run while the " +
|
|
125
|
+
"systemPrompt and model are unchanged. Requires an unlocked wallet with a bc1q address and a little sBTC. Mainnet only.",
|
|
126
|
+
inputSchema: z.object({
|
|
127
|
+
maxRepliesPerRun: z
|
|
128
|
+
.number()
|
|
129
|
+
.int()
|
|
130
|
+
.min(1)
|
|
131
|
+
.max(20)
|
|
132
|
+
.optional()
|
|
133
|
+
.describe(`Most messages to answer in this call. Default ${DEFAULT_MAX_REPLIES_PER_RUN}.`),
|
|
134
|
+
dryRun: z
|
|
135
|
+
.boolean()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe("Generate replies without posting them or marking messages handled. A new draft is paid for once; " +
|
|
138
|
+
"a saved draft with the same systemPrompt and model is reused free, here and by the real run."),
|
|
139
|
+
}),
|
|
140
|
+
}, async ({ maxRepliesPerRun, dryRun }) => {
|
|
141
|
+
try {
|
|
142
|
+
requireMainnet();
|
|
143
|
+
const config = (await readAppConfig()).inboxAutoReply;
|
|
144
|
+
if (!config?.systemPrompt) {
|
|
145
|
+
throw new Error("Inbox auto-reply is not configured. Call inbox_auto_reply_configure with a systemPrompt first.");
|
|
146
|
+
}
|
|
147
|
+
const isDryRun = dryRun ?? false;
|
|
148
|
+
if (!config.enabled && !isDryRun) {
|
|
149
|
+
throw new Error("Inbox auto-reply is disabled. Enable it with inbox_auto_reply_configure { enabled: true }, or pass dryRun: true to preview replies.");
|
|
150
|
+
}
|
|
151
|
+
const limits = autoReplyLimits(config);
|
|
152
|
+
const account = await getAccount();
|
|
153
|
+
const summary = await runAutoReply({
|
|
154
|
+
account,
|
|
155
|
+
systemPrompt: config.systemPrompt,
|
|
156
|
+
...limits,
|
|
157
|
+
maxRepliesPerRun: maxRepliesPerRun ?? DEFAULT_MAX_REPLIES_PER_RUN,
|
|
158
|
+
dryRun: isDryRun,
|
|
159
|
+
generator: marketplaceReplyGenerator(config.model),
|
|
160
|
+
});
|
|
161
|
+
return createJsonResponse({
|
|
162
|
+
success: summary.failed.length === 0,
|
|
163
|
+
dryRun: isDryRun,
|
|
164
|
+
model: config.model,
|
|
165
|
+
...summary,
|
|
166
|
+
repliedCount: summary.replied.length,
|
|
167
|
+
skippedCount: summary.skipped.length,
|
|
168
|
+
failedCount: summary.failed.length,
|
|
169
|
+
...limits,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return createErrorResponse(error);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=inbox-auto-reply.tools.js.map
|
|
@@ -9,13 +9,8 @@ export declare function checkDirectInboxBalance(address: string, amount: string,
|
|
|
9
9
|
* x402 inbox messaging. The transaction is signed but not broadcast; the
|
|
10
10
|
* inbox endpoint settles it through its x402 facilitator.
|
|
11
11
|
*
|
|
12
|
-
* The inbox decides who pays gas
|
|
13
|
-
*
|
|
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).
|
|
12
|
+
* The inbox decides who pays gas (402 `extra.feePayer`); the sponsored and
|
|
13
|
+
* self-paid paths live in `postWithX402Payment` (`services/x402-post.ts`).
|
|
19
14
|
*
|
|
20
15
|
* Server side: `lib/inbox/x402-verify.ts` deserializes the payment tx and
|
|
21
16
|
* branches on `tx.auth.authType` (sponsored → relay sponsors; standard →
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { getSpendLimiter } from "../services/spend-limiter.js";
|
|
2
|
+
import { AxiosError } from "axios";
|
|
3
|
+
import { getAccount, NETWORK } from "../services/x402.service.js";
|
|
4
|
+
import { postWithX402Payment } from "../services/x402-post.js";
|
|
6
5
|
import { getSbtcService } from "../services/sbtc.service.js";
|
|
7
6
|
import { getHiroApi } from "../services/hiro-api.js";
|
|
8
7
|
import { getExplorerTxUrl } from "../config/networks.js";
|
|
@@ -56,33 +55,12 @@ export async function checkDirectInboxBalance(address, amount, sponsored = false
|
|
|
56
55
|
`This send requires STX for gas — deposit STX to cover the transfer fee.`, "STX", stxBalance.toString(), feeBudget.toString(), shortfall.toString());
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
|
-
/**
|
|
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
58
|
/**
|
|
76
59
|
* x402 inbox messaging. The transaction is signed but not broadcast; the
|
|
77
60
|
* inbox endpoint settles it through its x402 facilitator.
|
|
78
61
|
*
|
|
79
|
-
* The inbox decides who pays gas
|
|
80
|
-
*
|
|
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).
|
|
62
|
+
* The inbox decides who pays gas (402 `extra.feePayer`); the sponsored and
|
|
63
|
+
* self-paid paths live in `postWithX402Payment` (`services/x402-post.ts`).
|
|
86
64
|
*
|
|
87
65
|
* Server side: `lib/inbox/x402-verify.ts` deserializes the payment tx and
|
|
88
66
|
* branches on `tx.auth.authType` (sponsored → relay sponsors; standard →
|
|
@@ -126,79 +104,24 @@ export function registerInboxX402Tools(server) {
|
|
|
126
104
|
toStxAddress: recipientStxAddress,
|
|
127
105
|
content,
|
|
128
106
|
};
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
107
|
+
// Probe for the 402, pre-flight the balance (sBTC for the message, plus
|
|
108
|
+
// a realistic STX gas budget when the sender pays its own fee), then pay.
|
|
109
|
+
const result = await postWithX402Payment({
|
|
110
|
+
baseUrl: INBOX_BASE,
|
|
111
|
+
url: inboxUrl,
|
|
112
|
+
body,
|
|
113
|
+
toolName: "send_inbox_message_direct",
|
|
114
|
+
beforePay: (accept, sponsored) => checkDirectInboxBalance(account.address, accept.amount, sponsored),
|
|
115
|
+
timeoutMs: 120_000,
|
|
138
116
|
});
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
message: "Message sent (no payment required)",
|
|
145
|
-
response: text,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
throw new Error(`Expected 402 payment challenge, got ${probe.status}: ${text}`);
|
|
149
|
-
}
|
|
150
|
-
const paymentRequired = decodePaymentRequired(probe.headers.get(X402_HEADERS.PAYMENT_REQUIRED));
|
|
151
|
-
if (!paymentRequired?.accepts?.length) {
|
|
152
|
-
throw new Error("402 response missing or empty payment-required header");
|
|
153
|
-
}
|
|
154
|
-
const accept = paymentRequired.accepts[0];
|
|
155
|
-
// The SDK signs a sponsored (fee 0) transfer exactly when the
|
|
156
|
-
// requirement names a fee payer — mirror its check.
|
|
157
|
-
const sponsored = typeof accept.extra?.feePayer === "string";
|
|
158
|
-
// Step 2: Pre-flight balance check — sBTC for the message, plus a
|
|
159
|
-
// realistic STX gas budget when the sender pays its own fee.
|
|
160
|
-
await checkDirectInboxBalance(account.address, accept.amount, sponsored);
|
|
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
|
-
// x402-stacks is CJS, so its AxiosInstance comes from axios' .d.cts
|
|
173
|
-
// types; ours comes from .d.ts. Same runtime object, different type.
|
|
174
|
-
const api = wrapAxiosWithPayment(axios.create({ timeout: 120_000 }), stacksAccount);
|
|
175
|
-
try {
|
|
176
|
-
response = await api.post(inboxUrl, body, {
|
|
177
|
-
headers: { "Content-Type": "application/json" },
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
// Once the signed payment rode the retried request, the inbox may
|
|
182
|
-
// have settled it, so the booking stands.
|
|
183
|
-
if (!paymentWasSent(error))
|
|
184
|
-
await spendLimiter.release(reservation);
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
// Self-paid: createApiClient clamps the fee and meters the spend.
|
|
190
|
-
const api = await createApiClient(INBOX_BASE, {
|
|
191
|
-
toolName: "send_inbox_message_direct",
|
|
192
|
-
asset: accept.asset,
|
|
193
|
-
});
|
|
194
|
-
response = await api.post(inboxUrl, body, {
|
|
195
|
-
headers: { "Content-Type": "application/json" },
|
|
117
|
+
if (!result.paid) {
|
|
118
|
+
return createJsonResponse({
|
|
119
|
+
success: true,
|
|
120
|
+
message: "Message sent (no payment required)",
|
|
121
|
+
response: result.text,
|
|
196
122
|
});
|
|
197
123
|
}
|
|
198
|
-
|
|
199
|
-
const settlement = decodePaymentResponse(response.headers[X402_HEADERS.PAYMENT_RESPONSE]);
|
|
200
|
-
const txid = settlement?.transaction;
|
|
201
|
-
const payer = settlement?.payer;
|
|
124
|
+
const { response, accept, sponsored, txid, payer } = result;
|
|
202
125
|
return createJsonResponse({
|
|
203
126
|
success: true,
|
|
204
127
|
message: sponsored
|
package/dist/tools/index.js
CHANGED
|
@@ -50,6 +50,7 @@ import { registerBountyScannerTools } from "./bounty-scanner.tools.js";
|
|
|
50
50
|
import { registerRunesTools } from "./runes.tools.js";
|
|
51
51
|
import { registerInboxTools } from "./inbox.tools.js";
|
|
52
52
|
import { registerInboxX402Tools } from "./inbox-x402.tools.js";
|
|
53
|
+
import { registerInboxAutoReplyTools } from "./inbox-auto-reply.tools.js";
|
|
53
54
|
import { registerArxivResearchTools } from "./arxiv-research.tools.js";
|
|
54
55
|
import { registerEarningTools } from "./earning.tools.js";
|
|
55
56
|
import { registerTaskmarketTools } from "./taskmarket.tools.js";
|
|
@@ -211,6 +212,8 @@ export function registerAllTools(server, selection) {
|
|
|
211
212
|
inGroup("social", registerInboxTools);
|
|
212
213
|
// Inbox direct x402 (non-sponsored — sender pays own STX gas, no relay)
|
|
213
214
|
inGroup("social", registerInboxX402Tools);
|
|
215
|
+
// Inbox auto-reply (answer paid inbox messages with an inference marketplace model)
|
|
216
|
+
inGroup("social", registerInboxAutoReplyTools);
|
|
214
217
|
// arXiv Research (public arXiv Atom API — paper search and digest compilation)
|
|
215
218
|
inGroup("dev", registerArxivResearchTools);
|
|
216
219
|
// Earning Opportunities (static "how to put your assets to work" menu)
|
package/dist/tools/profiles.js
CHANGED
|
@@ -44,6 +44,9 @@ export const CORE_TOOLS = new Set([
|
|
|
44
44
|
"bounty_get",
|
|
45
45
|
"bounty_submit",
|
|
46
46
|
"identity_register",
|
|
47
|
+
// Inbox auto-reply (off until the agent sets a system prompt and enables it)
|
|
48
|
+
"inbox_auto_reply",
|
|
49
|
+
"inbox_auto_reply_configure",
|
|
47
50
|
]);
|
|
48
51
|
export const TOOL_GROUPS = {
|
|
49
52
|
wallet: "Wallet management extras, encrypted credential store",
|
|
@@ -107,6 +107,8 @@ export const TOOL_SKILL_MAP = {
|
|
|
107
107
|
openrouter_models: "x402",
|
|
108
108
|
send_inbox_message: "x402",
|
|
109
109
|
send_inbox_message_direct: "x402",
|
|
110
|
+
inbox_auto_reply: "x402",
|
|
111
|
+
inbox_auto_reply_configure: "x402",
|
|
110
112
|
// defi skill — ALEX DEX and Zest Protocol
|
|
111
113
|
alex_list_pools: "defi",
|
|
112
114
|
alex_get_swap_quote: "defi",
|
package/dist/utils/storage.d.ts
CHANGED
|
@@ -39,6 +39,15 @@ export interface AppConfig {
|
|
|
39
39
|
autoLockTimeout: number;
|
|
40
40
|
hiroApiKey?: string;
|
|
41
41
|
stacksApiUrl?: string;
|
|
42
|
+
/** Set by inbox_auto_reply_configure; unset means auto-reply was never configured. */
|
|
43
|
+
inboxAutoReply?: {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
model: string;
|
|
46
|
+
systemPrompt: string;
|
|
47
|
+
dailyCapSats: number;
|
|
48
|
+
/** Most sats paid for one reply. Missing in configs written before it existed. */
|
|
49
|
+
maxSatsPerReply?: number;
|
|
50
|
+
};
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Keystore file structure (contains encrypted mnemonic)
|
|
@@ -14,7 +14,10 @@ import { type Network } from "../config/networks.js";
|
|
|
14
14
|
* The payment-signature header is a base64-encoded JSON PaymentPayloadV2 object
|
|
15
15
|
* whose payload.transaction field contains the signed transaction hex.
|
|
16
16
|
*
|
|
17
|
-
* Returns the txid string (64-char hex, no 0x prefix), or null if extraction
|
|
17
|
+
* Returns the txid string (64-char hex, no 0x prefix), or null if extraction
|
|
18
|
+
* fails or the payment is sponsored. A sponsored transaction gets its txid only
|
|
19
|
+
* when the relay adds its sponsor signature and fee, so the txid of the signed
|
|
20
|
+
* payment in the header never appears on chain.
|
|
18
21
|
*/
|
|
19
22
|
export declare function extractTxidFromPaymentSignature(paymentSignatureHeader: string): string | null;
|
|
20
23
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Used by inbox.tools.ts and endpoint.tools.ts as an operational fallback
|
|
8
8
|
* when canonical paymentId polling is unavailable or inconclusive.
|
|
9
9
|
*/
|
|
10
|
-
import { deserializeTransaction } from "@stacks/transactions";
|
|
10
|
+
import { AuthType, deserializeTransaction } from "@stacks/transactions";
|
|
11
11
|
import { getHiroApi } from "../services/hiro-api.js";
|
|
12
12
|
import { getExplorerTxUrl } from "../config/networks.js";
|
|
13
13
|
import { decodePaymentPayload } from "./x402-protocol.js";
|
|
@@ -20,7 +20,10 @@ import { decodePaymentPayload } from "./x402-protocol.js";
|
|
|
20
20
|
* The payment-signature header is a base64-encoded JSON PaymentPayloadV2 object
|
|
21
21
|
* whose payload.transaction field contains the signed transaction hex.
|
|
22
22
|
*
|
|
23
|
-
* Returns the txid string (64-char hex, no 0x prefix), or null if extraction
|
|
23
|
+
* Returns the txid string (64-char hex, no 0x prefix), or null if extraction
|
|
24
|
+
* fails or the payment is sponsored. A sponsored transaction gets its txid only
|
|
25
|
+
* when the relay adds its sponsor signature and fee, so the txid of the signed
|
|
26
|
+
* payment in the header never appears on chain.
|
|
24
27
|
*/
|
|
25
28
|
export function extractTxidFromPaymentSignature(paymentSignatureHeader) {
|
|
26
29
|
try {
|
|
@@ -33,6 +36,9 @@ export function extractTxidFromPaymentSignature(paymentSignatureHeader) {
|
|
|
33
36
|
const hexWithout0x = txHex.startsWith("0x") ? txHex.slice(2) : txHex;
|
|
34
37
|
const txBytes = Buffer.from(hexWithout0x, "hex");
|
|
35
38
|
const tx = deserializeTransaction(txBytes);
|
|
39
|
+
if (tx.auth.authType === AuthType.Sponsored) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
36
42
|
return tx.txid();
|
|
37
43
|
}
|
|
38
44
|
catch {
|