@aibtc/mcp-server 1.76.2 → 1.78.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 +18 -1
- package/dist/services/inbox-auto-reply.service.d.ts +156 -0
- package/dist/services/inbox-auto-reply.service.js +386 -0
- package/dist/services/taskmarket.service.d.ts +92 -0
- package/dist/services/taskmarket.service.js +100 -0
- package/dist/services/x402-post.d.ts +50 -0
- package/dist/services/x402-post.js +107 -0
- package/dist/tools/inbox-auto-reply.tools.d.ts +14 -0
- package/dist/tools/inbox-auto-reply.tools.js +142 -0
- package/dist/tools/inbox-x402.tools.d.ts +2 -7
- package/dist/tools/inbox-x402.tools.js +20 -95
- package/dist/tools/index.js +6 -0
- package/dist/tools/profiles.js +3 -0
- package/dist/tools/skill-mappings.js +2 -0
- package/dist/tools/taskmarket.tools.d.ts +12 -0
- package/dist/tools/taskmarket.tools.js +166 -0
- package/dist/transactions/clarity-values.js +10 -2
- package/dist/utils/storage.d.ts +7 -0
- package/package.json +6 -6
- package/skill/SKILL.md +77 -1
- package/skill/references/taskmarket.md +26 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskMarket service — read-only discovery.
|
|
3
|
+
*
|
|
4
|
+
* TaskMarket (https://taskmarket.dev) is an onchain agent work marketplace on
|
|
5
|
+
* Base: requesters escrow USDC, workers submit deliverables, and the requester
|
|
6
|
+
* accepts a winner. This service reads the public, anonymous API (search, get,
|
|
7
|
+
* list-submissions, market-stats). It never signs or spends.
|
|
8
|
+
*/
|
|
9
|
+
const TASKMARKET_API_URL = process.env.TASKMARKET_API_URL || "https://api.taskmarket.dev";
|
|
10
|
+
/** Bound every read so a hung TaskMarket API cannot hold a tool call open. */
|
|
11
|
+
const READ_TIMEOUT_MS = 15_000;
|
|
12
|
+
/** Convert base-unit reward (e.g. "2000000") to human USDC (e.g. "2"). */
|
|
13
|
+
export function baseUnitsToUsdc(raw) {
|
|
14
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
15
|
+
return "0";
|
|
16
|
+
const padded = raw.padStart(7, "0");
|
|
17
|
+
const whole = padded.slice(0, padded.length - 6) || "0";
|
|
18
|
+
const frac = padded.slice(padded.length - 6).replace(/0+$/, "");
|
|
19
|
+
return frac ? `${whole}.${frac}` : whole;
|
|
20
|
+
}
|
|
21
|
+
/** Convert human USDC (e.g. "2") to base units (e.g. "2000000"). */
|
|
22
|
+
export function usdcToBaseUnits(amount) {
|
|
23
|
+
if (!/^\d+(\.\d+)?$/.test(amount)) {
|
|
24
|
+
throw new Error(`Invalid USDC amount: "${amount}"`);
|
|
25
|
+
}
|
|
26
|
+
const [whole, frac = ""] = amount.split(".");
|
|
27
|
+
if (frac.length > 6) {
|
|
28
|
+
throw new Error(`USDC amount has more than 6 decimals: "${amount}"`);
|
|
29
|
+
}
|
|
30
|
+
const wholeInt = BigInt(whole || "0");
|
|
31
|
+
const fracPadded = frac.padEnd(6, "0");
|
|
32
|
+
const fracInt = fracPadded ? BigInt(fracPadded) : 0n;
|
|
33
|
+
return (wholeInt * 1000000n + fracInt).toString();
|
|
34
|
+
}
|
|
35
|
+
async function apiGet(path, signal) {
|
|
36
|
+
const response = await fetch(`${TASKMARKET_API_URL}${path}`, {
|
|
37
|
+
signal: signal ?? AbortSignal.timeout(READ_TIMEOUT_MS),
|
|
38
|
+
headers: { accept: "application/json" },
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const text = await response.text().catch(() => "Unknown error");
|
|
42
|
+
throw new Error(`TaskMarket API error ${response.status}: ${text}`);
|
|
43
|
+
}
|
|
44
|
+
const json = (await response.json());
|
|
45
|
+
return (json.data ?? json);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* List/search tasks. Public, anonymous, read-only.
|
|
49
|
+
*/
|
|
50
|
+
export async function listTasks(options) {
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (options.status)
|
|
53
|
+
params.set("status", options.status);
|
|
54
|
+
if (options.phase)
|
|
55
|
+
params.set("phase", options.phase);
|
|
56
|
+
if (options.mode)
|
|
57
|
+
params.set("mode", options.mode);
|
|
58
|
+
if (options.tags)
|
|
59
|
+
params.set("tags", options.tags);
|
|
60
|
+
if (options.rewardMin)
|
|
61
|
+
params.set("minReward", usdcToBaseUnits(options.rewardMin));
|
|
62
|
+
if (options.rewardMax)
|
|
63
|
+
params.set("maxReward", usdcToBaseUnits(options.rewardMax));
|
|
64
|
+
params.set("limit", String(options.limit ?? 20));
|
|
65
|
+
const result = await apiGet(`/api/tasks?${params.toString()}`, options.signal);
|
|
66
|
+
return {
|
|
67
|
+
tasks: result.tasks ?? [],
|
|
68
|
+
hasMore: result.hasMore ?? false,
|
|
69
|
+
nextCursor: result.nextCursor ?? null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get a single task with its live status. Public, anonymous, read-only.
|
|
74
|
+
*/
|
|
75
|
+
export async function getTask(taskId, signal) {
|
|
76
|
+
return apiGet(`/api/tasks/${taskId}`, signal);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* List submissions for a task so the requester can review them for real work.
|
|
80
|
+
* Public, anonymous, read-only. Never auto-accepts or auto-rejects anything.
|
|
81
|
+
*/
|
|
82
|
+
export async function listSubmissions(taskId, signal) {
|
|
83
|
+
const result = await apiGet(`/api/tasks/${taskId}/submissions`, signal);
|
|
84
|
+
if (Array.isArray(result))
|
|
85
|
+
return result;
|
|
86
|
+
return (result.submissions ?? []);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Market/agent statistics. Anonymous read of a public stats endpoint.
|
|
90
|
+
*/
|
|
91
|
+
export async function getMarketStats(options) {
|
|
92
|
+
const params = new URLSearchParams();
|
|
93
|
+
if (options.address)
|
|
94
|
+
params.set("address", options.address);
|
|
95
|
+
if (options.agentId)
|
|
96
|
+
params.set("agentId", options.agentId);
|
|
97
|
+
const query = params.toString();
|
|
98
|
+
return apiGet(`/api/agents/stats${query ? `?${query}` : ""}`, options.signal);
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=taskmarket.service.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type AxiosResponse } from "axios";
|
|
2
|
+
import { decodePaymentRequired } from "x402-stacks";
|
|
3
|
+
/** One `accepts` entry of an x402 v2 `payment-required` header. */
|
|
4
|
+
export type X402Accept = NonNullable<ReturnType<typeof decodePaymentRequired>>["accepts"][number];
|
|
5
|
+
export type X402PostResult = {
|
|
6
|
+
paid: false;
|
|
7
|
+
status: number;
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
paid: true;
|
|
11
|
+
response: AxiosResponse;
|
|
12
|
+
accept: X402Accept;
|
|
13
|
+
sponsored: boolean;
|
|
14
|
+
txid?: string;
|
|
15
|
+
payer?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* True when a failed x402-stacks request had already carried the signed
|
|
19
|
+
* payment: the retried request's config holds the payment-signature header,
|
|
20
|
+
* either on the error itself (the retry failed outright) or on its `cause`
|
|
21
|
+
* (the interceptor's "402 after payment was sent").
|
|
22
|
+
*/
|
|
23
|
+
export declare function paymentWasSent(error: unknown): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* POST a JSON body to an x402 endpoint, paying the 402 it answers with.
|
|
26
|
+
*
|
|
27
|
+
* The endpoint decides who pays gas. When its 402 advertises `extra.feePayer`,
|
|
28
|
+
* the `x402-stacks` interceptor (`wrapAxiosWithPayment`, >=2.1.0) signs a
|
|
29
|
+
* sponsored sBTC transfer with fee 0 and the facilitator co-signs and pays the
|
|
30
|
+
* STX gas; the amount is booked on the spend limiter here, before signing.
|
|
31
|
+
* Without it, the payment goes through `createApiClient`, which signs a
|
|
32
|
+
* standard transfer at the `sbtc_transfer` fee clamp (max 0.003 STX) with an
|
|
33
|
+
* exact-amount post-condition and meters it itself. The SDK's standard path
|
|
34
|
+
* leaves the fee to stacks.js auto-estimation, which has no ceiling (#700).
|
|
35
|
+
*
|
|
36
|
+
* A bare POST never charges, so the first request is a plain-fetch probe that
|
|
37
|
+
* reads the payment requirements; `beforePay` runs between the probe and the
|
|
38
|
+
* signature (throw from it to abort, e.g. a balance pre-flight). A 2xx probe
|
|
39
|
+
* means the endpoint asked for no payment and is returned as-is.
|
|
40
|
+
*/
|
|
41
|
+
export declare function postWithX402Payment(options: {
|
|
42
|
+
/** Base URL handed to `createApiClient` on the self-paid path. */
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
url: string;
|
|
45
|
+
body: unknown;
|
|
46
|
+
toolName: string;
|
|
47
|
+
beforePay?: (accept: X402Accept, sponsored: boolean) => Promise<void>;
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
}): Promise<X402PostResult>;
|
|
50
|
+
//# sourceMappingURL=x402-post.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { wrapAxiosWithPayment, decodePaymentRequired, decodePaymentResponse, X402_HEADERS, } from "x402-stacks";
|
|
3
|
+
import { createApiClient, getAccount, NETWORK } from "./x402.service.js";
|
|
4
|
+
import { getSpendLimiter } from "./spend-limiter.js";
|
|
5
|
+
/**
|
|
6
|
+
* True when a failed x402-stacks request had already carried the signed
|
|
7
|
+
* payment: the retried request's config holds the payment-signature header,
|
|
8
|
+
* either on the error itself (the retry failed outright) or on its `cause`
|
|
9
|
+
* (the interceptor's "402 after payment was sent").
|
|
10
|
+
*/
|
|
11
|
+
export function paymentWasSent(error) {
|
|
12
|
+
const carried = (e) => {
|
|
13
|
+
const headers = e?.config?.headers;
|
|
14
|
+
if (!headers || typeof headers !== "object")
|
|
15
|
+
return false;
|
|
16
|
+
const wanted = X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase();
|
|
17
|
+
return Object.entries(headers).some(([name, value]) => name.toLowerCase() === wanted && value != null);
|
|
18
|
+
};
|
|
19
|
+
return carried(error) || carried(error?.cause);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* POST a JSON body to an x402 endpoint, paying the 402 it answers with.
|
|
23
|
+
*
|
|
24
|
+
* The endpoint decides who pays gas. When its 402 advertises `extra.feePayer`,
|
|
25
|
+
* the `x402-stacks` interceptor (`wrapAxiosWithPayment`, >=2.1.0) signs a
|
|
26
|
+
* sponsored sBTC transfer with fee 0 and the facilitator co-signs and pays the
|
|
27
|
+
* STX gas; the amount is booked on the spend limiter here, before signing.
|
|
28
|
+
* Without it, the payment goes through `createApiClient`, which signs a
|
|
29
|
+
* standard transfer at the `sbtc_transfer` fee clamp (max 0.003 STX) with an
|
|
30
|
+
* exact-amount post-condition and meters it itself. The SDK's standard path
|
|
31
|
+
* leaves the fee to stacks.js auto-estimation, which has no ceiling (#700).
|
|
32
|
+
*
|
|
33
|
+
* A bare POST never charges, so the first request is a plain-fetch probe that
|
|
34
|
+
* reads the payment requirements; `beforePay` runs between the probe and the
|
|
35
|
+
* signature (throw from it to abort, e.g. a balance pre-flight). A 2xx probe
|
|
36
|
+
* means the endpoint asked for no payment and is returned as-is.
|
|
37
|
+
*/
|
|
38
|
+
export async function postWithX402Payment(options) {
|
|
39
|
+
const { baseUrl, url, body, toolName, beforePay } = options;
|
|
40
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
41
|
+
const probe = await fetch(url, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify(body),
|
|
45
|
+
signal: AbortSignal.timeout(30_000),
|
|
46
|
+
});
|
|
47
|
+
if (probe.status !== 402) {
|
|
48
|
+
const text = await probe.text();
|
|
49
|
+
if (probe.ok)
|
|
50
|
+
return { paid: false, status: probe.status, text };
|
|
51
|
+
throw new Error(`Expected 402 payment challenge, got ${probe.status}: ${text}`);
|
|
52
|
+
}
|
|
53
|
+
const paymentRequired = decodePaymentRequired(probe.headers.get(X402_HEADERS.PAYMENT_REQUIRED));
|
|
54
|
+
if (!paymentRequired?.accepts?.length) {
|
|
55
|
+
throw new Error("402 response missing or empty payment-required header");
|
|
56
|
+
}
|
|
57
|
+
const accept = paymentRequired.accepts[0];
|
|
58
|
+
// The SDK signs a sponsored (fee 0) transfer exactly when the requirement
|
|
59
|
+
// names a fee payer — mirror its check.
|
|
60
|
+
const sponsored = typeof accept.extra?.feePayer === "string";
|
|
61
|
+
if (beforePay)
|
|
62
|
+
await beforePay(accept, sponsored);
|
|
63
|
+
let response;
|
|
64
|
+
if (sponsored) {
|
|
65
|
+
const account = await getAccount();
|
|
66
|
+
// The amount is a signed sBTC spend: book it before signing.
|
|
67
|
+
const spendLimiter = getSpendLimiter();
|
|
68
|
+
const reservation = await spendLimiter.reserve([{ unit: "sats", amount: BigInt(accept.amount) }], account.address);
|
|
69
|
+
const stacksAccount = {
|
|
70
|
+
address: account.address,
|
|
71
|
+
privateKey: account.privateKey,
|
|
72
|
+
network: NETWORK,
|
|
73
|
+
};
|
|
74
|
+
// x402-stacks is CJS, so its AxiosInstance comes from axios' .d.cts
|
|
75
|
+
// types; ours comes from .d.ts. Same runtime object, different type.
|
|
76
|
+
const api = wrapAxiosWithPayment(axios.create({ timeout: timeoutMs }), stacksAccount);
|
|
77
|
+
try {
|
|
78
|
+
response = await api.post(url, body, {
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// Once the signed payment rode the retried request, the endpoint may
|
|
84
|
+
// have settled it, so the booking stands.
|
|
85
|
+
if (!paymentWasSent(error))
|
|
86
|
+
await spendLimiter.release(reservation);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Self-paid: createApiClient clamps the fee and meters the spend.
|
|
92
|
+
const api = await createApiClient(baseUrl, { toolName, asset: accept.asset });
|
|
93
|
+
response = await api.post(url, body, {
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const settlement = decodePaymentResponse(response.headers[X402_HEADERS.PAYMENT_RESPONSE]);
|
|
98
|
+
return {
|
|
99
|
+
paid: true,
|
|
100
|
+
response,
|
|
101
|
+
accept,
|
|
102
|
+
sponsored,
|
|
103
|
+
...(settlement?.transaction && { txid: settlement.transaction }),
|
|
104
|
+
...(settlement?.payer && { payer: settlement.payer }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=x402-post.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox auto-reply tools.
|
|
3
|
+
*
|
|
4
|
+
* - inbox_auto_reply_configure: store enabled / model / systemPrompt / dailyCapSats
|
|
5
|
+
* 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,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox auto-reply tools.
|
|
3
|
+
*
|
|
4
|
+
* - inbox_auto_reply_configure: store enabled / model / systemPrompt / dailyCapSats
|
|
5
|
+
* 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, 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
|
+
function requireMainnet() {
|
|
18
|
+
if (NETWORK !== "mainnet") {
|
|
19
|
+
throw new Error("Inbox auto-reply is mainnet only: the aibtc.com inbox and the inference marketplace settle in mainnet sBTC. Set NETWORK=mainnet.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function registerInboxAutoReplyTools(server) {
|
|
23
|
+
server.registerTool("inbox_auto_reply_configure", {
|
|
24
|
+
description: "Configure inbox auto-reply (stored in ~/.aibtc/config.json). Call with no arguments to see the current " +
|
|
25
|
+
"settings. A systemPrompt (the agent's voice / personality / what it can help with) is required before " +
|
|
26
|
+
"auto-reply can be enabled. The model must be live on inference.aibtc.com; it is checked against the " +
|
|
27
|
+
"catalog when set. dailyCapSats caps sats spent on reply inference per UTC day, on top of the wallet-wide " +
|
|
28
|
+
"spending limit.",
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
enabled: z.boolean().optional().describe("Allow inbox_auto_reply to post replies. Dry runs work while disabled."),
|
|
31
|
+
model: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe(`Inference marketplace model id (see GET https://inference.aibtc.com/v1/models). Default ${DEFAULT_AUTO_REPLY_MODEL}.`),
|
|
35
|
+
systemPrompt: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(4000)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("System prompt for reply generation: who the agent is, its tone, what it offers, what it won't do."),
|
|
41
|
+
dailyCapSats: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.min(0)
|
|
45
|
+
.optional()
|
|
46
|
+
.describe(`Most sats spent on reply inference per UTC day. Default ${DEFAULT_DAILY_CAP_SATS}.`),
|
|
47
|
+
}),
|
|
48
|
+
}, async ({ enabled, model, systemPrompt, dailyCapSats }) => {
|
|
49
|
+
try {
|
|
50
|
+
const config = await readAppConfig();
|
|
51
|
+
const current = config.inboxAutoReply;
|
|
52
|
+
if (enabled === undefined && model === undefined && systemPrompt === undefined && dailyCapSats === undefined) {
|
|
53
|
+
return createJsonResponse({
|
|
54
|
+
configured: Boolean(current),
|
|
55
|
+
settings: current ?? null,
|
|
56
|
+
...(!current && {
|
|
57
|
+
note: "Not configured. Set at least systemPrompt, then enabled: true.",
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (model !== undefined) {
|
|
62
|
+
const live = await listLiveModels();
|
|
63
|
+
if (!live.includes(model)) {
|
|
64
|
+
throw new Error(`Model "${model}" is not live on the inference marketplace. Live models: ${live.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const next = {
|
|
68
|
+
enabled: enabled ?? current?.enabled ?? false,
|
|
69
|
+
model: model ?? current?.model ?? DEFAULT_AUTO_REPLY_MODEL,
|
|
70
|
+
systemPrompt: systemPrompt ?? current?.systemPrompt ?? "",
|
|
71
|
+
dailyCapSats: dailyCapSats ?? current?.dailyCapSats ?? DEFAULT_DAILY_CAP_SATS,
|
|
72
|
+
};
|
|
73
|
+
if (next.enabled && !next.systemPrompt) {
|
|
74
|
+
throw new Error("Set a systemPrompt before enabling auto-reply.");
|
|
75
|
+
}
|
|
76
|
+
await writeAppConfig({ ...config, inboxAutoReply: next });
|
|
77
|
+
return createJsonResponse({ success: true, settings: next });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return createErrorResponse(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.registerTool("inbox_auto_reply", {
|
|
84
|
+
description: "Answer this agent's paid aibtc.com inbox messages automatically. In one call: reads the inbox, picks paid, " +
|
|
85
|
+
"settled, unanswered messages (oldest first), writes each reply with an AIBTC Inference Marketplace model " +
|
|
86
|
+
"(paid per request in sBTC via x402, ~1 sat), signs it (BIP-322) and posts it to the outbox (free). " +
|
|
87
|
+
"Never answers a message twice, stops at the daily cap, and returns a summary: checked, replied, " +
|
|
88
|
+
"skipped (with reason), failed, inference sats spent and inference cap remaining.\n\n" +
|
|
89
|
+
"Posting a reply is free; the only cost is the inference call that writes it. " +
|
|
90
|
+
"Call it from the autonomy loop when the heartbeat or inbox shows unread messages. Configure first with " +
|
|
91
|
+
"inbox_auto_reply_configure. dryRun writes (and pays for) replies without posting them, so the operator " +
|
|
92
|
+
"can review the voice. Requires an unlocked wallet with a bc1q address and a little sBTC. Mainnet only.",
|
|
93
|
+
inputSchema: z.object({
|
|
94
|
+
maxRepliesPerRun: z
|
|
95
|
+
.number()
|
|
96
|
+
.int()
|
|
97
|
+
.min(1)
|
|
98
|
+
.max(20)
|
|
99
|
+
.optional()
|
|
100
|
+
.describe(`Most messages to answer in this call. Default ${DEFAULT_MAX_REPLIES_PER_RUN}.`),
|
|
101
|
+
dryRun: z
|
|
102
|
+
.boolean()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Generate replies (inference is still paid) but do not post them or mark messages handled."),
|
|
105
|
+
}),
|
|
106
|
+
}, async ({ maxRepliesPerRun, dryRun }) => {
|
|
107
|
+
try {
|
|
108
|
+
requireMainnet();
|
|
109
|
+
const config = (await readAppConfig()).inboxAutoReply;
|
|
110
|
+
if (!config?.systemPrompt) {
|
|
111
|
+
throw new Error("Inbox auto-reply is not configured. Call inbox_auto_reply_configure with a systemPrompt first.");
|
|
112
|
+
}
|
|
113
|
+
const isDryRun = dryRun ?? false;
|
|
114
|
+
if (!config.enabled && !isDryRun) {
|
|
115
|
+
throw new Error("Inbox auto-reply is disabled. Enable it with inbox_auto_reply_configure { enabled: true }, or pass dryRun: true to preview replies.");
|
|
116
|
+
}
|
|
117
|
+
const account = await getAccount();
|
|
118
|
+
const summary = await runAutoReply({
|
|
119
|
+
account,
|
|
120
|
+
systemPrompt: config.systemPrompt,
|
|
121
|
+
dailyCapSats: config.dailyCapSats,
|
|
122
|
+
maxRepliesPerRun: maxRepliesPerRun ?? DEFAULT_MAX_REPLIES_PER_RUN,
|
|
123
|
+
dryRun: isDryRun,
|
|
124
|
+
generator: marketplaceReplyGenerator(config.model),
|
|
125
|
+
});
|
|
126
|
+
return createJsonResponse({
|
|
127
|
+
success: summary.failed.length === 0,
|
|
128
|
+
dryRun: isDryRun,
|
|
129
|
+
model: config.model,
|
|
130
|
+
...summary,
|
|
131
|
+
repliedCount: summary.replied.length,
|
|
132
|
+
skippedCount: summary.skipped.length,
|
|
133
|
+
failedCount: summary.failed.length,
|
|
134
|
+
dailyCapSats: config.dailyCapSats,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return createErrorResponse(error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//# 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,77 +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
|
-
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" },
|
|
117
|
+
if (!result.paid) {
|
|
118
|
+
return createJsonResponse({
|
|
119
|
+
success: true,
|
|
120
|
+
message: "Message sent (no payment required)",
|
|
121
|
+
response: result.text,
|
|
194
122
|
});
|
|
195
123
|
}
|
|
196
|
-
|
|
197
|
-
const settlement = decodePaymentResponse(response.headers[X402_HEADERS.PAYMENT_RESPONSE]);
|
|
198
|
-
const txid = settlement?.transaction;
|
|
199
|
-
const payer = settlement?.payer;
|
|
124
|
+
const { response, accept, sponsored, txid, payer } = result;
|
|
200
125
|
return createJsonResponse({
|
|
201
126
|
success: true,
|
|
202
127
|
message: sponsored
|
package/dist/tools/index.js
CHANGED
|
@@ -50,8 +50,10 @@ 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";
|
|
56
|
+
import { registerTaskmarketTools } from "./taskmarket.tools.js";
|
|
55
57
|
import { getSkillForTool } from "./skill-mappings.js";
|
|
56
58
|
import { isToolSelected } from "./profiles.js";
|
|
57
59
|
/**
|
|
@@ -210,10 +212,14 @@ export function registerAllTools(server, selection) {
|
|
|
210
212
|
inGroup("social", registerInboxTools);
|
|
211
213
|
// Inbox direct x402 (non-sponsored — sender pays own STX gas, no relay)
|
|
212
214
|
inGroup("social", registerInboxX402Tools);
|
|
215
|
+
// Inbox auto-reply (answer paid inbox messages with an inference marketplace model)
|
|
216
|
+
inGroup("social", registerInboxAutoReplyTools);
|
|
213
217
|
// arXiv Research (public arXiv Atom API — paper search and digest compilation)
|
|
214
218
|
inGroup("dev", registerArxivResearchTools);
|
|
215
219
|
// Earning Opportunities (static "how to put your assets to work" menu)
|
|
216
220
|
inGroup("earn", registerEarningTools);
|
|
221
|
+
// TaskMarket (onchain agent work marketplace on Base — read-only discovery + gated create)
|
|
222
|
+
inGroup("earn", registerTaskmarketTools);
|
|
217
223
|
restoreRegisterTool();
|
|
218
224
|
}
|
|
219
225
|
//# sourceMappingURL=index.js.map
|
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",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskMarket Tools (https://taskmarket.dev) — onchain agent work marketplace on Base.
|
|
3
|
+
*
|
|
4
|
+
* Read tools (public, anonymous, no wallet, no spend):
|
|
5
|
+
* - taskmarket_search — list/search open tasks (filters: status, phase, mode, tags, reward)
|
|
6
|
+
* - taskmarket_get — full task detail + live status by task ID
|
|
7
|
+
* - taskmarket_submissions — list submissions for a task
|
|
8
|
+
* - taskmarket_stats — public market/agent statistics
|
|
9
|
+
*/
|
|
10
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
11
|
+
export declare function registerTaskmarketTools(server: McpServer): void;
|
|
12
|
+
//# sourceMappingURL=taskmarket.tools.d.ts.map
|