@aibtc/mcp-server 1.68.0 → 1.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/at-stake.d.ts +120 -0
- package/dist/config/at-stake.js +183 -0
- package/dist/services/at-stake.service.d.ts +144 -0
- package/dist/services/at-stake.service.js +272 -0
- package/dist/services/spend-limiter.d.ts +53 -0
- package/dist/services/spend-limiter.js +97 -39
- package/dist/services/state-lock.d.ts +73 -0
- package/dist/services/state-lock.js +229 -0
- package/dist/tools/at-stake-legion.tools.d.ts +31 -0
- package/dist/tools/at-stake-legion.tools.js +776 -0
- package/dist/tools/at-stake.tools.d.ts +29 -0
- package/dist/tools/at-stake.tools.js +684 -0
- package/dist/tools/competition.tools.d.ts +12 -11
- package/dist/tools/competition.tools.js +3 -115
- package/dist/tools/index.js +6 -3
- package/dist/tools/legion.tools.d.ts +4 -3
- package/dist/tools/legion.tools.js +9 -12
- package/dist/tools/ordinals.tools.js +28 -2
- package/dist/transactions/builder.js +14 -1
- package/package.json +1 -1
- package/skill/SKILL.md +37 -1
- package/skill/references/at-stake.md +214 -0
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AIBTC Trading Competition Tools
|
|
2
|
+
* AIBTC Trading Competition Tools (read-only, NOT REGISTERED)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The competition has concluded. Trade submission and the DEX allowlist were
|
|
5
|
+
* removed, and these two remaining read tools are no longer exposed over MCP:
|
|
6
|
+
* `registerCompetitionTools` is intentionally not called from `src/tools/index.ts`.
|
|
7
|
+
*
|
|
8
|
+
* The module is kept because the standing/history endpoints still exist and the
|
|
9
|
+
* P&L math (`computeCampaignStats`) is the reference implementation. Re-expose by
|
|
10
|
+
* restoring the import + call in `src/tools/index.ts`; the tests here exercise the
|
|
11
|
+
* module directly, so it stays compiled and verified while unwired.
|
|
8
12
|
*
|
|
9
13
|
* Tools:
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* - competition_list_trades — Paginated trade history (submitted + indexed)
|
|
14
|
+
* - competition_status — Standing + mark-to-current P&L for an address
|
|
15
|
+
* - competition_list_trades — Paginated trade history
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
* already carries the agent's signature and sender), and status/list reads
|
|
16
|
-
* are over public addresses. Rate-limited per IP server-side.
|
|
17
|
+
* Reads are over public addresses, no signing. Rate-limited per IP server-side.
|
|
17
18
|
*
|
|
18
19
|
* API spec + verifier implementation: aibtcdev/landing-page#734 (Phase 3.1).
|
|
19
20
|
* Schema source of truth: docs/rfc-d1-schema.md §swaps (migration 005).
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AIBTC_CAMPAIGN_API_URL } from "../config/competition.js";
|
|
3
|
-
import { NETWORK } from "../config/networks.js";
|
|
4
|
-
import { getTransactionStatus } from "../services/hiro-api.js";
|
|
5
3
|
import { getTokenInfo } from "../services/tenero-api.js";
|
|
6
4
|
import { getWalletManager } from "../services/wallet-manager.js";
|
|
7
5
|
import { createJsonResponse } from "../utils/formatting.js";
|
|
8
6
|
import { createErrorResponse } from "../utils/errors.js";
|
|
9
|
-
/**
|
|
10
|
-
* Stacks tx_status values from Hiro's /extended/v1/tx endpoint.
|
|
11
|
-
* `pending` = in mempool, not confirmed. Everything else is terminal:
|
|
12
|
-
* `success`, `abort_by_response`, `abort_by_post_condition`, and the
|
|
13
|
-
* five `dropped_*` codes. The verifier records terminal failures too
|
|
14
|
-
* (migration 005's CHECK allows all 8 terminal codes) so we only block
|
|
15
|
-
* submission when status is `pending`.
|
|
16
|
-
*/
|
|
17
|
-
const PENDING_TX_STATUS = "pending";
|
|
18
7
|
const stacksAddressSchema = z
|
|
19
8
|
.string()
|
|
20
9
|
.regex(/^S[PTM][0-9A-HJKMNP-TV-Z]{38,40}$/, "Expected a Stacks address (SP… mainnet, ST… testnet, SM… contract)");
|
|
@@ -33,14 +22,6 @@ async function resolveAddress(provided) {
|
|
|
33
22
|
}
|
|
34
23
|
return meta.address;
|
|
35
24
|
}
|
|
36
|
-
function normalizeTxid(txid) {
|
|
37
|
-
const trimmed = txid.trim();
|
|
38
|
-
const withPrefix = trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
|
|
39
|
-
if (!/^0x[0-9a-fA-F]{64}$/.test(withPrefix)) {
|
|
40
|
-
throw new Error(`Invalid Stacks txid: expected 64 hex chars (with optional 0x prefix), got ${JSON.stringify(txid)}`);
|
|
41
|
-
}
|
|
42
|
-
return withPrefix.toLowerCase();
|
|
43
|
-
}
|
|
44
25
|
const COMPETITION_FETCH_TIMEOUT_MS = 10_000;
|
|
45
26
|
/**
|
|
46
27
|
* Cap on `/trades` pagination when computing campaign stats. 10 pages × 200
|
|
@@ -290,66 +271,8 @@ async function resolveTokenPricesViaTenero(swaps) {
|
|
|
290
271
|
return prices;
|
|
291
272
|
}
|
|
292
273
|
export function registerCompetitionTools(server) {
|
|
293
|
-
server.registerTool("competition_submit_trade", {
|
|
294
|
-
description: `Submit a trade txid to the AIBTC trading competition for verification and P&L scoring.
|
|
295
|
-
|
|
296
|
-
**Two-step registration prerequisite** (both required, both one-time):
|
|
297
|
-
1. Register on aibtc.com via the website's dual-sig flow (BIP-322 + SIP-018). This is not an MCP tool — agents go to https://aibtc.com to complete it.
|
|
298
|
-
2. Register on the ERC-8004 identity contract via the \`identity_register\` MCP tool. This mints the on-chain agent ID that the campaign joins against.
|
|
299
|
-
|
|
300
|
-
Mainnet-only in v1.
|
|
301
|
-
|
|
302
|
-
The competition service fetches the tx from the Stacks chain and validates:
|
|
303
|
-
- sender is registered on aibtc.com AND has an ERC-8004 agent_id
|
|
304
|
-
- contract+function is on the campaign allowlist (e.g. Bitflow swap helpers, ALEX, Zest)
|
|
305
|
-
- transaction status is terminal (success or any of the 7 terminal-failure codes)
|
|
306
|
-
|
|
307
|
-
**No additional signed message is needed** — the txid itself is the agent's signed intent. The on-chain tx already carries their address (= identity) and the trade (= the on-chain effect). Tx history is the ledger.
|
|
308
|
-
|
|
309
|
-
Submission is a fast-path hint — the backend also indexes registered agent addresses passively via a frequent catch-up cron, so a missed submission still gets picked up before final scoring. Submitting the same txid twice is idempotent (\`(txid)\` is the DB primary key; first writer wins).
|
|
310
|
-
|
|
311
|
-
**Pre-flight:** This tool checks tx status on Stacks via Hiro before forwarding to the verifier. If the tx is still \`pending\` (in mempool), the call returns \`{ accepted: false, tx_status: "pending", message: "..." }\` without hitting the verifier — wait ~30s for confirmation and retry. Use \`get_transaction_status\` to poll explicitly.
|
|
312
|
-
|
|
313
|
-
Response shapes:
|
|
314
|
-
- \`{ accepted: false, txid, tx_status: "pending", message }\` (pre-flight gate): tx not yet confirmed. Retry after ~30s.
|
|
315
|
-
- \`200 OK\` with the swap row once verified: \`{ txid, sender, contract_id, function_name, token_in, amount_in, token_out, amount_out, burn_block_time, tx_status, source, ... }\`. Field names follow on-chain vocabulary (migration 005). \`tx_status\` is one of \`success\` or 7 terminal-failure codes (verifier records terminal failures too); \`source\` is \`"agent" | "cron"\`.
|
|
316
|
-
- Permanent rejection (HTTP 4xx, thrown as error): sender not registered, contract not on allowlist, or txid malformed. Do not retry — fix the inputs.
|
|
317
|
-
- Transient failure (HTTP 5xx or timeout, thrown as error): retry with backoff.`,
|
|
318
|
-
inputSchema: {
|
|
319
|
-
txid: z
|
|
320
|
-
.string()
|
|
321
|
-
.min(1)
|
|
322
|
-
.describe("Stacks transaction id (with or without 0x prefix)"),
|
|
323
|
-
},
|
|
324
|
-
}, async ({ txid }) => {
|
|
325
|
-
try {
|
|
326
|
-
const normalized = normalizeTxid(txid);
|
|
327
|
-
// Pre-flight: confirm the tx is terminal on Stacks before forwarding
|
|
328
|
-
// to the verifier. Stacks blocks settle in ~30s, so a "pending" status
|
|
329
|
-
// means the agent submitted too early — we tell them to wait and
|
|
330
|
-
// resubmit rather than burning a backend round-trip.
|
|
331
|
-
const txStatus = await getTransactionStatus(normalized, NETWORK);
|
|
332
|
-
if (txStatus.status === PENDING_TX_STATUS) {
|
|
333
|
-
return createJsonResponse({
|
|
334
|
-
accepted: false,
|
|
335
|
-
txid: normalized,
|
|
336
|
-
tx_status: PENDING_TX_STATUS,
|
|
337
|
-
message: "Transaction is still pending on Stacks. Wait ~30s for confirmation and resubmit. Use get_transaction_status to poll.",
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
const parsed = await competitionFetch("/trades", {
|
|
341
|
-
method: "POST",
|
|
342
|
-
headers: { "content-type": "application/json" },
|
|
343
|
-
body: JSON.stringify({ txid: normalized }),
|
|
344
|
-
});
|
|
345
|
-
return createJsonResponse(parsed);
|
|
346
|
-
}
|
|
347
|
-
catch (error) {
|
|
348
|
-
return createErrorResponse(error);
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
274
|
server.registerTool("competition_status", {
|
|
352
|
-
description: `Get
|
|
275
|
+
description: `Get an agent's final AIBTC trading competition standing, with mark-to-current P&L computed locally. The competition has concluded; this reads the historical record.
|
|
353
276
|
|
|
354
277
|
**Latency note:** by default this call also paginates the agent's trade history and parallel-fetches Tenero prices to compute live P&L. That adds a few seconds to the round-trip for agents with many trades or many distinct tokens. Pass \`include_pnl: false\` to skip that path and get only the cheap registration/count fields back.
|
|
355
278
|
|
|
@@ -399,9 +322,9 @@ To be eligible for scoring, the agent needs **both** an aibtc.com website regist
|
|
|
399
322
|
}
|
|
400
323
|
});
|
|
401
324
|
server.registerTool("competition_list_trades", {
|
|
402
|
-
description: `List
|
|
325
|
+
description: `List an agent's trades from the concluded AIBTC trading competition.
|
|
403
326
|
|
|
404
|
-
|
|
327
|
+
Covers txids the agent submitted while the competition ran and txids discovered via passive address monitoring. Each entry is a swap row from migration 005: \`{ txid, sender, contract_id, function_name, token_in, amount_in, token_out, amount_out, burn_block_time, tx_status, source, scored_value, scored_at }\`. \`source\` distinguishes \`"agent"\` (your submission) from \`"cron"\` (nightly catch-up). Response: \`{ trades, next_cursor }\` — opaque cursor for pagination. If no address is provided, uses the active wallet's Stacks address.`,
|
|
405
328
|
inputSchema: {
|
|
406
329
|
address: stacksAddressSchema
|
|
407
330
|
.optional()
|
|
@@ -432,40 +355,5 @@ Includes txids the agent submitted directly (via \`competition_submit_trade\`) a
|
|
|
432
355
|
return createErrorResponse(error);
|
|
433
356
|
}
|
|
434
357
|
});
|
|
435
|
-
server.registerTool("competition_allowlist", {
|
|
436
|
-
description: `Get the set of \`(contract_id, function_name)\` tuples the AIBTC trading-competition verifier will accept. Swaps against any contract/function not in this list are rejected with \`contract_not_allowlisted\` at \`POST /api/competition/trades\`.
|
|
437
|
-
|
|
438
|
-
Use this **before submitting a txid** to confirm the swap you just made will score, or as discovery for "what protocols / pools can I trade for the competition right now?"
|
|
439
|
-
|
|
440
|
-
Returns the payload from \`GET /api/competition/allowlist\` verbatim:
|
|
441
|
-
|
|
442
|
-
\`\`\`
|
|
443
|
-
{
|
|
444
|
-
entries: [
|
|
445
|
-
{ contract_id: "SP….contract-name", functions: ["swap-x-for-y", ...] },
|
|
446
|
-
...
|
|
447
|
-
],
|
|
448
|
-
total_contracts: number, // count of distinct contract_ids
|
|
449
|
-
total_functions: number, // sum of allowed function names across entries
|
|
450
|
-
provider_address: string, // AIBTC attribution string (audit-only)
|
|
451
|
-
protocols: { bitflow: number }, // per-protocol entry count
|
|
452
|
-
}
|
|
453
|
-
\`\`\`
|
|
454
|
-
|
|
455
|
-
**Notes:**
|
|
456
|
-
- Source of truth: \`lib/competition/allowlist.ts\` in landing-page. Reviewed per PR; no runtime mutation surface.
|
|
457
|
-
- Current scope is **Bitflow only** (stableswap, XYK, DLMM router, cross-DEX routers, wrappers). ALEX direct and Zest are NOT yet in scope — \`alex_swap\` / \`zest_*\` calls land on-chain but get rejected as \`contract_not_allowlisted\`.
|
|
458
|
-
- \`provider_address\` is the AIBTC attribution tag Bitflow's optional \`provider\` clarity arg can carry — it's recorded for audit but does NOT gate acceptance. Only the \`(contract, function)\` tuple match matters.
|
|
459
|
-
- The list is cached server-side; tool returns whatever the live endpoint reports at call time.`,
|
|
460
|
-
inputSchema: {},
|
|
461
|
-
}, async () => {
|
|
462
|
-
try {
|
|
463
|
-
const parsed = await competitionFetch("/allowlist");
|
|
464
|
-
return createJsonResponse(parsed);
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
return createErrorResponse(error);
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
358
|
}
|
|
471
359
|
//# sourceMappingURL=competition.tools.js.map
|
package/dist/tools/index.js
CHANGED
|
@@ -37,6 +37,8 @@ import { registerSigningTools } from "./signing.tools.js";
|
|
|
37
37
|
import { registerInferenceMarketplaceTools } from "./inference-marketplace.tools.js";
|
|
38
38
|
import { registerNewsTools } from "./news.tools.js";
|
|
39
39
|
import { registerLegionTools } from "./legion.tools.js";
|
|
40
|
+
import { registerAtStakeTools } from "./at-stake.tools.js";
|
|
41
|
+
import { registerAtStakeLegionTools } from "./at-stake-legion.tools.js";
|
|
40
42
|
import { registerIdentityTools } from "./identity.tools.js";
|
|
41
43
|
import { registerCredentialsTools } from "./credentials.tools.js";
|
|
42
44
|
import { registerSouldinalsTools } from "./souldinals.tools.js";
|
|
@@ -47,7 +49,6 @@ import { registerRunesTools } from "./runes.tools.js";
|
|
|
47
49
|
import { registerInboxTools } from "./inbox.tools.js";
|
|
48
50
|
import { registerInboxX402Tools } from "./inbox-x402.tools.js";
|
|
49
51
|
import { registerArxivResearchTools } from "./arxiv-research.tools.js";
|
|
50
|
-
import { registerCompetitionTools } from "./competition.tools.js";
|
|
51
52
|
import { registerEarningTools } from "./earning.tools.js";
|
|
52
53
|
import { getSkillForTool } from "./skill-mappings.js";
|
|
53
54
|
/**
|
|
@@ -170,6 +171,10 @@ export function registerAllTools(server) {
|
|
|
170
171
|
registerNewsTools(server);
|
|
171
172
|
// AIBTC News Legion (mainnet aibtc-news-gov — inscribe, propose, vote, conclude)
|
|
172
173
|
registerLegionTools(server);
|
|
174
|
+
// At Stake (elsalvadorstakesbtc.com — complete-set prediction market)
|
|
175
|
+
registerAtStakeTools(server);
|
|
176
|
+
// At Stake side legions (aibtc.com/legions — weight is the share balance)
|
|
177
|
+
registerAtStakeLegionTools(server);
|
|
173
178
|
// Identity (ERC-8004 on-chain agent identity management)
|
|
174
179
|
registerIdentityTools(server);
|
|
175
180
|
// Credentials (encrypted credential store — list, get, set, delete, unlock)
|
|
@@ -190,8 +195,6 @@ export function registerAllTools(server) {
|
|
|
190
195
|
registerInboxX402Tools(server);
|
|
191
196
|
// arXiv Research (public arXiv Atom API — paper search and digest compilation)
|
|
192
197
|
registerArxivResearchTools(server);
|
|
193
|
-
// AIBTC Trading Competition (submit trade txids, check standing, list trades)
|
|
194
|
-
registerCompetitionTools(server);
|
|
195
198
|
// Earning Opportunities (static "how to put your assets to work" menu)
|
|
196
199
|
registerEarningTools(server);
|
|
197
200
|
restoreRegisterTool();
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* EVERYTHING HERE MOVES REAL VALUE. The legion is on Stacks mainnet holding
|
|
9
9
|
* real sBTC, and neither `contribute` nor `sponsor` has a withdrawal path, so
|
|
10
|
-
* both meter against the wallet's `sats` spending rail before signing
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* both meter against the wallet's `sats` spending rail before signing. That
|
|
11
|
+
* metering happens inside `callContract`, which reads the exact sBTC
|
|
12
|
+
* post-condition these calls already sign in DENY mode — so it must NOT be
|
|
13
|
+
* repeated here, or every contribute would bill the rail twice.
|
|
13
14
|
*
|
|
14
15
|
* These tools pin their own network: `getLegionAccount()` re-derives the
|
|
15
16
|
* unlocked wallet for the legion's chain, so a testnet-configured server still
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* EVERYTHING HERE MOVES REAL VALUE. The legion is on Stacks mainnet holding
|
|
9
9
|
* real sBTC, and neither `contribute` nor `sponsor` has a withdrawal path, so
|
|
10
|
-
* both meter against the wallet's `sats` spending rail before signing
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* both meter against the wallet's `sats` spending rail before signing. That
|
|
11
|
+
* metering happens inside `callContract`, which reads the exact sBTC
|
|
12
|
+
* post-condition these calls already sign in DENY mode — so it must NOT be
|
|
13
|
+
* repeated here, or every contribute would bill the rail twice.
|
|
13
14
|
*
|
|
14
15
|
* These tools pin their own network: `getLegionAccount()` re-derives the
|
|
15
16
|
* unlocked wallet for the legion's chain, so a testnet-configured server still
|
|
@@ -27,7 +28,6 @@ import { DUST_THRESHOLD } from "../config/bitcoin-constants.js";
|
|
|
27
28
|
import { LEGION_ERAS, LEGION_ERRORS, LEGION_NETWORK, LEGION_SITE_URL, LIVE_ERA, MAX_DESCRIPTION_LENGTH, MAX_LINK_LENGTH, MAX_RATIONALE_LENGTH, MAX_SPONSOR_LINK_LENGTH, MAX_SPONSOR_MEMO_LENGTH, MAX_SPONSOR_NAME_LENGTH, MAX_TITLE_LENGTH, ORDINALS_CONTENT_BASE, ORDINALS_INSCRIPTION_BASE, STORY_STATUS, resolveEra, } from "../config/legion.js";
|
|
28
29
|
import { assertAscii, buildTimeline, contentUrlFromLink, explainError, getClockHeight, getLastProposalId, getLegionAccount, getLegionToken, getMembership, getParams, getPhase, getProposeStatus, getStory, getStoryMeta, getVoteRecord, inscriptionIdFromLink, normalizeOrdinalsLink, num, predictOutcome, proposeBlockers, readGov, readLegionContract, readTreasury, } from "../services/legion.service.js";
|
|
29
30
|
import { callContract } from "../transactions/builder.js";
|
|
30
|
-
import { getSpendLimiter } from "../services/spend-limiter.js";
|
|
31
31
|
import { getWalletManager } from "../services/wallet-manager.js";
|
|
32
32
|
import { MempoolApi, getMempoolTxUrl } from "../services/mempool-api.js";
|
|
33
33
|
import { buildCommitTransaction, buildRevealTransaction, deriveRevealScript, } from "../transactions/inscription-builder.js";
|
|
@@ -596,10 +596,9 @@ export function registerLegionTools(server) {
|
|
|
596
596
|
return createErrorResponse(new Error(`Your sBTC balance is ${balance} sats — not enough to contribute ${sats}. ` +
|
|
597
597
|
`Acquire sBTC with sbtc_deposit or styx_deposit first.`));
|
|
598
598
|
}
|
|
599
|
-
// Real, unrecoverable sBTC leaves the wallet here.
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
await getSpendLimiter().check("sats", BigInt(sats), account.address);
|
|
599
|
+
// Real, unrecoverable sBTC leaves the wallet here. The `sats` rail is
|
|
600
|
+
// metered by callContract off the willSendEq post-condition below —
|
|
601
|
+
// metering again here would double-bill the cap.
|
|
603
602
|
const quotedWeight = num(await readGov("quote-weight", [uintCV(sats)]));
|
|
604
603
|
const result = await callContract(account, {
|
|
605
604
|
contractAddress: GOV_ADDRESS,
|
|
@@ -613,7 +612,6 @@ export function registerLegionTools(server) {
|
|
|
613
612
|
.ft(token.contract, token.assetName),
|
|
614
613
|
],
|
|
615
614
|
});
|
|
616
|
-
await getSpendLimiter().record("sats", BigInt(sats), account.address);
|
|
617
615
|
const weightAfter = num(weightBefore) + quotedWeight;
|
|
618
616
|
const wasMember = num(weightBefore) >= params.minWeightToAct;
|
|
619
617
|
const willBeMember = weightAfter >= params.minWeightToAct;
|
|
@@ -700,8 +698,8 @@ export function registerLegionTools(server) {
|
|
|
700
698
|
if (balance < sats) {
|
|
701
699
|
return createErrorResponse(new Error(`Your sBTC balance is ${balance} sats — not enough to sponsor ${sats}.`));
|
|
702
700
|
}
|
|
703
|
-
// Same rail as contribute: real sBTC, no refund,
|
|
704
|
-
|
|
701
|
+
// Same rail as contribute: real sBTC, no refund, metered by callContract
|
|
702
|
+
// off the post-condition below.
|
|
705
703
|
const result = await callContract(account, {
|
|
706
704
|
contractAddress: TREASURY_ADDRESS,
|
|
707
705
|
contractName: TREASURY_NAME,
|
|
@@ -719,7 +717,6 @@ export function registerLegionTools(server) {
|
|
|
719
717
|
.ft(token.contract, token.assetName),
|
|
720
718
|
],
|
|
721
719
|
});
|
|
722
|
-
await getSpendLimiter().record("sats", BigInt(sats), account.address);
|
|
723
720
|
return createJsonResponse({
|
|
724
721
|
success: true,
|
|
725
722
|
txid: result.txid,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Uses micro-ordinals library to parse and create inscriptions.
|
|
12
12
|
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import { NETWORK } from "../config/networks.js";
|
|
15
16
|
import { P2WPKH_INPUT_VBYTES, P2WPKH_OUTPUT_VBYTES, P2TR_OUTPUT_VBYTES, TX_OVERHEAD_VBYTES, DUST_THRESHOLD, P2TR_INPUT_BASE_VBYTES, WITNESS_OVERHEAD_VBYTES, } from "../config/bitcoin-constants.js";
|
|
@@ -223,8 +224,10 @@ export function registerOrdinalsTools(server) {
|
|
|
223
224
|
feeRate: actualFeeRate,
|
|
224
225
|
contentType,
|
|
225
226
|
contentSize: body.length,
|
|
227
|
+
contentSha256: createHash("sha256").update(body).digest("hex"),
|
|
226
228
|
nextStep: "After commit confirms, call inscribe_reveal with the same contentType, contentBase64, " +
|
|
227
|
-
"plus commitTxid and revealAmount from this response."
|
|
229
|
+
"plus commitTxid and revealAmount from this response. Pass contentSha256 back to inscribe_reveal " +
|
|
230
|
+
"to have the server abort before the reveal fee is spent if the decoded body differs by even one byte.",
|
|
228
231
|
});
|
|
229
232
|
}
|
|
230
233
|
catch (error) {
|
|
@@ -252,12 +255,20 @@ export function registerOrdinalsTools(server) {
|
|
|
252
255
|
contentBase64: z
|
|
253
256
|
.string()
|
|
254
257
|
.describe("Content as base64-encoded string (must match the commit step)"),
|
|
258
|
+
contentSha256: z
|
|
259
|
+
.string()
|
|
260
|
+
.regex(/^[0-9a-f]{64}$/i)
|
|
261
|
+
.optional()
|
|
262
|
+
.describe("Optional: sha256 hex digest of the decoded body from the inscribe commit response. " +
|
|
263
|
+
"If provided, the server computes sha256 over the locally-decoded body and aborts before " +
|
|
264
|
+
"broadcasting the reveal if they differ — prevents burning the reveal fee on a witness-hash " +
|
|
265
|
+
"mismatch when the base64 param drifts between the inscribe and inscribe_reveal calls."),
|
|
255
266
|
feeRate: z
|
|
256
267
|
.union([z.enum(["fast", "medium", "slow"]), z.number().positive()])
|
|
257
268
|
.optional()
|
|
258
269
|
.describe("Fee rate for reveal tx (default: medium)"),
|
|
259
270
|
},
|
|
260
|
-
}, async ({ commitTxid, revealAmount, contentType, contentBase64, feeRate }) => {
|
|
271
|
+
}, async ({ commitTxid, revealAmount, contentType, contentBase64, contentSha256, feeRate }) => {
|
|
261
272
|
try {
|
|
262
273
|
// Check wallet session
|
|
263
274
|
const walletManager = getWalletManager();
|
|
@@ -277,6 +288,21 @@ export function registerOrdinalsTools(server) {
|
|
|
277
288
|
const mempoolApi = new MempoolApi(NETWORK);
|
|
278
289
|
// Reconstruct the inscription and reveal script
|
|
279
290
|
const body = Buffer.from(contentBase64, "base64");
|
|
291
|
+
// If the caller supplied the sha256 digest from the commit response, verify the
|
|
292
|
+
// decoded body matches BEFORE we spend the reveal fee. Silent byte drift between
|
|
293
|
+
// the two calls would otherwise commit a tapscript hash that Bitcoin Core rejects
|
|
294
|
+
// as witness-hash-mismatch, burning the reveal-address sats permanently.
|
|
295
|
+
if (contentSha256) {
|
|
296
|
+
const localSha256 = createHash("sha256").update(body).digest("hex");
|
|
297
|
+
if (localSha256.toLowerCase() !== contentSha256.toLowerCase()) {
|
|
298
|
+
return createErrorResponse(new Error(`contentSha256 mismatch: the decoded body's sha256 (${localSha256}) does not match ` +
|
|
299
|
+
`the digest from the inscribe commit response (${contentSha256}). This means the ` +
|
|
300
|
+
`base64 payload passed to inscribe_reveal is not the exact bytes committed on-chain. ` +
|
|
301
|
+
`Aborting BEFORE broadcasting the reveal so the reveal-address sats are not burned. ` +
|
|
302
|
+
`Re-send the exact base64 from the successful inscribe call (or fix the transport / ` +
|
|
303
|
+
`serialization layer that mutated it).`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
280
306
|
const inscription = {
|
|
281
307
|
contentType,
|
|
282
308
|
body,
|
|
@@ -3,7 +3,7 @@ import { hexToBytes } from "@stacks/common";
|
|
|
3
3
|
import { getStacksNetwork, getApiBaseUrl } from "../config/networks.js";
|
|
4
4
|
import { getHiroApi } from "../services/hiro-api.js";
|
|
5
5
|
import { resolveDefaultFee } from "../utils/fee.js";
|
|
6
|
-
import { getSpendLimiter } from "../services/spend-limiter.js";
|
|
6
|
+
import { getSpendLimiter, totalBoundedSpends } from "../services/spend-limiter.js";
|
|
7
7
|
import { getTrackedNonce, recordNonceUsed, resetTrackedNonce, reconcileWithChain, } from "../services/nonce-tracker.js";
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Pending nonce tracking (unified via SharedNonceTracker, issue #413)
|
|
@@ -117,6 +117,16 @@ export async function transferStx(account, recipient, amount, memo, fee) {
|
|
|
117
117
|
* Call a smart contract function
|
|
118
118
|
*/
|
|
119
119
|
export async function callContract(account, options) {
|
|
120
|
+
// Safety rail. Swaps, lending and pot entries all funnel through here, and
|
|
121
|
+
// their amounts are denominated in whatever token the call moves — there is no
|
|
122
|
+
// ledger to bill 1,000 ALEX against. The post conditions are the way in: they
|
|
123
|
+
// already bound, in native units, how much of each asset may leave this
|
|
124
|
+
// address. Meter the ones this rail has a ledger for and ignore the rest.
|
|
125
|
+
// Deliberately before makeContractCall, so an over-cap call never signs.
|
|
126
|
+
const meteredSpends = totalBoundedSpends(options.postConditions, account.address);
|
|
127
|
+
for (const spend of meteredSpends) {
|
|
128
|
+
await getSpendLimiter().check(spend.unit, spend.amount, account.address);
|
|
129
|
+
}
|
|
120
130
|
const networkName = getStacksNetwork(account.network);
|
|
121
131
|
const nonce = await getNextNonce(account.address, account.network);
|
|
122
132
|
// Always resolve a clamped fee — prevents @stacks/transactions from over-estimating.
|
|
@@ -141,6 +151,9 @@ export async function callContract(account, options) {
|
|
|
141
151
|
throw new Error(`Broadcast failed: ${broadcastResponse.error} - ${broadcastResponse.reason}`);
|
|
142
152
|
}
|
|
143
153
|
advancePendingNonce(account.address, nonce, broadcastResponse.txid);
|
|
154
|
+
for (const spend of meteredSpends) {
|
|
155
|
+
await getSpendLimiter().record(spend.unit, spend.amount, account.address);
|
|
156
|
+
}
|
|
144
157
|
return {
|
|
145
158
|
txid: broadcastResponse.txid,
|
|
146
159
|
rawTx: transaction.serialize(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibtc/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.70.0",
|
|
4
4
|
"description": "Bitcoin-native MCP server for AI agents: BTC/STX wallets, DeFi yield, sBTC peg, NFTs, and x402 payments.",
|
|
5
5
|
"mcpName": "io.github.aibtcdev/mcp-server",
|
|
6
6
|
"type": "module",
|
package/skill/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Bitcoin L1 wallet for agents - check balances, send BTC, manage UTX
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: aibtcdev
|
|
7
|
-
version: 1.
|
|
7
|
+
version: 1.70.0 # x-release-please-version
|
|
8
8
|
npm: "@aibtc/mcp-server"
|
|
9
9
|
github: https://github.com/aibtcdev/aibtc-mcp-server
|
|
10
10
|
---
|
|
@@ -241,6 +241,42 @@ unlocked wallet on the gateway's network. Every tool takes an optional `gateway`
|
|
|
241
241
|
arg (defaults to `https://inference.aibtc.com`; use `http://localhost:8787` for
|
|
242
242
|
local dev).
|
|
243
243
|
|
|
244
|
+
### Prediction Market + Legions (elsalvadorstakesbtc.com)
|
|
245
|
+
|
|
246
|
+
A market on one bit of Bitcoin history — did twenty frozen El Salvador reserve
|
|
247
|
+
scripts spend into a Stacks PoX-5 bond before burn block 994,699 — with two DAOs,
|
|
248
|
+
one per side, that pay agents in shares for checking the claim and publishing
|
|
249
|
+
what they found. Chips are sBTC; there is no admin key and no oracle.
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
"What is the status of the El Salvador stakes BTC market?"
|
|
253
|
+
"Mint 1000 sats of complete sets so I can vote in the no legion"
|
|
254
|
+
"Show me every live proposal in both legions"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Complete sets are the thing to understand first: **1 sat mints 1 BONDED (yes)
|
|
258
|
+
share AND 1 IDLE (no) share**, and the pair merges back to 1 sat before resolve.
|
|
259
|
+
Minting is therefore a hedge, not a bet — a directional position comes from
|
|
260
|
+
`atstake_place_bid` on the side you believe. Minting is also how you buy legion voting weight, because **weight is
|
|
261
|
+
the share balance**, read live on every call.
|
|
262
|
+
|
|
263
|
+
| Tool | Description | Wallet |
|
|
264
|
+
|------|-------------|:------:|
|
|
265
|
+
| `atstake_market_status` | The claim, status, escrow, blocks to close | — |
|
|
266
|
+
| `atstake_position` | Share balances and payout under each outcome | optional |
|
|
267
|
+
| `atstake_mint_complete_set` | Spend sBTC for a matched pair | ✅ |
|
|
268
|
+
| `atstake_merge_complete_set` | Hand a pair back for its sats | ✅ |
|
|
269
|
+
| `atstake_place_bid` / `atstake_cancel_bid` | Resting bid below par | ✅ |
|
|
270
|
+
| `atstake_transfer_shares` | Send one side (moves legion weight) | ✅ |
|
|
271
|
+
| `atstake_redeem` | Cash a resolved position | ✅ |
|
|
272
|
+
| `atstake_legion_propose` / `_vote` / `_conclude` | Side governance | ✅ |
|
|
273
|
+
| `atstake_legion_status` / `_list_proposals` | Eligibility and the board | optional |
|
|
274
|
+
|
|
275
|
+
Distinct from `legion_*` (which governs `aibtc-news-gov`) and from
|
|
276
|
+
`stacks_market_*` (which trades stacksmarket.app).
|
|
277
|
+
|
|
278
|
+
See: [references/at-stake.md](references/at-stake.md)
|
|
279
|
+
|
|
244
280
|
### Genesis Lifecycle
|
|
245
281
|
|
|
246
282
|
Agent identity and reputation on Bitcoin and Stacks:
|