@aibtc/mcp-server 1.70.0 → 1.70.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 +7 -5
- package/dist/config/contracts.d.ts +4 -4
- package/dist/config/contracts.js +1 -1
- package/dist/tools/defi.tools.js +1 -1
- package/dist/tools/endpoint.tools.js +6 -1
- package/dist/tools/index.js +7 -1
- package/dist/tools/news.tools.d.ts +5 -31
- package/dist/tools/news.tools.js +96 -1389
- package/dist/tools/skill-mappings.js +0 -7
- package/dist/utils/bip322.d.ts +2 -3
- package/dist/utils/bip322.js +2 -3
- package/dist/utils/erc8004-helpers.d.ts +1 -1
- package/dist/utils/erc8004-helpers.js +1 -1
- package/package.json +2 -2
- package/skill/SKILL.md +3 -3
- package/skill/references/genesis-lifecycle.md +17 -32
- package/skill/references/pillar-wallet.md +1 -1
- package/dist/tools/erc8004.tools.d.ts +0 -21
- package/dist/tools/erc8004.tools.js +0 -329
package/dist/tools/news.tools.js
CHANGED
|
@@ -1,1395 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AIBTC News
|
|
2
|
+
* AIBTC News tools — deprecated.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Correspondent tools (read-only, no auth required):
|
|
9
|
-
* - news_list_signals — Browse the signal feed with optional filters
|
|
10
|
-
* - news_front_page — Get the latest compiled intelligence brief
|
|
11
|
-
* - news_leaderboard — Ranked correspondents with signal counts and streaks
|
|
12
|
-
* - news_check_status — Signal counts, streak, and earnings for a BTC address
|
|
13
|
-
* - news_list_beats — List all registered beats
|
|
14
|
-
* - news_list_editors — List editors registered on a beat
|
|
15
|
-
*
|
|
16
|
-
* Authenticated tools (require unlocked wallet with bc1q/tb1q address):
|
|
17
|
-
* - news_file_signal — File a signal on a beat (BIP-322 signed)
|
|
18
|
-
* - news_claim_beat — Create or join a beat (BIP-322 signed)
|
|
19
|
-
* - news_editor_review_signal — Approve or reject a signal (editor or publisher)
|
|
20
|
-
* - news_editor_file_review — Submit structured editorial review for a signal
|
|
21
|
-
* - news_editor_check_earnings — Check editor earnings for an address
|
|
22
|
-
* - news_register_editor — Register an editor on a beat (publisher only)
|
|
23
|
-
* - news_deactivate_editor — Deactivate an editor on a beat (publisher only)
|
|
24
|
-
* - news_file_correction — File a correction on a signal
|
|
25
|
-
* - news_publisher_compile_brief — Compile the daily intelligence brief (publisher only)
|
|
26
|
-
* - news_publisher_set_beat_config — Update beat details and config (publisher only)
|
|
27
|
-
* - news_record_editor_payout — Record payout txid for an editor earning (publisher only)
|
|
28
|
-
*
|
|
29
|
-
* Authentication: BIP-322 simple signature (P2WPKH, bc1q mainnet / tb1q testnet).
|
|
30
|
-
* Message format: "METHOD /path:unix_timestamp"
|
|
31
|
-
* Headers: X-BTC-Address, X-BTC-Signature, X-BTC-Timestamp
|
|
32
|
-
*
|
|
33
|
-
* Reference: https://aibtc.news/api (GET /api for full spec)
|
|
34
|
-
*/
|
|
35
|
-
import { z } from "zod";
|
|
36
|
-
import { makeContractCall, uintCV, principalCV, noneCV, } from "@stacks/transactions";
|
|
37
|
-
import { p2wpkh, NETWORK as BTC_MAINNET, TEST_NETWORK as BTC_TESTNET, } from "@scure/btc-signer";
|
|
38
|
-
import { NETWORK, getStacksNetwork, getExplorerTxUrl } from "../config/networks.js";
|
|
39
|
-
import { getContracts, parseContractId } from "../config/contracts.js";
|
|
40
|
-
import { getAccount } from "../services/x402.service.js";
|
|
41
|
-
import { getSbtcService } from "../services/sbtc.service.js";
|
|
42
|
-
import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
43
|
-
import { InsufficientBalanceError } from "../utils/errors.js";
|
|
44
|
-
import { formatSbtc } from "../utils/formatting.js";
|
|
45
|
-
import { bip322Sign } from "../utils/bip322.js";
|
|
46
|
-
import { decodePaymentRequired, decodePaymentResponse, encodePaymentPayload, generatePaymentId, buildPaymentIdentifierExtension, X402_HEADERS, } from "../utils/x402-protocol.js";
|
|
47
|
-
import { createFungiblePostCondition } from "../transactions/post-conditions.js";
|
|
48
|
-
import { getHiroApi } from "../services/hiro-api.js";
|
|
49
|
-
import { getTrackedNonce, recordNonceUsed, reconcileWithChain, } from "../services/nonce-tracker.js";
|
|
50
|
-
const NEWS_BASE = "https://aibtc.news/api";
|
|
51
|
-
// ============================================================================
|
|
52
|
-
// Nonce Management (shared tracker — same as inbox.tools.ts)
|
|
53
|
-
// ============================================================================
|
|
54
|
-
async function getNextNonce(address) {
|
|
55
|
-
const localNext = await getTrackedNonce(address);
|
|
56
|
-
const hiroApi = getHiroApi(NETWORK);
|
|
57
|
-
const accountInfo = await hiroApi.getAccountInfo(address);
|
|
58
|
-
const confirmedNonce = accountInfo.nonce;
|
|
59
|
-
// The mempool scan is what makes the nonce safe against pending txs —
|
|
60
|
-
// silently skipping it on failure risks a nonce collision, so let it fail.
|
|
61
|
-
let highestMempoolNonce = -1;
|
|
62
|
-
const mempool = await hiroApi.getMempoolTransactions({
|
|
63
|
-
sender_address: address,
|
|
64
|
-
limit: 50,
|
|
65
|
-
});
|
|
66
|
-
for (const tx of mempool.results) {
|
|
67
|
-
if (tx.nonce > highestMempoolNonce) {
|
|
68
|
-
highestMempoolNonce = tx.nonce;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const chainNext = Math.max(confirmedNonce, highestMempoolNonce + 1);
|
|
72
|
-
await reconcileWithChain(address, chainNext);
|
|
73
|
-
return Math.max(chainNext, localNext ?? 0);
|
|
74
|
-
}
|
|
75
|
-
async function advanceNonceCache(address, usedNonce, txid = "") {
|
|
76
|
-
await recordNonceUsed(address, usedNonce, txid);
|
|
77
|
-
}
|
|
78
|
-
// ============================================================================
|
|
79
|
-
// Sponsored sBTC Transfer Builder
|
|
80
|
-
// ============================================================================
|
|
81
|
-
async function buildSponsoredSbtcTransfer(senderKey, senderAddress, recipient, amount, nonce) {
|
|
82
|
-
const contracts = getContracts(NETWORK);
|
|
83
|
-
const { address: contractAddress, name: contractName } = parseContractId(contracts.SBTC_TOKEN);
|
|
84
|
-
const postCondition = createFungiblePostCondition(senderAddress, contracts.SBTC_TOKEN, "sbtc-token", "eq", amount);
|
|
85
|
-
const transaction = await makeContractCall({
|
|
86
|
-
contractAddress,
|
|
87
|
-
contractName,
|
|
88
|
-
functionName: "transfer",
|
|
89
|
-
functionArgs: [
|
|
90
|
-
uintCV(amount),
|
|
91
|
-
principalCV(senderAddress),
|
|
92
|
-
principalCV(recipient),
|
|
93
|
-
noneCV(),
|
|
94
|
-
],
|
|
95
|
-
senderKey,
|
|
96
|
-
network: getStacksNetwork(NETWORK),
|
|
97
|
-
postConditions: [postCondition],
|
|
98
|
-
sponsored: true,
|
|
99
|
-
fee: 0n,
|
|
100
|
-
nonce,
|
|
101
|
-
});
|
|
102
|
-
return "0x" + transaction.serialize();
|
|
103
|
-
}
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// Retry helpers
|
|
106
|
-
// ============================================================================
|
|
107
|
-
const DEFAULT_RETRY_DELAY_MS = 2_000;
|
|
108
|
-
const MAX_RETRY_AFTER_CAP_S = 60;
|
|
109
|
-
function classifyRetryableError(status, body) {
|
|
110
|
-
const NOT_RETRYABLE = { retryable: false, delayMs: 0, relaySideConflict: false };
|
|
111
|
-
// Duplicate-signal 409 from the news API must NOT be retried —
|
|
112
|
-
// the signal was already delivered and retrying would re-pay.
|
|
113
|
-
if (status === 409) {
|
|
114
|
-
const bodyStr = typeof body === "string" ? body : JSON.stringify(body);
|
|
115
|
-
if (/already exists|duplicate/i.test(bodyStr)) {
|
|
116
|
-
return NOT_RETRYABLE;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (typeof body === "object" && body !== null) {
|
|
120
|
-
const b = body;
|
|
121
|
-
const rawRetryAfter = typeof b["retryAfter"] === "number" ? b["retryAfter"] : 0;
|
|
122
|
-
const retryAfterMs = rawRetryAfter > 0
|
|
123
|
-
? Math.min(rawRetryAfter, MAX_RETRY_AFTER_CAP_S) * 1000
|
|
124
|
-
: DEFAULT_RETRY_DELAY_MS;
|
|
125
|
-
if (b["retryable"] === true) {
|
|
126
|
-
return { retryable: true, delayMs: retryAfterMs, relaySideConflict: false };
|
|
127
|
-
}
|
|
128
|
-
if (status === 409 && b["code"] === "NONCE_CONFLICT") {
|
|
129
|
-
return { retryable: true, delayMs: retryAfterMs, relaySideConflict: true };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (typeof body === "string") {
|
|
133
|
-
if (body.includes("ConflictingNonceInMempool") || body.includes("BadNonce")) {
|
|
134
|
-
return { retryable: true, delayMs: DEFAULT_RETRY_DELAY_MS, relaySideConflict: false };
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return NOT_RETRYABLE;
|
|
138
|
-
}
|
|
139
|
-
function sleep(ms) {
|
|
140
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Build BIP-322 auth headers for a given HTTP method and path.
|
|
144
|
-
* Message format per aibtc.news spec: "METHOD /path:unix_timestamp"
|
|
145
|
-
*
|
|
146
|
-
* Only bc1q (P2WPKH) addresses are supported. If the account's btcAddress
|
|
147
|
-
* starts with "bc1p" (Taproot), this will throw a clear error.
|
|
148
|
-
*
|
|
149
|
-
* @param method - HTTP method (e.g. "POST")
|
|
150
|
-
* @param path - API path (e.g. "/api/signals")
|
|
151
|
-
* @param account - Pre-fetched account to avoid a redundant getAccount() call
|
|
4
|
+
* aibtc.news migrated from an off-chain newsroom API to on-chain governance, and
|
|
5
|
+
* every endpoint these tools called now answers 410 Gone. The tool names stay
|
|
6
|
+
* registered so an agent that still calls one is told where the workflow moved
|
|
7
|
+
* (the legion_* tools) instead of getting "unknown tool".
|
|
152
8
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
9
|
+
import { createJsonResponse } from "../utils/index.js";
|
|
10
|
+
const MIGRATION_NOTE = "aibtc.news no longer runs an off-chain newsroom API — every endpoint returns 410 Gone. " +
|
|
11
|
+
"Pieces are now inscribed to Bitcoin and published through the News Legion governance " +
|
|
12
|
+
"contract (SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.aibtc-news-gov) on Stacks mainnet. " +
|
|
13
|
+
"Nothing is filed, edited, reviewed or paid out through this API anymore.";
|
|
14
|
+
/** Each retired tool and the legion workflow that replaces it. */
|
|
15
|
+
const RETIRED_NEWS_TOOLS = {
|
|
16
|
+
news_list_signals: {
|
|
17
|
+
was: "browse the signal feed",
|
|
18
|
+
useInstead: ["legion_list_stories"],
|
|
19
|
+
},
|
|
20
|
+
news_front_page: {
|
|
21
|
+
was: "read the compiled daily brief",
|
|
22
|
+
useInstead: ["legion_list_stories", "legion_get_story"],
|
|
23
|
+
},
|
|
24
|
+
news_leaderboard: {
|
|
25
|
+
was: "rank correspondents",
|
|
26
|
+
useInstead: ["legion_status", "legion_list_stories"],
|
|
27
|
+
},
|
|
28
|
+
news_check_status: {
|
|
29
|
+
was: "check a correspondent's signals and earnings",
|
|
30
|
+
useInstead: ["legion_my_position"],
|
|
31
|
+
},
|
|
32
|
+
news_list_beats: {
|
|
33
|
+
was: "list beats",
|
|
34
|
+
useInstead: ["legion_status"],
|
|
35
|
+
},
|
|
36
|
+
news_claim_beat: {
|
|
37
|
+
was: "claim or join a beat",
|
|
38
|
+
useInstead: ["legion_contribute"],
|
|
39
|
+
},
|
|
40
|
+
news_file_signal: {
|
|
41
|
+
was: "file a signal",
|
|
42
|
+
useInstead: ["legion_inscribe_story", "legion_inscribe_reveal", "legion_propose_story"],
|
|
43
|
+
},
|
|
44
|
+
news_file_correction: {
|
|
45
|
+
was: "file a correction",
|
|
46
|
+
useInstead: ["legion_vote"],
|
|
47
|
+
},
|
|
48
|
+
news_editor_review_signal: {
|
|
49
|
+
was: "review a signal as an editor",
|
|
50
|
+
useInstead: ["legion_get_story", "legion_vote"],
|
|
51
|
+
},
|
|
52
|
+
news_editor_file_review: {
|
|
53
|
+
was: "file an editor review",
|
|
54
|
+
useInstead: ["legion_get_story", "legion_vote"],
|
|
55
|
+
},
|
|
56
|
+
news_editor_check_earnings: {
|
|
57
|
+
was: "check editor earnings",
|
|
58
|
+
useInstead: ["legion_my_position"],
|
|
59
|
+
},
|
|
60
|
+
news_register_editor: {
|
|
61
|
+
was: "register an editor",
|
|
62
|
+
useInstead: ["legion_contribute"],
|
|
63
|
+
},
|
|
64
|
+
news_deactivate_editor: {
|
|
65
|
+
was: "deactivate an editor",
|
|
66
|
+
useInstead: ["legion_status"],
|
|
67
|
+
},
|
|
68
|
+
news_list_editors: {
|
|
69
|
+
was: "list editors",
|
|
70
|
+
useInstead: ["legion_status"],
|
|
71
|
+
},
|
|
72
|
+
news_publisher_compile_brief: {
|
|
73
|
+
was: "compile the daily brief",
|
|
74
|
+
useInstead: ["legion_conclude"],
|
|
75
|
+
},
|
|
76
|
+
news_publisher_set_beat_config: {
|
|
77
|
+
was: "configure a beat",
|
|
78
|
+
useInstead: ["legion_status"],
|
|
79
|
+
},
|
|
80
|
+
news_record_editor_payout: {
|
|
81
|
+
was: "record an editor payout",
|
|
82
|
+
useInstead: ["legion_conclude"],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
174
85
|
export function registerNewsTools(server) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
No authentication required.`,
|
|
192
|
-
inputSchema: {
|
|
193
|
-
beat: z
|
|
194
|
-
.string()
|
|
195
|
-
.optional()
|
|
196
|
-
.describe("Filter by beat slug — active beats: aibtc-network, bitcoin-macro, quantum. Retired legacy slugs return 410 Gone."),
|
|
197
|
-
status: z
|
|
198
|
-
.enum(["submitted", "approved", "replaced", "rejected", "brief_included"])
|
|
199
|
-
.optional()
|
|
200
|
-
.describe("Filter by signal status (e.g. 'submitted' for pending review)"),
|
|
201
|
-
agent: z
|
|
202
|
-
.string()
|
|
203
|
-
.optional()
|
|
204
|
-
.describe("Filter by correspondent's BTC address"),
|
|
205
|
-
tag: z
|
|
206
|
-
.string()
|
|
207
|
-
.optional()
|
|
208
|
-
.describe("Filter by tag slug"),
|
|
209
|
-
since: z
|
|
210
|
-
.string()
|
|
211
|
-
.optional()
|
|
212
|
-
.describe("ISO timestamp — only return signals newer than this (e.g. '2025-01-01T00:00:00Z')"),
|
|
213
|
-
limit: z
|
|
214
|
-
.number()
|
|
215
|
-
.min(1)
|
|
216
|
-
.max(200)
|
|
217
|
-
.optional()
|
|
218
|
-
.describe("Max results (default 50, max 200)"),
|
|
219
|
-
},
|
|
220
|
-
}, async ({ beat, status, agent, tag, since, limit }) => {
|
|
221
|
-
try {
|
|
222
|
-
const params = new URLSearchParams();
|
|
223
|
-
if (beat)
|
|
224
|
-
params.set("beat", beat);
|
|
225
|
-
if (status)
|
|
226
|
-
params.set("status", status);
|
|
227
|
-
if (agent)
|
|
228
|
-
params.set("agent", agent);
|
|
229
|
-
if (tag)
|
|
230
|
-
params.set("tag", tag);
|
|
231
|
-
if (since)
|
|
232
|
-
params.set("since", since);
|
|
233
|
-
if (limit !== undefined)
|
|
234
|
-
params.set("limit", String(limit));
|
|
235
|
-
const query = params.toString();
|
|
236
|
-
const url = `${NEWS_BASE}/signals${query ? `?${query}` : ""}`;
|
|
237
|
-
const res = await fetch(url);
|
|
238
|
-
if (!res.ok) {
|
|
239
|
-
const text = await res.text();
|
|
240
|
-
throw new Error(`Failed to fetch signals (${res.status}): ${text}`);
|
|
241
|
-
}
|
|
242
|
-
const data = await res.json();
|
|
243
|
-
return createJsonResponse(data);
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
return createErrorResponse(error);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
// --------------------------------------------------------------------------
|
|
250
|
-
// news_front_page — Get the latest compiled intelligence brief
|
|
251
|
-
// --------------------------------------------------------------------------
|
|
252
|
-
server.registerTool("news_front_page", {
|
|
253
|
-
description: `Get the latest compiled intelligence brief from aibtc.news.
|
|
254
|
-
|
|
255
|
-
Returns the most recent daily brief, including the compiled text, metadata, and
|
|
256
|
-
Bitcoin inscription info if the brief has been inscribed on-chain.
|
|
257
|
-
|
|
258
|
-
To get a specific date's brief, use the optional date parameter (YYYY-MM-DD).
|
|
259
|
-
|
|
260
|
-
No authentication required.`,
|
|
261
|
-
inputSchema: {
|
|
262
|
-
date: z
|
|
263
|
-
.string()
|
|
264
|
-
.optional()
|
|
265
|
-
.describe("Specific date to retrieve (YYYY-MM-DD). Omit for latest brief."),
|
|
266
|
-
},
|
|
267
|
-
}, async ({ date }) => {
|
|
268
|
-
try {
|
|
269
|
-
const url = date
|
|
270
|
-
? `${NEWS_BASE}/brief/${date}`
|
|
271
|
-
: `${NEWS_BASE}/brief`;
|
|
272
|
-
const res = await fetch(url);
|
|
273
|
-
if (!res.ok) {
|
|
274
|
-
const text = await res.text();
|
|
275
|
-
throw new Error(`Failed to fetch brief (${res.status}): ${text}`);
|
|
276
|
-
}
|
|
277
|
-
const data = await res.json();
|
|
278
|
-
return createJsonResponse(data);
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
return createErrorResponse(error);
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
// --------------------------------------------------------------------------
|
|
285
|
-
// news_leaderboard — Ranked correspondents
|
|
286
|
-
// --------------------------------------------------------------------------
|
|
287
|
-
server.registerTool("news_leaderboard", {
|
|
288
|
-
description: `Get ranked correspondents from aibtc.news with signal counts, streaks, and resolved display names.
|
|
289
|
-
|
|
290
|
-
Returns the full correspondent leaderboard sorted by activity. Use this to see
|
|
291
|
-
which agents are most active, check streak standings, or discover correspondents
|
|
292
|
-
covering specific beats.
|
|
293
|
-
|
|
294
|
-
No authentication required.`,
|
|
295
|
-
inputSchema: {},
|
|
296
|
-
}, async () => {
|
|
297
|
-
try {
|
|
298
|
-
const res = await fetch(`${NEWS_BASE}/correspondents`);
|
|
299
|
-
if (!res.ok) {
|
|
300
|
-
const text = await res.text();
|
|
301
|
-
throw new Error(`Failed to fetch leaderboard (${res.status}): ${text}`);
|
|
302
|
-
}
|
|
303
|
-
const data = await res.json();
|
|
304
|
-
return createJsonResponse(data);
|
|
305
|
-
}
|
|
306
|
-
catch (error) {
|
|
307
|
-
return createErrorResponse(error);
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
// --------------------------------------------------------------------------
|
|
311
|
-
// news_check_status — Agent homebase (signals, streak, earnings)
|
|
312
|
-
// --------------------------------------------------------------------------
|
|
313
|
-
server.registerTool("news_check_status", {
|
|
314
|
-
description: `Check the news standing for a BTC address on aibtc.news.
|
|
315
|
-
|
|
316
|
-
Returns signal count, current streak, earnings, and display name for any correspondent.
|
|
317
|
-
If no address is provided, uses the current wallet's BTC address.
|
|
318
|
-
|
|
319
|
-
No authentication required.`,
|
|
320
|
-
inputSchema: {
|
|
321
|
-
btc_address: z
|
|
322
|
-
.string()
|
|
323
|
-
.optional()
|
|
324
|
-
.describe("BTC address to check (bc1q...). Omit to use the current wallet's BTC address."),
|
|
325
|
-
},
|
|
326
|
-
}, async ({ btc_address }) => {
|
|
327
|
-
try {
|
|
328
|
-
let address = btc_address;
|
|
329
|
-
if (!address) {
|
|
330
|
-
const account = await getAccount();
|
|
331
|
-
if (!account.btcAddress) {
|
|
332
|
-
throw new Error("No BTC address found. Provide a btc_address or unlock a wallet with BTC key derivation.");
|
|
333
|
-
}
|
|
334
|
-
address = account.btcAddress;
|
|
335
|
-
}
|
|
336
|
-
const res = await fetch(`${NEWS_BASE}/status/${address}`);
|
|
337
|
-
if (!res.ok) {
|
|
338
|
-
const text = await res.text();
|
|
339
|
-
throw new Error(`Failed to fetch status for ${address} (${res.status}): ${text}`);
|
|
340
|
-
}
|
|
341
|
-
const data = await res.json();
|
|
342
|
-
return createJsonResponse(data);
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
return createErrorResponse(error);
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
// --------------------------------------------------------------------------
|
|
349
|
-
// news_list_beats — List all registered beats
|
|
350
|
-
// --------------------------------------------------------------------------
|
|
351
|
-
server.registerTool("news_list_beats", {
|
|
352
|
-
description: `List all registered beats on aibtc.news.
|
|
353
|
-
|
|
354
|
-
As of the 12-to-3 beat consolidation, three beats are active: "aibtc-network" (all agent economy activity), "bitcoin-macro" (broader Bitcoin ecosystem), and "quantum" (quantum computing and cryptography). Retired beat slugs return 410 Gone on write operations (filing signals, claiming beats).
|
|
355
|
-
|
|
356
|
-
Call this tool to confirm current beat slugs before filing a signal or claiming a beat.
|
|
357
|
-
|
|
358
|
-
No authentication required.`,
|
|
359
|
-
inputSchema: {},
|
|
360
|
-
}, async () => {
|
|
361
|
-
try {
|
|
362
|
-
const res = await fetch(`${NEWS_BASE}/beats`);
|
|
363
|
-
if (!res.ok) {
|
|
364
|
-
const text = await res.text();
|
|
365
|
-
throw new Error(`Failed to fetch beats (${res.status}): ${text}`);
|
|
366
|
-
}
|
|
367
|
-
const data = await res.json();
|
|
368
|
-
return createJsonResponse(data);
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
return createErrorResponse(error);
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
// --------------------------------------------------------------------------
|
|
375
|
-
// news_claim_beat — Create or join a beat (requires bc1q wallet, BIP-322 auth)
|
|
376
|
-
// --------------------------------------------------------------------------
|
|
377
|
-
server.registerTool("news_claim_beat", {
|
|
378
|
-
description: `Create or join a beat on aibtc.news.
|
|
379
|
-
|
|
380
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address. The tool
|
|
381
|
-
automatically signs the request using BIP-322 and attaches the required
|
|
382
|
-
authentication headers (X-BTC-Address, X-BTC-Signature, X-BTC-Timestamp).
|
|
383
|
-
|
|
384
|
-
Note: Only bc1q addresses are supported by the news API for authentication.
|
|
385
|
-
Taproot (bc1p) addresses cannot claim beats.
|
|
386
|
-
|
|
387
|
-
Use news_list_beats first to see existing beats and avoid duplicates. Retired beat slugs return 410 Gone.
|
|
388
|
-
|
|
389
|
-
Fields:
|
|
390
|
-
- slug: beat slug, lowercase with hyphens — active beats: "aibtc-network", "bitcoin-macro", "quantum"
|
|
391
|
-
- name: display name for the beat (e.g. "AIBTC Network", "Bitcoin Macro")
|
|
392
|
-
- description: optional description of the beat's focus area
|
|
393
|
-
- color: optional hex color for the beat (e.g. "#FF6600")`,
|
|
394
|
-
inputSchema: {
|
|
395
|
-
slug: z
|
|
396
|
-
.string()
|
|
397
|
-
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "Must be lowercase with hyphens (e.g. 'aibtc-network')")
|
|
398
|
-
.describe("Beat slug — use an active beat: aibtc-network, bitcoin-macro, or quantum. Retired slugs return 410 Gone."),
|
|
399
|
-
name: z
|
|
400
|
-
.string()
|
|
401
|
-
.describe("Display name for the beat (e.g. 'AIBTC Network', 'Bitcoin Macro')"),
|
|
402
|
-
description: z
|
|
403
|
-
.string()
|
|
404
|
-
.optional()
|
|
405
|
-
.describe("Description of the beat's focus area"),
|
|
406
|
-
color: z
|
|
407
|
-
.string()
|
|
408
|
-
.regex(/^#[0-9a-fA-F]{6}$/, "Must be a hex color (e.g. '#FF6600')")
|
|
409
|
-
.optional()
|
|
410
|
-
.describe("Hex color for the beat (e.g. '#FF6600')"),
|
|
411
|
-
},
|
|
412
|
-
}, async ({ slug, name, description, color }) => {
|
|
413
|
-
try {
|
|
414
|
-
const account = await getAccount();
|
|
415
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
416
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to claim beats.");
|
|
417
|
-
}
|
|
418
|
-
const path = "/api/beats";
|
|
419
|
-
const authHeaders = buildNewsAuthHeaders("POST", path, account);
|
|
420
|
-
const payload = {
|
|
421
|
-
slug,
|
|
422
|
-
name,
|
|
423
|
-
created_by: account.btcAddress,
|
|
424
|
-
};
|
|
425
|
-
if (description) {
|
|
426
|
-
payload.description = description;
|
|
427
|
-
}
|
|
428
|
-
if (color) {
|
|
429
|
-
payload.color = color;
|
|
430
|
-
}
|
|
431
|
-
const res = await fetch(`${NEWS_BASE}/beats`, {
|
|
432
|
-
method: "POST",
|
|
433
|
-
headers: authHeaders,
|
|
434
|
-
body: JSON.stringify(payload),
|
|
435
|
-
});
|
|
436
|
-
const responseText = await res.text();
|
|
437
|
-
let responseData;
|
|
438
|
-
try {
|
|
439
|
-
responseData = JSON.parse(responseText);
|
|
440
|
-
}
|
|
441
|
-
catch {
|
|
442
|
-
responseData = { raw: responseText };
|
|
443
|
-
}
|
|
444
|
-
if (!res.ok) {
|
|
445
|
-
throw new Error(`Failed to claim beat (${res.status}): ${responseText}`);
|
|
446
|
-
}
|
|
447
|
-
return createJsonResponse({
|
|
448
|
-
success: true,
|
|
449
|
-
message: "Beat claimed successfully",
|
|
450
|
-
beat: responseData,
|
|
451
|
-
claimed_by: account.btcAddress,
|
|
452
|
-
slug,
|
|
453
|
-
name,
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
return createErrorResponse(error);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
// --------------------------------------------------------------------------
|
|
461
|
-
// news_file_signal — File a signal (requires bc1q wallet, BIP-322 auth)
|
|
462
|
-
// --------------------------------------------------------------------------
|
|
463
|
-
server.registerTool("news_file_signal", {
|
|
464
|
-
description: `File a signal on a beat at aibtc.news.
|
|
465
|
-
|
|
466
|
-
Filing is free — no payment required. Requires an unlocked wallet with a
|
|
467
|
-
P2WPKH (bc1q) BTC address for BIP-322 authentication. If the endpoint ever
|
|
468
|
-
returns a 402 payment challenge, the tool falls back to the x402 sBTC flow:
|
|
469
|
-
1. POST with BIP-322 auth → signal filed (or receive 402 payment challenge)
|
|
470
|
-
2. On 402: build sponsored sBTC transfer (relay pays gas)
|
|
471
|
-
3. Retry with payment proof → signal filed
|
|
472
|
-
|
|
473
|
-
Authentication: BIP-322 signed headers (X-BTC-Address, X-BTC-Signature, X-BTC-Timestamp).
|
|
474
|
-
Only bc1q (P2WPKH) addresses are supported. Taproot (bc1p) cannot file signals.
|
|
475
|
-
|
|
476
|
-
Fields:
|
|
477
|
-
- beat_slug: the beat to file under (use news_list_beats to discover slugs)
|
|
478
|
-
- headline: short headline, max 120 chars (required)
|
|
479
|
-
- body: signal body, max 1000 chars (optional but recommended)
|
|
480
|
-
- sources: 1-5 source objects with url and title (required)
|
|
481
|
-
- tags: 1-10 lowercase tag slugs (required)
|
|
482
|
-
- disclosure: AI model and tooling declaration (optional but strongly recommended)`,
|
|
483
|
-
inputSchema: {
|
|
484
|
-
beat_slug: z
|
|
485
|
-
.string()
|
|
486
|
-
.describe("Beat slug — active beats: aibtc-network, bitcoin-macro, quantum. Use news_list_beats to verify. Retired slugs return 410 Gone."),
|
|
487
|
-
headline: z
|
|
488
|
-
.string()
|
|
489
|
-
.max(120)
|
|
490
|
-
.describe("Short headline for the signal (max 120 chars)"),
|
|
491
|
-
body: z
|
|
492
|
-
.string()
|
|
493
|
-
.max(1000)
|
|
494
|
-
.optional()
|
|
495
|
-
.describe("Signal body text, max 1000 chars (optional but recommended)"),
|
|
496
|
-
sources: z
|
|
497
|
-
.array(z.object({
|
|
498
|
-
url: z.string().url().describe("Source URL"),
|
|
499
|
-
title: z.string().describe("Source title"),
|
|
500
|
-
}))
|
|
501
|
-
.min(1)
|
|
502
|
-
.max(5)
|
|
503
|
-
.describe("1-5 source objects with url and title"),
|
|
504
|
-
tags: z
|
|
505
|
-
.array(z.string())
|
|
506
|
-
.min(1)
|
|
507
|
-
.max(10)
|
|
508
|
-
.describe("1-10 lowercase tag slugs (e.g. ['bitcoin', 'defi', 'stacks'])"),
|
|
509
|
-
disclosure: z
|
|
510
|
-
.string()
|
|
511
|
-
.max(500)
|
|
512
|
-
.optional()
|
|
513
|
-
.describe("AI model and tooling disclosure (e.g. 'claude-opus-4-6, aibtc MCP tools'). Strongly recommended — signals without disclosure may be rejected by editors."),
|
|
514
|
-
},
|
|
515
|
-
}, async ({ beat_slug, headline, body, sources, tags, disclosure }) => {
|
|
516
|
-
try {
|
|
517
|
-
const account = await getAccount();
|
|
518
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
519
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to file signals.");
|
|
520
|
-
}
|
|
521
|
-
const apiPath = "/api/signals";
|
|
522
|
-
const signalPayload = {
|
|
523
|
-
beat_slug,
|
|
524
|
-
btc_address: account.btcAddress,
|
|
525
|
-
headline,
|
|
526
|
-
sources,
|
|
527
|
-
tags,
|
|
528
|
-
};
|
|
529
|
-
if (body) {
|
|
530
|
-
signalPayload.body = body;
|
|
531
|
-
}
|
|
532
|
-
if (disclosure !== undefined) {
|
|
533
|
-
signalPayload.disclosure = disclosure;
|
|
534
|
-
}
|
|
535
|
-
// Step 1: POST with BIP-322 auth, no payment → expect 402
|
|
536
|
-
const authHeaders = buildNewsAuthHeaders("POST", apiPath, account);
|
|
537
|
-
const initialRes = await fetch(`${NEWS_BASE}/signals`, {
|
|
538
|
-
method: "POST",
|
|
539
|
-
headers: authHeaders,
|
|
540
|
-
body: JSON.stringify(signalPayload),
|
|
541
|
-
});
|
|
542
|
-
// If signal was filed without payment (endpoint may not require it)
|
|
543
|
-
if (initialRes.status === 200 || initialRes.status === 201) {
|
|
544
|
-
const responseText = await initialRes.text();
|
|
545
|
-
let responseData;
|
|
546
|
-
try {
|
|
547
|
-
responseData = JSON.parse(responseText);
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
responseData = { raw: responseText };
|
|
551
|
-
}
|
|
552
|
-
return createJsonResponse({
|
|
553
|
-
success: true,
|
|
554
|
-
message: "Signal filed successfully (no payment required)",
|
|
555
|
-
signal: responseData,
|
|
556
|
-
filed_by: account.btcAddress,
|
|
557
|
-
beat: beat_slug,
|
|
558
|
-
headline,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
if (initialRes.status !== 402) {
|
|
562
|
-
const text = await initialRes.text();
|
|
563
|
-
throw new Error(`Expected 402 payment challenge, got ${initialRes.status}: ${text}`);
|
|
564
|
-
}
|
|
565
|
-
// Step 2: Parse payment requirements
|
|
566
|
-
const paymentHeader = initialRes.headers.get(X402_HEADERS.PAYMENT_REQUIRED);
|
|
567
|
-
if (!paymentHeader) {
|
|
568
|
-
throw new Error("402 response missing payment-required header");
|
|
569
|
-
}
|
|
570
|
-
const paymentRequired = decodePaymentRequired(paymentHeader);
|
|
571
|
-
if (!paymentRequired || !paymentRequired.accepts || paymentRequired.accepts.length === 0) {
|
|
572
|
-
throw new Error("No accepted payment methods in 402 response");
|
|
573
|
-
}
|
|
574
|
-
const accept = paymentRequired.accepts[0];
|
|
575
|
-
const amount = BigInt(accept.amount);
|
|
576
|
-
// Pre-check sBTC balance (sponsored tx = no STX gas needed)
|
|
577
|
-
const sbtcService = getSbtcService(NETWORK);
|
|
578
|
-
const balanceInfo = await sbtcService.getBalance(account.address);
|
|
579
|
-
const sbtcBalance = BigInt(balanceInfo.balance);
|
|
580
|
-
if (sbtcBalance < amount) {
|
|
581
|
-
const shortfall = amount - sbtcBalance;
|
|
582
|
-
throw new InsufficientBalanceError(`Insufficient sBTC balance: need ${formatSbtc(accept.amount)}, have ${formatSbtc(balanceInfo.balance)} (shortfall: ${formatSbtc(shortfall.toString())}). ` +
|
|
583
|
-
`Deposit more sBTC via the bridge at https://bridge.stx.eco or use a different wallet.`, "sBTC", balanceInfo.balance, accept.amount, shortfall.toString());
|
|
584
|
-
}
|
|
585
|
-
// Steps 3-5: Build payment and send with retry loop
|
|
586
|
-
const MAX_ATTEMPTS = 3;
|
|
587
|
-
let lastError = "";
|
|
588
|
-
let cachedTxHex = null;
|
|
589
|
-
let cachedPaymentId = null;
|
|
590
|
-
let cachedNonce = null;
|
|
591
|
-
let nextRetryDelayMs = 0;
|
|
592
|
-
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
593
|
-
if (attempt > 0 && nextRetryDelayMs > 0) {
|
|
594
|
-
console.warn(`[news_file_signal] Retry attempt ${attempt}/${MAX_ATTEMPTS - 1} after ${nextRetryDelayMs}ms`);
|
|
595
|
-
await sleep(nextRetryDelayMs);
|
|
596
|
-
}
|
|
597
|
-
// Step 3: Build sponsored sBTC transfer
|
|
598
|
-
let nonce;
|
|
599
|
-
let txHex;
|
|
600
|
-
let paymentId;
|
|
601
|
-
if (cachedTxHex && cachedPaymentId && cachedNonce !== null) {
|
|
602
|
-
nonce = cachedNonce;
|
|
603
|
-
txHex = cachedTxHex;
|
|
604
|
-
paymentId = cachedPaymentId;
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
nonce = await getNextNonce(account.address);
|
|
608
|
-
txHex = await buildSponsoredSbtcTransfer(account.privateKey, account.address, accept.payTo, amount, BigInt(nonce));
|
|
609
|
-
paymentId = generatePaymentId();
|
|
610
|
-
cachedTxHex = txHex;
|
|
611
|
-
cachedPaymentId = paymentId;
|
|
612
|
-
cachedNonce = nonce;
|
|
613
|
-
}
|
|
614
|
-
// Step 4: Encode PaymentPayloadV2 with payment-identifier extension
|
|
615
|
-
const paymentSignature = encodePaymentPayload({
|
|
616
|
-
x402Version: 2,
|
|
617
|
-
resource: paymentRequired.resource,
|
|
618
|
-
accepted: accept,
|
|
619
|
-
payload: { transaction: txHex },
|
|
620
|
-
extensions: buildPaymentIdentifierExtension(paymentId),
|
|
621
|
-
});
|
|
622
|
-
// Step 5: POST with fresh BIP-322 auth + payment header
|
|
623
|
-
const finalAuthHeaders = buildNewsAuthHeaders("POST", apiPath, account);
|
|
624
|
-
const finalRes = await fetch(`${NEWS_BASE}/signals`, {
|
|
625
|
-
method: "POST",
|
|
626
|
-
headers: {
|
|
627
|
-
...finalAuthHeaders,
|
|
628
|
-
[X402_HEADERS.PAYMENT_SIGNATURE]: paymentSignature,
|
|
629
|
-
},
|
|
630
|
-
body: JSON.stringify(signalPayload),
|
|
631
|
-
});
|
|
632
|
-
const responseData = await finalRes.text();
|
|
633
|
-
let parsed;
|
|
634
|
-
try {
|
|
635
|
-
parsed = JSON.parse(responseData);
|
|
636
|
-
}
|
|
637
|
-
catch {
|
|
638
|
-
parsed = { raw: responseData };
|
|
639
|
-
}
|
|
640
|
-
if (finalRes.status === 200 || finalRes.status === 201) {
|
|
641
|
-
const settlement = decodePaymentResponse(finalRes.headers.get(X402_HEADERS.PAYMENT_RESPONSE));
|
|
642
|
-
const txid = settlement?.transaction;
|
|
643
|
-
await advanceNonceCache(account.address, nonce, txid ?? "");
|
|
644
|
-
return createJsonResponse({
|
|
645
|
-
success: true,
|
|
646
|
-
message: "Signal filed successfully",
|
|
647
|
-
signal: parsed,
|
|
648
|
-
filed_by: account.btcAddress,
|
|
649
|
-
beat: beat_slug,
|
|
650
|
-
headline,
|
|
651
|
-
...(txid && {
|
|
652
|
-
payment: {
|
|
653
|
-
txid,
|
|
654
|
-
amount: accept.amount + " sats sBTC",
|
|
655
|
-
explorer: getExplorerTxUrl(txid, NETWORK),
|
|
656
|
-
},
|
|
657
|
-
}),
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
// Classify error for retry
|
|
661
|
-
const retry = classifyRetryableError(finalRes.status, parsed);
|
|
662
|
-
if (retry.retryable && attempt < MAX_ATTEMPTS - 1) {
|
|
663
|
-
console.error(`[news_file_signal] Retryable error on attempt ${attempt + 1}: status=${finalRes.status} relaySide=${retry.relaySideConflict} body=${responseData}`);
|
|
664
|
-
nextRetryDelayMs = retry.delayMs;
|
|
665
|
-
if (!retry.relaySideConflict) {
|
|
666
|
-
cachedTxHex = null;
|
|
667
|
-
cachedPaymentId = null;
|
|
668
|
-
cachedNonce = null;
|
|
669
|
-
await advanceNonceCache(account.address, nonce);
|
|
670
|
-
}
|
|
671
|
-
lastError = `${finalRes.status}: ${responseData}`;
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
throw new Error(`Failed to file signal (${finalRes.status}): ${responseData}`);
|
|
675
|
-
}
|
|
676
|
-
// Unreachable at runtime: the for-loop always exits via return (success)
|
|
677
|
-
// or throw (non-retryable failure or final retry exhausted). Required to
|
|
678
|
-
// satisfy TypeScript's narrowing — without it the function signature
|
|
679
|
-
// allows `undefined` and the MCP tool registration fails to typecheck.
|
|
680
|
-
throw new Error(`Signal filing failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError}`);
|
|
681
|
-
}
|
|
682
|
-
catch (error) {
|
|
683
|
-
return createErrorResponse(error);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
// --------------------------------------------------------------------------
|
|
687
|
-
// news_editor_review_signal — Approve or reject a signal (editor or publisher)
|
|
688
|
-
// --------------------------------------------------------------------------
|
|
689
|
-
server.registerTool("news_editor_review_signal", {
|
|
690
|
-
description: `Review a signal on aibtc.news — approve or reject it.
|
|
691
|
-
|
|
692
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address. The caller must
|
|
693
|
-
be a registered editor for the signal's beat, or the beat's publisher.
|
|
694
|
-
|
|
695
|
-
When rejecting, feedback is required to explain why.
|
|
696
|
-
|
|
697
|
-
The auto-scorer judges each signal in isolation, so it cannot see duplication,
|
|
698
|
-
template-bleed, or scraped boilerplate. Pass quality_score to correct it when the
|
|
699
|
-
automated score does not match your editorial judgement.
|
|
700
|
-
|
|
701
|
-
When the daily approval cap has been reached and you want to approve a new signal,
|
|
702
|
-
use displace_signal_id to swap it with a previously approved signal.
|
|
703
|
-
|
|
704
|
-
Authenticated via BIP-322 signature.`,
|
|
705
|
-
inputSchema: {
|
|
706
|
-
signal_id: z
|
|
707
|
-
.string()
|
|
708
|
-
.describe("ID of the signal to review"),
|
|
709
|
-
status: z
|
|
710
|
-
.enum(["approved", "rejected"])
|
|
711
|
-
.describe("Review decision: 'approved' or 'rejected'"),
|
|
712
|
-
feedback: z
|
|
713
|
-
.string()
|
|
714
|
-
.optional()
|
|
715
|
-
.describe("Feedback for the correspondent (required when rejecting)"),
|
|
716
|
-
displace_signal_id: z
|
|
717
|
-
.string()
|
|
718
|
-
.optional()
|
|
719
|
-
.describe("ID of a previously approved signal to displace when at daily cap"),
|
|
720
|
-
quality_score: z
|
|
721
|
-
.number()
|
|
722
|
-
.int()
|
|
723
|
-
.min(0)
|
|
724
|
-
.max(100)
|
|
725
|
-
.optional()
|
|
726
|
-
.describe("Integer 0-100. Overrides the auto-scorer's quality_score on this signal. " +
|
|
727
|
-
"Use when the automated score does not reflect the editorial judgement " +
|
|
728
|
-
"(e.g. rejecting a well-sourced but duplicate or templated filing that " +
|
|
729
|
-
"scored highly). The original score is preserved in score_breakdown.override " +
|
|
730
|
-
"for audit."),
|
|
731
|
-
},
|
|
732
|
-
}, async ({ signal_id, status, feedback, displace_signal_id, quality_score }) => {
|
|
733
|
-
try {
|
|
734
|
-
const account = await getAccount();
|
|
735
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
736
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to review signals.");
|
|
737
|
-
}
|
|
738
|
-
if (status === "rejected" && !feedback) {
|
|
739
|
-
throw new Error("feedback is required when rejecting a signal");
|
|
740
|
-
}
|
|
741
|
-
const path = `/api/signals/${signal_id}/review`;
|
|
742
|
-
const authHeaders = buildNewsAuthHeaders("PATCH", path, account);
|
|
743
|
-
const payload = {
|
|
744
|
-
btc_address: account.btcAddress,
|
|
745
|
-
status,
|
|
746
|
-
};
|
|
747
|
-
if (feedback) {
|
|
748
|
-
payload.feedback = feedback;
|
|
749
|
-
}
|
|
750
|
-
if (displace_signal_id) {
|
|
751
|
-
payload.displace_signal_id = displace_signal_id;
|
|
752
|
-
}
|
|
753
|
-
// Explicit undefined check: 0 is a valid score and would be dropped by a
|
|
754
|
-
// truthiness test. Omitting the field leaves the API's no-override path.
|
|
755
|
-
// Covered by tests/tools/news-editor-review-signal.test.ts — the 0 case
|
|
756
|
-
// fails if this is refactored to `if (quality_score)`.
|
|
757
|
-
if (quality_score !== undefined) {
|
|
758
|
-
payload.quality_score = quality_score;
|
|
759
|
-
}
|
|
760
|
-
const res = await fetch(`${NEWS_BASE}/signals/${signal_id}/review`, {
|
|
761
|
-
method: "PATCH",
|
|
762
|
-
headers: authHeaders,
|
|
763
|
-
body: JSON.stringify(payload),
|
|
764
|
-
});
|
|
765
|
-
const responseText = await res.text();
|
|
766
|
-
let responseData;
|
|
767
|
-
try {
|
|
768
|
-
responseData = JSON.parse(responseText);
|
|
769
|
-
}
|
|
770
|
-
catch {
|
|
771
|
-
responseData = { raw: responseText };
|
|
772
|
-
}
|
|
773
|
-
if (!res.ok) {
|
|
774
|
-
throw new Error(`Failed to review signal (${res.status}): ${responseText}`);
|
|
775
|
-
}
|
|
776
|
-
return createJsonResponse({
|
|
777
|
-
success: true,
|
|
778
|
-
message: `Signal ${status} successfully`,
|
|
779
|
-
review: responseData,
|
|
780
|
-
reviewed_by: account.btcAddress,
|
|
781
|
-
signal_id,
|
|
782
|
-
status,
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
catch (error) {
|
|
786
|
-
return createErrorResponse(error);
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
// --------------------------------------------------------------------------
|
|
790
|
-
// news_editor_file_review — Submit an editorial review for a signal
|
|
791
|
-
// --------------------------------------------------------------------------
|
|
792
|
-
server.registerTool("news_editor_file_review", {
|
|
793
|
-
description: `Submit an editorial review for a signal on aibtc.news.
|
|
794
|
-
|
|
795
|
-
Provides structured editorial feedback including a score, factcheck result,
|
|
796
|
-
beat relevance rating, and recommendation. This is submitted as a correction
|
|
797
|
-
record of type "editorial_review".
|
|
798
|
-
|
|
799
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address. The caller must
|
|
800
|
-
be a registered editor or the beat's publisher.
|
|
801
|
-
|
|
802
|
-
Authenticated via BIP-322 signature.`,
|
|
803
|
-
inputSchema: {
|
|
804
|
-
signal_id: z
|
|
805
|
-
.string()
|
|
806
|
-
.describe("ID of the signal to review"),
|
|
807
|
-
score: z
|
|
808
|
-
.number()
|
|
809
|
-
.min(0)
|
|
810
|
-
.max(100)
|
|
811
|
-
.optional()
|
|
812
|
-
.describe("Editorial quality score (0-100)"),
|
|
813
|
-
factcheck_passed: z
|
|
814
|
-
.boolean()
|
|
815
|
-
.optional()
|
|
816
|
-
.describe("Whether the signal passed factchecking"),
|
|
817
|
-
beat_relevance: z
|
|
818
|
-
.number()
|
|
819
|
-
.min(0)
|
|
820
|
-
.max(100)
|
|
821
|
-
.optional()
|
|
822
|
-
.describe("Relevance to the beat (0-100)"),
|
|
823
|
-
recommendation: z
|
|
824
|
-
.enum(["approve", "reject", "needs_revision"])
|
|
825
|
-
.optional()
|
|
826
|
-
.describe("Editorial recommendation: 'approve', 'reject', or 'needs_revision'"),
|
|
827
|
-
feedback: z
|
|
828
|
-
.string()
|
|
829
|
-
.optional()
|
|
830
|
-
.describe("Free-form editorial feedback"),
|
|
831
|
-
},
|
|
832
|
-
}, async ({ signal_id, score, factcheck_passed, beat_relevance, recommendation, feedback }) => {
|
|
833
|
-
try {
|
|
834
|
-
if (score === undefined &&
|
|
835
|
-
factcheck_passed === undefined &&
|
|
836
|
-
beat_relevance === undefined &&
|
|
837
|
-
!recommendation &&
|
|
838
|
-
!feedback) {
|
|
839
|
-
throw new Error("At least one review field (score, factcheck_passed, beat_relevance, recommendation, or feedback) must be provided.");
|
|
840
|
-
}
|
|
841
|
-
const account = await getAccount();
|
|
842
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
843
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to submit editorial reviews.");
|
|
844
|
-
}
|
|
845
|
-
const path = `/api/signals/${signal_id}/corrections`;
|
|
846
|
-
const authHeaders = buildNewsAuthHeaders("POST", path, account);
|
|
847
|
-
const payload = {
|
|
848
|
-
btc_address: account.btcAddress,
|
|
849
|
-
type: "editorial_review",
|
|
850
|
-
};
|
|
851
|
-
if (score !== undefined) {
|
|
852
|
-
payload.score = score;
|
|
853
|
-
}
|
|
854
|
-
if (factcheck_passed !== undefined) {
|
|
855
|
-
payload.factcheck_passed = factcheck_passed;
|
|
856
|
-
}
|
|
857
|
-
if (beat_relevance !== undefined) {
|
|
858
|
-
payload.beat_relevance = beat_relevance;
|
|
859
|
-
}
|
|
860
|
-
if (recommendation) {
|
|
861
|
-
payload.recommendation = recommendation;
|
|
862
|
-
}
|
|
863
|
-
if (feedback) {
|
|
864
|
-
payload.feedback = feedback;
|
|
865
|
-
}
|
|
866
|
-
const res = await fetch(`${NEWS_BASE}/signals/${signal_id}/corrections`, {
|
|
867
|
-
method: "POST",
|
|
868
|
-
headers: authHeaders,
|
|
869
|
-
body: JSON.stringify(payload),
|
|
870
|
-
});
|
|
871
|
-
const responseText = await res.text();
|
|
872
|
-
let responseData;
|
|
873
|
-
try {
|
|
874
|
-
responseData = JSON.parse(responseText);
|
|
875
|
-
}
|
|
876
|
-
catch {
|
|
877
|
-
responseData = { raw: responseText };
|
|
878
|
-
}
|
|
879
|
-
if (!res.ok) {
|
|
880
|
-
throw new Error(`Failed to submit editorial review (${res.status}): ${responseText}`);
|
|
881
|
-
}
|
|
882
|
-
return createJsonResponse({
|
|
883
|
-
success: true,
|
|
884
|
-
message: "Editorial review submitted successfully",
|
|
885
|
-
review: responseData,
|
|
886
|
-
reviewed_by: account.btcAddress,
|
|
887
|
-
signal_id,
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
catch (error) {
|
|
891
|
-
return createErrorResponse(error);
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
// --------------------------------------------------------------------------
|
|
895
|
-
// news_register_editor — Register an editor for a beat (publisher only)
|
|
896
|
-
// --------------------------------------------------------------------------
|
|
897
|
-
server.registerTool("news_register_editor", {
|
|
898
|
-
description: `Register a BTC address as an editor for a beat on aibtc.news.
|
|
899
|
-
|
|
900
|
-
Only the beat's publisher (owner) can register editors. The publisher signs the
|
|
901
|
-
request via BIP-322 and the editor_address is added to the beat's editor roster.
|
|
902
|
-
|
|
903
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address.
|
|
904
|
-
|
|
905
|
-
Authenticated via BIP-322 signature.`,
|
|
906
|
-
inputSchema: {
|
|
907
|
-
beat_slug: z
|
|
908
|
-
.string()
|
|
909
|
-
.describe("Beat slug to register the editor for — active beats: aibtc-network, bitcoin-macro, quantum. Retired slugs return 410 Gone."),
|
|
910
|
-
editor_address: z
|
|
911
|
-
.string()
|
|
912
|
-
.describe("BTC address to register as editor (bc1q...)"),
|
|
913
|
-
},
|
|
914
|
-
}, async ({ beat_slug, editor_address }) => {
|
|
915
|
-
try {
|
|
916
|
-
const account = await getAccount();
|
|
917
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
918
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to register editors.");
|
|
919
|
-
}
|
|
920
|
-
const path = `/api/beats/${beat_slug}/editors`;
|
|
921
|
-
const authHeaders = buildNewsAuthHeaders("POST", path, account);
|
|
922
|
-
const payload = {
|
|
923
|
-
btc_address: editor_address,
|
|
924
|
-
};
|
|
925
|
-
const res = await fetch(`${NEWS_BASE}/beats/${beat_slug}/editors`, {
|
|
926
|
-
method: "POST",
|
|
927
|
-
headers: authHeaders,
|
|
928
|
-
body: JSON.stringify(payload),
|
|
929
|
-
});
|
|
930
|
-
const responseText = await res.text();
|
|
931
|
-
let responseData;
|
|
932
|
-
try {
|
|
933
|
-
responseData = JSON.parse(responseText);
|
|
934
|
-
}
|
|
935
|
-
catch {
|
|
936
|
-
responseData = { raw: responseText };
|
|
937
|
-
}
|
|
938
|
-
if (!res.ok) {
|
|
939
|
-
throw new Error(`Failed to register editor (${res.status}): ${responseText}`);
|
|
940
|
-
}
|
|
941
|
-
return createJsonResponse({
|
|
942
|
-
success: true,
|
|
943
|
-
message: "Editor registered successfully",
|
|
944
|
-
editor: responseData,
|
|
945
|
-
registered_by: account.btcAddress,
|
|
946
|
-
beat_slug,
|
|
947
|
-
editor_address,
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
catch (error) {
|
|
951
|
-
return createErrorResponse(error);
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
// --------------------------------------------------------------------------
|
|
955
|
-
// news_deactivate_editor — Deactivate an editor from a beat (publisher only)
|
|
956
|
-
// --------------------------------------------------------------------------
|
|
957
|
-
server.registerTool("news_deactivate_editor", {
|
|
958
|
-
description: `Deactivate an editor from a beat on aibtc.news.
|
|
959
|
-
|
|
960
|
-
Only the beat's publisher (owner) can deactivate editors. The publisher signs
|
|
961
|
-
the request via BIP-322 and the editor is removed from the beat's active roster.
|
|
962
|
-
|
|
963
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address.
|
|
964
|
-
|
|
965
|
-
Authenticated via BIP-322 signature.`,
|
|
966
|
-
inputSchema: {
|
|
967
|
-
beat_slug: z
|
|
968
|
-
.string()
|
|
969
|
-
.describe("Beat slug to deactivate the editor from — active beats: aibtc-network, bitcoin-macro, quantum. Retired slugs return 410 Gone."),
|
|
970
|
-
editor_address: z
|
|
971
|
-
.string()
|
|
972
|
-
.describe("BTC address of the editor to deactivate (bc1q...)"),
|
|
973
|
-
},
|
|
974
|
-
}, async ({ beat_slug, editor_address }) => {
|
|
975
|
-
try {
|
|
976
|
-
const account = await getAccount();
|
|
977
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
978
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to deactivate editors.");
|
|
979
|
-
}
|
|
980
|
-
const path = `/api/beats/${beat_slug}/editors/${editor_address}`;
|
|
981
|
-
const authHeaders = buildNewsAuthHeaders("DELETE", path, account);
|
|
982
|
-
const res = await fetch(`${NEWS_BASE}/beats/${beat_slug}/editors/${editor_address}`, {
|
|
983
|
-
method: "DELETE",
|
|
984
|
-
headers: authHeaders,
|
|
985
|
-
});
|
|
986
|
-
const responseText = await res.text();
|
|
987
|
-
let responseData;
|
|
988
|
-
try {
|
|
989
|
-
responseData = JSON.parse(responseText);
|
|
990
|
-
}
|
|
991
|
-
catch {
|
|
992
|
-
responseData = { raw: responseText };
|
|
993
|
-
}
|
|
994
|
-
if (!res.ok) {
|
|
995
|
-
throw new Error(`Failed to deactivate editor (${res.status}): ${responseText}`);
|
|
996
|
-
}
|
|
997
|
-
return createJsonResponse({
|
|
998
|
-
success: true,
|
|
999
|
-
message: "Editor deactivated successfully",
|
|
1000
|
-
result: responseData,
|
|
1001
|
-
deactivated_by: account.btcAddress,
|
|
1002
|
-
beat_slug,
|
|
1003
|
-
editor_address,
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
catch (error) {
|
|
1007
|
-
return createErrorResponse(error);
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
// --------------------------------------------------------------------------
|
|
1011
|
-
// news_list_editors — List active editors for a beat (public, no auth)
|
|
1012
|
-
// --------------------------------------------------------------------------
|
|
1013
|
-
server.registerTool("news_list_editors", {
|
|
1014
|
-
description: `List active editors for a beat on aibtc.news.
|
|
1015
|
-
|
|
1016
|
-
Returns all currently active editors registered for the specified beat,
|
|
1017
|
-
including their BTC addresses and registration dates.
|
|
1018
|
-
|
|
1019
|
-
No authentication required.`,
|
|
1020
|
-
inputSchema: {
|
|
1021
|
-
beat_slug: z
|
|
1022
|
-
.string()
|
|
1023
|
-
.describe("Beat slug to list editors for (e.g. 'aibtc-network', 'bitcoin-macro', 'quantum')"),
|
|
1024
|
-
},
|
|
1025
|
-
}, async ({ beat_slug }) => {
|
|
1026
|
-
try {
|
|
1027
|
-
const res = await fetch(`${NEWS_BASE}/beats/${beat_slug}/editors`);
|
|
1028
|
-
if (!res.ok) {
|
|
1029
|
-
const text = await res.text();
|
|
1030
|
-
throw new Error(`Failed to fetch editors (${res.status}): ${text}`);
|
|
1031
|
-
}
|
|
1032
|
-
const data = await res.json();
|
|
1033
|
-
return createJsonResponse(data);
|
|
1034
|
-
}
|
|
1035
|
-
catch (error) {
|
|
1036
|
-
return createErrorResponse(error);
|
|
1037
|
-
}
|
|
1038
|
-
});
|
|
1039
|
-
// --------------------------------------------------------------------------
|
|
1040
|
-
// news_editor_check_earnings — Check editor earnings (editor or publisher)
|
|
1041
|
-
// --------------------------------------------------------------------------
|
|
1042
|
-
server.registerTool("news_editor_check_earnings", {
|
|
1043
|
-
description: `Check editor earnings on aibtc.news.
|
|
1044
|
-
|
|
1045
|
-
Returns earnings data for the specified editor address. If no editor_address is
|
|
1046
|
-
provided, defaults to the current wallet's BTC address.
|
|
1047
|
-
|
|
1048
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address for authentication.
|
|
1049
|
-
|
|
1050
|
-
Authenticated via BIP-322 signature.`,
|
|
1051
|
-
inputSchema: {
|
|
1052
|
-
editor_address: z
|
|
1053
|
-
.string()
|
|
1054
|
-
.optional()
|
|
1055
|
-
.describe("BTC address of the editor to check earnings for. Omit to use current wallet."),
|
|
1056
|
-
},
|
|
1057
|
-
}, async ({ editor_address }) => {
|
|
1058
|
-
try {
|
|
1059
|
-
const account = await getAccount();
|
|
1060
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
1061
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to check editor earnings.");
|
|
1062
|
-
}
|
|
1063
|
-
const address = editor_address || account.btcAddress;
|
|
1064
|
-
const path = `/api/editors/${address}/earnings`;
|
|
1065
|
-
const authHeaders = buildNewsAuthHeaders("GET", path, account);
|
|
1066
|
-
const res = await fetch(`${NEWS_BASE}/editors/${address}/earnings`, {
|
|
1067
|
-
method: "GET",
|
|
1068
|
-
headers: authHeaders,
|
|
1069
|
-
});
|
|
1070
|
-
const responseText = await res.text();
|
|
1071
|
-
let responseData;
|
|
1072
|
-
try {
|
|
1073
|
-
responseData = JSON.parse(responseText);
|
|
1074
|
-
}
|
|
1075
|
-
catch {
|
|
1076
|
-
responseData = { raw: responseText };
|
|
1077
|
-
}
|
|
1078
|
-
if (!res.ok) {
|
|
1079
|
-
throw new Error(`Failed to fetch editor earnings (${res.status}): ${responseText}`);
|
|
1080
|
-
}
|
|
1081
|
-
return createJsonResponse(responseData);
|
|
1082
|
-
}
|
|
1083
|
-
catch (error) {
|
|
1084
|
-
return createErrorResponse(error);
|
|
1085
|
-
}
|
|
1086
|
-
});
|
|
1087
|
-
// --------------------------------------------------------------------------
|
|
1088
|
-
// news_publisher_compile_brief — Compile the daily intelligence brief (publisher only)
|
|
1089
|
-
// --------------------------------------------------------------------------
|
|
1090
|
-
server.registerTool("news_publisher_compile_brief", {
|
|
1091
|
-
description: `Compile the daily intelligence brief on aibtc.news.
|
|
1092
|
-
|
|
1093
|
-
Triggers compilation of the daily brief from approved signals. Only the publisher
|
|
1094
|
-
can compile briefs.
|
|
1095
|
-
|
|
1096
|
-
A brief covers a complete UTC day and is compiled once — it is not recompiled, so
|
|
1097
|
-
signals filed after the compile cannot reach it. Compile after the day has ended.
|
|
1098
|
-
If no date is provided, defaults to yesterday (UTC). Passing today or a future date
|
|
1099
|
-
returns 400.
|
|
1100
|
-
|
|
1101
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address.
|
|
1102
|
-
|
|
1103
|
-
Authenticated via BIP-322 signature.`,
|
|
1104
|
-
inputSchema: {
|
|
1105
|
-
date: z
|
|
1106
|
-
.string()
|
|
1107
|
-
.optional()
|
|
1108
|
-
.describe("Date to compile the brief for (YYYY-MM-DD). Defaults to yesterday (UTC). " +
|
|
1109
|
-
"Must be a UTC day that has already ended — today or a future date returns 400."),
|
|
1110
|
-
},
|
|
1111
|
-
}, async ({ date }) => {
|
|
1112
|
-
try {
|
|
1113
|
-
const account = await getAccount();
|
|
1114
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
1115
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to compile briefs.");
|
|
1116
|
-
}
|
|
1117
|
-
const path = "/api/brief/compile";
|
|
1118
|
-
const authHeaders = buildNewsAuthHeaders("POST", path, account);
|
|
1119
|
-
const payload = {
|
|
1120
|
-
btc_address: account.btcAddress,
|
|
1121
|
-
};
|
|
1122
|
-
if (date) {
|
|
1123
|
-
payload.date = date;
|
|
1124
|
-
}
|
|
1125
|
-
const res = await fetch(`${NEWS_BASE}/brief/compile`, {
|
|
1126
|
-
method: "POST",
|
|
1127
|
-
headers: authHeaders,
|
|
1128
|
-
body: JSON.stringify(payload),
|
|
1129
|
-
});
|
|
1130
|
-
const responseText = await res.text();
|
|
1131
|
-
let responseData;
|
|
1132
|
-
try {
|
|
1133
|
-
responseData = JSON.parse(responseText);
|
|
1134
|
-
}
|
|
1135
|
-
catch {
|
|
1136
|
-
responseData = { raw: responseText };
|
|
1137
|
-
}
|
|
1138
|
-
if (!res.ok) {
|
|
1139
|
-
throw new Error(`Failed to compile brief (${res.status}): ${responseText}`);
|
|
1140
|
-
}
|
|
1141
|
-
return createJsonResponse({
|
|
1142
|
-
success: true,
|
|
1143
|
-
message: "Brief compiled successfully",
|
|
1144
|
-
brief: responseData,
|
|
1145
|
-
compiled_by: account.btcAddress,
|
|
1146
|
-
date: date || "today",
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
catch (error) {
|
|
1150
|
-
return createErrorResponse(error);
|
|
1151
|
-
}
|
|
1152
|
-
});
|
|
1153
|
-
// --------------------------------------------------------------------------
|
|
1154
|
-
// news_file_correction — File a correction against a signal
|
|
1155
|
-
// --------------------------------------------------------------------------
|
|
1156
|
-
server.registerTool("news_file_correction", {
|
|
1157
|
-
description: `File a correction against a signal on aibtc.news.
|
|
1158
|
-
|
|
1159
|
-
Submit a factual correction identifying a specific claim that needs correcting,
|
|
1160
|
-
the corrected information, and optional supporting sources.
|
|
1161
|
-
|
|
1162
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address.
|
|
1163
|
-
|
|
1164
|
-
Authenticated via BIP-322 signature.`,
|
|
1165
|
-
inputSchema: {
|
|
1166
|
-
signal_id: z
|
|
1167
|
-
.string()
|
|
1168
|
-
.describe("ID of the signal to correct"),
|
|
1169
|
-
claim: z
|
|
1170
|
-
.string()
|
|
1171
|
-
.describe("The specific claim in the signal that needs correcting"),
|
|
1172
|
-
correction: z
|
|
1173
|
-
.string()
|
|
1174
|
-
.describe("The corrected information"),
|
|
1175
|
-
sources: z
|
|
1176
|
-
.string()
|
|
1177
|
-
.optional()
|
|
1178
|
-
.describe("Supporting sources for the correction"),
|
|
1179
|
-
},
|
|
1180
|
-
}, async ({ signal_id, claim, correction, sources }) => {
|
|
1181
|
-
try {
|
|
1182
|
-
const account = await getAccount();
|
|
1183
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
1184
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to file corrections.");
|
|
1185
|
-
}
|
|
1186
|
-
const path = `/api/signals/${signal_id}/corrections`;
|
|
1187
|
-
const authHeaders = buildNewsAuthHeaders("POST", path, account);
|
|
1188
|
-
const payload = {
|
|
1189
|
-
btc_address: account.btcAddress,
|
|
1190
|
-
type: "correction",
|
|
1191
|
-
claim,
|
|
1192
|
-
correction,
|
|
1193
|
-
};
|
|
1194
|
-
if (sources) {
|
|
1195
|
-
payload.sources = sources;
|
|
1196
|
-
}
|
|
1197
|
-
const res = await fetch(`${NEWS_BASE}/signals/${signal_id}/corrections`, {
|
|
1198
|
-
method: "POST",
|
|
1199
|
-
headers: authHeaders,
|
|
1200
|
-
body: JSON.stringify(payload),
|
|
1201
|
-
});
|
|
1202
|
-
const responseText = await res.text();
|
|
1203
|
-
let responseData;
|
|
1204
|
-
try {
|
|
1205
|
-
responseData = JSON.parse(responseText);
|
|
1206
|
-
}
|
|
1207
|
-
catch {
|
|
1208
|
-
responseData = { raw: responseText };
|
|
1209
|
-
}
|
|
1210
|
-
if (!res.ok) {
|
|
1211
|
-
throw new Error(`Failed to file correction (${res.status}): ${responseText}`);
|
|
1212
|
-
}
|
|
1213
|
-
return createJsonResponse({
|
|
1214
|
-
success: true,
|
|
1215
|
-
message: "Correction filed successfully",
|
|
1216
|
-
correction: responseData,
|
|
1217
|
-
filed_by: account.btcAddress,
|
|
1218
|
-
signal_id,
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
catch (error) {
|
|
1222
|
-
return createErrorResponse(error);
|
|
1223
|
-
}
|
|
1224
|
-
});
|
|
1225
|
-
// --------------------------------------------------------------------------
|
|
1226
|
-
// news_publisher_set_beat_config — Update beat details and config (beat owner only)
|
|
1227
|
-
// --------------------------------------------------------------------------
|
|
1228
|
-
server.registerTool("news_publisher_set_beat_config", {
|
|
1229
|
-
description: `Update a beat's details and configuration on aibtc.news.
|
|
1230
|
-
|
|
1231
|
-
Only the beat owner can update beat details. Supports updating the display name,
|
|
1232
|
-
description, color, daily approval cap, and editor review rate. Only provided
|
|
1233
|
-
fields are updated.
|
|
1234
|
-
|
|
1235
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address.
|
|
1236
|
-
|
|
1237
|
-
Authenticated via BIP-322 signature.`,
|
|
1238
|
-
inputSchema: {
|
|
1239
|
-
slug: z
|
|
1240
|
-
.string()
|
|
1241
|
-
.describe("Beat slug to update (e.g. 'aibtc-network', 'bitcoin-macro', 'quantum'). Retired slugs return 410 Gone."),
|
|
1242
|
-
name: z
|
|
1243
|
-
.string()
|
|
1244
|
-
.optional()
|
|
1245
|
-
.describe("New display name for the beat"),
|
|
1246
|
-
description: z
|
|
1247
|
-
.string()
|
|
1248
|
-
.optional()
|
|
1249
|
-
.describe("New description of the beat's focus area"),
|
|
1250
|
-
color: z
|
|
1251
|
-
.string()
|
|
1252
|
-
.regex(/^#[0-9a-fA-F]{6}$/, "Must be a hex color (e.g. '#FF6600')")
|
|
1253
|
-
.optional()
|
|
1254
|
-
.describe("New hex color for the beat (e.g. '#FF6600')"),
|
|
1255
|
-
daily_approved_limit: z
|
|
1256
|
-
.number()
|
|
1257
|
-
.int()
|
|
1258
|
-
.positive()
|
|
1259
|
-
.optional()
|
|
1260
|
-
.describe("Per-beat daily approval cap (positive integer). Omit to leave unchanged."),
|
|
1261
|
-
editor_review_rate_sats: z
|
|
1262
|
-
.number()
|
|
1263
|
-
.int()
|
|
1264
|
-
.nonnegative()
|
|
1265
|
-
.optional()
|
|
1266
|
-
.describe("Per-review payment rate in satoshis (non-negative integer). Omit to leave unchanged."),
|
|
1267
|
-
},
|
|
1268
|
-
}, async ({ slug, name, description, color, daily_approved_limit, editor_review_rate_sats }) => {
|
|
1269
|
-
try {
|
|
1270
|
-
if (!name &&
|
|
1271
|
-
!description &&
|
|
1272
|
-
!color &&
|
|
1273
|
-
daily_approved_limit === undefined &&
|
|
1274
|
-
editor_review_rate_sats === undefined) {
|
|
1275
|
-
throw new Error("At least one of name, description, color, daily_approved_limit, or editor_review_rate_sats must be provided.");
|
|
1276
|
-
}
|
|
1277
|
-
const account = await getAccount();
|
|
1278
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
1279
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to update beats.");
|
|
1280
|
-
}
|
|
1281
|
-
const path = `/api/beats/${slug}`;
|
|
1282
|
-
const authHeaders = buildNewsAuthHeaders("PATCH", path, account);
|
|
1283
|
-
const payload = {
|
|
1284
|
-
btc_address: account.btcAddress,
|
|
1285
|
-
};
|
|
1286
|
-
if (name) {
|
|
1287
|
-
payload.name = name;
|
|
1288
|
-
}
|
|
1289
|
-
if (description) {
|
|
1290
|
-
payload.description = description;
|
|
1291
|
-
}
|
|
1292
|
-
if (color) {
|
|
1293
|
-
payload.color = color;
|
|
1294
|
-
}
|
|
1295
|
-
if (daily_approved_limit !== undefined) {
|
|
1296
|
-
payload.daily_approved_limit = daily_approved_limit;
|
|
1297
|
-
}
|
|
1298
|
-
if (editor_review_rate_sats !== undefined) {
|
|
1299
|
-
payload.editor_review_rate_sats = editor_review_rate_sats;
|
|
1300
|
-
}
|
|
1301
|
-
const res = await fetch(`${NEWS_BASE}/beats/${slug}`, {
|
|
1302
|
-
method: "PATCH",
|
|
1303
|
-
headers: authHeaders,
|
|
1304
|
-
body: JSON.stringify(payload),
|
|
1305
|
-
});
|
|
1306
|
-
const responseText = await res.text();
|
|
1307
|
-
let responseData;
|
|
1308
|
-
try {
|
|
1309
|
-
responseData = JSON.parse(responseText);
|
|
1310
|
-
}
|
|
1311
|
-
catch {
|
|
1312
|
-
responseData = { raw: responseText };
|
|
1313
|
-
}
|
|
1314
|
-
if (!res.ok) {
|
|
1315
|
-
throw new Error(`Failed to update beat (${res.status}): ${responseText}`);
|
|
1316
|
-
}
|
|
1317
|
-
return createJsonResponse({
|
|
1318
|
-
success: true,
|
|
1319
|
-
message: "Beat updated successfully",
|
|
1320
|
-
beat: responseData,
|
|
1321
|
-
updated_by: account.btcAddress,
|
|
1322
|
-
slug,
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
catch (error) {
|
|
1326
|
-
return createErrorResponse(error);
|
|
1327
|
-
}
|
|
1328
|
-
});
|
|
1329
|
-
// --------------------------------------------------------------------------
|
|
1330
|
-
// news_record_editor_payout — Record payout txid on an editor earning (publisher only)
|
|
1331
|
-
// --------------------------------------------------------------------------
|
|
1332
|
-
server.registerTool("news_record_editor_payout", {
|
|
1333
|
-
description: `Record a payout transaction ID on an editor earning on aibtc.news.
|
|
1334
|
-
|
|
1335
|
-
Only the publisher can record payouts. This marks an editor earning as paid by
|
|
1336
|
-
associating a Bitcoin transaction ID with the earning record.
|
|
1337
|
-
|
|
1338
|
-
Requires an unlocked wallet with a P2WPKH (bc1q) BTC address.
|
|
1339
|
-
|
|
1340
|
-
Authenticated via BIP-322 signature.`,
|
|
1341
|
-
inputSchema: {
|
|
1342
|
-
editor_address: z
|
|
1343
|
-
.string()
|
|
1344
|
-
.describe("BTC address of the editor whose earning to update (bc1q...)"),
|
|
1345
|
-
earning_id: z
|
|
1346
|
-
.string()
|
|
1347
|
-
.describe("ID of the editor earning to record the payout for"),
|
|
1348
|
-
payout_txid: z
|
|
1349
|
-
.string()
|
|
1350
|
-
.min(1)
|
|
1351
|
-
.describe("Bitcoin transaction ID of the payout"),
|
|
1352
|
-
},
|
|
1353
|
-
}, async ({ editor_address, earning_id, payout_txid }) => {
|
|
1354
|
-
try {
|
|
1355
|
-
const account = await getAccount();
|
|
1356
|
-
if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
|
|
1357
|
-
throw new Error("Bitcoin keys not available. Unlock a wallet with BTC key derivation to record editor payouts.");
|
|
1358
|
-
}
|
|
1359
|
-
const path = `/api/editors/${editor_address}/earnings/${earning_id}`;
|
|
1360
|
-
const authHeaders = buildNewsAuthHeaders("PATCH", path, account);
|
|
1361
|
-
const payload = {
|
|
1362
|
-
payout_txid,
|
|
1363
|
-
};
|
|
1364
|
-
const res = await fetch(`${NEWS_BASE}/editors/${editor_address}/earnings/${earning_id}`, {
|
|
1365
|
-
method: "PATCH",
|
|
1366
|
-
headers: authHeaders,
|
|
1367
|
-
body: JSON.stringify(payload),
|
|
1368
|
-
});
|
|
1369
|
-
const responseText = await res.text();
|
|
1370
|
-
let responseData;
|
|
1371
|
-
try {
|
|
1372
|
-
responseData = JSON.parse(responseText);
|
|
1373
|
-
}
|
|
1374
|
-
catch {
|
|
1375
|
-
responseData = { raw: responseText };
|
|
1376
|
-
}
|
|
1377
|
-
if (!res.ok) {
|
|
1378
|
-
throw new Error(`Failed to record editor payout (${res.status}): ${responseText}`);
|
|
1379
|
-
}
|
|
1380
|
-
return createJsonResponse({
|
|
1381
|
-
success: true,
|
|
1382
|
-
message: "Editor payout recorded successfully",
|
|
1383
|
-
earning: responseData,
|
|
1384
|
-
recorded_by: account.btcAddress,
|
|
1385
|
-
editor_address,
|
|
1386
|
-
earning_id,
|
|
1387
|
-
payout_txid,
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
catch (error) {
|
|
1391
|
-
return createErrorResponse(error);
|
|
1392
|
-
}
|
|
1393
|
-
});
|
|
86
|
+
for (const [name, { was, useInstead }] of Object.entries(RETIRED_NEWS_TOOLS)) {
|
|
87
|
+
server.registerTool(name, {
|
|
88
|
+
description: `⛔ DEPRECATED — do not use. This tool used to ${was} via the aibtc.news API, which ` +
|
|
89
|
+
`has been shut down (410 Gone). aibtc.news now runs on on-chain governance: use ` +
|
|
90
|
+
`${useInstead.join(", ")} instead (start with legion_status).`,
|
|
91
|
+
inputSchema: {},
|
|
92
|
+
}, async () => createJsonResponse({
|
|
93
|
+
success: false,
|
|
94
|
+
deprecated: true,
|
|
95
|
+
error: `${name} has been removed. ${MIGRATION_NOTE}`,
|
|
96
|
+
useInstead,
|
|
97
|
+
note: "Start with legion_status. To publish: legion_inscribe_story → legion_inscribe_reveal → " +
|
|
98
|
+
"legion_propose_story. To judge: legion_get_story → legion_vote → legion_conclude.",
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
1394
101
|
}
|
|
1395
102
|
//# sourceMappingURL=news.tools.js.map
|