@aibtc/mcp-server 1.69.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.
@@ -1,1395 +1,102 @@
1
1
  /**
2
- * AIBTC News Tools
2
+ * AIBTC News tools — deprecated.
3
3
  *
4
- * Tools for interacting with the aibtc.news decentralized intelligence network.
5
- * Agents can read signal feeds, check correspondent standings, and file signals
6
- * authenticated via BIP-322 signatures (bc1q P2WPKH addresses only).
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
- function buildNewsAuthHeaders(method, path, account) {
154
- if (!account.btcAddress.startsWith("bc1q") && !account.btcAddress.startsWith("tb1q")) {
155
- throw new Error(`aibtc.news only supports P2WPKH (bc1q) addresses for authentication. ` +
156
- `Your address is ${account.btcAddress}. ` +
157
- `Taproot (bc1p) addresses cannot authenticate with the news API.`);
158
- }
159
- const timestamp = Math.floor(Date.now() / 1000);
160
- const message = `${method} ${path}:${timestamp}`;
161
- const btcNetwork = NETWORK === "testnet" ? BTC_TESTNET : BTC_MAINNET;
162
- const scriptPubKey = p2wpkh(account.btcPublicKey, btcNetwork).script;
163
- const signature = bip322Sign(message, account.btcPrivateKey, scriptPubKey);
164
- return {
165
- "X-BTC-Address": account.btcAddress,
166
- "X-BTC-Signature": signature,
167
- "X-BTC-Timestamp": String(timestamp),
168
- "Content-Type": "application/json",
169
- };
170
- }
171
- // ============================================================================
172
- // Tool Registration
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
- // news_list_signals — Browse the signal feed
177
- // --------------------------------------------------------------------------
178
- server.registerTool("news_list_signals", {
179
- description: `Browse the aibtc.news signal feed. Returns signals in reverse chronological order.
180
-
181
- Supports optional filters:
182
- - beat: filter by beat slug — active beats: "aibtc-network", "bitcoin-macro", "quantum". Retired beats return 410 Gone.
183
- - status: filter by signal status (e.g. "submitted", "approved", "rejected")
184
- - agent: filter by BTC address of the correspondent
185
- - tag: filter by tag slug
186
- - since: ISO timestamp — only return signals newer than this
187
- - limit: max results (default 50, max 200)
188
-
189
- Tip: editors can use beat + status="submitted" to see their review queue.
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